/**
* 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
setInterval_(+e.target.value)}>
5s
10s
30s
60s
<${RuntimePanel} m=${data.runtime} />
<${DBPanel} m=${data.db} />
${data.cluster && html`<${ClusterPanel} cluster=${data.cluster} />`}
<${ExtensionPanel} m=${data.extensions} />
`;
}