/* ============================================================
   TELEMETRY DASHBOARD — live ops frontend
   ============================================================ */
(() => {
    'use strict';

    const API_BASE = '/api/telemetry';
    const POLL_OVERVIEW = 5000;
    const POLL_HISTORY = 60000;
    const POLL_TAIL = 5000;
    const POLL_GEO = 60000;
    const POLL_ALERTS = 10000;
    const POLL_VISITORS = 15000;
    const POLL_COMMITS = 120000;

    // ─── State ──────────────────────────────────────────
    const state = {
        lastOverview: null,
        lastHistory: null,
        lastAlertSig: '',
        lastSync: 0,
        alarm: false,
        audioCtx: null,
        seenTailKeys: new Set(),
    };

    // ─── Utils ──────────────────────────────────────────
    const $ = (id) => document.getElementById(id);

    const fmt = {
        bytes(n) {
            if (!n) return '0 B';
            const u = ['B', 'KB', 'MB', 'GB', 'TB'];
            const i = Math.min(Math.floor(Math.log(n) / Math.log(1024)), u.length - 1);
            return (n / Math.pow(1024, i)).toFixed(1) + ' ' + u[i];
        },
        bps(n) {
            return fmt.bytes(n) + '/s';
        },
        uptime(s) {
            if (!s) return '—';
            s = Math.floor(s);
            const d = Math.floor(s / 86400);
            const h = Math.floor((s % 86400) / 3600);
            const m = Math.floor((s % 3600) / 60);
            if (d > 0) return `${d}d ${h}h ${m}m`;
            if (h > 0) return `${h}h ${m}m`;
            return `${m}m`;
        },
        numGrp(n) {
            return (n || 0).toLocaleString('en-GB');
        },
        iso(iso) {
            if (!iso) return '—';
            try {
                const d = new Date(iso);
                return d.toLocaleString('en-GB', { hour12: false });
            } catch { return iso; }
        },
        ago(iso) {
            if (!iso) return '—';
            try {
                const s = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
                if (s < 60) return `${s}s ago`;
                if (s < 3600) return `${Math.floor(s / 60)}m ago`;
                if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
                return `${Math.floor(s / 86400)}d ago`;
            } catch { return iso; }
        },
        escape(s) {
            return String(s || '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
        }
    };

    async function fetchJSON(url) {
        try {
            const r = await fetch(url, { credentials: 'same-origin' });
            if (!r.ok) throw new Error('HTTP ' + r.status);
            return await r.json();
        } catch (e) {
            console.warn('[telemetry] fetch failed', url, e);
            return null;
        }
    }

    // ─── Canvas gauge ───────────────────────────────────
    function drawGauge(canvas, value, label) {
        if (!canvas) return;
        const ctx = canvas.getContext('2d');
        const dpr = window.devicePixelRatio || 1;
        const size = canvas.clientWidth;
        canvas.width = size * dpr;
        canvas.height = size * dpr;
        ctx.scale(dpr, dpr);
        const cx = size / 2, cy = size / 2;
        const r = size / 2 - 6;

        ctx.clearRect(0, 0, size, size);

        // Background arc
        ctx.beginPath();
        ctx.arc(cx, cy, r, Math.PI * 0.75, Math.PI * 2.25);
        ctx.lineWidth = 6;
        ctx.strokeStyle = '#1a1a1a';
        ctx.stroke();

        // Tick marks
        for (let i = 0; i <= 10; i++) {
            const a = Math.PI * 0.75 + (Math.PI * 1.5 * i / 10);
            const r1 = r - 2, r2 = r + 4;
            ctx.beginPath();
            ctx.moveTo(cx + Math.cos(a) * r1, cy + Math.sin(a) * r1);
            ctx.lineTo(cx + Math.cos(a) * r2, cy + Math.sin(a) * r2);
            ctx.lineWidth = 1;
            ctx.strokeStyle = '#2a2a2a';
            ctx.stroke();
        }

        // Value arc
        const pct = Math.max(0, Math.min(100, value)) / 100;
        const endA = Math.PI * 0.75 + Math.PI * 1.5 * pct;
        const color = value > 90 ? '#ff2d2d' : value > 80 ? '#c9a227' : '#00cc33';
        ctx.beginPath();
        ctx.arc(cx, cy, r, Math.PI * 0.75, endA);
        ctx.lineWidth = 6;
        ctx.strokeStyle = color;
        ctx.shadowColor = color;
        ctx.shadowBlur = 8;
        ctx.stroke();
        ctx.shadowBlur = 0;

        // Needle
        ctx.beginPath();
        ctx.moveTo(cx, cy);
        ctx.lineTo(cx + Math.cos(endA) * (r - 12), cy + Math.sin(endA) * (r - 12));
        ctx.lineWidth = 2;
        ctx.strokeStyle = color;
        ctx.stroke();
        // Hub
        ctx.beginPath();
        ctx.arc(cx, cy, 4, 0, Math.PI * 2);
        ctx.fillStyle = color;
        ctx.fill();
    }

    // ─── Sparkline ──────────────────────────────────────
    function drawSpark(canvas, values, { color = '#00cc33', fill = true, max = null } = {}) {
        if (!canvas) return;
        const ctx = canvas.getContext('2d');
        const dpr = window.devicePixelRatio || 1;
        const w = canvas.clientWidth;
        const h = canvas.clientHeight;
        canvas.width = w * dpr;
        canvas.height = h * dpr;
        ctx.scale(dpr, dpr);
        ctx.clearRect(0, 0, w, h);

        if (!values || values.length < 2) {
            ctx.fillStyle = '#333';
            ctx.font = '10px JetBrains Mono';
            ctx.fillText('awaiting history...', 6, h / 2 + 3);
            return;
        }

        const peak = max !== null ? max : Math.max(...values, 1);
        const step = w / (values.length - 1);
        ctx.beginPath();
        values.forEach((v, i) => {
            const x = i * step;
            const y = h - (v / peak) * (h - 4) - 2;
            i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
        });
        ctx.strokeStyle = color;
        ctx.lineWidth = 1.2;
        ctx.shadowColor = color;
        ctx.shadowBlur = 4;
        ctx.stroke();
        ctx.shadowBlur = 0;

        if (fill) {
            ctx.lineTo(w, h);
            ctx.lineTo(0, h);
            ctx.closePath();
            ctx.fillStyle = color + '22';
            ctx.fill();
        }
    }

    // ─── Disk ASCII bars ────────────────────────────────
    function renderDisks(disks) {
        const el = $('tmDisks');
        if (!el) return;
        if (!disks || !disks.length) { el.innerHTML = '<div class="tm-err">NO MOUNTS</div>'; return; }
        const bars = 18;
        el.innerHTML = disks.map(d => {
            const filled = Math.round(d.pct / 100 * bars);
            const cls = d.pct > 90 ? 'crit' : d.pct > 85 ? 'warn' : '';
            const bar = '█'.repeat(filled) + '░'.repeat(bars - filled);
            return `
            <div class="tm-disk">
                <div class="tm-disk-head">
                    <span class="mount">${fmt.escape(d.mount)}</span>
                    <span>${d.used_gb}G / ${d.total_gb}G · ${d.pct}%</span>
                </div>
                <div class="tm-disk-bar ${cls}">[${bar}]</div>
            </div>`;
        }).join('');
    }

    // ─── Services ──────────────────────────────────────
    function renderServices(services) {
        const el = $('tmServicesBody');
        if (!el) return;
        if (!services || !services.length) { el.innerHTML = '<tr><td colspan="5" class="tm-err">NO SERVICES</td></tr>'; return; }
        const sorted = [...services].sort((a, b) => {
            const rank = { down: 0, unknown: 1, up: 2 };
            return (rank[a.status] ?? 3) - (rank[b.status] ?? 3);
        });
        el.innerHTML = sorted.map(s => `
            <tr>
                <td><span class="dot ${s.status}"></span>${s.status.toUpperCase()}</td>
                <td class="svc-name">${fmt.escape(s.name)}</td>
                <td>${fmt.uptime(s.uptime_seconds)}</td>
                <td>${s.memory_mb ? s.memory_mb.toFixed(0) + ' MB' : '—'}</td>
                <td>${s.cpu_percent ? s.cpu_percent.toFixed(1) + '%' : '—'}</td>
            </tr>
        `).join('');

        const upCount = services.filter(s => s.status === 'up').length;
        const badge = $('tmSvcBadge');
        if (badge) badge.textContent = `${upCount}/${services.length} UP`;
    }

    // ─── Crons ─────────────────────────────────────────
    function renderCrons(crons) {
        const el = $('tmCronsBody');
        if (!el) return;
        if (!crons || !crons.length) { el.innerHTML = '<tr><td colspan="4" class="tm-err">NO CRONS</td></tr>'; return; }
        el.innerHTML = crons.map((c, i) => `
            <tr class="tm-expandable" data-cron-idx="${i}">
                <td class="svc-name">${fmt.escape(c.name)}</td>
                <td><code>${fmt.escape(c.schedule)}</code></td>
                <td>${fmt.ago(c.last_run_iso)}</td>
                <td class="cron-status ${c.last_status}">${c.last_status === 'ok' ? '✓ OK' : c.last_status === 'fail' ? '✗ FAIL' : '? UNKNOWN'}</td>
            </tr>
            <tr style="display:none" data-cron-exp="${i}">
                <td colspan="4"><div class="tm-expanded-row">${fmt.escape(c.last_output_tail || '(no output)')}</div></td>
            </tr>
        `).join('');
        el.querySelectorAll('[data-cron-idx]').forEach(row => {
            row.addEventListener('click', () => {
                const idx = row.getAttribute('data-cron-idx');
                const exp = el.querySelector(`[data-cron-exp="${idx}"]`);
                if (exp) exp.style.display = exp.style.display === 'none' ? 'table-row' : 'none';
            });
        });
    }

    // ─── Nginx ─────────────────────────────────────────
    function renderNginx(nginx) {
        const s = $('tmNginxStats');
        const t = $('tmTopPages');
        if (!s || !t) return;
        if (!nginx || nginx.total_requests === 0) {
            s.innerHTML = '<div class="tm-err" style="grid-column:1/-1">NO ACCESS LOG DATA YET</div>';
            t.innerHTML = '';
            return;
        }
        s.innerHTML = `
            <div class="tm-stat"><span class="v">${fmt.numGrp(nginx.total_requests)}</span><span class="l">REQUESTS / 24h</span></div>
            <div class="tm-stat"><span class="v">${fmt.bytes(nginx.total_bytes)}</span><span class="l">BANDWIDTH</span></div>
            <div class="tm-stat"><span class="v">${nginx.avg_rpm}</span><span class="l">AVG REQ / MIN</span></div>
            <div class="tm-stat ${nginx.status_4xx_count > 100 ? 'err' : ''}"><span class="v">${fmt.numGrp(nginx.status_4xx_count)}</span><span class="l">4xx ERRORS</span></div>
            <div class="tm-stat ${nginx.status_5xx_count > 0 ? 'err' : ''}"><span class="v">${fmt.numGrp(nginx.status_5xx_count)}</span><span class="l">5xx ERRORS</span></div>
        `;
        const top = (list, head) => `
            <div><h4>${head}</h4><ul>
                ${list.slice(0, 8).map(x => `<li><span class="path" title="${fmt.escape(x.path || x.ip)}">${fmt.escape(x.path || x.ip)}</span><span class="c">${fmt.numGrp(x.count)}</span></li>`).join('')}
            </ul></div>`;
        t.innerHTML = top(nginx.top_pages || [], 'TOP PAGES') + top(nginx.top_ips_redacted || [], 'TOP IPs (REDACTED)');
    }

    // ─── Security ──────────────────────────────────────
    function renderSecurity(sec) {
        const s = $('tmSecurityStats');
        const j = $('tmJails');
        if (!s) return;
        if (!sec) { s.innerHTML = '<div class="tm-err">NO DATA</div>'; return; }
        s.innerHTML = `
            <div class="tm-stat"><span class="v">${sec.ufw_rules_count}</span><span class="l">UFW RULES</span></div>
            <div class="tm-stat ${sec.fail2ban_bans_total > 0 ? '' : ''}"><span class="v">${sec.fail2ban_bans_total}</span><span class="l">BANNED IPs</span></div>
            <div class="tm-stat ${sec.ssh_attempts_24h > 200 ? 'err' : ''}"><span class="v">${sec.ssh_attempts_24h}</span><span class="l">SSH BRUTEFORCE 24h</span></div>
            <div class="tm-stat"><span class="v" style="font-size:0.85rem">${sec.last_reboot_iso ? fmt.ago(sec.last_reboot_iso) : '—'}</span><span class="l">LAST REBOOT</span></div>
        `;
        if (j) {
            if (!sec.fail2ban_jails || !sec.fail2ban_jails.length) {
                j.innerHTML = '<div style="color:var(--tm-muted);font-size:0.7rem">no fail2ban jails detected</div>';
            } else {
                j.innerHTML = '<h4 style="color:var(--tm-green-dim);font-size:0.68rem;letter-spacing:0.15em;margin:0.5rem 0 0.3rem;font-weight:400">JAILS</h4>' +
                    sec.fail2ban_jails.map(x => `<div class="jail"><span>${fmt.escape(x.name)}</span><span style="color:var(--tm-amber);font-family:'Orbitron',sans-serif">${x.banned}</span></div>`).join('');
            }
        }
    }

    // ─── SSL ──────────────────────────────────────────
    function renderSSL(ssl) {
        const el = $('tmSsl');
        if (!el) return;
        if (!ssl || !ssl.length) { el.innerHTML = '<div class="tm-err">NO CERT DATA</div>'; return; }
        el.innerHTML = ssl.map(s => {
            const cls = s.days_left < 0 ? 'fail' : s.days_left < 14 ? 'crit' : s.days_left < 30 ? 'warn' : '';
            const daysTxt = s.days_left < 0 ? 'FAIL' : s.days_left + 'd';
            return `<div class="ssl-row">
                <span>${fmt.escape(s.domain)}</span>
                <span class="days ${cls}">${daysTxt}</span>
            </div>`;
        }).join('');
    }

    // ─── Stack ─────────────────────────────────────────
    function renderStack(stack) {
        const el = $('tmStack');
        if (!el) return;
        if (!stack) { el.innerHTML = '<div class="tm-err">NO DATA</div>'; return; }
        const keys = [
            ['OS', stack.os], ['kernel', stack.kernel],
            ['python', stack.python_version], ['node', stack.node_version],
            ['nginx', stack.nginx_version], ['docker', stack.docker_version],
            ['ffmpeg', stack.ffmpeg_version], ['arch', stack.uname],
        ];
        el.innerHTML = keys.filter(([_, v]) => v).map(([k, v]) =>
            `<div class="tm-stack-item"><span class="k">${k}</span><span class="v" title="${fmt.escape(v)}">${fmt.escape(v)}</span></div>`
        ).join('');
    }

    // ─── Repos ─────────────────────────────────────────
    function renderRepos(repos) {
        const el = $('tmRepos');
        if (!el) return;
        if (!repos || !repos.length) { el.innerHTML = '<div class="tm-err">NO REPOS</div>'; return; }
        el.innerHTML = repos.map(r => `
            <div class="tm-repo">
                <div class="tm-repo-head">
                    <span class="name">${fmt.escape(r.name)}</span>
                    <span class="sha">${fmt.escape(r.last_commit_sha)}${r.dirty ? '<span class="dirty-badge"> ● DIRTY</span>' : ''}</span>
                </div>
                <div class="msg">${fmt.escape(r.last_commit_msg)}</div>
                <div class="meta">${fmt.ago(r.last_commit_iso)} · ${fmt.escape(r.path)}</div>
            </div>
        `).join('');
    }

    // ─── Tail ──────────────────────────────────────────
    function renderTail(lines) {
        const el = $('tmTail');
        if (!el) return;
        if (!lines || !lines.length) return;
        // Filter to only new lines
        const newOnes = [];
        for (const l of lines) {
            const key = `${l.time}|${l.ip_masked}|${l.path}|${l.status}`;
            if (!state.seenTailKeys.has(key)) {
                state.seenTailKeys.add(key);
                newOnes.push(l);
            }
        }
        if (state.seenTailKeys.size > 200) {
            // Trim
            const arr = [...state.seenTailKeys].slice(-100);
            state.seenTailKeys = new Set(arr);
        }
        if (!newOnes.length && el.querySelector('.tm-loading')) {
            el.innerHTML = '<div style="color:var(--tm-muted);font-size:0.7rem">(no recent requests)</div>';
            return;
        }
        // Clear loading once we have data
        if (el.querySelector('.tm-loading')) el.innerHTML = '';

        newOnes.forEach(l => {
            const stCls = l.status >= 500 ? 'st-5xx' : l.status >= 400 ? 'st-4xx' : l.status >= 300 ? 'st-3xx' : 'st-2xx';
            const div = document.createElement('div');
            div.className = 'tm-tail-line';
            div.innerHTML = `<span class="time">${fmt.escape(l.time.split(' ')[0].split(':').slice(1).join(':'))}</span> <span class="ip">${fmt.escape(l.ip_masked)}</span> <span class="method">${fmt.escape(l.method)}</span> <span class="path">${fmt.escape(l.path)}</span> <span class="${stCls}">${l.status}</span> <span style="color:#3a3a3a">${fmt.bytes(l.size)}</span>`;
            el.appendChild(div);
        });

        // Trim DOM to last 40 lines
        while (el.children.length > 40) el.removeChild(el.firstChild);
        // Scroll to bottom
        el.scrollTop = el.scrollHeight;
    }

    // ─── Geo ───────────────────────────────────────────
    function renderGeo(geo) {
        const el = $('tmGeo');
        const top = $('tmGeoTop');
        if (!el) return;
        if (!geo || !geo.length) {
            el.innerHTML = '<div class="tm-geo-empty">GEO INTEL UNAVAILABLE<br><span style="font-size:0.65rem">(GeoIP mmdb not installed on VPS)</span></div>';
            if (top) top.innerHTML = '';
            return;
        }
        const max = geo[0].count;
        // Simple horizontal bar chart instead of a world map (keeps dep-free, no external svg)
        el.innerHTML = `
            <div style="display:flex;flex-direction:column;gap:0.35rem;padding:0.3rem 0;">
                ${geo.slice(0, 15).map(g => {
                    const pct = Math.round(g.count / max * 100);
                    return `
                    <div style="display:flex;align-items:center;gap:0.6rem;font-size:0.72rem;">
                        <span style="width:40px;color:var(--tm-sol);font-family:'Orbitron',sans-serif;">${fmt.escape(g.country_code)}</span>
                        <div style="flex:1;background:#0a0a0a;height:14px;border:1px solid #1a1a1a;position:relative;">
                            <div style="height:100%;width:${pct}%;background:linear-gradient(90deg,#00cc33,#14F195);box-shadow:0 0 6px rgba(0,204,51,0.4);"></div>
                        </div>
                        <span style="color:var(--tm-text);min-width:100px;font-size:0.68rem;">${fmt.escape(g.country_name)}</span>
                        <span style="color:var(--tm-green);font-family:'Orbitron',sans-serif;min-width:45px;text-align:right;">${fmt.numGrp(g.count)}</span>
                    </div>`;
                }).join('')}
            </div>
        `;
        if (top) {
            top.innerHTML = geo.slice(0, 12).map(g =>
                `<div class="c-item"><span>${fmt.escape(g.country_code)}</span><span class="n">${fmt.numGrp(g.count)}</span></div>`
            ).join('');
        }
    }

    // ─── Burn rate ──────────────────────────────────────
    function renderBurn(overview, history) {
        const el = $('tmBurn');
        if (!el) return;
        if (!overview) { el.innerHTML = '<div class="tm-err">NO DATA</div>'; return; }
        // Disk burn: find root /, compute daily growth from history if available
        const root = (overview.system.disk_per_mount || []).find(d => d.mount === '/') || (overview.system.disk_per_mount || [])[0];
        const lines = [];
        if (root) {
            const freeGb = root.total_gb - root.used_gb;
            lines.push(`<div><span class="label">ROOT DISK FREE</span><span class="big">${freeGb} GB</span> of ${root.total_gb} GB (${root.pct}% used)</div>`);
        }
        // Network today — sum of last 24h net_tx from history
        if (history && history.net_tx && history.net_tx.length) {
            const sumTx = history.net_tx.reduce((a, b) => a + b, 0);
            const sumRx = history.net_rx.reduce((a, b) => a + b, 0);
            const avgInterval = 300; // 5min
            const totalTx = sumTx * avgInterval;
            const totalRx = sumRx * avgInterval;
            lines.push(`<div><span class="label">BANDWIDTH 24h</span><span class="big">↑ ${fmt.bytes(totalTx)}</span> · <span class="big">↓ ${fmt.bytes(totalRx)}</span></div>`);
        }
        lines.push(`<div><span class="label">LOAD AVG</span><span class="big">${overview.system.load_1.toFixed(2)}</span> / ${overview.system.load_5.toFixed(2)} / ${overview.system.load_15.toFixed(2)} (${overview.system.ncpu} CPU)</div>`);
        lines.push(`<div><span class="label">KERNEL</span>${fmt.escape(overview.system.kernel)} @ ${fmt.escape(overview.system.hostname)}</div>`);
        el.innerHTML = lines.join('');
    }

    // ─── Ticker ─────────────────────────────────────────
    function renderTicker(commits) {
        const el = $('tmTicker');
        if (!el) return;
        if (!commits || !commits.length) { el.innerHTML = '<div class="tm-err">NO COMMITS</div>'; return; }
        el.innerHTML = `<div class="tm-ticker-inner">${commits.map(c =>
            `<span class="tm-ticker-item"><span class="sha">${fmt.escape(c.sha)}</span>${fmt.escape(c.msg)}<span class="iso">${fmt.ago(c.iso)}</span></span>`
        ).join('')}</div>`;
    }

    // ─── Alerts ─────────────────────────────────────────
    function renderAlerts(alerts) {
        const banner = $('tmAlerts');
        const content = $('tmAlertsContent');
        if (!banner || !content) return;
        if (!alerts || !alerts.length) {
            banner.classList.remove('visible', 'amber');
            state.lastAlertSig = '';
            return;
        }
        const sig = alerts.map(a => a.level + a.message).join('|');
        const hasRed = alerts.some(a => a.level === 'red');
        banner.classList.add('visible');
        banner.classList.toggle('amber', !hasRed);
        if (sig !== state.lastAlertSig) {
            content.innerHTML = alerts.map(a =>
                `<span class="tm-alert-item ${a.level}">${fmt.escape(a.message)}</span>`
            ).join('') + alerts.map(a =>
                `<span class="tm-alert-item ${a.level}">${fmt.escape(a.message)}</span>`
            ).join(''); // doubled for smooth marquee
            state.lastAlertSig = sig;
            if (state.alarm) playBeep(hasRed);
        }
    }

    // ─── Alarm beep (Web Audio) ─────────────────────────
    function playBeep(urgent) {
        try {
            if (!state.audioCtx) state.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
            const ctx = state.audioCtx;
            const o = ctx.createOscillator();
            const g = ctx.createGain();
            o.connect(g); g.connect(ctx.destination);
            o.type = 'square';
            o.frequency.value = urgent ? 880 : 440;
            g.gain.setValueAtTime(0.15, ctx.currentTime);
            g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.2);
            o.start(ctx.currentTime);
            o.stop(ctx.currentTime + 0.22);
        } catch (e) { }
    }

    // ─── Overview render ────────────────────────────────
    function applyOverview(data) {
        if (!data) return;
        state.lastOverview = data;
        state.lastSync = Date.now();
        const sys = data.system || {};

        // CPU
        drawGauge($('tmCpuGauge'), sys.cpu_percent);
        const cpuVal = $('tmCpuVal');
        if (cpuVal) {
            cpuVal.textContent = sys.cpu_percent + '%';
            cpuVal.className = 'tm-gauge-value ' + (sys.cpu_percent > 90 ? 'crit' : sys.cpu_percent > 80 ? 'warn' : '');
        }
        const cpuLoad = $('tmCpuLoad');
        if (cpuLoad) cpuLoad.textContent = `load ${sys.load_1?.toFixed(2)} · ${sys.ncpu} cores`;
        const cpuBadge = $('tmCpuBadge');
        if (cpuBadge) cpuBadge.textContent = `${sys.ncpu} CPU`;

        // MEM
        drawGauge($('tmMemGauge'), sys.mem_percent);
        const memVal = $('tmMemVal');
        if (memVal) {
            memVal.textContent = sys.mem_percent + '%';
            memVal.className = 'tm-gauge-value ' + (sys.mem_percent > 90 ? 'crit' : sys.mem_percent > 80 ? 'warn' : '');
        }
        const memSize = $('tmMemSize');
        if (memSize) memSize.textContent = `${fmt.bytes(sys.mem_used_bytes)} / ${fmt.bytes(sys.mem_total_bytes)}`;
        const memBadge = $('tmMemBadge');
        if (memBadge) memBadge.textContent = fmt.bytes(sys.mem_total_bytes);

        // Disks
        renderDisks(sys.disk_per_mount);

        // Net
        const rx = $('tmNetRx'); if (rx) rx.textContent = fmt.bps(sys.net_rx_bps);
        const tx = $('tmNetTx'); if (tx) tx.textContent = fmt.bps(sys.net_tx_bps);
        const up = $('tmUptime'); if (up) up.textContent = `uptime ${fmt.uptime(sys.uptime_seconds)}`;

        // Tables & sections
        renderServices(data.services);
        renderCrons(data.crons);
        renderNginx(data.nginx_24h);
        renderSecurity(data.security);
        renderSSL(data.ssl);
        renderStack(data.stack);
        renderRepos(data.repos);
        renderBurn(data, state.lastHistory);
    }

    function applyHistory(data) {
        if (!data) return;
        state.lastHistory = data;
        drawSpark($('tmCpuSpark'), data.cpu, { color: '#00cc33', max: 100 });
        drawSpark($('tmMemSpark'), data.mem, { color: '#c9a227', max: 100 });
        // Combined net: overlay
        const canvas = $('tmNetSpark');
        if (canvas && (data.net_rx?.length || data.net_tx?.length)) {
            drawSpark(canvas, data.net_rx, { color: '#14F195', fill: true });
            // Overlay tx on top without clearing
            const ctx = canvas.getContext('2d');
            ctx.globalCompositeOperation = 'source-over';
            // redraw-ish: tx overlay
            const dpr = window.devicePixelRatio || 1;
            const w = canvas.clientWidth, h = canvas.clientHeight;
            const values = data.net_tx || [];
            if (values.length > 1) {
                const peak = Math.max(...(data.net_rx || []), ...values, 1);
                const step = w / (values.length - 1);
                ctx.beginPath();
                values.forEach((v, i) => {
                    const x = i * step;
                    const y = h - (v / peak) * (h - 4) - 2;
                    i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
                });
                ctx.strokeStyle = '#00cc33';
                ctx.lineWidth = 1.2;
                ctx.shadowColor = '#00cc33';
                ctx.shadowBlur = 4;
                ctx.stroke();
                ctx.shadowBlur = 0;
            }
        }
        // Re-render burn with fresh history
        if (state.lastOverview) renderBurn(state.lastOverview, data);
    }

    function applyVisitors(data) {
        if (!data) return;
        const a = $('tmVisitorsActive'); if (a) a.textContent = fmt.numGrp(data.active);
        const r = $('tmVisitorsRpm'); if (r) r.textContent = data.req_per_min;
    }

    // ─── Live indicator ─────────────────────────────────
    function tickLive() {
        const secs = state.lastSync ? Math.floor((Date.now() - state.lastSync) / 1000) : 0;
        const el = $('tmLastSync');
        const dot = $('tmLiveDot');
        if (el) el.textContent = state.lastSync ? secs : '—';
        if (dot) {
            dot.className = 'tm-live-dot ' + (secs < 30 ? '' : secs < 90 ? 'amber' : 'red');
        }
    }

    // ─── Polling loops ──────────────────────────────────
    async function pollOverview() {
        const d = await fetchJSON(API_BASE + '/overview');
        if (d) applyOverview(d);
    }
    async function pollHistory() {
        const d = await fetchJSON(API_BASE + '/history');
        if (d) applyHistory(d);
    }
    async function pollTail() {
        const d = await fetchJSON(API_BASE + '/nginx-tail');
        if (d) renderTail(d);
    }
    async function pollGeo() {
        const d = await fetchJSON(API_BASE + '/geo');
        renderGeo(d || []);
    }
    async function pollAlerts() {
        const d = await fetchJSON(API_BASE + '/alerts');
        if (Array.isArray(d)) renderAlerts(d);
    }
    async function pollVisitors() {
        const d = await fetchJSON(API_BASE + '/visitors');
        if (d) applyVisitors(d);
    }
    async function pollCommits() {
        const d = await fetchJSON(API_BASE + '/commits');
        if (d) renderTicker(d);
    }

    // ─── Sound toggle ───────────────────────────────────
    function bindSoundToggle() {
        const btn = $('tmSoundToggle');
        if (!btn) return;
        btn.addEventListener('click', () => {
            state.alarm = !state.alarm;
            btn.classList.toggle('on', state.alarm);
            btn.textContent = state.alarm ? '🔊 ALARM ON' : '🔇 ALARM OFF';
            if (state.alarm) {
                // Init audio context on user gesture
                if (!state.audioCtx) state.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
                playBeep(false);
            }
        });
    }

    // ─── Boot ──────────────────────────────────────────
    function hideBoot() {
        const boot = $('tmBoot');
        if (boot) setTimeout(() => { boot.classList.add('hidden'); setTimeout(() => boot.remove(), 600); }, 1500);
    }

    async function init() {
        hideBoot();
        bindSoundToggle();

        // Initial fetches — parallel
        await Promise.all([pollOverview(), pollHistory(), pollCommits(), pollGeo(), pollVisitors(), pollAlerts(), pollTail()]);

        // Intervals
        setInterval(pollOverview, POLL_OVERVIEW);
        setInterval(pollHistory, POLL_HISTORY);
        setInterval(pollTail, POLL_TAIL);
        setInterval(pollGeo, POLL_GEO);
        setInterval(pollAlerts, POLL_ALERTS);
        setInterval(pollVisitors, POLL_VISITORS);
        setInterval(pollCommits, POLL_COMMITS);
        setInterval(tickLive, 1000);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();
