// 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

Raw /api/system

Raw /api/version

`; 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, });