← 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 →