This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/src/js/sw/surfaces/admin/health.js
Jeffrey Smith 7915d84c8b
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m47s
CI/CD / test-sqlite (push) Successful in 3m1s
CI/CD / build-and-deploy (push) Successful in 29s
Feat v0.6.6 final hardening (#41)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-03-31 17:40:40 +00:00

210 lines
10 KiB
JavaScript

/**
* 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`
<div style="padding:8px 12px;background:var(--bg-secondary, #f5f5f5);border-radius:6px;min-width:120px;" title=${hint || ''}>
<div style="font-size:11px;color:var(--text-muted, #888);margin-bottom:2px;">
${label}${hint ? html`<span style="margin-left:3px;cursor:help;opacity:0.5;" title=${hint}>\u24D8</span>` : ''}
</div>
<div style="font-size:16px;font-weight:600;font-variant-numeric:tabular-nums;">${value}</div>
</div>
`;
}
// ── Panels ──────────────────────────────────────
function PanelHeader({ title }) {
return html`<h3 style="margin:0 0 12px;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--text-muted, #888);">${title}</h3>`;
}
function RuntimePanel({ m }) {
return html`
<div style="margin-bottom:24px;">
<${PanelHeader} title="Runtime" />
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px;">
<${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'} />
</div>
</div>
`;
}
function DBPanel({ m }) {
return html`
<div style="margin-bottom:24px;">
<${PanelHeader} title="Database" />
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px;">
<${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)} />`}
</div>
</div>
`;
}
function ClusterPanel({ cluster }) {
return html`
<div style="margin-bottom:24px;">
<${PanelHeader} title=${'Cluster \u2014 ' + cluster.size + ' node' + (cluster.size !== 1 ? 's' : '')} />
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px;">
${cluster.nodes.map(n => html`<${NodeCard} key=${n.node_id} node=${n} />`)}
</div>
</div>
`;
}
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`
<div style="border:1px solid var(--border, #ddd);border-radius:8px;padding:12px;background:var(--bg-primary, #fff);overflow:hidden;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;gap:8px;">
<strong style="font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;" title=${node.node_id}>${node.node_id}</strong>
<span style="font-size:11px;color:${ageColor};font-weight:600;flex-shrink:0;">${ageLabel}</span>
</div>
${node.endpoint && html`<div style="font-size:11px;color:var(--text-muted, #888);margin-bottom:8px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${node.endpoint}</div>`}
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px;overflow:hidden;">
<${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)} />
</div>
</div>
`;
}
function ExtensionPanel({ m }) {
return html`
<div style="margin-bottom:24px;">
<${PanelHeader} title="Extensions" />
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px;">
<${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)." />
</div>
</div>
`;
}
// ── 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`<div class="settings-placeholder" style="color:var(--error,red);">Failed to load metrics: ${error}</div>`;
}
if (!data) {
return html`<div class="settings-placeholder">Loading metrics\u2026</div>`;
}
return html`
<div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;gap:8px;">
${data.node_id && html`<span style="font-size:11px;color:var(--text-muted, #888);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;" title=${data.node_id}>Viewing from <strong style="color:var(--text-primary, #ccc);">${data.node_id}</strong></span>`}
<span style="font-size:11px;color:var(--text-muted, #888);flex-shrink:0;">Refresh</span>
<select style="font-size:12px;padding:2px 6px;" value=${interval} onChange=${e => setInterval_(+e.target.value)}>
<option value="5">5s</option>
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">60s</option>
</select>
</div>
<${RuntimePanel} m=${data.runtime} />
<${DBPanel} m=${data.db} />
${data.cluster && html`<${ClusterPanel} cluster=${data.cluster} />`}
<${ExtensionPanel} m=${data.extensions} />
</div>
`;
}