<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Meshtastic · Network Topology</title>

    <!-- Tailwind (CDN – warning is harmless) -->
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- Font Awesome -->
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
    <!-- D3 for force graph -->
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <!-- Chart.js -->
    <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
    <!-- Vue 3 -->
    <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

    <style>
        [v-cloak] { display: none; }
        body {
            background: #020617;
            color: #e2e8f0;
            font-family: 'Inter', system-ui, sans-serif;
        }
        #topology-graph {
            width: 100%;
            height: 500px;
            background: #0f172a;
            border-radius: 1rem;
            border: 1px solid #1e293b;
            position: relative;
            overflow: hidden;
        }
        .graph-tooltip {
            position: absolute;
            background: rgba(15, 23, 42, 0.95);
            backdrop-filter: blur(8px);
            border: 1px solid #334155;
            border-radius: 0.5rem;
            padding: 0.75rem;
            font-size: 0.75rem;
            color: #f1f5f9;
            pointer-events: none;
            box-shadow: 0 10px 25px -5px rgba(0,0,0,0.5);
            z-index: 1000;
            max-width: 220px;
        }
        .link {
            stroke-opacity: 0.6;
            stroke-width: 2;
        }
        .node {
            stroke: white;
            stroke-width: 1.5;
            cursor: pointer;
            transition: stroke-width 0.1s;
        }
        .node:hover {
            stroke-width: 3;
            stroke: #6366f1;
        }
        ::-webkit-scrollbar { width: 6px; height: 6px; }
        ::-webkit-scrollbar-track { background: #0f172a; }
        ::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
        ::-webkit-scrollbar-thumb:hover { background: #475569; }
        .glass-card {
            background: rgba(15, 23, 42, 0.7);
            backdrop-filter: blur(8px);
            border: 1px solid rgba(255,255,255,0.05);
            border-radius: 1rem;
        }
        .stat-tile {
            background: #0f172a;
            border: 1px solid #1e293b;
            border-radius: 0.75rem;
            padding: 1rem;
        }
        .error-banner {
            background: #7f1d1d;
            border: 1px solid #b91c1c;
            color: #fecaca;
            padding: 1rem;
            border-radius: 0.5rem;
            margin-bottom: 1rem;
        }
    </style>
</head>
<body class="p-6 antialiased" v-cloak>

    <div id="app" class="max-w-[1600px] mx-auto">
        <!-- Error display (if any) -->
        <div v-if="fatalError" class="error-banner">
            <i class="fa-solid fa-triangle-exclamation mr-2"></i>
            <span v-text="fatalError"></span>
        </div>

        <template v-if="!fatalError">
            <!-- ========== HEADER ========== -->
            <div class="flex items-center justify-between mb-6">
                <div class="flex items-center gap-3">
                    <div class="w-10 h-10 rounded-xl bg-indigo-600 flex items-center justify-center shadow-lg shadow-indigo-500/20">
                        <i class="fa-solid fa-project-diagram text-white text-lg"></i>
                    </div>
                    <div>
                        <h1 class="text-2xl font-bold bg-gradient-to-r from-indigo-400 to-cyan-400 bg-clip-text text-transparent">
                            Network Topology
                        </h1>
                        <p class="text-xs text-slate-500 uppercase tracking-wider font-semibold">Live force‑directed graph · signal coverage</p>
                    </div>
                </div>
                <a href="/" class="px-4 py-2 bg-slate-800/80 hover:bg-slate-700 rounded-lg text-sm text-slate-300 transition-colors border border-slate-700 flex items-center gap-2">
                    <i class="fa-solid fa-arrow-left"></i> Dashboard
                </a>
            </div>

            <!-- ========== STATS ROW ========== -->
            <div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 mb-6">
                <div class="stat-tile flex items-center justify-between">
                    <div>
                        <div class="text-xs text-slate-500 uppercase">Nodes</div>
                        <div class="text-2xl font-bold text-white">${ stats.nodes?.total || 0 }</div>
                        <div class="text-[10px] text-slate-600">active 1h: ${ stats.nodes?.active_1h || 0 }</div>
                    </div>
                    <i class="fa-solid fa-diagram-project text-indigo-400/70 text-2xl"></i>
                </div>
                <div class="stat-tile flex items-center justify-between">
                    <div>
                        <div class="text-xs text-slate-500 uppercase">Traced</div>
                        <div class="text-2xl font-bold text-emerald-400">${ stats.nodes?.traced || 0 }</div>
                        <div class="text-[10px] text-slate-600">${ stats.nodes?.trace_coverage || 0 }% coverage</div>
                    </div>
                    <i class="fa-solid fa-route text-emerald-400/70 text-2xl"></i>
                </div>
                <div class="stat-tile flex items-center justify-between">
                    <div>
                        <div class="text-xs text-slate-500 uppercase">Avg SNR</div>
                        <div class="text-2xl font-bold" :class="snrTextClass(stats.signal?.avg_snr)">${ stats.signal?.avg_snr?.toFixed(1) || 0 } dB</div>
                        <div class="text-[10px] text-slate-600">best ${ stats.signal?.max_snr?.toFixed(1) || 0 }</div>
                    </div>
                    <i class="fa-solid fa-signal text-blue-400/70 text-2xl"></i>
                </div>
                <div class="stat-tile flex items-center justify-between">
                    <div>
                        <div class="text-xs text-slate-500 uppercase">Avg Hops</div>
                        <div class="text-2xl font-bold text-purple-400">${ stats.traces?.avg_hops || 0 }</div>
                        <div class="text-[10px] text-slate-600">${ stats.traces?.total || 0 } traces</div>
                    </div>
                    <i class="fa-solid fa-arrows-spread text-purple-400/70 text-2xl"></i>
                </div>
                <div class="stat-tile flex items-center justify-between">
                    <div>
                        <div class="text-xs text-slate-500 uppercase">Direct</div>
                        <div class="text-2xl font-bold text-indigo-400">${ stats.nodes?.neighbors || 0 }</div>
                        <div class="text-[10px] text-slate-600">neighbors</div>
                    </div>
                    <i class="fa-solid fa-handshake text-indigo-400/70 text-2xl"></i>
                </div>
                <div class="stat-tile flex items-center justify-between">
                    <div>
                        <div class="text-xs text-slate-500 uppercase">Success</div>
                        <div class="text-2xl font-bold text-amber-400">${ stats.traces?.success_rate || 0 }%</div>
                        <div class="text-[10px] text-slate-600">trace success</div>
                    </div>
                    <i class="fa-solid fa-circle-check text-amber-400/70 text-2xl"></i>
                </div>
            </div>

            <!-- ========== GRAPH + CHARTS ========== -->
            <div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
                <!-- Force graph (span 2) -->
                <div class="lg:col-span-2 glass-card p-4">
                    <div class="flex items-center justify-between mb-3">
                        <h2 class="font-bold text-slate-300 uppercase text-xs tracking-wider">
                            <i class="fa-solid fa-share-nodes mr-2 text-indigo-500"></i> TOPOLOGY · SIGNAL STRENGTH
                        </h2>
                        <div class="flex items-center gap-3 text-xs">
                            <span class="flex items-center gap-1"><span class="w-3 h-3 rounded-full bg-emerald-500"></span> >5dB</span>
                            <span class="flex items-center gap-1"><span class="w-3 h-3 rounded-full bg-blue-500"></span> >0dB</span>
                            <span class="flex items-center gap-1"><span class="w-3 h-3 rounded-full bg-orange-500"></span> >-10dB</span>
                            <span class="flex items-center gap-1"><span class="w-3 h-3 rounded-full bg-red-500"></span> ≤-10dB</span>
                        </div>
                    </div>
                    <div id="topology-graph" class="relative"></div>

                    <!-- Node detail modal -->
                    <div v-if="selectedNode" class="fixed inset-0 bg-black/70 flex items-center justify-center z-50" @click.self="selectedNode = null">
                        <div class="bg-slate-900 border border-slate-700 rounded-2xl max-w-2xl w-full mx-4 shadow-2xl">
                            <div class="flex justify-between items-center p-5 border-b border-slate-800">
                                <div class="flex items-center gap-3">
                                    <div class="w-8 h-8 rounded-full" :style="{background: getSnrColor(selectedNode.snr_avg)}"></div>
                                    <div>
                                        <h3 class="font-bold text-white">${ selectedNode.name || selectedNode.id }</h3>
                                        <p class="text-xs text-slate-400 font-mono">${ selectedNode.id }</p>
                                    </div>
                                </div>
                                <button @click="selectedNode = null" class="text-slate-400 hover:text-white text-xl">&times;</button>
                            </div>
                            <div class="p-5 max-h-[70vh] overflow-y-auto">
                                <div class="grid grid-cols-2 gap-4 mb-4">
                                    <div class="bg-slate-800/50 p-3 rounded-lg border border-slate-700">
                                        <span class="text-xs text-slate-500">Role</span>
                                        <div class="font-mono text-sm">${ selectedNode.role || 'CLIENT' }</div>
                                    </div>
                                    <div class="bg-slate-800/50 p-3 rounded-lg border border-slate-700">
                                        <span class="text-xs text-slate-500">Hardware</span>
                                        <div class="font-mono text-sm">${ selectedNode.hw_model || '?' }</div>
                                    </div>
                                    <div class="bg-slate-800/50 p-3 rounded-lg border border-slate-700">
                                        <span class="text-xs text-slate-500">Avg SNR</span>
                                        <div class="font-mono text-lg" :class="snrTextClass(selectedNode.snr_avg)">${ selectedNode.snr_avg?.toFixed(1) || '?' } dB</div>
                                    </div>
                                    <div class="bg-slate-800/50 p-3 rounded-lg border border-slate-700">
                                        <span class="text-xs text-slate-500">Battery</span>
                                        <div class="font-mono text-lg" :class="batTextClass(selectedNode.battery_level)">${ selectedNode.battery_level || '?' }%</div>
                                    </div>
                                </div>
                                <div class="mb-4">
                                    <div class="text-xs font-bold text-slate-400 uppercase mb-2">Last Path</div>
                                    <div class="bg-slate-800/80 p-3 rounded-lg border border-slate-700 text-sm font-mono flex flex-wrap gap-1">
                                        <span class="text-indigo-400">HOME</span>
                                        <span v-for="(hop, idx) in selectedNode.path" :key="idx">
                                            <i class="fa-solid fa-arrow-right text-xs text-slate-600 mx-1"></i>
                                            <span :class="hop === selectedNode.id ? 'text-emerald-400' : 'text-slate-300'">${ getName(hop) }</span>
                                        </span>
                                    </div>
                                </div>
                                <div v-if="selectedNode.links && selectedNode.links.length">
                                    <div class="text-xs font-bold text-slate-400 uppercase mb-2">Direct Links</div>
                                    <table class="w-full text-xs">
                                        <thead class="text-slate-500 border-b border-slate-700">
                                            <tr><th class="text-left py-2">Neighbor</th><th class="text-left">Avg SNR</th><th class="text-left">Count</th></tr>
                                        </thead>
                                        <tbody>
                                            <tr v-for="link in selectedNode.links" :key="link.to" class="border-b border-slate-800/50">
                                                <td class="py-2">${ getName(link.to) }</td>
                                                <td><span :class="snrTextClass(link.snr)">${ link.snr?.toFixed(1) || '?' } dB</span></td>
                                                <td>${ link.count }</td>
                                            </tr>
                                        </tbody>
                                    </table>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>

                <!-- CHARTS -->
                <div class="flex flex-col gap-6">
                    <div class="glass-card p-4">
                        <h3 class="text-xs font-bold text-slate-400 uppercase mb-3 flex items-center">
                            <i class="fa-solid fa-chart-simple mr-2 text-indigo-500"></i> SNR Distribution
                        </h3>
                        <div class="h-32 w-full">
                            <canvas id="snrChart"></canvas>
                        </div>
                    </div>
                    <div class="glass-card p-4">
                        <h3 class="text-xs font-bold text-slate-400 uppercase mb-3 flex items-center">
                            <i class="fa-solid fa-chart-pie mr-2 text-purple-500"></i> Hop Counts
                        </h3>
                        <div class="h-32 w-full">
                            <canvas id="hopChart"></canvas>
                        </div>
                    </div>
                    <div class="glass-card p-4 flex-1">
                        <h3 class="text-xs font-bold text-slate-400 uppercase mb-3 flex items-center">
                            <i class="fa-solid fa-clock mr-2 text-cyan-500"></i> Activity (last 24h)
                        </h3>
                        <div class="h-28 w-full">
                            <canvas id="timelineChart"></canvas>
                        </div>
                    </div>
                </div>
            </div>

            <!-- ========== BOTTOM PANELS ========== -->
            <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
                <!-- Node registry -->
                <div class="glass-card p-4 overflow-hidden flex flex-col" style="max-height: 320px;">
                    <div class="flex items-center justify-between mb-2">
                        <h3 class="text-xs font-bold text-slate-400 uppercase flex items-center gap-2">
                            <i class="fa-solid fa-database"></i> Node Registry
                        </h3>
                        <div class="relative">
                            <i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-slate-500 text-xs"></i>
                            <input type="text" v-model="nodeFilter" placeholder="Filter..." class="bg-slate-800 border border-slate-700 rounded-full py-1 pl-8 pr-3 text-xs text-white focus:outline-none focus:border-indigo-500 w-40">
                        </div>
                    </div>
                    <div class="overflow-auto flex-1">
                        <table class="w-full text-xs">
                            <thead class="text-slate-500 border-b border-slate-800 sticky top-0 bg-slate-900">
                                <tr>
                                    <th class="text-left py-2">Node</th>
                                    <th class="text-left">SNR</th>
                                    <th class="text-left">Hops</th>
                                    <th class="text-left">Last</th>
                                    <th class="text-left">Bat</th>
                                </tr>
                            </thead>
                            <tbody>
                                <tr v-for="node in filteredNodeList" :key="node.node_id" @click="selectNodeById(node.node_id)" class="border-b border-slate-800/50 hover:bg-slate-800/30 cursor-pointer">
                                    <td class="py-2 font-mono text-indigo-300">${ node.short_name || node.node_id.slice(0,8) }</td>
                                    <td><span :class="snrTextClass(node.snr_avg)">${ node.snr_avg?.toFixed(1) || '-' }</span></td>
                                    <td>${ node.hop_count ?? '-' }</td>
                                    <td class="text-slate-500">${ timeAgo(node.last_heard) }</td>
                                    <td><span :class="batTextClass(node.battery_level)">${ node.battery_level || '-' }%</span></td>
                                </tr>
                            </tbody>
                        </table>
                    </div>
                </div>

                <!-- Link quality explorer -->
                <div class="glass-card p-4 overflow-hidden flex flex-col" style="max-height: 320px;">
                    <h3 class="text-xs font-bold text-slate-400 uppercase mb-2 flex items-center gap-2">
                        <i class="fa-solid fa-link"></i> Link Quality
                    </h3>
                    <div class="overflow-auto flex-1">
                        <table class="w-full text-xs">
                            <thead class="text-slate-500 border-b border-slate-800 sticky top-0 bg-slate-900">
                                <tr>
                                    <th class="text-left py-2">From</th>
                                    <th class="text-left">To</th>
                                    <th class="text-left">Avg SNR</th>
                                    <th class="text-left">Count</th>
                                </tr>
                            </thead>
                            <tbody>
                                <tr v-for="link in sortedLinks" :key="link.from+link.to" class="border-b border-slate-800/50 hover:bg-slate-800/30">
                                    <td class="py-2 font-mono">${ getName(link.from) }</td>
                                    <td class="font-mono">${ getName(link.to) }</td>
                                    <td><span :class="snrTextClass(link.snr)">${ link.snr?.toFixed(1) || '?' } dB</span></td>
                                    <td>${ link.count }</td>
                                </tr>
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>
        </template>
    </div>

    <script>
        (function() {
            "use strict";

            // Global error catcher
            window.addEventListener('error', function(e) {
                console.error('Caught global error:', e.error || e.message);
                const appDiv = document.getElementById('app');
                if (appDiv) {
                    appDiv.innerHTML = `<div class="error-banner">
                        <i class="fa-solid fa-triangle-exclamation mr-2"></i>
                        JavaScript error: ${e.message}. Check console.
                    </div>`;
                }
            });

            // Wait for DOM ready
            document.addEventListener('DOMContentLoaded', function() {
                try {
                    const { createApp, ref, computed, onMounted, nextTick } = Vue;

                    const app = createApp({
                        delimiters: ['${', '}'],   // ✅ NO JINJA2 CONFLICT
                        setup() {
                            // ---------- REACTIVE STATE ----------
                            const stats = ref({});
                            const nodes = ref([]);
                            const topology = ref({ nodes: [], links: [] });
                            const timeline = ref([]);
                            const selectedNode = ref(null);
                            const nodeFilter = ref('');
                            const fatalError = ref(null);

                            let snrChart = null, hopChart = null, timelineChart = null;

                            // ---------- FETCH ALL DATA ----------
                            const fetchAll = async () => {
                                try {
                                    const [statsRes, nodesRes, topoRes, timelineRes] = await Promise.all([
                                        fetch('/api/network/stats'),
                                        fetch('/api/nodes'),
                                        fetch('/api/network/topology'),
                                        fetch('/api/network/timeline')
                                    ]);

                                    if (!statsRes.ok) throw new Error(`Stats API: ${statsRes.status}`);
                                    if (!nodesRes.ok) throw new Error(`Nodes API: ${nodesRes.status}`);
                                    if (!topoRes.ok) throw new Error(`Topology API: ${topoRes.status}`);
                                    if (!timelineRes.ok) throw new Error(`Timeline API: ${timelineRes.status}`);

                                    stats.value = await statsRes.json();
                                    nodes.value = (await nodesRes.json()).nodes || [];
                                    topology.value = await topoRes.json();
                                    timeline.value = (await timelineRes.json()).timeline || [];

                                    enrichTopology();
                                    nextTick(() => {
                                        drawGraph();
                                        updateCharts();
                                    });
                                } catch (e) {
                                    console.error('fetchAll error:', e);
                                    fatalError.value = `Failed to load network data: ${e.message}`;
                                }
                            };

                            // ---------- ENRICH NODES WITH METADATA ----------
                            const enrichTopology = () => {
                                const nodeMap = new Map();
                                nodes.value.forEach(n => nodeMap.set(n.node_id, n));
                                topology.value.nodes = topology.value.nodes.map(tn => {
                                    const details = nodeMap.get(tn.id) || {};
                                    return {
                                        ...tn,
                                        name: details.long_name || details.short_name || tn.id.slice(0,8),
                                        snr_avg: details.snr_avg,
                                        battery_level: details.battery_level,
                                        role: details.role,
                                        hw_model: details.hw_model,
                                        last_heard: details.last_heard,
                                        hop_count: details.hop_count,
                                        path: details.last_hops ? JSON.parse(details.last_hops) : []
                                    };
                                });
                                const linkMap = new Map();
                                topology.value.links.forEach(link => {
                                    if (!linkMap.has(link.from)) linkMap.set(link.from, []);
                                    linkMap.get(link.from).push({ to: link.to, snr: link.snr, count: link.count });
                                });
                                topology.value.nodes.forEach(node => {
                                    node.links = linkMap.get(node.id) || [];
                                });
                            };

                            // ---------- D3 FORCE GRAPH ----------
                            const drawGraph = () => {
                                const container = document.getElementById('topology-graph');
                                if (!container) return;
                                const w = container.clientWidth;
                                const h = container.clientHeight;
                                d3.select('#topology-graph').selectAll('*').remove();

                                const svg = d3.select('#topology-graph')
                                    .append('svg')
                                    .attr('width', w)
                                    .attr('height', h)
                                    .style('background', '#0f172a')
                                    .style('border-radius', '0.75rem');

                                svg.append('defs').selectAll('marker')
                                    .data(['end'])
                                    .enter().append('marker')
                                    .attr('id', 'arrow')
                                    .attr('viewBox', '0 -5 10 10')
                                    .attr('refX', 20)
                                    .attr('refY', 0)
                                    .attr('markerWidth', 6)
                                    .attr('markerHeight', 6)
                                    .attr('orient', 'auto')
                                    .append('path')
                                    .attr('d', 'M0,-5L10,0L0,5')
                                    .attr('fill', '#64748b');

                                const links = topology.value.links.map(l => ({
                                    source: l.from,
                                    target: l.to,
                                    snr: l.snr,
                                    count: l.count
                                }));
                                const nodesData = topology.value.nodes.map(n => ({ ...n }));

                                const simulation = d3.forceSimulation(nodesData)
                                    .force('link', d3.forceLink(links).id(d => d.id).distance(120))
                                    .force('charge', d3.forceManyBody().strength(-300))
                                    .force('center', d3.forceCenter(w / 2, h / 2))
                                    .force('collision', d3.forceCollide().radius(25));

                                const link = svg.append('g')
                                    .selectAll('line')
                                    .data(links)
                                    .enter().append('line')
                                    .attr('class', 'link')
                                    .attr('stroke', d => getSnrColor(d.snr))
                                    .attr('stroke-width', d => Math.max(1, Math.min(5, d.count / 2)))
                                    .attr('marker-end', d => d.count > 0 ? 'url(#arrow)' : null);

                                const node = svg.append('g')
                                    .selectAll('circle')
                                    .data(nodesData)
                                    .enter().append('circle')
                                    .attr('class', 'node')
                                    .attr('r', 10)
                                    .attr('fill', d => getSnrColor(d.snr_avg))
                                    .attr('stroke', '#fff')
                                    .attr('stroke-width', 1.5)
                                    .on('click', (event, d) => { selectedNode.value = d; })
                                    .on('mouseover', showTooltip)
                                    .on('mouseout', hideTooltip);

                                const label = svg.append('g')
                                    .selectAll('text')
                                    .data(nodesData)
                                    .enter().append('text')
                                    .text(d => d.name || d.id.slice(0,6))
                                    .attr('font-size', '9px')
                                    .attr('fill', '#cbd5e1')
                                    .attr('dy', -15)
                                    .attr('text-anchor', 'middle');

                                simulation.on('tick', () => {
                                    link
                                        .attr('x1', d => d.source.x)
                                        .attr('y1', d => d.source.y)
                                        .attr('x2', d => d.target.x)
                                        .attr('y2', d => d.target.y);
                                    node
                                        .attr('cx', d => d.x)
                                        .attr('cy', d => d.y);
                                    label
                                        .attr('x', d => d.x)
                                        .attr('y', d => d.y);
                                });

                                function showTooltip(event, d) {
                                    d3.select('body').append('div')
                                        .attr('class', 'graph-tooltip')
                                        .style('left', (event.pageX + 10) + 'px')
                                        .style('top', (event.pageY - 40) + 'px')
                                        .html(`
                                            <div class="font-bold">${d.name || d.id}</div>
                                            <div class="text-indigo-300 text-[10px]">${d.id}</div>
                                            <div class="flex justify-between mt-1">
                                                <span>SNR</span> <span class="${snrTextClass(d.snr_avg)}">${d.snr_avg?.toFixed(1) || '?'} dB</span>
                                            </div>
                                            <div class="flex justify-between">
                                                <span>Hops</span> <span>${d.hop_count ?? '?'}</span>
                                            </div>
                                            <div class="flex justify-between">
                                                <span>Battery</span> <span class="${batTextClass(d.battery_level)}">${d.battery_level || '?'}%</span>
                                            </div>
                                        `);
                                }
                                function hideTooltip() {
                                    d3.selectAll('.graph-tooltip').remove();
                                }
                            };

                            // ---------- CHARTS ----------
                            const updateCharts = () => {
                                // SNR
                                const snrCtx = document.getElementById('snrChart')?.getContext('2d');
                                if (snrCtx) {
                                    const snrValues = nodes.value.map(n => n.snr_avg).filter(v => v !== null);
                                    const buckets = { '>5dB': 0, '0-5dB': 0, '-10-0dB': 0, '<-10dB': 0 };
                                    snrValues.forEach(s => {
                                        if (s > 5) buckets['>5dB']++;
                                        else if (s > 0) buckets['0-5dB']++;
                                        else if (s > -10) buckets['-10-0dB']++;
                                        else buckets['<-10dB']++;
                                    });
                                    if (snrChart) snrChart.destroy();
                                    snrChart = new Chart(snrCtx, {
                                        type: 'bar',
                                        data: {
                                            labels: Object.keys(buckets),
                                            datasets: [{
                                                data: Object.values(buckets),
                                                backgroundColor: ['#10b981', '#3b82f6', '#f59e0b', '#ef4444'],
                                                borderRadius: 4
                                            }]
                                        },
                                        options: {
                                            responsive: true, maintainAspectRatio: false,
                                            plugins: { legend: { display: false } },
                                            scales: { y: { display: false, beginAtZero: true }, x: { ticks: { color: '#94a3b8', font: { size: 9 } }, grid: { display: false } } }
                                        }
                                    });
                                }

                                // Hop counts
                                const hopCtx = document.getElementById('hopChart')?.getContext('2d');
                                if (hopCtx) {
                                    const hopCounts = {};
                                    nodes.value.forEach(n => {
                                        let h = n.hop_count ?? 0;
                                        const key = h === 0 ? 'Direct' : (h + ' Hop' + (h > 1 ? 's' : ''));
                                        hopCounts[key] = (hopCounts[key] || 0) + 1;
                                    });
                                    if (hopChart) hopChart.destroy();
                                    hopChart = new Chart(hopCtx, {
                                        type: 'doughnut',
                                        data: {
                                            labels: Object.keys(hopCounts),
                                            datasets: [{
                                                data: Object.values(hopCounts),
                                                backgroundColor: ['#6366f1', '#8b5cf6', '#a855f7', '#d946ef', '#ec4899'],
                                                borderWidth: 0
                                            }]
                                        },
                                        options: {
                                            responsive: true, maintainAspectRatio: false,
                                            plugins: { legend: { position: 'right', labels: { color: '#94a3b8', font: { size: 9 }, boxWidth: 8 } } },
                                            cutout: '70%'
                                        }
                                    });
                                }

                                // Timeline
                                const timelineCtx = document.getElementById('timelineChart')?.getContext('2d');
                                if (timelineCtx && timeline.value.length) {
                                    const hours = timeline.value.map(t => t.hour + ':00');
                                    const counts = timeline.value.map(t => t.count);
                                    if (timelineChart) timelineChart.destroy();
                                    timelineChart = new Chart(timelineCtx, {
                                        type: 'line',
                                        data: {
                                            labels: hours,
                                            datasets: [{
                                                data: counts,
                                                borderColor: '#6366f1',
                                                backgroundColor: 'rgba(99,102,241,0.1)',
                                                tension: 0.3,
                                                fill: true,
                                                pointRadius: 2,
                                                pointHoverRadius: 5
                                            }]
                                        },
                                        options: {
                                            responsive: true, maintainAspectRatio: false,
                                            plugins: { legend: { display: false } },
                                            scales: { y: { beginAtZero: true, grid: { color: '#1e293b' }, ticks: { color: '#94a3b8' } }, x: { ticks: { color: '#94a3b8', maxRotation: 0, font: { size: 8 } } } }
                                        }
                                    });
                                }
                            };

                            // ---------- UTILITIES ----------
                            const getSnrColor = (snr) => {
                                if (snr === null || snr === undefined) return '#64748b';
                                if (snr > 5) return '#10b981';
                                if (snr > 0) return '#3b82f6';
                                if (snr > -10) return '#f59e0b';
                                return '#ef4444';
                            };
                            const snrTextClass = (snr) => {
                                if (!snr && snr !== 0) return 'text-slate-500';
                                if (snr > 5) return 'text-emerald-400';
                                if (snr > 0) return 'text-blue-400';
                                if (snr > -10) return 'text-yellow-400';
                                return 'text-red-400';
                            };
                            const batTextClass = (lvl) => {
                                if (!lvl) return 'text-slate-500';
                                if (lvl > 50) return 'text-emerald-400';
                                if (lvl > 20) return 'text-yellow-400';
                                return 'text-red-400';
                            };
                            const timeAgo = (ts) => {
                                if (!ts) return '-';
                                const s = Math.floor(Date.now() / 1000 - ts);
                                if (s < 60) return 'now';
                                if (s < 3600) return Math.floor(s / 60) + 'm';
                                if (s < 86400) return Math.floor(s / 3600) + 'h';
                                return Math.floor(s / 86400) + 'd';
                            };
                            const getName = (nodeId) => {
                                const n = topology.value.nodes.find(n => n.id === nodeId);
                                return n?.name || nodeId.slice(0, 8);
                            };
                            const selectNodeById = (id) => {
                                const node = topology.value.nodes.find(n => n.id === id);
                                if (node) selectedNode.value = node;
                            };
                            const filteredNodeList = computed(() => {
                                let list = nodes.value;
                                if (nodeFilter.value) {
                                    const q = nodeFilter.value.toLowerCase();
                                    list = list.filter(n => 
                                        (n.long_name && n.long_name.toLowerCase().includes(q)) ||
                                        (n.short_name && n.short_name.toLowerCase().includes(q)) ||
                                        n.node_id.toLowerCase().includes(q)
                                    );
                                }
                                return list.slice(0, 30);
                            });
                            const sortedLinks = computed(() => {
                                return [...topology.value.links]
                                    .sort((a,b) => (b.snr || -100) - (a.snr || -100))
                                    .slice(0, 30);
                            });

                            // ---------- MOUNTED ----------
                            onMounted(() => {
                                fetchAll();
                                setInterval(fetchAll, 15000);
                                window.addEventListener('resize', () => drawGraph());
                            });

                            return {
                                stats,
                                nodes,
                                topology,
                                timeline,
                                selectedNode,
                                nodeFilter,
                                fatalError,
                                filteredNodeList,
                                sortedLinks,
                                getSnrColor,
                                snrTextClass,
                                batTextClass,
                                timeAgo,
                                getName,
                                selectNodeById
                            };
                        }
                    });

                    app.mount('#app');
                } catch (e) {
                    console.error('Vue initialization failed:', e);
                    document.getElementById('app').innerHTML = `<div class="error-banner">
                        <i class="fa-solid fa-triangle-exclamation mr-2"></i>
                        Vue initialization failed: ${e.message}. Check console.
                    </div>`;
                }
            });
        })();
    </script>
</body>
</html>