/** * Admin > Monitoring > Health * * Polls GET /api/v1/admin/metrics and displays runtime, DB, cluster, * and extension metrics. Cluster panel only shows on PG multi-node. */ const { html } = window; const { useState, useEffect, useRef, useCallback } = hooks; // ── Formatting helpers ────────────────────────── function formatBytes(b) { if (b == null) return '\u2014'; if (b < 1024) return b + ' B'; if (b < 1024 * 1024) return (b / 1024).toFixed(1) + ' KB'; if (b < 1024 * 1024 * 1024) return (b / (1024 * 1024)).toFixed(1) + ' MB'; return (b / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; } function formatDuration(sec) { if (sec == null) return '\u2014'; if (sec < 60) return Math.floor(sec) + 's'; if (sec < 3600) return Math.floor(sec / 60) + 'm ' + Math.floor(sec % 60) + 's'; const h = Math.floor(sec / 3600); const m = Math.floor((sec % 3600) / 60); if (h < 24) return h + 'h ' + m + 'm'; const d = Math.floor(h / 24); return d + 'd ' + (h % 24) + 'h'; } function formatMs(ms) { if (ms == null) return '\u2014'; if (ms < 1) return '<1 ms'; if (ms < 1000) return ms.toFixed(1) + ' ms'; return (ms / 1000).toFixed(2) + ' s'; } function formatPct(v) { if (v == null) return '\u2014'; return v.toFixed(2) + '%'; } function formatNum(v) { if (v == null) return '\u2014'; if (typeof v === 'number') return v.toLocaleString(); return String(v); } // ── Stat card ─────────────────────────────────── function Stat({ label, value, hint }) { return html`
${label}${hint ? html`\u24D8` : ''}
${value}
`; } // ── Panels ────────────────────────────────────── function PanelHeader({ title }) { return html`

${title}

`; } function RuntimePanel({ m }) { return html`
<${PanelHeader} title="Runtime" />
<${Stat} label="Uptime" value=${formatDuration(m.uptime_sec)} /> <${Stat} label="Goroutines" value=${formatNum(m.goroutines)} /> <${Stat} label="Heap Alloc" value=${formatBytes(m.heap_alloc)} /> <${Stat} label="Heap Sys" value=${formatBytes(m.heap_sys)} /> <${Stat} label="Stack In Use" value=${formatBytes(m.stack_in_use)} /> <${Stat} label="GC Cycles" value=${formatNum(m.gc_cycles)} /> <${Stat} label="Last GC Pause" value=${formatMs((m.gc_pause_ns || 0) / 1e6)} /> <${Stat} label="GC CPU" value=${formatPct(m.gc_cpu_pct)} /> <${Stat} label="WS Clients" value=${formatNum(m.ws_clients)} /> <${Stat} label="Extensions" value=${formatNum(m.extensions_loaded)} /> <${Stat} label="Open FDs" value=${m.open_fds >= 0 ? formatNum(m.open_fds) : '\u2014'} />
`; } function DBPanel({ m }) { return html`
<${PanelHeader} title="Database" />
<${Stat} label="Latency" value=${formatMs(m.latency_ms)} /> <${Stat} label="Pool Active" value=${formatNum(m.pool_active)} /> <${Stat} label="Pool Idle" value=${formatNum(m.pool_idle)} /> <${Stat} label="Pool Max" value=${formatNum(m.pool_max)} /> <${Stat} label="Wait Count" value=${formatNum(m.wait_count)} /> <${Stat} label="Wait Duration" value=${formatMs(m.wait_duration_ms)} /> ${m.dead_tuples != null && html`<${Stat} label="Dead Tuples" value=${formatNum(m.dead_tuples)} />`} ${m.active_backends != null && html`<${Stat} label="Active Backends" value=${formatNum(m.active_backends)} />`}
`; } function ClusterPanel({ cluster }) { return html`
<${PanelHeader} title=${'Cluster \u2014 ' + cluster.size + ' node' + (cluster.size !== 1 ? 's' : '')} />
${cluster.nodes.map(n => html`<${NodeCard} key=${n.node_id} node=${n} />`)}
`; } function NodeCard({ node }) { const stats = node.stats || {}; const ageLabel = node.heartbeat_age_ms < 15000 ? Math.floor(node.heartbeat_age_ms / 1000) + 's ago' : Math.floor(node.heartbeat_age_ms / 60000) + 'm ago'; const ageColor = node.heartbeat_age_ms < 15000 ? 'var(--accent, #2a6)' : 'var(--warning, #c80)'; return html`
${node.node_id} ${ageLabel}
${node.endpoint && html`
${node.endpoint}
`}
<${Stat} label="Uptime" value=${formatDuration(stats.uptime_sec)} /> <${Stat} label="WS Clients" value=${formatNum(stats.ws_clients)} /> <${Stat} label="Goroutines" value=${formatNum(stats.goroutines)} /> <${Stat} label="Heap" value=${formatBytes(stats.heap_alloc)} /> <${Stat} label="GC Pause" value=${formatMs((stats.gc_pause_ns || 0) / 1e6)} /> <${Stat} label="GC Cycles" value=${formatNum(stats.gc_cycles)} />
`; } function ExtensionPanel({ m }) { return html`
<${PanelHeader} title="Extensions" />
<${Stat} label="Starlark Execs" value=${formatNum(m.starlark_exec_total)} /> <${Stat} label="Starlark Errors" value=${formatNum(m.starlark_errors_total)} /> <${Stat} label="Avg Duration" value=${formatMs(m.starlark_avg_duration_ms)} /> <${Stat} label="Trigger Fires" value=${formatNum(m.trigger_fires_total)} /> <${Stat} label="Events Published" value=${formatNum(m.event_bus_published)} hint="Total events emitted by the event bus. Not all events have active subscribers \u2014 published > delivered is normal (e.g. startup events fire before any clients connect)." /> <${Stat} label="Events Delivered" value=${formatNum(m.event_bus_delivered)} hint="Events that matched at least one subscriber and were dispatched. Lower than published when events fire with no listeners (e.g. during startup)." />
`; } // ── Main component ────────────────────────────── export default function HealthSection() { const [data, setData] = useState(null); const [error, setError] = useState(null); const [interval, setInterval_] = useState(10); const timerRef = useRef(null); const load = useCallback(async () => { try { const resp = await sw.api.admin.metrics(); setData(resp); setError(null); } catch (e) { setError(e.message); } }, []); useEffect(() => { load(); timerRef.current = setInterval(load, interval * 1000); return () => clearInterval(timerRef.current); }, [load, interval]); if (error && !data) { return html`
Failed to load metrics: ${error}
`; } if (!data) { return html`
Loading metrics\u2026
`; } return html`
${data.node_id && html`Viewing from ${data.node_id}`} Refresh
<${RuntimePanel} m=${data.runtime} /> <${DBPanel} m=${data.db} /> ${data.cluster && html`<${ClusterPanel} cluster=${data.cluster} />`} <${ExtensionPanel} m=${data.extensions} />
`; }