@@ -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, '&' )
1505+ . replace ( / < / g, '<' )
1506+ . replace ( / > / g, '>' ) ;
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