// panes/system.js — process & host introspection with live sparklines.
import { registerPane } from '../app.js';
import { jget, formatUptime, escapeHtml } from '../lib/api.js';
import { fmtJSON, fmtBytes, meterRow, sparkline, copyToClipboard, ok } from '../lib/ui.js';
const TPL = `
System
Process, host and runtime introspection.
Resources
—
Resident memory (last 60 samples)
CPU load (1m)
Build & identity
Configured services
`;
const rssHistory = [];
const cpuHistory = [];
let timer = null;
function kpi(label, value, sub) {
return `${escapeHtml(label)}
${escapeHtml(value)}
${escapeHtml(sub || '')}
`;
}
function row(k, v) { return `${escapeHtml(k)}${escapeHtml(v ?? '—')}`; }
function memState(pct) { return pct > 85 ? 'err' : pct > 60 ? 'warn' : 'ok'; }
async function load(host) {
let v = {}, s = {}, svc = { services: [] };
try { [v, s, svc] = await Promise.all([jget('/api/version'), jget('/api/system'), jget('/api/services').catch(() => ({ services: [] }))]); }
catch (e) { host.querySelector('#sys-s').textContent = 'error: ' + e.message; return; }
host.querySelector('#sys-v').textContent = fmtJSON(v);
host.querySelector('#sys-s').textContent = fmtJSON(s);
// KPIs
host.querySelector('#sys-kpis').innerHTML = [
kpi('Uptime', formatUptime(s.uptime_seconds), `pid ${s.pid ?? '—'}`),
kpi('Memory', fmtBytes(s.rss_bytes), s.mem_used_pct != null ? `${s.mem_used_pct.toFixed(1)}% of host` : 'resident set'),
kpi('CPUs', String(s.cpu_count || '—'), `load ${(s.loadavg?.[0] ?? 0).toFixed(2)} / ${(s.loadavg?.[1] ?? 0).toFixed(2)} / ${(s.loadavg?.[2] ?? 0).toFixed(2)}`),
kpi('Host', s.hostname || '—', `${s.sysname || ''} ${s.release || ''}`.trim()),
].join('');
// sparkline history
rssHistory.push(+s.rss_bytes || 0); while (rssHistory.length > 60) rssHistory.shift();
cpuHistory.push(+s.loadavg?.[0] || 0); while (cpuHistory.length > 60) cpuHistory.shift();
host.querySelector('#sys-spark-rss').innerHTML = sparkline(rssHistory);
host.querySelector('#sys-spark-cpu').innerHTML = sparkline(cpuHistory);
// gauges
const cpus = s.cpu_count || 1;
const loadPct = Math.min(100, (+s.loadavg?.[0] || 0) / cpus * 100);
const memPct = +s.mem_used_pct || 0;
host.querySelector('#sys-gauges').innerHTML = [
meterRow('Memory', memPct, `${memPct.toFixed(1)}%`, memState(memPct)),
meterRow('Load 1m', loadPct, (+s.loadavg?.[0] || 0).toFixed(2), loadPct > 80 ? 'err' : loadPct > 50 ? 'warn' : 'ok'),
meterRow('User CPU', Math.min(100, (s.user_cpu_s || 0) / Math.max(1, s.uptime_seconds) * 100), `${(s.user_cpu_s || 0).toFixed(1)}s`),
meterRow('Sys CPU', Math.min(100, (s.system_cpu_s || 0) / Math.max(1, s.uptime_seconds) * 100), `${(s.system_cpu_s || 0).toFixed(1)}s`),
].join('');
host.querySelector('#sys-cpu-ts').textContent = new Date().toLocaleTimeString();
// version dl
host.querySelector('#sys-version').innerHTML =
row('Name', v.name) +
row('Version', v.version) +
row('Git SHA', v.git_sha) +
row('Build time', v.build_time) +
row('C++ standard', s.cxx_standard) +
row('Architecture', s.machine) +
row('Total memory', fmtBytes(s.mem_total));
// configured services
host.querySelector('#sys-services').innerHTML = (svc.services || []).map(x => `
${escapeHtml(x.service)}
${escapeHtml(x.prefix)}
${x.configured ? `${escapeHtml(x.upstream_scheme || '')}://${escapeHtml(x.upstream_host || '')}:${x.upstream_port || ''}` : 'not configured'}
`).join('') || 'no services exposed
';
}
registerPane('system', {
label: 'System',
init(host) {
host.innerHTML = TPL;
host.querySelector('#sys-refresh').addEventListener('click', () => load(host));
host.querySelectorAll('[data-copy]').forEach(b =>
b.addEventListener('click', () => copyToClipboard(host.querySelector('#' + b.dataset.copy)?.textContent || ''))
);
load(host);
timer = setInterval(() => {
if (!host.classList.contains('active')) return;
if (!host.querySelector('#sys-live')?.checked) return;
load(host);
}, 5000);
},
refresh: load,
});