CxWebApp/static/js/panes/system.js
CxAI Agent 75153b7fe9
Some checks are pending
build-and-push / image (push) Waiting to run
feat(cxwebapp): comprehensive pane enhancements
- All 14 panes rewritten with rich controls: KPIs, sparklines, meter
  gauges, presets, history sidebars, search/filter, bulk actions,
  schema-driven forms, copy buttons, auto-refresh.
- Backend /api/system enriched with cpu_count, rss_bytes, mem_total,
  mem_used_pct, loadavg[], user/system CPU times, max_rss_kb.
- CSS additions: .meter/.gauge, .seg, .chip(s), .spark, .slider, .dl,
  .link-card, details.card-d plus utility classes.
- ui.js helpers: copyToClipboard, withCopy/bindCopyButtons, sparkline,
  meterRow, fmtBytes/fmtNum, historyStore, inputFromSchema,
  collectSchemaForm.
- Files pane added; iCloud duplicate '*2' files removed.
2026-05-17 09:36:19 -05:00

129 lines
5.8 KiB
JavaScript

// 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 = `
<div class="pane-head">
<div><div class="title">System</div><div class="sub">Process, host and runtime introspection.</div></div>
<div class="grow"></div>
<label class="check"><input type="checkbox" id="sys-live" checked/> live (5s)</label>
<button class="btn btn-secondary" id="sys-refresh">Refresh</button>
</div>
<div class="grid cols-4 mb-3" id="sys-kpis"></div>
<div class="grid cols-2 mb-3">
<div class="card">
<div class="card-title"><h2>Resources</h2><span class="muted xsmall" id="sys-cpu-ts">—</span></div>
<div id="sys-gauges"></div>
<div class="divider"></div>
<div class="muted xsmall mb-1">Resident memory (last 60 samples)</div>
<div id="sys-spark-rss"></div>
<div class="muted xsmall mt-2 mb-1">CPU load (1m)</div>
<div id="sys-spark-cpu"></div>
</div>
<div class="card">
<div class="card-title"><h2>Build &amp; identity</h2></div>
<dl class="dl" id="sys-version"></dl>
<div class="divider"></div>
<div class="card-title"><h2 style="font-size:14px">Configured services</h2></div>
<div id="sys-services"></div>
</div>
</div>
<div class="grid cols-2">
<div class="card">
<div class="card-title"><h2>Raw /api/system</h2><button class="btn btn-ghost" data-copy="sys-s">Copy</button></div>
<pre id="sys-s" class="code" style="max-height:36vh">…</pre>
</div>
<div class="card">
<div class="card-title"><h2>Raw /api/version</h2><button class="btn btn-ghost" data-copy="sys-v">Copy</button></div>
<pre id="sys-v" class="code" style="max-height:36vh">…</pre>
</div>
</div>
`;
const rssHistory = [];
const cpuHistory = [];
let timer = null;
function kpi(label, value, sub) {
return `<div class="kpi"><div class="label">${escapeHtml(label)}</div><div class="value">${escapeHtml(value)}</div><div class="sub">${escapeHtml(sub || '')}</div></div>`;
}
function row(k, v) { return `<dt>${escapeHtml(k)}</dt><dd>${escapeHtml(v ?? '—')}</dd>`; }
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 => `
<div class="row">
<span class="pill ${x.configured ? 'ok' : 'muted'}"><span class="dot"></span>${escapeHtml(x.service)}</span>
<div class="grow muted small">${escapeHtml(x.prefix)}</div>
<span class="muted mono small">${x.configured ? `${escapeHtml(x.upstream_scheme || '')}://${escapeHtml(x.upstream_host || '')}:${x.upstream_port || ''}` : 'not configured'}</span>
</div>
`).join('') || '<div class="muted xsmall p-2">no services exposed</div>';
}
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,
});