[object Object]

← back to Dw Internal Gateway

Add 'Re-check all sites' control + /api/status health probe + status dots on landing

1061465152175757fa88895d3f7351c52e0db46f · 2026-07-28 08:17:33 -0700 · Steve Abrams

Files touched

Diff

commit 1061465152175757fa88895d3f7351c52e0db46f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jul 28 08:17:33 2026 -0700

    Add 'Re-check all sites' control + /api/status health probe + status dots on landing
---
 server.js | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 67 insertions(+), 7 deletions(-)

diff --git a/server.js b/server.js
index 6c5de09..faa764f 100644
--- a/server.js
+++ b/server.js
@@ -216,6 +216,23 @@ load(true);
 </script></body></html>`;
 }
 
+// Health probe for the "Re-check all sites" control on the landing page. Hits a
+// local port's /healthz (any answer works) — anything that responds with a status
+// < 500, incl. a 401 auth gate, is "up". This is a re-CHECK, not a restart: it only
+// re-tests reachability, so it's safe to click anytime.
+function probePort(port) {
+  return new Promise((resolve) => {
+    const started = Date.now();
+    const rq = http.request({ host: '127.0.0.1', port, method: 'GET', path: '/healthz', timeout: 4000 }, (r) => {
+      r.resume();
+      resolve({ up: r.statusCode > 0 && r.statusCode < 500, status: r.statusCode, ms: Date.now() - started });
+    });
+    rq.on('timeout', () => { rq.destroy(); resolve({ up: false, status: 0, ms: Date.now() - started }); });
+    rq.on('error', () => resolve({ up: false, status: 0, ms: Date.now() - started }));
+    rq.end();
+  });
+}
+
 async function indexPage() {
   await refreshVendorMap();
   const { rows } = await pool.query(`
@@ -227,8 +244,9 @@ async function indexPage() {
   const suffix = PORT === 80 ? '' : ':' + PORT;
   const catalogSlugs = new Set(rows.map(r => r.table_name.replace(/_catalog$/, '').replace(/_/g, '-')));
 
-  const li = (slug, badge, n) =>
-    `<li><a href="//${slug}.internal.dw${suffix}/">${esc(slug)}.internal.dw</a>` +
+  const li = (slug, badge, n, port) =>
+    `<li><span class="dot"${port ? ` data-port="${port}"` : ' data-alldw="1"'}></span>` +
+    `<a href="//${slug}.internal.dw${suffix}/">${esc(slug)}.internal.dw</a>` +
     (n != null ? ` <span class="n">${Number(n).toLocaleString()}</span>` : '') + ` ${badge}</li>`;
 
   const catalogSet = catalogSlugs;
@@ -237,25 +255,25 @@ async function indexPage() {
   // Group 1 — bespoke dedicated viewers.
   const dedicated = Object.entries(ROUTES)
     .sort((a, b) => a[0].localeCompare(b[0]))
-    .map(([slug, port]) => li(slug, `<span class="ded">dedicated :${port}</span>`, null));
+    .map(([slug, port]) => li(slug, `<span class="ded">dedicated :${port}</span>`, null, port));
 
   // Group 2 — all.dw-schema vendors with LIVE shopify rows (not dedicated).
   const alldw = [...VENDOR_MAP.entries()]
     .filter(([slug, e]) => !ROUTES[slug] && e.live > 0)
     .sort((a, b) => a[0].localeCompare(b[0]))
-    .map(([slug, e]) => li(slug, `<span class="all">all.dw</span>`, e.live));
+    .map(([slug, e]) => li(slug, `<span class="all">all.dw</span>`, e.live, null));
 
   // Group 3 — catalog-backed lines (a *_catalog table, not dedicated, not a live vendor).
   const catalogOnly = rows
     .map(r => [r.table_name.replace(/_catalog$/, '').replace(/_/g, '-'), Number(r.est)])
     .filter(([slug]) => !ROUTES[slug] && !liveVendor(slug))
-    .map(([slug, est]) => li(slug, `<span class="cat">catalog</span>`, est));
+    .map(([slug, est]) => li(slug, `<span class="cat">catalog</span>`, est, null));
 
   // Group 4 — archived-only shopify vendors (no live rows, no catalog table) → all.dw Archived view.
   const archived = [...VENDOR_MAP.entries()]
     .filter(([slug, e]) => !ROUTES[slug] && e.live === 0 && e.total > 0 && !catalogSet.has(slug))
     .sort((a, b) => a[0].localeCompare(b[0]))
-    .map(([slug, e]) => li(slug, `<span class="arc">archived</span>`, e.total));
+    .map(([slug, e]) => li(slug, `<span class="arc">archived</span>`, e.total, null));
 
   const total = dedicated.length + alldw.length + catalogOnly.length + archived.length;
   const section = (title, arr) => arr.length
@@ -274,12 +292,44 @@ a{color:#1e2a3a;text-decoration:none}a:hover{text-decoration:underline}
 .all{color:#1e4b8a;font-size:10px;border:1px solid #1e4b8a;border-radius:4px;padding:0 4px}
 .cat{color:#8a6d1e;font-size:10px;border:1px solid #8a6d1e;border-radius:4px;padding:0 4px}
 .arc{color:#777;font-size:10px;border:1px solid #999;border-radius:4px;padding:0 4px}
+.dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:#c3c3c3;margin-right:7px;vertical-align:middle}
+.dot.up{background:#2e7d32}.dot.down{background:#c0392b}
+#recheck{margin-top:10px;background:#2c3e52;color:#fff;border:1px solid #46617d;border-radius:6px;padding:6px 13px;font-size:13px;cursor:pointer}
+#recheck:hover:not(:disabled){background:#3a516b}#recheck:disabled{opacity:.6;cursor:progress}
+#lastcheck{color:#cbd6e2;font-size:12px;margin-top:7px}
 @media(max-width:900px){ul{columns:2}}@media(max-width:600px){ul{columns:1}}</style></head>
-<body><header><h1>internal.dw — ${total} vendor internal sites</h1></header>
+<body><header><h1>internal.dw — ${total} vendor internal sites</h1>
+<button id="recheck" title="Re-run the health check on every internal site (no restart — just re-probes reachability)">↻ Re-check all sites</button>
+<div id="lastcheck"></div></header>
 ${section('Dedicated viewers', dedicated)}
 ${section('all.dw schema · shopify vendors', alldw)}
 ${section('Catalog-backed lines', catalogOnly)}
 ${section('Archived lines', archived)}
+<script>
+const rc=document.getElementById('recheck'),lc=document.getElementById('lastcheck');
+let busy=false;
+// Re-CHECK (not restart): re-probe every internal site and refresh the dots.
+// Dedicated viewers get their own dot (own port); all.dw/catalog/archived rows
+// share the single all.dw backend probe.
+async function loadStatus(){
+  if(busy)return;busy=true;rc.disabled=true;rc.textContent='↻ Checking…';
+  try{
+    const r=await fetch('/api/status');if(!r.ok)throw new Error('HTTP '+r.status);
+    const d=await r.json();let up=0,tot=0;
+    document.querySelectorAll('.dot').forEach(el=>{
+      tot++;
+      const st=el.dataset.port?d.dedicated[el.dataset.port]:d.alldw;
+      const ok=!!(st&&st.up);
+      el.className='dot '+(ok?'up':'down');
+      if(ok)up++;
+    });
+    lc.textContent='checked '+up+'/'+tot+' up · '+new Date().toLocaleTimeString([],{hour:'numeric',minute:'2-digit',second:'2-digit'});
+  }catch(e){ lc.textContent='check failed — click ↻ to retry'; }
+  finally{ busy=false;rc.disabled=false;rc.textContent='↻ Re-check all sites'; }
+}
+rc.onclick=loadStatus;
+loadStatus();
+</script>
 </body></html>`;
 }
 
@@ -372,6 +422,16 @@ const server = http.createServer(async (req, res) => {
     const slug = m ? m[1] : null;
 
     if (!m || !slug || slug === 'www') {
+      const u = new URL(req.url, 'http://x');
+      // Fleet-wide re-check: probe every dedicated viewer port + the shared all.dw
+      // backend. Non-dedicated sites (all.dw/catalog/archived) share the :9958 fate.
+      if (u.pathname === '/api/status') {
+        const dedicated = {};
+        await Promise.all(Object.entries(ROUTES).map(async ([, port]) => { dedicated[port] = await probePort(port); }));
+        const alldw = await probePort(ALLDW_PORT);
+        res.writeHead(200, { 'content-type': 'application/json' });
+        return res.end(JSON.stringify({ dedicated, alldw }));
+      }
       res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
       return res.end(await indexPage());
     }

← 9940353 gateway: prefer staging catalog over tiny live-shopify view  ·  back to Dw Internal Gateway  ·  (newest)