<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Meshtastic Command Center · Signal Heatmap</title>
    
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
    <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
    
    <style>
        [v-cloak] { display: none; }
        
        :root {
            --bg-dark: #020617;
            --panel-bg: #0f172a;
            --accent: #6366f1;
            --success: #10b981;
            --warning: #f59e0b;
            --danger: #ef4444;
        }

        body { 
            background-color: var(--bg-dark); 
            color: #e2e8f0; 
            font-family: 'Segoe UI', system-ui, sans-serif;
        }

        /* Glassmorphism Panels */
        .glass-panel {
            background: rgba(15, 23, 42, 0.7);
            backdrop-filter: blur(12px);
            -webkit-backdrop-filter: blur(12px);
            border: 1px solid rgba(255, 255, 255, 0.05);
            box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
            border-radius: 1rem;
        }

        /* Map Styling */
        #map {
            height: 65vh;
            width: 100%;
            border-radius: 0.75rem;
            z-index: 1;
            background: #0f172a;
        }

        /* Dark Mode Map Filter */
        .leaflet-layer, 
        .leaflet-control-zoom-in, 
        .leaflet-control-zoom-out, 
        .leaflet-control-attribution {
            filter: invert(100%) hue-rotate(180deg) brightness(95%) contrast(90%);
        }
        
        /* Un-invert specific elements */
        .leaflet-marker-icon, 
        .leaflet-popup-content-wrapper, 
        .leaflet-tooltip,
        .custom-overlay-pane {
            filter: invert(100%) hue-rotate(180deg);
        }

        .leaflet-popup-content-wrapper {
            background: var(--panel-bg);
            color: #f8fafc;
            border: 1px solid var(--accent);
            border-radius: 0.5rem;
        }
        .leaflet-popup-tip { background: var(--panel-bg); border: 1px solid var(--accent); border-top: none; border-left: none; }

        /* Data Grid */
        .data-grid th { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: #94a3b8; padding: 0.75rem; }
        .data-grid td { padding: 0.75rem; border-top: 1px solid #1e293b; font-size: 0.875rem; }
        .data-grid tr:hover { background: rgba(99, 102, 241, 0.05); }

        /* Scrollbars */
        ::-webkit-scrollbar { width: 8px; height: 8px; }
        ::-webkit-scrollbar-track { background: #0f172a; }
        ::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
        ::-webkit-scrollbar-thumb:hover { background: #475569; }
    </style>
</head>
<body class="h-screen flex flex-col overflow-hidden">
    <div id="app" v-cloak class="flex flex-col h-full">
    {% raw %}
        
        <header class="h-16 shrink-0 border-b border-slate-800 bg-slate-900/50 flex items-center justify-between px-6">
            <div class="flex items-center gap-3">
                <div class="w-8 h-8 rounded bg-indigo-600 flex items-center justify-center shadow-lg shadow-indigo-500/30">
                    <i class="fa-solid fa-network-wired text-white"></i>
                </div>
                <div>
                    <h1 class="font-bold text-lg leading-tight bg-gradient-to-r from-indigo-400 to-cyan-400 bg-clip-text text-transparent">
                        Meshtastic LQM
                    </h1>
                    <div class="text-[10px] text-slate-400 tracking-wider uppercase font-bold">Topology & Coverage Analyzer</div>
                </div>
            </div>

            <div class="flex gap-6">
                <div class="text-right">
                    <div class="text-2xl font-bold text-white leading-none">{{ nodes.length }}</div>
                    <div class="text-[10px] text-slate-500 font-bold uppercase">Nodes</div>
                </div>
                <div class="w-px h-8 bg-slate-800"></div>
                <div class="text-right">
                    <div class="text-2xl font-bold text-emerald-400 leading-none">{{ stats.traces?.successful || 0 }}</div>
                    <div class="text-[10px] text-slate-500 font-bold uppercase">Traces</div>
                </div>
                <div class="w-px h-8 bg-slate-800"></div>
                <div class="text-right">
                    <div class="text-2xl font-bold text-indigo-400 leading-none">{{ gpsNodeCount }}</div>
                    <div class="text-[10px] text-slate-500 font-bold uppercase">Mapped</div>
                </div>
            </div>

            <a href="/" class="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 rounded text-sm text-slate-300 transition-colors border border-slate-700">
                <i class="fa-solid fa-xmark mr-1"></i> Close
            </a>
        </header>

        <main class="flex-1 overflow-hidden flex gap-4 p-4">
            
            <!-- LEFT COLUMN (MAP + NODE TABLE) -->
            <div class="flex-1 flex flex-col gap-4 min-w-0">
                <div class="glass-panel p-1 relative flex-1 flex flex-col">
                    <!-- Map overlay controls -->
                    <div class="absolute top-4 left-4 z-[500] flex gap-2">
                        <div class="bg-slate-900/90 backdrop-blur border border-slate-700 px-3 py-1.5 rounded text-xs font-bold shadow-xl flex items-center gap-2">
                            <span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span> Live
                        </div>
                        <!-- NEW: Heatmap Toggle Button -->
                        <button @click="toggleHeatmap" 
                                class="bg-slate-900/90 backdrop-blur border border-slate-700 px-3 py-1.5 rounded text-xs font-bold shadow-xl flex items-center gap-2 hover:bg-slate-800 transition-colors"
                                :class="showHeatmap ? 'text-indigo-400 border-indigo-500/50' : 'text-slate-400'">
                            <i class="fa-solid fa-fire"></i>
                            {{ showHeatmap ? 'Hide Heatmap' : 'Show Heatmap' }}
                        </button>
                    </div>

                    <!-- Signal Legend (existing) -->
                    <div class="absolute bottom-6 right-4 z-[500] bg-slate-900/95 backdrop-blur border border-slate-700 p-3 rounded-lg text-xs shadow-2xl w-56 pointer-events-none">
                        <div class="font-bold mb-2 text-slate-300 border-b border-slate-700 pb-1">Link Quality (SNR)</div>
                        <div class="h-2 w-full rounded-full bg-gradient-to-r from-red-500 via-yellow-500 to-emerald-500 mb-1"></div>
                        <div class="flex justify-between text-[10px] text-slate-500 mb-3">
                            <span>Poor (-20dB)</span>
                            <span>Great (+10dB)</span>
                        </div>
                        <!-- NEW: Heatmap legend hint -->
                        <div class="mt-2 pt-2 border-t border-slate-700">
                            <div class="font-bold mb-1 text-slate-400 text-[10px] uppercase">Signal Grid</div>
                            <div class="flex items-center gap-2 text-[10px]">
                                <span class="w-3 h-3 bg-emerald-500 opacity-30"></span> >5dB
                                <span class="w-3 h-3 bg-blue-500 opacity-30 ml-2"></span> >0dB
                                <span class="w-3 h-3 bg-orange-500 opacity-30 ml-2"></span> >-10dB
                                <span class="w-3 h-3 bg-red-500 opacity-30 ml-2"></span> ≤-10dB
                            </div>
                        </div>
                    </div>

                    <div id="map" class="flex-1 rounded-lg"></div>
                </div>

                <!-- Node Database Table (unchanged) -->
                <div class="glass-panel h-64 flex flex-col overflow-hidden">
                    <div class="px-4 py-2 border-b border-slate-800 flex justify-between items-center bg-slate-900/50">
                        <h3 class="font-bold text-sm text-slate-300 uppercase tracking-wider"><i class="fa-solid fa-database mr-2 text-indigo-500"></i>Node Database</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="searchQuery" placeholder="Filter nodes..." 
                                   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 transition-colors w-48">
                        </div>
                    </div>
                    <div class="flex-1 overflow-auto">
                        <table class="w-full text-left border-collapse data-grid">
                            <thead class="sticky top-0 bg-slate-900 shadow-sm z-10">
                                <tr>
                                    <th>ID / Name</th>
                                    <th>Role / HW</th>
                                    <th>Metrics</th>
                                    <th>Signal (Avg)</th>
                                    <th>Hops</th>
                                    <th>Last Seen</th>
                                </tr>
                            </thead>
                            <tbody>
                                <tr v-for="node in filteredNodes" :key="node.node_id">
                                    <td class="font-mono text-indigo-300">
                                        <div class="font-bold text-white">{{ node.short_name || 'UNK' }}</div>
                                        <div class="text-[10px] opacity-70">{{ node.node_id }}</div>
                                    </td>
                                    <td>
                                        <div class="text-xs font-bold text-slate-300">{{ node.role || 'CLIENT' }}</div>
                                        <div class="text-[10px] text-slate-500">{{ node.hw_model || 'Unknown' }}</div>
                                    </td>
                                    <td>
                                        <div class="flex gap-2 text-xs">
                                            <span v-if="node.battery_level" class="px-1.5 py-0.5 rounded bg-slate-800 border border-slate-700" :class="getBatColorClass(node.battery_level)">
                                                <i class="fa-solid fa-battery-half mr-1"></i>{{ node.battery_level }}%
                                            </span>
                                            <span v-if="node.voltage" class="px-1.5 py-0.5 rounded bg-slate-800 border border-slate-700 text-slate-400">
                                                {{ node.voltage.toFixed(2) }}V
                                            </span>
                                        </div>
                                    </td>
                                    <td>
                                        <div class="flex items-center gap-2" v-if="node.snr_avg !== null">
                                            <div class="h-1.5 w-16 bg-slate-800 rounded-full overflow-hidden">
                                                <div class="h-full" :class="getSnrColorClass(node.snr_avg)" :style="{width: getSnrPercent(node.snr_avg)}"></div>
                                            </div>
                                            <span class="font-mono font-bold text-xs" :class="getSnrTextClass(node.snr_avg)">{{ node.snr_avg.toFixed(1) }}</span>
                                        </div>
                                        <span v-else class="text-xs text-slate-600">-</span>
                                    </td>
                                    <td>
                                        <span v-if="node.hop_count !== null" class="font-mono font-bold text-xs px-2 py-0.5 rounded bg-slate-800 border border-slate-700">
                                            {{ node.hop_count }}
                                        </span>
                                        <span v-else class="text-xs text-slate-600">-</span>
                                    </td>
                                    <td class="text-xs font-mono text-slate-400">
                                        {{ timeAgo(node.last_heard) }}
                                    </td>
                                </tr>
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>

            <!-- RIGHT COLUMN (Charts & Stats) unchanged -->
            <div class="w-80 flex flex-col gap-4">
                <div class="glass-panel p-4">
                    <h3 class="text-xs font-bold text-slate-400 uppercase mb-3 tracking-wider">Signal Distribution</h3>
                    <div class="h-32 w-full">
                        <canvas id="snrChart"></canvas>
                    </div>
                </div>
                <div class="glass-panel p-4">
                    <h3 class="text-xs font-bold text-slate-400 uppercase mb-3 tracking-wider">Hop Counts</h3>
                    <div class="h-32 w-full">
                        <canvas id="hopChart"></canvas>
                    </div>
                </div>
                <div class="glass-panel p-4 flex-1">
                    <h3 class="text-xs font-bold text-slate-400 uppercase mb-3 tracking-wider">Network Health</h3>
                    <div class="space-y-3 text-sm">
                        <div class="flex justify-between p-2 bg-slate-800/50 rounded border border-slate-700/50">
                            <span class="text-slate-400">Avg SNR</span>
                            <span class="font-bold text-emerald-400">{{ stats.signal?.avg_snr || 0 }} dB</span>
                        </div>
                        <div class="flex justify-between p-2 bg-slate-800/50 rounded border border-slate-700/50">
                            <span class="text-slate-400">Avg Hops</span>
                            <span class="font-bold text-purple-400">{{ stats.traces?.avg_hops || 0 }}</span>
                        </div>
                        <div class="flex justify-between p-2 bg-slate-800/50 rounded border border-slate-700/50">
                            <span class="text-slate-400">Direct Neighbors</span>
                            <span class="font-bold text-indigo-400">{{ stats.nodes?.neighbors || 0 }}</span>
                        </div>
                        <div class="flex justify-between p-2 bg-slate-800/50 rounded border border-slate-700/50">
                            <span class="text-slate-400">Active (1hr)</span>
                            <span class="font-bold text-white">{{ stats.nodes?.active_1h || 0 }}</span>
                        </div>
                        <div class="flex justify-between p-2 bg-slate-800/50 rounded border border-slate-700/50">
                            <span class="text-slate-400">Low Battery</span>
                            <span class="font-bold text-red-400">{{ stats.battery?.low_count || 0 }}</span>
                        </div>
                    </div>
                </div>
            </div>
        </main>

    {% endraw %}
    </div>

    <script>
        const { createApp, nextTick, computed, ref } = Vue;

        createApp({
            setup() {
                const stats = ref({});
                const nodes = ref([]);
                const searchQuery = ref("");
                const map = ref(null);
                const routeLayers = [];
                const charts = { snr: null, hops: null };

                // ---------- HEATMAP GRID ----------
                const coverageGrid = ref([]);        // raw grid data from backend
                const heatmapLayer = ref(null);      // Leaflet layer group
                const showHeatmap = ref(true);       // toggle state
                
                // Config
                const homeLat = 50.84512;
                const homeLon = -1.07275;

                // --- DATA FETCHING (now includes coverage) ---
                const loadData = async () => {
                    try {
                        const [statsRes, nodesRes, coverageRes] = await Promise.all([
                            fetch('/api/network/stats'),
                            fetch('/api/nodes'),
                            fetch('/api/map/coverage')   // NEW: fetch signal grid
                        ]);
                        stats.value = await statsRes.json();
                        const nodesData = await nodesRes.json();
                        nodes.value = nodesData.nodes || [];
                        const coverageData = await coverageRes.json();
                        coverageGrid.value = coverageData.grid || [];

                        await nextTick();
                        initMap();
                        drawTopology();
                        drawHeatmap();                // NEW: draw the heatmap grid
                        updateCharts();
                    } catch (e) { console.error("Load Error", e); }
                };

                // --- COMPUTED ---
                const filteredNodes = computed(() => {
                    if (!searchQuery.value) return nodes.value;
                    const q = searchQuery.value.toLowerCase();
                    return nodes.value.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)
                    );
                });

                const gpsNodeCount = computed(() => nodes.value.filter(n => n.latitude && n.longitude).length);

                // --- MAP INITIALIZATION ---
                const initMap = () => {
                    if (map.value) return;
                    if (!document.getElementById('map')) return;

                    map.value = L.map('map', { zoomControl: false }).setView([homeLat, homeLon], 11);
                    L.control.zoom({ position: 'topleft' }).addTo(map.value);
                    
                    L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
                        attribution: '&copy; OpenStreetMap &copy; CARTO',
                        maxZoom: 20
                    }).addTo(map.value);

                    // Add Home
                    const homeIcon = L.divIcon({
                        className: 'custom-home-icon',
                        html: `<div style="background:#6366f1; width:24px; height:24px; border-radius:50%; border:3px solid white; box-shadow:0 0 15px #6366f1; display:flex; align-items:center; justify-content:center;"><i class="fa-solid fa-house" style="color:white; font-size:10px;"></i></div>`,
                        iconSize: [24, 24], iconAnchor: [12, 12]
                    });
                    L.marker([homeLat, homeLon], { icon: homeIcon }).addTo(map.value).bindPopup('<b>Home Base</b>');

                    // Create heatmap layer group (initially hidden, will be toggled)
                    heatmapLayer.value = L.layerGroup().addTo(map.value);
                    if (!showHeatmap.value) {
                        map.value.removeLayer(heatmapLayer.value);
                    }
                };

                // --- NEW: DRAW HEATMAP GRID ---
                const drawHeatmap = () => {
                    if (!map.value || !heatmapLayer.value) return;

                    // Clear previous rectangles
                    heatmapLayer.value.clearLayers();

                    // Add rectangles for each grid cell
                    coverageGrid.value.forEach(cell => {
                        const { lat, lon, snr } = cell;
                        if (lat === undefined || lon === undefined || snr === undefined) return;

                        // Determine color based on SNR thresholds
                        let color;
                        if (snr > 5) color = '#10b981';      // green
                        else if (snr > 0) color = '#3b82f6'; // blue
                        else if (snr > -10) color = '#f59e0b'; // orange
                        else color = '#ef4444';              // red

                        // 100m approx box (0.0005 deg each side)
                        const bounds = [
                            [lat - 0.0005, lon - 0.0005],
                            [lat + 0.0005, lon + 0.0005]
                        ];

                        L.rectangle(bounds, {
                            color: color,
                            weight: 0,
                            fillOpacity: 0.3,
                            interactive: false,
                            className: 'signal-grid-cell'
                        }).addTo(heatmapLayer.value);
                    });
                };

                // --- TOGGLE HEATMAP VISIBILITY ---
                const toggleHeatmap = () => {
                    showHeatmap.value = !showHeatmap.value;
                    if (map.value && heatmapLayer.value) {
                        if (showHeatmap.value) {
                            map.value.addLayer(heatmapLayer.value);
                        } else {
                            map.value.removeLayer(heatmapLayer.value);
                        }
                    }
                };

                // --- TOPOLOGY DRAWING (unchanged, but uses heatmap layer separately) ---
                const drawTopology = () => {
                    if (!map.value) return;
                    
                    // Clear route layers
                    routeLayers.forEach(l => map.value.removeLayer(l));
                    routeLayers.length = 0;

                    const locLookup = { 'HOME': [homeLat, homeLon] };
                    const nameLookup = {};
                    nodes.value.forEach(n => {
                        if (n.latitude && n.longitude) locLookup[n.node_id] = [n.latitude, n.longitude];
                        nameLookup[n.node_id] = n.short_name || n.node_id;
                    });

                    nodes.value.forEach(target => {
                        if (!target.latitude || !target.longitude) return;

                        let hops = [];
                        try { hops = JSON.parse(target.last_hops || '[]'); } catch(e) {}
                        
                        const pathData = [{ id: 'HOME', loc: locLookup['HOME'] }];
                        hops.forEach(h => { if(locLookup[h]) pathData.push({ id: h, loc: locLookup[h] }); });
                        
                        const last = pathData[pathData.length-1];
                        if (last.id !== target.node_id) pathData.push({ id: target.node_id, loc: locLookup[target.node_id] });

                        for (let i = 0; i < pathData.length - 1; i++) {
                            const start = pathData[i];
                            const end = pathData[i+1];
                            const color = getLinkColor(target.snr_avg);
                            
                            const poly = L.polyline([start.loc, end.loc], {
                                color: color, weight: 3, opacity: 0.7, className: 'custom-overlay-pane'
                            }).addTo(map.value);
                            routeLayers.push(poly);
                        }

                        // Hop markers
                        for (let i = 1; i < pathData.length - 1; i++) {
                            const pt = pathData[i];
                            const m = L.divIcon({
                                html: `<div style="width:8px; height:8px; background:#0f172a; border:2px solid #94a3b8; transform:rotate(45deg);"></div>`,
                                iconSize: [8,8]
                            });
                            const hopMarker = L.marker(pt.loc, {icon: m}).addTo(map.value);
                            hopMarker.bindTooltip(`Hop: ${nameLookup[pt.id]}`, {direction:'top'});
                            routeLayers.push(hopMarker);
                        }

                        // Target marker
                        const tIcon = L.divIcon({
                            html: `<div style="width:12px; height:12px; background:${getLinkColor(target.snr_avg)}; border:2px solid white; border-radius:50%;"></div>`,
                            iconSize: [12,12]
                        });
                        const tMarker = L.marker(locLookup[target.node_id], {icon: tIcon}).addTo(map.value);
                        
                        const popup = `
                            <div class="min-w-[200px]">
                                <div class="font-bold text-base mb-1" style="color:${getLinkColor(target.snr_avg)}">${target.long_name || target.short_name}</div>
                                <div class="text-xs text-slate-400 mb-2 font-mono">${target.node_id}</div>
                                <div class="grid grid-cols-2 gap-2 text-xs bg-slate-800 p-2 rounded border border-slate-700">
                                    <div>SNR: <b class="text-white">${target.snr_avg?.toFixed(1) || '?'} dB</b></div>
                                    <div>Bat: <b class="text-white">${target.battery_level || '?'}%</b></div>
                                    <div>Hops: <b class="text-white">${hops.length}</b></div>
                                    <div>Model: <b class="text-white">${target.hw_model || '?'}</b></div>
                                </div>
                                <div class="mt-2 text-[10px] text-slate-500 uppercase font-bold">Path</div>
                                <div class="text-xs text-slate-300">
                                    <span class="text-indigo-400">HOME</span> ➜ 
                                    ${hops.map(h => nameLookup[h] || 'UNK').join(' ➜ ')} 
                                    ➜ <span class="text-emerald-400">TARGET</span>
                                </div>
                            </div>
                        `;
                        tMarker.bindPopup(popup);
                        tMarker.bindTooltip(target.short_name, {direction:'top', offset:[0,-8], permanent: false});
                        routeLayers.push(tMarker);
                    });
                };

                // --- HELPERS (unchanged) ---
                const getLinkColor = (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 getSnrPercent = (snr) => {
                    const p = Math.min(Math.max((snr + 20) / 40 * 100, 5), 100);
                    return p + '%';
                };

                const getSnrColorClass = (snr) => {
                    if (snr >= 5) return 'bg-emerald-500';
                    if (snr >= 0) return 'bg-blue-500';
                    if (snr >= -10) return 'bg-yellow-500';
                    return 'bg-red-500';
                };
                
                const getSnrTextClass = (snr) => {
                    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 getBatColorClass = (lvl) => {
                    if (lvl > 50) return 'text-emerald-400 border-emerald-500/30';
                    if (lvl > 20) return 'text-yellow-400 border-yellow-500/30';
                    return 'text-red-400 border-red-500/30';
                };

                const timeAgo = (ts) => {
                    if (!ts) return '-';
                    const s = Math.floor(Date.now()/1000 - ts);
                    if (s < 60) return 'Just now';
                    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';
                };

                // --- CHARTS (unchanged) ---
                const updateCharts = () => {
                    if (!nodes.value.length) return;
                    
                    const snrCtx = document.getElementById('snrChart');
                    if (snrCtx && !charts.snr) {
                        const data = nodes.value.map(n => n.snr_avg).filter(n => n !== null);
                        const buckets = { 'Excellent (>5)': 0, 'Good (0-5)': 0, 'Fair (-10-0)': 0, 'Poor (<-10)': 0 };
                        data.forEach(s => {
                            if(s > 5) buckets['Excellent (>5)']++;
                            else if(s > 0) buckets['Good (0-5)']++;
                            else if(s > -10) buckets['Fair (-10-0)']++;
                            else buckets['Poor (<-10)']++;
                        });
                        
                        charts.snr = new Chart(snrCtx, {
                            type: 'bar',
                            data: {
                                labels: Object.keys(buckets),
                                datasets: [{ label: 'Nodes', data: Object.values(buckets), backgroundColor: ['#10b981', '#3b82f6', '#f59e0b', '#ef4444'], borderRadius: 4 }]
                            },
                            options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { display:false }, x: { ticks: { color: '#64748b', font:{size:10} }, grid: {display:false} } } }
                        });
                    }

                    const hopCtx = document.getElementById('hopChart');
                    if (hopCtx && !charts.hops) {
                        const hopCounts = {};
                        nodes.value.forEach(n => {
                            let h = 0;
                            try { h = JSON.parse(n.last_hops || '[]').length; } catch(e){}
                            if(h===0 && n.is_neighbor) h = 0;
                            const k = h === 0 ? 'Direct' : (h + ' Hops');
                            hopCounts[k] = (hopCounts[k] || 0) + 1;
                        });

                        charts.hops = 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:10}, boxWidth: 10 } } }, cutout: '70%' }
                        });
                    }
                };

                // Init
                setInterval(loadData, 10000);
                loadData();

                return { 
                    stats, nodes, filteredNodes, gpsNodeCount, searchQuery,
                    getBatColorClass, getSnrColorClass, getSnrPercent, getSnrTextClass, timeAgo,
                    showHeatmap, toggleHeatmap   // expose toggle
                };
            }
        }).mount('#app');
    </script>
</body>
</html>