diff --git a/static/css/index.css b/static/css/index.css index 8866312..fe9e6a4 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -3866,100 +3866,158 @@ header h1 .tagline { to { transform: rotate(360deg); } } -/* WiFi Analysis Panel (RIGHT) */ +/* WiFi Analysis Panel */ .wifi-analysis-panel { display: flex; flex-direction: column; - gap: 10px; - min-width: 0; - overflow: hidden; -} - -.wifi-channel-section, -.wifi-security-section { background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px; - padding: 12px; + overflow: hidden; } -.wifi-channel-section { - flex: 1; -} - -.wifi-channel-section h5, -.wifi-security-section h5 { - margin: 0 0 10px 0; - color: var(--accent-cyan); - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.wifi-channel-tabs { - display: flex; - gap: 4px; - margin-bottom: 10px; -} - -.channel-band-tab { - flex: 1; - padding: 6px 10px; - font-size: 10px; - background: var(--bg-tertiary); - border: 1px solid var(--border-color); - border-radius: 4px; - color: var(--text-dim); - cursor: pointer; - transition: all 0.2s; -} - -.channel-band-tab:hover { - background: var(--bg-secondary); -} - -.channel-band-tab.active { - background: var(--accent-cyan); - color: var(--text-inverse); - border-color: var(--accent-cyan); -} - -.wifi-channel-chart { - min-height: 120px; - overflow-x: auto; - overflow-y: hidden; -} - -.wifi-security-stats { - display: flex; - flex-direction: column; - gap: 6px; -} - -.wifi-security-item { +.wifi-analysis-panel-header { display: flex; align-items: center; - gap: 8px; - font-size: 11px; + justify-content: space-between; + padding: 10px 12px; + background: var(--bg-tertiary); + border-bottom: 1px solid var(--border-color); + flex-shrink: 0; } -.wifi-security-dot { - width: 10px; - height: 10px; +.wifi-analysis-panel-header .panel-title { + color: var(--accent-cyan); + font-size: 10px; + letter-spacing: 1.5px; + text-transform: uppercase; +} + +.wifi-detail-back-btn { + font-family: inherit; + font-size: 9px; + color: var(--text-dim); + background: none; + border: 1px solid var(--border-color); + border-radius: 3px; + padding: 2px 8px; + cursor: pointer; + transition: color 0.15s; +} + +.wifi-detail-back-btn:hover { color: var(--text-primary); } + +/* Heatmap */ +.wifi-heatmap-wrap { + padding: 10px 12px; + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; + overflow: hidden; +} + +.wifi-heatmap-label { + font-size: 9px; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 2px; +} + +.wifi-heatmap-ch-labels { + display: grid; + grid-template-columns: 26px repeat(11, 1fr); + gap: 2px; +} + +.wifi-heatmap-ch-label { + text-align: center; + font-size: 8px; + color: var(--text-dim); +} + +.wifi-heatmap-grid { + display: grid; + grid-template-columns: 26px repeat(11, 1fr); + gap: 2px; + flex: 1; + min-height: 0; +} + +.wifi-heatmap-time-label { + font-size: 8px; + color: var(--text-dim); + display: flex; + align-items: center; + justify-content: flex-end; + padding-right: 4px; +} + +.wifi-heatmap-cell { border-radius: 2px; + min-height: 10px; } -.wifi-security-item.wpa3 .wifi-security-dot { background: var(--accent-green); } -.wifi-security-item.wpa2 .wifi-security-dot { background: var(--accent-cyan); } -.wifi-security-item.wep .wifi-security-dot { background: var(--accent-orange); } -.wifi-security-item.open .wifi-security-dot { background: var(--accent-red); } - -.wifi-security-count { - margin-left: auto; - font-weight: 600; - color: var(--text-primary); +.wifi-heatmap-empty { + grid-column: 1 / -1; + padding: 16px; + text-align: center; + color: var(--text-dim); + font-size: 10px; } +.wifi-heatmap-legend { + display: flex; + align-items: center; + gap: 6px; + font-size: 9px; + color: var(--text-dim); + margin-top: 2px; +} + +.wifi-heatmap-legend-grad { + flex: 1; + height: 6px; + border-radius: 3px; + background: linear-gradient(90deg, #0d1117 0%, #0d4a6e 30%, #0ea5e9 60%, #f97316 80%, #ef4444 100%); +} + +/* Security ring */ +.wifi-security-ring-wrap { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + background: var(--bg-secondary); + border-top: 1px solid var(--border-color); + flex-shrink: 0; +} + +.wifi-security-ring-legend { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +} + +.wifi-security-ring-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 10px; +} + +.wifi-security-ring-dot { + width: 7px; + height: 7px; + border-radius: 1px; + flex-shrink: 0; +} + +.wifi-security-ring-name { color: var(--text-dim); flex: 1; } +.wifi-security-ring-count { color: var(--text-primary); font-weight: 600; } + /* WiFi Detail Drawer */ .wifi-detail-drawer { display: none; diff --git a/static/js/modes/wifi.js b/static/js/modes/wifi.js index 8fb44c4..7e28408 100644 --- a/static/js/modes/wifi.js +++ b/static/js/modes/wifi.js @@ -119,6 +119,7 @@ const WiFiMode = (function() { let probeRequests = []; let channelStats = []; let recommendations = []; + let channelHistory = []; // max 10 entries, each { timestamp, channels: {1:N,...,11:N} } // UI state let selectedBssid = null; @@ -167,7 +168,7 @@ const WiFiMode = (function() { initScanModeTabs(); initNetworkFilters(); initSortControls(); - initChannelChart(); + initHeatmap(); scheduleRender({ table: true, stats: true, radar: true, chart: true }); // Check if already scanning @@ -199,9 +200,16 @@ const WiFiMode = (function() { networkList: document.getElementById('wifiNetworkList'), networkFilters: document.getElementById('wifiNetworkFilters'), - // Visualizations - channelChart: document.getElementById('wifiChannelChart'), - channelBandTabs: document.getElementById('wifiChannelBandTabs'), + // Visualizations — heatmap & security ring + heatmapGrid: document.getElementById('wifiHeatmapGrid'), + heatmapChLabels: document.getElementById('wifiHeatmapChLabels'), + heatmapCount: document.getElementById('wifiHeatmapCount'), + securityRingSvg: document.getElementById('wifiSecurityRingSvg'), + securityRingLegend: document.getElementById('wifiSecurityRingLegend'), + heatmapView: document.getElementById('wifiHeatmapView'), + detailView: document.getElementById('wifiDetailView'), + rightPanelTitle: document.getElementById('wifiRightPanelTitle'), + detailBackBtn: document.getElementById('wifiDetailBackBtn'), // Zone summary zoneImmediate: document.getElementById('wifiZoneImmediate'), @@ -1076,7 +1084,6 @@ const WiFiMode = (function() { if (pendingRender.table) renderNetworks(); if (pendingRender.stats) updateStats(); if (pendingRender.radar) renderRadar(Array.from(networks.values())); - if (pendingRender.chart) updateChannelChart(); if (pendingRender.detail && selectedBssid) { updateDetailPanel(selectedBssid, { refreshClients: false }); } @@ -1092,6 +1099,18 @@ const WiFiMode = (function() { function renderNetworks() { if (!elements.networkList) return; + // Snapshot 2.4 GHz channel utilisation (use all networks, not filtered) + const snapshot = { timestamp: Date.now(), channels: {} }; + for (let ch = 1; ch <= 11; ch++) snapshot.channels[ch] = 0; + Array.from(networks.values()) + .filter(n => n.band && n.band.startsWith('2.4')) + .forEach(n => { + const ch = parseInt(n.channel); + if (ch >= 1 && ch <= 11) snapshot.channels[ch]++; + }); + channelHistory.unshift(snapshot); + if (channelHistory.length > 10) channelHistory.pop(); + // Filter networks let filtered = Array.from(networks.values()); @@ -1162,6 +1181,9 @@ const WiFiMode = (function() { const sel = elements.networkList.querySelector(`[data-bssid="${CSS.escape(selectedBssid)}"]`); if (sel) sel.classList.add('selected'); } + + renderHeatmap(); + renderSecurityRing(Array.from(networks.values())); } function createNetworkRow(network) { @@ -1549,81 +1571,101 @@ const WiFiMode = (function() { // Channel Chart // ========================================================================== - function initChannelChart() { - if (!elements.channelChart) return; + function initHeatmap() { + if (!elements.heatmapChLabels) return; + // Time-label placeholder + 11 channel labels + elements.heatmapChLabels.innerHTML = + '
' + + [1,2,3,4,5,6,7,8,9,10,11].map(ch => + `
${ch}
` + ).join(''); + } - // Initialize channel chart component - if (typeof ChannelChart !== 'undefined') { - ChannelChart.init('wifiChannelChart'); + function renderHeatmap() { + if (!elements.heatmapGrid) return; + + if (channelHistory.length === 0) { + elements.heatmapGrid.innerHTML = + '
Scan to populate channel history
'; + if (elements.heatmapCount) elements.heatmapCount.textContent = '0'; + return; } - // Band tabs - if (elements.channelBandTabs) { - elements.channelBandTabs.addEventListener('click', (e) => { - if (e.target.matches('.channel-band-tab')) { - const band = e.target.dataset.band; - elements.channelBandTabs.querySelectorAll('.channel-band-tab').forEach(t => { - t.classList.toggle('active', t.dataset.band === band); - }); - updateChannelChart(band); - } + if (elements.heatmapCount) elements.heatmapCount.textContent = channelHistory.length; + + // Find max value for colour scale + let maxVal = 1; + channelHistory.forEach(snap => { + Object.values(snap.channels).forEach(v => { if (v > maxVal) maxVal = v; }); + }); + + const rows = channelHistory.map((snap, i) => { + const timeLabel = i === 0 ? 'now' : ''; + const cells = [1,2,3,4,5,6,7,8,9,10,11].map(ch => { + const v = snap.channels[ch] || 0; + return `
`; }); - } + return `
${timeLabel}
${cells.join('')}`; + }); + + elements.heatmapGrid.innerHTML = rows.join(''); } - function calculateChannelStats() { - // Calculate channel stats from current networks - const stats = {}; - const networksList = Array.from(networks.values()); - - // Initialize all channels - // 2.4 GHz: channels 1-13 - for (let ch = 1; ch <= 13; ch++) { - stats[ch] = { channel: ch, band: '2.4GHz', ap_count: 0, client_count: 0, utilization_score: 0 }; - } - // 5 GHz: common channels - [36, 40, 44, 48, 52, 56, 60, 64, 100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144, 149, 153, 157, 161, 165].forEach(ch => { - stats[ch] = { channel: ch, band: '5GHz', ap_count: 0, client_count: 0, utilization_score: 0 }; - }); - - // Count APs per channel - networksList.forEach(net => { - const ch = parseInt(net.channel); - if (stats[ch]) { - stats[ch].ap_count++; - stats[ch].client_count += (net.client_count || 0); - } - }); - - // Calculate utilization score (0-1) - const maxAPs = Math.max(1, ...Object.values(stats).map(s => s.ap_count)); - Object.values(stats).forEach(s => { - s.utilization_score = s.ap_count / maxAPs; - }); - - return Object.values(stats).filter(s => s.ap_count > 0 || [1, 6, 11, 36, 40, 44, 48, 149, 153, 157, 161, 165].includes(s.channel)); + function congestionColor(value, maxValue) { + if (value === 0 || maxValue === 0) return '#0d1117'; + const ratio = value / maxValue; + if (ratio < 0.05) return '#0d1117'; + if (ratio < 0.25) return `rgba(13,74,110,${(ratio * 4).toFixed(2)})`; + if (ratio < 0.5) return `rgba(14,165,233,${ratio.toFixed(2)})`; + if (ratio < 0.75) return `rgba(249,115,22,${ratio.toFixed(2)})`; + return `rgba(239,68,68,${ratio.toFixed(2)})`; } - function updateChannelChart(band) { - if (typeof ChannelChart === 'undefined') return; + function renderSecurityRing(networksList) { + const svg = elements.securityRingSvg; + const legend = elements.securityRingLegend; + if (!svg || !legend) return; - // Use the currently active band tab if no band specified - if (!band) { - const activeTab = elements.channelBandTabs && elements.channelBandTabs.querySelector('.channel-band-tab.active'); - band = activeTab ? activeTab.dataset.band : '2.4'; - } + const C = 2 * Math.PI * 15; // circumference ≈ 94.25 + const sec = networksList.reduce((acc, n) => { + const s = (n.security || '').toLowerCase(); + if (s.includes('wpa3')) acc.wpa3++; + else if (s.includes('wpa')) acc.wpa2++; + else if (s.includes('wep')) acc.wep++; + else acc.open++; + return acc; + }, { wpa2: 0, open: 0, wpa3: 0, wep: 0 }); - // Recalculate channel stats from networks if needed - if (channelStats.length === 0 && networks.size > 0) { - channelStats = calculateChannelStats(); - } + const total = networksList.length || 1; + const segments = [ + { label: 'WPA2', color: '#38c180', count: sec.wpa2 }, + { label: 'Open', color: '#e25d5d', count: sec.open }, + { label: 'WPA3', color: '#4aa3ff', count: sec.wpa3 }, + { label: 'WEP', color: '#d6a85e', count: sec.wep }, + ]; - // Filter stats by band - const bandFilter = band === '2.4' ? '2.4GHz' : band === '5' ? '5GHz' : '6GHz'; - const filteredStats = channelStats.filter(s => s.band === bandFilter); - const filteredRecs = recommendations.filter(r => r.band === bandFilter); + let offset = 0; + const arcs = segments.map(seg => { + const arcLen = (seg.count / total) * C; + const arc = ``; + offset += arcLen; + return arc; + }); - ChannelChart.update(filteredStats, filteredRecs); + svg.innerHTML = arcs.join('') + + ''; + + legend.innerHTML = segments.map(seg => ` +
+
+ ${seg.label} + ${seg.count} +
+ `).join(''); } // ========================================================================== diff --git a/templates/index.html b/templates/index.html index fdc14be..3912819 100644 --- a/templates/index.html +++ b/templates/index.html @@ -936,40 +936,41 @@ - +
-
-
Channel Analysis
-
- - -
-
+
+ Channel Heatmap +
-
-
Security Overview
-
-
- - WPA3 - 0 + + +
+
+
+ 2.4 GHz · Last 0 scans
-
- - WPA2 - 0 +
+
-
- - WEP - 0 -
-
- - Open - 0 +
+
+ Low +
+ High
+
+ + + +
+
+
+ + +