Some checks are pending
build-and-push / image (push) Waiting to run
- 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.
129 lines
5.8 KiB
JavaScript
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 & 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,
|
|
});
|