← back to Butlr
admin: /admin/status — live health snapshot UI
03e71d275af06d035c1bb2b294ae2c56a3f332ca · 2026-05-13 13:04:43 -0700 · SteveStudio2
Surfaces what scripts/status-snapshot.sh writes to ~/status, but live
per-request (no dependence on cron persistence). Auto-refreshes every
30s. Shows butlr + wallco-ai with HTTP, uptime, restarts, pm2 status,
memory, CPU. Reads recent ALERT-*.txt files if present.
Admin-only. URL: https://butlr.agentabrams.com/admin/status
Files touched
Diff
commit 03e71d275af06d035c1bb2b294ae2c56a3f332ca
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Wed May 13 13:04:43 2026 -0700
admin: /admin/status — live health snapshot UI
Surfaces what scripts/status-snapshot.sh writes to ~/status, but live
per-request (no dependence on cron persistence). Auto-refreshes every
30s. Shows butlr + wallco-ai with HTTP, uptime, restarts, pm2 status,
memory, CPU. Reads recent ALERT-*.txt files if present.
Admin-only. URL: https://butlr.agentabrams.com/admin/status
---
routes/admin.js | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 105 insertions(+)
diff --git a/routes/admin.js b/routes/admin.js
index 2dce504..660f474 100644
--- a/routes/admin.js
+++ b/routes/admin.js
@@ -170,6 +170,111 @@ function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c]);
}
+// ── /admin/status — live system snapshot ─────────────────────────────
+// On-demand health check of butlr + wallco-ai (the two pm2 processes
+// Steve monitors). Mirrors the launchd cron at
+// scripts/status-snapshot.sh — same shape, but live per-request.
+// Cron polls every 2min and persists; this is the "now" view.
+router.get('/admin/status', requireAdmin, async (req, res) => {
+ const [butlrHealth, wallcoHealth] = await Promise.all([
+ fetch('https://butlr.agentabrams.com/healthz', { signal: AbortSignal.timeout(5000) })
+ .then(r => r.status).catch(() => 0),
+ fetch('https://wallco.ai/health', { signal: AbortSignal.timeout(5000) })
+ .then(r => r.status).catch(() => 0),
+ ]);
+
+ // pm2 stats via local-host pm2 jlist (this server runs on prod next to wallco-ai's pm2)
+ let pm2Stats = {};
+ try {
+ const { execSync } = require('child_process');
+ const blob = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf8', maxBuffer: 5_000_000 });
+ const list = JSON.parse(blob);
+ for (const name of ['butlr', 'wallco-ai']) {
+ const p = list.find(x => x.name === name);
+ if (!p) { pm2Stats[name] = null; continue; }
+ const e = p.pm2_env || {};
+ pm2Stats[name] = {
+ pid: p.pid,
+ uptime_s: Math.floor((Date.now() - (e.pm_uptime || 0)) / 1000),
+ restarts: e.restart_time || 0,
+ status: e.status || 'unknown',
+ memory_mb: Math.round((p.monit && p.monit.memory || 0) / 1024 / 1024 * 10) / 10,
+ cpu_pct: p.monit && p.monit.cpu || 0,
+ };
+ }
+ } catch (e) {
+ pm2Stats._error = e.message;
+ }
+
+ // Recent alerts from launchd cron (best-effort — only present if cron is on this host)
+ let recentAlerts = [];
+ try {
+ const fs = require('fs');
+ const path = require('path');
+ const alertDir = path.join(process.env.HOME || '/root', 'status');
+ if (fs.existsSync(alertDir)) {
+ recentAlerts = fs.readdirSync(alertDir)
+ .filter(f => f.startsWith('ALERT-'))
+ .sort().reverse().slice(0, 5)
+ .map(f => {
+ const body = fs.readFileSync(path.join(alertDir, f), 'utf8').slice(0, 400);
+ return { file: f, body };
+ });
+ }
+ } catch {}
+
+ const rowFor = (name, label, healthCode) => {
+ const s = pm2Stats[name];
+ const ok = healthCode >= 200 && healthCode < 300;
+ const pm2Ok = s && s.status === 'online';
+ const color = ok && pm2Ok ? '#047857' : '#b91c1c';
+ return `<tr>
+ <td><strong>${label}</strong></td>
+ <td style="color:${color};font-weight:600">${healthCode || 'ERR'}</td>
+ <td>${s ? s.uptime_s + 's' : '—'}</td>
+ <td>${s ? s.restarts : '—'}</td>
+ <td style="color:${pm2Ok ? '#047857' : '#b91c1c'}">${s ? s.status : 'not found'}</td>
+ <td>${s ? s.memory_mb + ' MB' : '—'}</td>
+ <td>${s ? s.cpu_pct + '%' : '—'}</td>
+ </tr>`;
+ };
+
+ res.type('html').send(`<!doctype html><html><head>
+ <meta charset="utf-8">
+ <title>Status — Butlr admin</title>
+ <meta http-equiv="refresh" content="30">
+ <link rel="stylesheet" href="/css/site.css">
+ <style>
+ body { font-family: 'Inter', system-ui, sans-serif; max-width: 920px; margin: 0 auto; padding: 24px; }
+ table { border-collapse: collapse; width: 100%; margin: 14px 0; background: #fff; }
+ th, td { padding: 10px 12px; border-bottom: 1px solid #e5e7eb; text-align: left; font-size: 14px; }
+ th { background: #f9fafb; font-weight: 600; }
+ .muted { color: #6b7280; font-size: 12px; }
+ .alert { background: #fef2f2; border: 1px solid #fecaca; border-radius: 6px; padding: 12px 14px; margin: 10px 0; }
+ .alert pre { font-size: 11px; margin: 6px 0 0; white-space: pre-wrap; }
+ .ok { background: #ecfdf5; border: 1px solid #047857; color: #047857; padding: 10px 14px; border-radius: 6px; font-size: 13px; }
+ </style>
+ </head><body>
+ <h1>Status</h1>
+ <p class="muted">Live snapshot · auto-refresh every 30s · ${new Date().toISOString().slice(0,19)}Z</p>
+
+ <table>
+ <thead><tr><th>Service</th><th>HTTP</th><th>Uptime</th><th>Restarts</th><th>pm2</th><th>Memory</th><th>CPU</th></tr></thead>
+ <tbody>
+ ${rowFor('butlr', 'butlr', butlrHealth)}
+ ${rowFor('wallco-ai', 'wallco-ai', wallcoHealth)}
+ </tbody>
+ </table>
+
+ <h2 style="font-size:18px;margin-top:24px">Recent alerts (last 5)</h2>
+ ${recentAlerts.length === 0
+ ? '<div class="ok">No alerts recorded.</div>'
+ : recentAlerts.map(a => `<div class="alert"><strong>${a.file.replace('ALERT-','').replace('.txt','')}</strong><pre>${a.body.replace(/</g, '<')}</pre></div>`).join('')}
+
+ <p class="muted" style="margin-top:24px"><a href="/admin">← Admin dashboard</a></p>
+ </body></html>`);
+});
+
router.get('/admin/orphan-recordings', requireAdmin, (req, res) => {
let orphans;
try { orphans = scanOrphans(); }
← 6db4767 sms: friendly GET handler on /twilio/sms-inbound (was 404 in
·
back to Butlr
·
Permissions-Policy: allow geolocation=(self) on wizard /new 0c6e41e →