// Admin → System → Metric — single-metric deep-dive PAGE.
//
// Replaces the previous fullscreen overlay (system-detail.js) with a real
// in-flow page mounted at /admin/system/metric/<key>. Bookmarkable,
// browser-back navigates, refresh keeps you here.
//
// Renders into #config-section as a full-width admin page. Owns its own
// DOM, range picker, live SSE subscription, and hover/scrub interactions.
//
// Mount lifecycle:
//   const p = new SystemMetricPage('config-section');
//   await p.mount('cpu');
//   ...
//   p.dispose();   // tears down SSE sub + global listeners

class SystemMetricPage {
    constructor(rootId = 'config-section') {
        this.rootId = rootId;
        this.metricKey = null;
        this.metric = null;
        this.rangeMin = 360;     // default 6h
        this.points = [];
        this.tier = '1m';
        this.hoverIdx = -1;
        this._rafHover = null;
        this._lpPct = null;      // disk only: LibrePortal's share of the disk (%)
        this._unsubLive = null;
        this._onKey = this._onKey.bind(this);
        this._onResize = this._onResize.bind(this);
        this._onClick = this._onClick.bind(this);
        this._onPointerMove = this._onPointerMove.bind(this);
        this._onPointerLeave = this._onPointerLeave.bind(this);
    }

    root() { return document.getElementById(this.rootId); }

    async mount(key) {
        this.metricKey = key;
        this.metric = this._metricDef(key);
        if (!this.metric) {
            this._renderUnknown();
            return;
        }
        // Optional ?range= override on the URL.
        const params = new URLSearchParams(window.location.search);
        const r = parseInt(params.get('range'), 10);
        if (Number.isFinite(r) && r > 0) this.rangeMin = Math.min(10080, Math.max(1, r));

        this._renderShell();
        this._bind();
        await this._loadRange();
        this._attachLive();
        this._loadLibrePortalShare();
    }

    // Disk page only: compute LibrePortal's current share of the disk so the
    // chart can mark it with a reference line beside the disk-usage trend.
    // It's a "now" value (app_storage + Docker df ÷ disk total) — there's no
    // historical LP series, so the line is flat across the range.
    async _loadLibrePortalShare() {
        if (this.metricKey !== 'disk') return;
        try {
            const [app, dock, metrics] = await Promise.all([
                fetch(`/data/system/app_storage.json?t=${Date.now()}`).then(r => r.ok ? r.json() : null).catch(() => null),
                fetch('/api/system/storage').then(r => r.ok ? r.json() : null).catch(() => null),
                fetch(`/data/system/metrics.json?t=${Date.now()}`).then(r => r.ok ? r.json() : null).catch(() => null),
            ]);
            const lpBytes = ((app && app.total_local) || 0) + ((dock && dock.total) || 0);
            const disks = (metrics && Array.isArray(metrics.disks)) ? metrics.disks : [];
            const root = disks.find(d => d.mount === '/') || disks[0];
            const diskTotal = root ? Number(root.total) || 0 : 0;
            this._lpPct = (diskTotal > 0 && lpBytes > 0) ? (lpBytes / diskTotal) * 100 : null;
            this._lpBytes = lpBytes;
            this._renderChart();
        } catch (_) { this._lpPct = null; }
    }

    dispose() {
        if (this._unsubLive) { try { this._unsubLive(); } catch (_) {} this._unsubLive = null; }
        document.removeEventListener('keydown', this._onKey);
        window.removeEventListener('resize', this._onResize);
        // Click handler is delegated on root and dies with the DOM swap.
        if (this._rafHover) cancelAnimationFrame(this._rafHover);
    }

    _onKey(e) {
        if (e.key === 'Escape' && window.navigateToRoute) {
            window.navigateToRoute('/admin/system');
        }
    }
    _onResize() { this._renderChart(); }

    _bind() {
        document.addEventListener('keydown', this._onKey);
        window.addEventListener('resize', this._onResize);
        const r = this.root();
        if (r) r.addEventListener('click', this._onClick);
    }

    _onClick(e) {
        const rb = e.target.closest('[data-rng]');
        if (rb) {
            const r = parseInt(rb.dataset.rng, 10) || 360;
            this.rangeMin = r;
            // Reflect in URL so refresh / share preserves the choice.
            const url = new URL(window.location.href);
            url.searchParams.set('range', String(r));
            window.history.replaceState({}, '', url.toString());
            this._loadRange();
            return;
        }
        const back = e.target.closest('[data-back]');
        if (back && window.navigateToRoute) {
            window.navigateToRoute('/admin/system');
            return;
        }
        const go = e.target.closest('[data-go-storage]');
        if (go && window.navigateToRoute) {
            window.navigateToRoute('/admin/system/storage');
        }
    }

    // Metric registry — keys map to label, unit, formatter, accent. Mirrors
    // the same list the index view exposes; lives here so the metric page
    // can mount cleanly without SystemPage present (direct URL load).
    _metricDef(key) {
        const fmt = window.SystemFmt || { bytes: (n) => `${n}`, rate: (n) => `${n}/s`, rgbVar: () => '0, 212, 255' };
        const pct = (v) => `${(v ?? 0).toFixed(1)}%`;
        const rateFmt = (v) => fmt.rate(v);
        const loadFmt = (v) => (v ?? 0).toFixed(2);
        const all = {
            cpu:    { key: 'cpu',    label: 'CPU usage',         unit: '%', max: 100, fmt: pct,     accent: 'accent' },
            mem:    { key: 'mem',    label: 'Memory usage',      unit: '%', max: 100, fmt: pct,     accent: 'status-info' },
            swap:   { key: 'swap',   label: 'Swap usage',        unit: '%', max: 100, fmt: pct,     accent: 'status-warning' },
            disk:   { key: 'disk',   label: 'Disk usage',        unit: '%', max: 100, fmt: pct,     accent: 'status-warning' },
            load1:  { key: 'load1',  label: 'Load average (1m)',                          fmt: loadFmt, accent: 'accent' },
            net_rx: { key: 'net_rx', label: 'Network — receive',                          fmt: rateFmt, accent: 'status-success' },
            net_tx: { key: 'net_tx', label: 'Network — transmit',                         fmt: rateFmt, accent: 'accent' },
        };
        const m = all[key];
        if (!m) return null;
        m.accentRgb = fmt.rgbVar(m.accent);
        return m;
    }

    _renderUnknown() {
        const root = this.root();
        if (!root) return;
        root.innerHTML = `
            <div class="admin-page sys-metric-page">
                <div class="page-header config-page-header">
                    <div class="page-header-title">
                        <div class="admin-breadcrumb">
                            <a href="/admin/system" data-back>Admin · System</a>
                        </div>
                        <h1>Unknown metric</h1>
                        <p>No metric registered under "${(window.SystemFmt?.escape || String)(this.metricKey)}".</p>
                    </div>
                </div>
            </div>`;
    }

    _renderShell() {
        const root = this.root();
        if (!root) return;
        const fmt = window.SystemFmt;
        root.innerHTML = `
            <div class="admin-page sys-metric-page" style="--metric-rgb:${this.metric.accentRgb}">
                <div class="page-header config-page-header">
                    <div class="page-header-title">
                        <div class="admin-breadcrumb">
                            <a href="/admin/system" data-back>Admin · System</a>
                        </div>
                        <h1 class="sys-metric-name">${fmt.escape(this.metric.label)}</h1>
                        <p class="sys-metric-sub">${fmt.escape(this._subline())}</p>
                    </div>
                    <div class="sys-metric-actions">
                        <div class="sys-detail-range" role="tablist" aria-label="Time range">
                            ${[[60,'1h'],[360,'6h'],[1440,'24h'],[10080,'7d']]
                                .map(([m,l]) => `<button type="button" role="tab" class="sys-detail-range-btn ${this.rangeMin===m?'active':''}" data-rng="${m}">${l}</button>`).join('')}
                        </div>
                        ${this.metricKey === 'disk' ? `<button type="button" class="sys-storage-cta" data-go-storage title="Open the full storage breakdown">
                            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21.21 15.89A10 10 0 1 1 8 2.83"/><path d="M22 12A10 10 0 0 0 12 2v10z"/></svg>
                            View storage breakdown
                        </button>` : ''}
                    </div>
                </div>
                <section class="sys-detail-stats">
                    ${['now','peak','avg','min'].map(k =>
                        `<div class="sys-detail-stat" data-stat="${k}">
                            <span class="sys-detail-stat-k">${k}</span>
                            <strong class="sys-detail-stat-v">—</strong>
                            <span class="sys-detail-stat-t"></span>
                        </div>`).join('')}
                </section>
                <section class="sys-metric-canvas">
                    <div class="sys-detail-empty" hidden>No samples in this range yet — check back in a minute.</div>
                    <div class="sys-detail-loading"><span class="lp-loading-spinner" aria-hidden="true"></span><span>Loading history…</span></div>
                    <svg class="sys-detail-svg" preserveAspectRatio="none" aria-hidden="true"></svg>
                    <div class="sys-detail-tooltip" hidden></div>
                </section>
                <footer class="sys-detail-foot">
                    <span class="sys-detail-foot-meta"></span>
                    <span class="sys-detail-foot-hint">Esc returns to System · hover the chart to scrub · range tier auto-selects</span>
                </footer>
            </div>`;
        const canvas = root.querySelector('.sys-metric-canvas');
        if (canvas) {
            canvas.addEventListener('pointermove', this._onPointerMove);
            canvas.addEventListener('pointerleave', this._onPointerLeave);
        }
    }

    // Plain-language description of what the page shows, per metric.
    _subline() {
        const descs = {
            cpu:    'Processor load over time, across all cores.',
            mem:    'Memory in use over time, as a share of total RAM.',
            swap:   'Swap in use over time, as a share of total swap.',
            disk:   'Root-filesystem usage over time.',
            load1:  'System load average (1-minute) over time.',
            net_rx: 'Inbound network throughput over time.',
            net_tx: 'Outbound network throughput over time.',
        };
        return (this.metric && descs[this.metric.key]) || 'Live history over time.';
    }

    async _loadRange() {
        const root = this.root();
        if (!root) return;
        const loading = root.querySelector('.sys-detail-loading');
        const empty = root.querySelector('.sys-detail-empty');
        if (empty) empty.hidden = true;
        if (loading) loading.hidden = false;
        // Range button active states.
        for (const b of root.querySelectorAll('.sys-detail-range-btn')) {
            b.classList.toggle('active', parseInt(b.dataset.rng, 10) === this.rangeMin);
        }
        try {
            const r = await fetch(`/api/system/history?range=${this.rangeMin}&keys=${encodeURIComponent(this.metric.key)}`, { credentials: 'same-origin' });
            const j = await r.json().catch(() => ({}));
            this.points = Array.isArray(j?.points) ? j.points : [];
            this.tier = j?.tier || (this.rangeMin > 1440 ? '5m' : '1m');
        } catch (_) {
            this.points = [];
        }
        if (loading) loading.hidden = true;
        this._renderChart();
        this._renderStats();
        this._renderFootMeta();
    }

    _attachLive() {
        if (this._unsubLive) { try { this._unsubLive(); } catch (_) {} this._unsubLive = null; }
        if (!window.LiveSystem) return;
        this._unsubLive = window.LiveSystem.subscribe((s) => this._applyLive(s));
    }

    _applyLive(s) {
        if (!s || !this.metric) return;
        const v = this._extractLive(s);
        if (v == null) return;
        const t = Math.floor((s.t || Date.now()) / 1000);
        if (this.points.length && this.points[this.points.length - 1].__live) {
            this.points[this.points.length - 1] = { t, [this.metric.key]: v, __live: true };
        } else {
            this.points.push({ t, [this.metric.key]: v, __live: true });
        }
        this._renderChart();
        this._renderStats(true);
    }

    _extractLive(s) {
        const k = this.metric.key;
        if (k === 'cpu')    return Number(s?.cpu?.percent) || 0;
        if (k === 'mem')    return Number(s?.memory?.percent) || 0;
        if (k === 'swap')   return Number(s?.memory?.swap_percent) || 0;
        if (k === 'load1')  return Number(s?.cpu?.load1) || 0;
        if (k === 'disk') {
            const ds = Array.isArray(s?.disks) ? s.disks : [];
            const r = ds.find(d => d.mount === '/') || ds[0];
            return r ? Number(r.percent) || 0 : null;
        }
        if (k === 'net_rx') return Number(s?.network?.rx_rate) || 0;
        if (k === 'net_tx') return Number(s?.network?.tx_rate) || 0;
        return null;
    }

    _renderStats(liveOnly = false) {
        const root = this.root();
        if (!root) return;
        const m = this.metric;
        const vals = this.points.map(p => Number(p[m.key]) || 0);
        const fmt = m.fmt;
        const stats = this._computeStats(vals);
        const now = vals[vals.length - 1] ?? 0;
        const nowAt = this.points.length ? this.points[this.points.length - 1].t : null;
        const peakAt = stats.peakIdx >= 0 ? this.points[stats.peakIdx]?.t : null;
        const minAt = stats.minIdx >= 0 ? this.points[stats.minIdx]?.t : null;
        const set = (k, v, t) => {
            const card = root.querySelector(`[data-stat="${k}"]`);
            if (!card) return;
            card.querySelector('.sys-detail-stat-v').textContent = vals.length ? fmt(v) : '—';
            card.querySelector('.sys-detail-stat-t').textContent = t ? window.SystemFmt.timeAgo(t) : '';
        };
        set('now', now, nowAt);
        if (!liveOnly) {
            set('peak', stats.peak, peakAt);
            set('avg',  stats.avg,  null);
            set('min',  stats.min,  minAt);
        }
    }

    _renderFootMeta() {
        const root = this.root();
        if (!root) return;
        const meta = root.querySelector('.sys-detail-foot-meta');
        if (!meta) return;
        const n = this.points.length;
        const span = n > 1 ? (this.points[n - 1].t - this.points[0].t) : 0;
        meta.textContent = n
            ? `${n} samples · tier ${this.tier} · spans ${this._formatDuration(span)}`
            : `tier ${this.tier} · empty range`;
    }

    _computeStats(vals) {
        if (!vals.length) return { peak: 0, min: 0, avg: 0, peakIdx: -1, minIdx: -1 };
        let peak = -Infinity, min = Infinity, sum = 0, peakIdx = 0, minIdx = 0;
        for (let i = 0; i < vals.length; i++) {
            const v = vals[i];
            sum += v;
            if (v > peak) { peak = v; peakIdx = i; }
            if (v < min)  { min  = v; minIdx  = i; }
        }
        return { peak, min, avg: sum / vals.length, peakIdx, minIdx };
    }

    _renderChart() {
        const m = this.metric;
        if (!m) return;
        const root = this.root();
        if (!root) return;
        const svg = root.querySelector('.sys-detail-svg');
        const canvas = root.querySelector('.sys-metric-canvas');
        if (!svg || !canvas) return;
        const W = canvas.clientWidth || 1200;
        const H = canvas.clientHeight || 480;
        svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
        svg.setAttribute('width', W);
        svg.setAttribute('height', H);
        // Single source of truth for the empty overlay: it tracks whether we
        // actually have points. Without this the "no samples" message, shown
        // by _loadRange when history came back empty, never cleared once live
        // ticks started filling the chart + stats — a confusing contradiction.
        const empty = root.querySelector('.sys-detail-empty');
        if (!this.points.length) {
            svg.innerHTML = '';
            if (empty) empty.hidden = false;
            return;
        }
        if (empty) empty.hidden = true;
        const padL = 64, padR = 24, padT = 18, padB = 36;
        const innerW = Math.max(1, W - padL - padR);
        const innerH = Math.max(1, H - padT - padB);
        const vals = this.points.map(p => Number(p[m.key]) || 0);
        const stats = this._computeStats(vals);
        const yMin = (m.min !== undefined) ? m.min : 0;
        const yMax = (m.max !== undefined) ? m.max : Math.max(stats.peak * 1.15, 1);
        const span = yMax - yMin || 1;
        const n = vals.length;
        const stepX = n > 1 ? (innerW / (n - 1)) : 0;
        const xAt = (i) => padL + i * stepX;
        const yAt = (v) => padT + innerH * (1 - (Math.max(yMin, Math.min(yMax, v)) - yMin) / span);

        let line = '';
        for (let i = 0; i < n; i++) line += `${i ? 'L' : 'M'}${xAt(i).toFixed(1)},${yAt(vals[i]).toFixed(1)} `;
        const area = `${line} L${xAt(n - 1).toFixed(1)},${padT + innerH} L${padL.toFixed(1)},${padT + innerH} Z`;

        const yTicks = 5;
        const fmtY = m.fmt;
        let grid = '', yLabels = '';
        for (let i = 0; i <= yTicks; i++) {
            const v = yMax - (i * span / yTicks);
            const y = padT + (i * innerH / yTicks);
            grid += `<line x1="${padL}" y1="${y.toFixed(1)}" x2="${padL + innerW}" y2="${y.toFixed(1)}" class="sys-detail-grid"/>`;
            yLabels += `<text x="${(padL - 10).toFixed(0)}" y="${(y + 4).toFixed(0)}" class="sys-detail-axis sys-detail-axis-y">${fmtY(v)}</text>`;
        }
        const xTicks = Math.min(6, n);
        let xLabels = '';
        for (let i = 0; i < xTicks; i++) {
            const idx = Math.round(i * (n - 1) / (xTicks - 1 || 1));
            const x = xAt(idx);
            const t = this.points[idx]?.t;
            xLabels += `<text x="${x.toFixed(0)}" y="${(padT + innerH + 22).toFixed(0)}" class="sys-detail-axis sys-detail-axis-x">${this._fmtTime(t)}</text>`;
        }
        const peakDot = (stats.peakIdx >= 0)
            ? `<circle cx="${xAt(stats.peakIdx).toFixed(1)}" cy="${yAt(stats.peak).toFixed(1)}" r="4" class="sys-detail-peak"/>` : '';
        const minDot = (stats.minIdx >= 0)
            ? `<circle cx="${xAt(stats.minIdx).toFixed(1)}" cy="${yAt(stats.min).toFixed(1)}" r="3" class="sys-detail-min"/>` : '';
        const nowIdx = n - 1;
        const nowDot = `<circle cx="${xAt(nowIdx).toFixed(1)}" cy="${yAt(vals[nowIdx]).toFixed(1)}" r="5" class="sys-detail-now"/>`;
        // Disk page: a flat reference line marking LibrePortal's share of the disk.
        let lpLine = '';
        if (this.metricKey === 'disk' && Number.isFinite(this._lpPct)) {
            const ly = yAt(this._lpPct);
            const label = `LibrePortal ${this._lpPct.toFixed(1)}%${this._lpBytes ? ` · ${window.SystemFmt.bytes(this._lpBytes)}` : ''}`;
            lpLine = `
                <line x1="${padL}" y1="${ly.toFixed(1)}" x2="${(padL + innerW).toFixed(1)}" y2="${ly.toFixed(1)}" class="sys-detail-lpline"/>
                <text x="${(padL + innerW - 6).toFixed(0)}" y="${(ly - 7).toFixed(0)}" text-anchor="end" class="sys-detail-lplabel">${label}</text>`;
        }
        const crosshair = `<line class="sys-detail-cross" x1="0" y1="${padT}" x2="0" y2="${(padT + innerH).toFixed(1)}" visibility="hidden"/>
                           <circle class="sys-detail-cross-dot" cx="0" cy="0" r="5" visibility="hidden"/>`;
        const gradId = 'sys-detail-grad';
        svg.innerHTML = `
            <defs>
                <linearGradient id="${gradId}" x1="0" y1="0" x2="0" y2="1">
                    <stop offset="0%"  stop-color="rgba(var(--metric-rgb), 0.45)"/>
                    <stop offset="60%" stop-color="rgba(var(--metric-rgb), 0.12)"/>
                    <stop offset="100%" stop-color="rgba(var(--metric-rgb), 0.00)"/>
                </linearGradient>
                <linearGradient id="${gradId}-line" x1="0" y1="0" x2="1" y2="0">
                    <stop offset="0%"  stop-color="rgba(var(--metric-rgb), 0.7)"/>
                    <stop offset="100%" stop-color="rgba(var(--metric-rgb), 1.0)"/>
                </linearGradient>
            </defs>
            ${grid}
            <line x1="${padL}" y1="${padT}" x2="${padL}" y2="${(padT + innerH).toFixed(1)}" class="sys-detail-axis-line"/>
            <line x1="${padL}" y1="${(padT + innerH).toFixed(1)}" x2="${(padL + innerW).toFixed(1)}" y2="${(padT + innerH).toFixed(1)}" class="sys-detail-axis-line"/>
            <path d="${area}" fill="url(#${gradId})" stroke="none"/>
            <path d="${line}" fill="none" stroke="url(#${gradId}-line)" stroke-width="2.25"
                  vector-effect="non-scaling-stroke" stroke-linejoin="round" stroke-linecap="round"/>
            ${lpLine}
            ${minDot}
            ${peakDot}
            ${nowDot}
            ${crosshair}
            ${yLabels}
            ${xLabels}
        `;
        this._geo = { padL, padT, innerW, innerH, n, xAt, yAt, vals };
    }

    _onPointerMove(e) {
        if (!this._geo) return;
        const canvas = e.currentTarget;
        const rect = canvas.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const { padL, innerW, n } = this._geo;
        const rel = (x - padL) / innerW;
        if (rel < -0.02 || rel > 1.02) { this._setHover(-1); return; }
        const idx = Math.max(0, Math.min(n - 1, Math.round(rel * (n - 1))));
        if (idx === this.hoverIdx) return;
        if (this._rafHover) cancelAnimationFrame(this._rafHover);
        this._rafHover = requestAnimationFrame(() => this._setHover(idx, e.clientY - rect.top));
    }
    _onPointerLeave() { this._setHover(-1); }

    _setHover(idx, yCursor) {
        this.hoverIdx = idx;
        const root = this.root();
        if (!root) return;
        const svg = root.querySelector('.sys-detail-svg');
        const tooltip = root.querySelector('.sys-detail-tooltip');
        if (!svg || !tooltip) return;
        const cross = svg.querySelector('.sys-detail-cross');
        const dot = svg.querySelector('.sys-detail-cross-dot');
        if (!cross || !dot) return;
        if (idx < 0 || !this._geo) {
            cross.setAttribute('visibility', 'hidden');
            dot.setAttribute('visibility', 'hidden');
            tooltip.hidden = true;
            return;
        }
        const { xAt, yAt, vals } = this._geo;
        const x = xAt(idx);
        const v = vals[idx];
        const y = yAt(v);
        cross.setAttribute('x1', x); cross.setAttribute('x2', x);
        cross.setAttribute('visibility', 'visible');
        dot.setAttribute('cx', x); dot.setAttribute('cy', y);
        dot.setAttribute('visibility', 'visible');
        const m = this.metric;
        const fmt = m.fmt;
        const t = this.points[idx]?.t;
        tooltip.innerHTML = `
            <div class="sys-detail-tip-v">${fmt(v)}</div>
            <div class="sys-detail-tip-t">${this._fmtTimeFull(t)}</div>
        `;
        tooltip.hidden = false;
        const canvas = root.querySelector('.sys-metric-canvas');
        const cw = canvas.clientWidth, ch = canvas.clientHeight;
        const tw = tooltip.offsetWidth || 140, th = tooltip.offsetHeight || 40;
        let tx = x + 14, ty = (yCursor !== undefined ? yCursor : y) - th - 8;
        if (tx + tw > cw - 8) tx = x - tw - 14;
        if (ty < 8) ty = 8;
        if (ty + th > ch - 8) ty = ch - th - 8;
        tooltip.style.transform = `translate(${tx.toFixed(0)}px, ${ty.toFixed(0)}px)`;
    }

    _fmtTime(unixSec) {
        if (!unixSec) return '';
        const d = new Date(unixSec * 1000);
        const sameDay = (new Date()).toDateString() === d.toDateString();
        if (this.rangeMin > 1440) {
            const dd = String(d.getDate()).padStart(2, '0');
            const mm = String(d.getMonth() + 1).padStart(2, '0');
            return `${dd}/${mm}`;
        }
        const hh = String(d.getHours()).padStart(2, '0');
        const min = String(d.getMinutes()).padStart(2, '0');
        return sameDay ? `${hh}:${min}` : `${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')} ${hh}:${min}`;
    }

    _fmtTimeFull(unixSec) {
        if (!unixSec) return '';
        return new Date(unixSec * 1000).toLocaleString();
    }

    _formatDuration(sec) {
        if (sec < 60) return `${sec}s`;
        if (sec < 3600) return `${Math.round(sec / 60)} min`;
        if (sec < 86400) return `${(sec / 3600).toFixed(1)} h`;
        return `${(sec / 86400).toFixed(1)} d`;
    }
}

window.SystemMetricPage = SystemMetricPage;
