[object Object]

← back to Domain Sniper

dashboard: /healthz liveness probe for pm2/launchd

dfb3b86f031614060624a81b970df547c975ef04 · 2026-05-12 18:12:31 -0700 · steve

Returns 200 + JSON when at least one CT source has reported (tick or
idle) within 90s, 503 otherwise. Includes per-source ageSec so the
body is also useful for eyeball checks. Cheap to poll, no external
calls. Pattern for pm2 ecosystem entry:

  scripts: [{ name: 'sniper-dashboard', script: 'dashboard.js',
              max_memory_restart: '500M',
              ... and a separate launchd job that curls /healthz
              every minute and bounces pm2 if it returns 503 }]

Also recorded 4.59h slice 4-5 RECHECK#2 datapoint: 0/10. Cumulative
0/140 across 15 datapoints.

Files touched

Diff

commit dfb3b86f031614060624a81b970df547c975ef04
Author: steve <steve@designerwallcoverings.com>
Date:   Tue May 12 18:12:31 2026 -0700

    dashboard: /healthz liveness probe for pm2/launchd
    
    Returns 200 + JSON when at least one CT source has reported (tick or
    idle) within 90s, 503 otherwise. Includes per-source ageSec so the
    body is also useful for eyeball checks. Cheap to poll, no external
    calls. Pattern for pm2 ecosystem entry:
    
      scripts: [{ name: 'sniper-dashboard', script: 'dashboard.js',
                  max_memory_restart: '500M',
                  ... and a separate launchd job that curls /healthz
                  every minute and bounces pm2 if it returns 503 }]
    
    Also recorded 4.59h slice 4-5 RECHECK#2 datapoint: 0/10. Cumulative
    0/140 across 15 datapoints.
---
 dashboard.js | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/dashboard.js b/dashboard.js
index e25d163..cbd7248 100644
--- a/dashboard.js
+++ b/dashboard.js
@@ -64,6 +64,30 @@ app.get('/api/perlog', (_req, res) => {
   res.json({ stats, perLog: out });
 });
 
+// Liveness probe — 200 if dashboard is up AND at least one CT source has
+// reported any event (tick or idle) within 90s. Otherwise 503 so pm2/launchd
+// can auto-restart a wedged process. Returns JSON so the body is also useful
+// for human eyeball checks. No external calls; cheap to poll.
+app.get('/healthz', (_req, res) => {
+  const now = Date.now();
+  const entries = Object.entries(perLog).map(([name, v]) => ({
+    name,
+    ageSec: v.lastSeenMs ? Math.round((now - v.lastSeenMs) / 1000) : null,
+  }));
+  const fresh = entries.filter((e) => e.ageSec !== null && e.ageSec < 90);
+  const ok = fresh.length > 0;
+  const uptimeSec = Math.round((now - stats.startedAt) / 1000);
+  res.status(ok ? 200 : 503).json({
+    ok,
+    uptimeSec,
+    sourcesFresh: fresh.length,
+    sourcesTotal: entries.length,
+    sources: entries,
+    seen: stats.seen,
+    hits: stats.hot + stats.brand,
+  });
+});
+
 const server = http.createServer(app);
 const wss = new WebSocket.Server({ server });
 const clients = new Set();

← 24e477f dashboard ui: per-log source band with health dots  ·  back to Domain Sniper  ·  watchdog: opt-in launchd plist + zsh script that bounces das a353d85 →