Skip to content

Commit ad651e7

Browse files
committed
feat: add deacon health dashboard overlay
1 parent 5c5fe51 commit ad651e7

1 file changed

Lines changed: 176 additions & 0 deletions

File tree

client/dashboard.js

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ class Dashboard {
123123
<button class="dashboard-topbar-btn" id="dashboard-open-performance" title="Per-terminal resource usage">⚙ Perf</button>
124124
<button class="dashboard-topbar-btn" id="dashboard-open-polecats" title="Manage sessions (restart/kill/logs)">🐾 Polecats</button>
125125
<button class="dashboard-topbar-btn" id="dashboard-open-hooks" title="Hook browser (automations/webhooks)">🪝 Hooks</button>
126+
<button class="dashboard-topbar-btn" id="dashboard-open-deacon" title="Deacon monitor (health dashboard)">🛡 Deacon</button>
126127
<button class="dashboard-topbar-btn" id="dashboard-open-tests" title="Run tests across worktrees">🧪 Tests</button>
127128
<button class="dashboard-topbar-btn" id="dashboard-export-telemetry" title="Download telemetry CSV export">⬇ Export</button>
128129
<button class="dashboard-topbar-btn" id="dashboard-export-telemetry-json" title="Download telemetry JSON export">⬇ JSON</button>
@@ -224,6 +225,10 @@ class Dashboard {
224225
e.preventDefault();
225226
this.showHooksOverlay().catch(() => {});
226227
});
228+
document.getElementById('dashboard-open-deacon')?.addEventListener('click', (e) => {
229+
e.preventDefault();
230+
this.showDeaconOverlay().catch(() => {});
231+
});
227232
document.getElementById('dashboard-open-polecats-card')?.addEventListener('click', (e) => {
228233
e.preventDefault();
229234
this.showPolecatOverlay().catch(() => {});
@@ -1430,6 +1435,177 @@ class Dashboard {
14301435
});
14311436
}
14321437

1438+
async showDeaconOverlay() {
1439+
const existing = document.getElementById('dashboard-deacon-overlay');
1440+
if (existing) {
1441+
existing.classList.remove('hidden');
1442+
return;
1443+
}
1444+
1445+
const overlay = document.createElement('div');
1446+
overlay.id = 'dashboard-deacon-overlay';
1447+
overlay.className = 'dashboard-telemetry-overlay';
1448+
overlay.innerHTML = `
1449+
<div class="dashboard-telemetry-panel" role="dialog" aria-label="Deacon monitor">
1450+
<div class="dashboard-telemetry-header">
1451+
<div class="dashboard-telemetry-title">Deacon — Health</div>
1452+
<button class="dashboard-topbar-btn" id="dashboard-deacon-close" title="Close (Esc)">✕</button>
1453+
</div>
1454+
<div class="dashboard-telemetry-controls">
1455+
<div class="dashboard-telemetry-actions">
1456+
<button class="btn-secondary" type="button" id="dashboard-deacon-refresh">Refresh</button>
1457+
</div>
1458+
</div>
1459+
<div id="dashboard-deacon-body" class="dashboard-telemetry-body">Loading…</div>
1460+
</div>
1461+
`;
1462+
1463+
document.body.appendChild(overlay);
1464+
1465+
const close = () => this.hideDeaconOverlay();
1466+
overlay.addEventListener('click', (e) => {
1467+
if (e.target === overlay) close();
1468+
});
1469+
overlay.querySelector('#dashboard-deacon-close')?.addEventListener('click', close);
1470+
overlay.querySelector('#dashboard-deacon-refresh')?.addEventListener('click', () => {
1471+
this.loadDeaconDetails().catch(() => {});
1472+
});
1473+
1474+
const onKey = (e) => {
1475+
if (e.key !== 'Escape') return;
1476+
const el = document.getElementById('dashboard-deacon-overlay');
1477+
if (!el || el.classList.contains('hidden')) return;
1478+
close();
1479+
};
1480+
overlay._escHandler = onKey;
1481+
document.addEventListener('keydown', onKey);
1482+
1483+
await this.loadDeaconDetails();
1484+
}
1485+
1486+
hideDeaconOverlay() {
1487+
const overlay = document.getElementById('dashboard-deacon-overlay');
1488+
if (!overlay) return;
1489+
overlay.classList.add('hidden');
1490+
const handler = overlay._escHandler;
1491+
if (handler) {
1492+
document.removeEventListener('keydown', handler);
1493+
overlay._escHandler = null;
1494+
}
1495+
overlay.remove();
1496+
}
1497+
1498+
async loadDeaconDetails() {
1499+
const bodyEl = document.getElementById('dashboard-deacon-body');
1500+
if (!bodyEl) return;
1501+
bodyEl.textContent = 'Loading…';
1502+
1503+
const escapeHtml = (value) => String(value ?? '')
1504+
.replace(/&/g, '&amp;')
1505+
.replace(/</g, '&lt;')
1506+
.replace(/>/g, '&gt;');
1507+
1508+
const check = async (path) => {
1509+
const started = performance.now();
1510+
try {
1511+
const res = await fetch(path);
1512+
const ms = Math.round(performance.now() - started);
1513+
const ok = !!res.ok;
1514+
return { path, ok, ms, status: res.status };
1515+
} catch (err) {
1516+
const ms = Math.round(performance.now() - started);
1517+
return { path, ok: false, ms, error: String(err?.message || err) };
1518+
}
1519+
};
1520+
1521+
const endpoints = [
1522+
'/api/user-settings',
1523+
'/api/workspaces',
1524+
'/api/process/status',
1525+
'/api/process/performance',
1526+
'/api/activity?limit=1'
1527+
];
1528+
1529+
const results = await Promise.all(endpoints.map(check));
1530+
1531+
let perf = null;
1532+
try {
1533+
const res = await fetch('/api/process/performance');
1534+
perf = res && res.ok ? await res.json().catch(() => null) : null;
1535+
} catch {
1536+
perf = null;
1537+
}
1538+
1539+
let activity = null;
1540+
try {
1541+
const res = await fetch('/api/activity?limit=200');
1542+
activity = res && res.ok ? await res.json().catch(() => null) : null;
1543+
} catch {
1544+
activity = null;
1545+
}
1546+
1547+
const events = Array.isArray(activity?.events) ? activity.events : [];
1548+
const isErrorEvent = (ev) => {
1549+
const kind = String(ev?.kind || '');
1550+
const data = ev?.data && typeof ev.data === 'object' ? ev.data : {};
1551+
if (data.ok === false) return true;
1552+
if (kind.includes('failed')) return true;
1553+
if (kind.includes('.error')) return true;
1554+
if (kind.endsWith('.failed')) return true;
1555+
if (kind.includes('close.failed')) return true;
1556+
return false;
1557+
};
1558+
const errors = events.filter(isErrorEvent).slice(0, 20);
1559+
1560+
const fmtBytes = (b) => {
1561+
const n = Number(b);
1562+
if (!Number.isFinite(n) || n < 0) return '—';
1563+
const mb = n / (1024 * 1024);
1564+
if (mb < 1024) return `${mb.toFixed(1)} MB`;
1565+
return `${(mb / 1024).toFixed(2)} GB`;
1566+
};
1567+
1568+
const node = perf?.node || {};
1569+
const uptime = Number(node?.uptimeSeconds || 0);
1570+
const rss = fmtBytes(node?.rssBytes);
1571+
1572+
const rows = results.map((r) => {
1573+
const cls = r.ok ? 'process-chip ok' : 'process-chip danger';
1574+
const statusText = r.ok ? `HTTP ${r.status}` : (r.error ? `ERR ${r.error}` : 'ERR');
1575+
return `
1576+
<tr>
1577+
<td class="mono">${escapeHtml(r.path)}</td>
1578+
<td><span class="${cls}">${r.ok ? 'ok' : 'fail'}</span></td>
1579+
<td class="mono">${escapeHtml(String(r.ms))}ms</td>
1580+
<td class="mono" style="opacity:0.85;">${escapeHtml(statusText)}</td>
1581+
</tr>
1582+
`;
1583+
}).join('');
1584+
1585+
const errorRows = errors.map((e) => {
1586+
const t = escapeHtml(String(e?.time || e?.ts || ''));
1587+
const kind = escapeHtml(String(e?.kind || ''));
1588+
const data = escapeHtml(JSON.stringify(e?.data || {}));
1589+
return `<div style="margin:6px 0;"><span class="mono" style="opacity:0.85;">${t}</span> • <code>${kind}</code><div class="mono" style="opacity:0.8; margin-top:4px;">${data}</div></div>`;
1590+
}).join('');
1591+
1592+
bodyEl.innerHTML = `
1593+
<div class="dashboard-telemetry-muted">Node RSS: <code>${escapeHtml(rss)}</code> • Uptime: <code>${escapeHtml(String(uptime))}s</code></div>
1594+
<table class="worktree-inspector-table" style="margin-top:10px;">
1595+
<thead>
1596+
<tr><th>Endpoint</th><th>Status</th><th>Latency</th><th>Detail</th></tr>
1597+
</thead>
1598+
<tbody>
1599+
${rows}
1600+
</tbody>
1601+
</table>
1602+
<div style="margin-top:14px;">
1603+
<div style="font-weight:600; margin-bottom:6px;">Recent errors (Activity)</div>
1604+
${errorRows || '<div style="opacity:0.8;">No recent error events.</div>'}
1605+
</div>
1606+
`;
1607+
}
1608+
14331609
hidePerformanceOverlay() {
14341610
const overlay = document.getElementById('dashboard-performance-overlay');
14351611
if (!overlay) return;

0 commit comments

Comments
 (0)