// Admin → System — in-depth host + per-app statistics. Live ring gauges for the
// headline numbers, SVG trend charts driven by the metrics history ring buffer,
// a Docker summary, and a per-app resource table. Data comes from the
// frontend/data/system/*.json files the webui_system_metrics generator refreshes
// every minute; this page just polls and draws. Renders into #config-section.
class AdminSystem {
constructor(rootId = 'config-section') {
this.rootId = rootId;
this.range = 60; // minutes of history to chart
this._timer = null;
this.d = {};
}
root() { return document.getElementById(this.rootId); }
async init() {
const r = this.root();
if (r) r.innerHTML = '
Loading system stats…
';
await this.refresh();
this.bind();
// Data regenerates ~1/min; poll at 30s so the view tracks it without
// hammering. Stop once the user has navigated off this page.
if (this._timer) clearInterval(this._timer);
this._timer = setInterval(() => {
if (!document.querySelector('.sys-page')) { clearInterval(this._timer); this._timer = null; return; }
this.refresh();
}, 30000);
}
async refresh() {
const [metrics, history, apps, appsHist, info] = await Promise.all([
this.fetchJson('/data/system/metrics.json'),
this.fetchJson('/data/system/metrics_history.json'),
this.fetchJson('/data/system/metrics_apps.json'),
this.fetchJson('/data/system/metrics_apps_history.json'),
this.fetchJson('/data/system/system_info.json')
]);
this.d = { metrics, history, apps, appsHist, info };
this.render();
}
async fetchJson(url) {
try { const r = await fetch(`${url}?t=${Date.now()}`); if (!r.ok) return null; return await r.json(); }
catch { return null; }
}
bind() {
if (this._bound) return;
this._bound = true;
document.addEventListener('click', (e) => {
const rb = e.target.closest('[data-sys-range]');
if (rb && document.querySelector('.sys-page')) {
this.range = parseInt(rb.dataset.sysRange) || 60;
this.render();
}
});
}
/* ---- formatting helpers ---- */
escape(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }
bytes(n) {
n = Number(n) || 0;
const u = ['B', 'KB', 'MB', 'GB', 'TB']; let i = 0;
while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; }
return `${n.toFixed(i ? 1 : 0)} ${u[i]}`;
}
rate(n) { return `${this.bytes(n)}/s`; }
// Last N minutes of a given key from the history ring buffer.
series(key) {
const pts = (this.d.history && Array.isArray(this.d.history.points)) ? this.d.history.points : [];
return pts.slice(-this.range).map(p => Number(p[key]) || 0);
}
rangeBtns() {
const opts = [[60, '1h'], [360, '6h'], [1440, '24h']];
return `