/** * WiFi Mode Controller (v2) * * Unified WiFi scanning with dual-mode architecture: * - Quick Scan: System tools without monitor mode * - Deep Scan: airodump-ng with monitor mode * * Features: * - Proximity radar visualization * - Channel utilization analysis * - Hidden SSID correlation * - Real-time SSE streaming */ const WiFiMode = (function() { 'use strict'; // ========================================================================== // Configuration // ========================================================================== const CONFIG = { apiBase: '/wifi/v2', pollInterval: 5000, keepaliveTimeout: 30000, maxNetworks: 500, maxClients: 500, maxProbes: 1000, }; // ========================================================================== // Agent Support // ========================================================================== /** * Get the API base URL, routing through agent proxy if agent is selected. */ function getApiBase() { if (typeof currentAgent !== 'undefined' && currentAgent !== 'local') { return `/controller/agents/${currentAgent}/wifi/v2`; } return CONFIG.apiBase; } /** * Get the current agent name for tagging data. */ function getCurrentAgentName() { if (typeof currentAgent === 'undefined' || currentAgent === 'local') { return 'Local'; } if (typeof agents !== 'undefined') { const agent = agents.find(a => a.id == currentAgent); return agent ? agent.name : `Agent ${currentAgent}`; } return `Agent ${currentAgent}`; } /** * Check for agent mode conflicts before starting WiFi scan. */ function checkAgentConflicts() { if (typeof currentAgent === 'undefined' || currentAgent === 'local') { return true; } if (typeof checkAgentModeConflict === 'function') { return checkAgentModeConflict('wifi'); } return true; } // ========================================================================== // State // ========================================================================== let isScanning = false; let scanMode = 'quick'; // 'quick' or 'deep' let eventSource = null; let pollTimer = null; let agentPollTimer = null; // Data stores let networks = new Map(); // bssid -> network let clients = new Map(); // mac -> client let probeRequests = []; let channelStats = []; let recommendations = []; // UI state let selectedNetwork = null; let currentFilter = 'all'; let currentSort = { field: 'rssi', order: 'desc' }; // Agent state let showAllAgentsMode = false; // Show combined results from all agents let lastAgentId = null; // Track agent switches // Capabilities let capabilities = null; // Callbacks for external integration let onNetworkUpdate = null; let onClientUpdate = null; let onProbeRequest = null; // ========================================================================== // Initialization // ========================================================================== function init() { console.log('[WiFiMode] Initializing...'); // Cache DOM elements cacheDOM(); // Check capabilities checkCapabilities(); // Initialize components initScanModeTabs(); initNetworkFilters(); initSortControls(); initProximityRadar(); initChannelChart(); // Check if already scanning checkScanStatus(); console.log('[WiFiMode] Initialized'); } // DOM element cache let elements = {}; function cacheDOM() { elements = { // Scan controls quickScanBtn: document.getElementById('wifiQuickScanBtn'), deepScanBtn: document.getElementById('wifiDeepScanBtn'), stopScanBtn: document.getElementById('wifiStopScanBtn'), scanModeQuick: document.getElementById('wifiScanModeQuick'), scanModeDeep: document.getElementById('wifiScanModeDeep'), // Status bar scanStatus: document.getElementById('wifiScanStatus'), networkCount: document.getElementById('wifiNetworkCount'), clientCount: document.getElementById('wifiClientCount'), hiddenCount: document.getElementById('wifiHiddenCount'), // Network table networkTable: document.getElementById('wifiNetworkTable'), networkTableBody: document.getElementById('wifiNetworkTableBody'), networkFilters: document.getElementById('wifiNetworkFilters'), // Visualizations proximityRadar: document.getElementById('wifiProximityRadar'), channelChart: document.getElementById('wifiChannelChart'), channelBandTabs: document.getElementById('wifiChannelBandTabs'), // Zone summary zoneImmediate: document.getElementById('wifiZoneImmediate'), zoneNear: document.getElementById('wifiZoneNear'), zoneFar: document.getElementById('wifiZoneFar'), // Security counts wpa3Count: document.getElementById('wpa3Count'), wpa2Count: document.getElementById('wpa2Count'), wepCount: document.getElementById('wepCount'), openCount: document.getElementById('openCount'), // Detail drawer detailDrawer: document.getElementById('wifiDetailDrawer'), detailEssid: document.getElementById('wifiDetailEssid'), detailBssid: document.getElementById('wifiDetailBssid'), detailRssi: document.getElementById('wifiDetailRssi'), detailChannel: document.getElementById('wifiDetailChannel'), detailBand: document.getElementById('wifiDetailBand'), detailSecurity: document.getElementById('wifiDetailSecurity'), detailCipher: document.getElementById('wifiDetailCipher'), detailVendor: document.getElementById('wifiDetailVendor'), detailClients: document.getElementById('wifiDetailClients'), detailFirstSeen: document.getElementById('wifiDetailFirstSeen'), detailClientList: document.getElementById('wifiDetailClientList'), // Interface select interfaceSelect: document.getElementById('wifiInterfaceSelect'), // Capability status capabilityStatus: document.getElementById('wifiCapabilityStatus'), // Export buttons exportCsvBtn: document.getElementById('wifiExportCsv'), exportJsonBtn: document.getElementById('wifiExportJson'), }; } // ========================================================================== // Capabilities // ========================================================================== async function checkCapabilities() { try { const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local'; let response; if (isAgentMode) { // Fetch capabilities from agent via controller proxy response = await fetch(`/controller/agents/${currentAgent}?refresh=true`); if (!response.ok) throw new Error('Failed to fetch agent capabilities'); const data = await response.json(); // Extract WiFi capabilities from agent data if (data.agent && data.agent.capabilities) { const agentCaps = data.agent.capabilities; const agentInterfaces = data.agent.interfaces || {}; // Build WiFi-compatible capabilities object capabilities = { can_quick_scan: agentCaps.wifi || false, can_deep_scan: agentCaps.wifi || false, interfaces: (agentInterfaces.wifi_interfaces || []).map(iface => ({ name: iface.name || iface, supports_monitor: iface.supports_monitor !== false })), default_interface: agentInterfaces.default_wifi || null, preferred_quick_tool: 'agent', issues: [] }; console.log('[WiFiMode] Agent capabilities:', capabilities); } else { throw new Error('Agent does not support WiFi mode'); } } else { // Local capabilities response = await fetch(`${CONFIG.apiBase}/capabilities`); if (!response.ok) throw new Error('Failed to fetch capabilities'); capabilities = await response.json(); console.log('[WiFiMode] Local capabilities:', capabilities); } updateCapabilityUI(); populateInterfaceSelect(); } catch (error) { console.error('[WiFiMode] Capability check failed:', error); showCapabilityError('Failed to check WiFi capabilities'); } } function updateCapabilityUI() { if (!capabilities || !elements.capabilityStatus) return; let html = ''; if (!capabilities.can_quick_scan && !capabilities.can_deep_scan) { html = `
WiFi scanning not available
`; } else { // Show available modes const modes = []; if (capabilities.can_quick_scan) modes.push('Quick Scan'); if (capabilities.can_deep_scan) modes.push('Deep Scan'); html = `
Available modes: ${modes.join(', ')} ${capabilities.preferred_quick_tool ? ` (using ${capabilities.preferred_quick_tool})` : ''}
`; if (capabilities.issues.length > 0) { html += `
${capabilities.issues.join('. ')}
`; } } elements.capabilityStatus.innerHTML = html; elements.capabilityStatus.style.display = html ? 'block' : 'none'; // Enable/disable scan buttons based on capabilities if (elements.quickScanBtn) { elements.quickScanBtn.disabled = !capabilities.can_quick_scan; } if (elements.deepScanBtn) { elements.deepScanBtn.disabled = !capabilities.can_deep_scan; } } function showCapabilityError(message) { if (!elements.capabilityStatus) return; elements.capabilityStatus.innerHTML = `
${escapeHtml(message)}
`; elements.capabilityStatus.style.display = 'block'; } function populateInterfaceSelect() { if (!elements.interfaceSelect || !capabilities) return; elements.interfaceSelect.innerHTML = ''; if (capabilities.interfaces.length === 0) { elements.interfaceSelect.innerHTML = ''; return; } capabilities.interfaces.forEach(iface => { const option = document.createElement('option'); option.value = iface.name; option.textContent = `${iface.name}${iface.supports_monitor ? ' (monitor capable)' : ''}`; elements.interfaceSelect.appendChild(option); }); // Select default if (capabilities.default_interface) { elements.interfaceSelect.value = capabilities.default_interface; } } // ========================================================================== // Scan Mode Tabs // ========================================================================== function initScanModeTabs() { if (elements.scanModeQuick) { elements.scanModeQuick.addEventListener('click', () => setScanMode('quick')); } if (elements.scanModeDeep) { elements.scanModeDeep.addEventListener('click', () => setScanMode('deep')); } } function setScanMode(mode) { scanMode = mode; // Update tab UI if (elements.scanModeQuick) { elements.scanModeQuick.classList.toggle('active', mode === 'quick'); } if (elements.scanModeDeep) { elements.scanModeDeep.classList.toggle('active', mode === 'deep'); } console.log('[WiFiMode] Scan mode set to:', mode); } // ========================================================================== // Scanning // ========================================================================== async function startQuickScan() { if (isScanning) return; // Check for agent mode conflicts if (!checkAgentConflicts()) { return; } console.log('[WiFiMode] Starting quick scan...'); setScanning(true, 'quick'); try { const iface = elements.interfaceSelect?.value || null; const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local'; const agentName = getCurrentAgentName(); let response; if (isAgentMode) { // Route through agent proxy response = await fetch(`/controller/agents/${currentAgent}/wifi/start`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ interface: iface, scan_type: 'quick' }), }); } else { response = await fetch(`${CONFIG.apiBase}/scan/quick`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ interface: iface }), }); } if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Quick scan failed'); } const result = await response.json(); console.log('[WiFiMode] Quick scan complete:', result); // Handle controller proxy response format (agent response is nested in 'result') const scanResult = isAgentMode && result.result ? result.result : result; // Check for error first if (scanResult.error || scanResult.status === 'error') { console.error('[WiFiMode] Quick scan error from server:', scanResult.error || scanResult.message); showError(scanResult.error || scanResult.message || 'Quick scan failed'); setScanning(false); return; } // Handle agent response format let accessPoints = scanResult.access_points || scanResult.networks || []; // Check if we got results if (accessPoints.length === 0) { // No error but no results let msg = 'Quick scan found no networks in range.'; if (scanResult.warnings && scanResult.warnings.length > 0) { msg += ' Warnings: ' + scanResult.warnings.join('; '); } console.warn('[WiFiMode] ' + msg); showError(msg + ' Try Deep Scan with monitor mode.'); setScanning(false); return; } // Tag results with agent source accessPoints.forEach(ap => { ap._agent = agentName; }); // Show any warnings even on success if (scanResult.warnings && scanResult.warnings.length > 0) { console.warn('[WiFiMode] Quick scan warnings:', scanResult.warnings); } // Process results processQuickScanResult({ ...scanResult, access_points: accessPoints }); // For quick scan, we're done after one scan // But keep polling if user wants continuous updates if (scanMode === 'quick') { startQuickScanPolling(); } } catch (error) { console.error('[WiFiMode] Quick scan error:', error); showError(error.message + '. Try using Deep Scan instead.'); setScanning(false); } } async function startDeepScan() { if (isScanning) return; // Check for agent mode conflicts if (!checkAgentConflicts()) { return; } console.log('[WiFiMode] Starting deep scan...'); setScanning(true, 'deep'); try { const iface = elements.interfaceSelect?.value || null; const band = document.getElementById('wifiBand')?.value || 'all'; const channel = document.getElementById('wifiChannel')?.value || null; const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local'; let response; if (isAgentMode) { // Route through agent proxy response = await fetch(`/controller/agents/${currentAgent}/wifi/start`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ interface: iface, scan_type: 'deep', band: band === 'abg' ? 'all' : band === 'bg' ? '2.4' : '5', channel: channel ? parseInt(channel) : null, }), }); } else { response = await fetch(`${CONFIG.apiBase}/scan/start`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ interface: iface, band: band === 'abg' ? 'all' : band === 'bg' ? '2.4' : '5', channel: channel ? parseInt(channel) : null, }), }); } if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to start deep scan'); } // Check for agent error in response if (isAgentMode) { const result = await response.json(); const scanResult = result.result || result; if (scanResult.status === 'error') { throw new Error(scanResult.message || 'Agent failed to start deep scan'); } console.log('[WiFiMode] Agent deep scan started:', scanResult); } // Start SSE stream for real-time updates (works with push-enabled agents) startEventStream(); // Also start polling for agent data (works without push enabled) if (isAgentMode) { startAgentDeepScanPolling(); } } catch (error) { console.error('[WiFiMode] Deep scan error:', error); showError(error.message); setScanning(false); } } async function stopScan() { console.log('[WiFiMode] Stopping scan...'); // Stop polling if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } // Stop agent polling stopAgentDeepScanPolling(); // Close event stream if (eventSource) { eventSource.close(); eventSource = null; } // Stop scan on server (local or agent) const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local'; try { if (isAgentMode) { await fetch(`/controller/agents/${currentAgent}/wifi/stop`, { method: 'POST' }); } else if (scanMode === 'deep') { await fetch(`${CONFIG.apiBase}/scan/stop`, { method: 'POST' }); } } catch (error) { console.warn('[WiFiMode] Error stopping scan:', error); } setScanning(false); } function setScanning(scanning, mode = null) { isScanning = scanning; if (mode) scanMode = mode; // Update buttons if (elements.quickScanBtn) { elements.quickScanBtn.style.display = scanning ? 'none' : 'inline-block'; } if (elements.deepScanBtn) { elements.deepScanBtn.style.display = scanning ? 'none' : 'inline-block'; } if (elements.stopScanBtn) { elements.stopScanBtn.style.display = scanning ? 'inline-block' : 'none'; } // Update status if (elements.scanStatus) { elements.scanStatus.textContent = scanning ? `Scanning (${scanMode === 'quick' ? 'Quick' : 'Deep'})...` : 'Idle'; elements.scanStatus.className = scanning ? 'status-scanning' : 'status-idle'; } } async function checkScanStatus() { try { const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local'; const endpoint = isAgentMode ? `/controller/agents/${currentAgent}/wifi/status` : `${CONFIG.apiBase}/scan/status`; const response = await fetch(endpoint); if (!response.ok) return; const data = await response.json(); // Handle agent response format (may be nested in 'result') const status = isAgentMode && data.result ? data.result : data; if (status.is_scanning || status.running) { // Agent returns scan_type in params, local returns scan_mode // Normalize: agent may return 'deepscan' or 'deep', UI expects 'deep' or 'quick' let detectedMode = status.scan_mode || (status.params && status.params.scan_type) || 'deep'; if (detectedMode === 'deepscan') detectedMode = 'deep'; setScanning(true, detectedMode); if (detectedMode === 'deep') { startEventStream(); // Also start polling for agent mode (works without push enabled) if (isAgentMode) { startAgentDeepScanPolling(); } } else { startQuickScanPolling(); } } } catch (error) { console.debug('[WiFiMode] Status check failed:', error); } } // ========================================================================== // Quick Scan Polling // ========================================================================== function startQuickScanPolling() { if (pollTimer) return; pollTimer = setInterval(async () => { if (!isScanning || scanMode !== 'quick') { clearInterval(pollTimer); pollTimer = null; return; } try { const iface = elements.interfaceSelect?.value || null; const response = await fetch(`${CONFIG.apiBase}/scan/quick`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ interface: iface }), }); if (response.ok) { const result = await response.json(); processQuickScanResult(result); } } catch (error) { console.debug('[WiFiMode] Poll error:', error); } }, CONFIG.pollInterval); } function processQuickScanResult(result) { // Update networks result.access_points.forEach(ap => { networks.set(ap.bssid, ap); }); // Update channel stats (calculate from networks if not provided by API) channelStats = result.channel_stats || []; recommendations = result.recommendations || []; // If no channel stats from API, calculate from networks if (channelStats.length === 0 && networks.size > 0) { channelStats = calculateChannelStats(); } // Update UI updateNetworkTable(); updateStats(); updateProximityRadar(); updateChannelChart(); // Callbacks result.access_points.forEach(ap => { if (onNetworkUpdate) onNetworkUpdate(ap); }); } // ========================================================================== // Agent Deep Scan Polling (fallback when push is not enabled) // ========================================================================== function startAgentDeepScanPolling() { if (agentPollTimer) return; console.log('[WiFiMode] Starting agent deep scan polling...'); agentPollTimer = setInterval(async () => { if (!isScanning || scanMode !== 'deep') { clearInterval(agentPollTimer); agentPollTimer = null; return; } const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local'; if (!isAgentMode) { clearInterval(agentPollTimer); agentPollTimer = null; return; } try { const response = await fetch(`/controller/agents/${currentAgent}/wifi/data`); if (!response.ok) return; const result = await response.json(); if (result.status !== 'success' || !result.data) return; const data = result.data.data || result.data; const agentName = result.agent_name || 'Remote'; // Process networks if (data.networks && Array.isArray(data.networks)) { data.networks.forEach(net => { net._agent = agentName; handleStreamEvent({ type: 'network_update', network: net }); }); } // Process clients if (data.clients && Array.isArray(data.clients)) { data.clients.forEach(client => { client._agent = agentName; handleStreamEvent({ type: 'client_update', client: client }); }); } console.debug(`[WiFiMode] Agent poll: ${data.networks?.length || 0} networks, ${data.clients?.length || 0} clients`); } catch (error) { console.debug('[WiFiMode] Agent poll error:', error); } }, 2000); // Poll every 2 seconds } function stopAgentDeepScanPolling() { if (agentPollTimer) { clearInterval(agentPollTimer); agentPollTimer = null; } } // ========================================================================== // SSE Event Stream // ========================================================================== function startEventStream() { if (eventSource) { eventSource.close(); } const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local'; const agentName = getCurrentAgentName(); let streamUrl; if (isAgentMode) { // Use multi-agent stream for remote agents streamUrl = '/controller/stream/all'; console.log('[WiFiMode] Starting multi-agent event stream...'); } else { streamUrl = `${CONFIG.apiBase}/stream`; console.log('[WiFiMode] Starting local event stream...'); } eventSource = new EventSource(streamUrl); eventSource.onopen = () => { console.log('[WiFiMode] Event stream connected'); }; eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); // For multi-agent stream, filter and transform data if (isAgentMode) { // Skip keepalive and non-wifi data if (data.type === 'keepalive') return; if (data.scan_type !== 'wifi') return; // Filter by current agent if not in "show all" mode if (!showAllAgentsMode && typeof agents !== 'undefined') { const currentAgentObj = agents.find(a => a.id == currentAgent); if (currentAgentObj && data.agent_name && data.agent_name !== currentAgentObj.name) { return; } } // Transform multi-agent payload to stream event format if (data.payload && data.payload.networks) { data.payload.networks.forEach(net => { net._agent = data.agent_name || 'Unknown'; handleStreamEvent({ type: 'network_update', network: net }); }); } if (data.payload && data.payload.clients) { data.payload.clients.forEach(client => { client._agent = data.agent_name || 'Unknown'; handleStreamEvent({ type: 'client_update', client: client }); }); } } else { // Local stream - tag with local if (data.network) data.network._agent = 'Local'; if (data.client) data.client._agent = 'Local'; handleStreamEvent(data); } } catch (error) { console.debug('[WiFiMode] Event parse error:', error); } }; eventSource.onerror = (error) => { console.warn('[WiFiMode] Event stream error:', error); if (isScanning) { // Attempt to reconnect setTimeout(() => { if (isScanning && scanMode === 'deep') { startEventStream(); } }, 3000); } }; } function handleStreamEvent(event) { switch (event.type) { case 'network_update': handleNetworkUpdate(event.network); break; case 'client_update': handleClientUpdate(event.client); break; case 'probe_request': handleProbeRequest(event.probe); break; case 'hidden_revealed': handleHiddenRevealed(event.bssid, event.revealed_essid); break; case 'scan_started': console.log('[WiFiMode] Scan started:', event); break; case 'scan_stopped': console.log('[WiFiMode] Scan stopped'); setScanning(false); break; case 'scan_error': console.error('[WiFiMode] Scan error:', event.error); showError(event.error); setScanning(false); break; case 'keepalive': // Ignore keepalives break; default: console.debug('[WiFiMode] Unknown event type:', event.type); } } function handleNetworkUpdate(network) { networks.set(network.bssid, network); updateNetworkRow(network); updateStats(); updateProximityRadar(); updateChannelChart(); if (onNetworkUpdate) onNetworkUpdate(network); } function handleClientUpdate(client) { clients.set(client.mac, client); updateStats(); // Update client display if this client belongs to the selected network updateClientInList(client); if (onClientUpdate) onClientUpdate(client); } function handleProbeRequest(probe) { probeRequests.push(probe); if (probeRequests.length > CONFIG.maxProbes) { probeRequests.shift(); } if (onProbeRequest) onProbeRequest(probe); } function handleHiddenRevealed(bssid, revealedSsid) { const network = networks.get(bssid); if (network) { network.revealed_essid = revealedSsid; network.display_name = `${revealedSsid} (revealed)`; updateNetworkRow(network); // Show notification showInfo(`Hidden SSID revealed: ${revealedSsid}`); } } // ========================================================================== // Network Table // ========================================================================== function initNetworkFilters() { if (!elements.networkFilters) return; elements.networkFilters.addEventListener('click', (e) => { if (e.target.matches('.wifi-filter-btn')) { const filter = e.target.dataset.filter; setNetworkFilter(filter); } }); } function setNetworkFilter(filter) { currentFilter = filter; // Update button states if (elements.networkFilters) { elements.networkFilters.querySelectorAll('.wifi-filter-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.filter === filter); }); } updateNetworkTable(); } function initSortControls() { if (!elements.networkTable) return; elements.networkTable.addEventListener('click', (e) => { const th = e.target.closest('th[data-sort]'); if (th) { const field = th.dataset.sort; if (currentSort.field === field) { currentSort.order = currentSort.order === 'desc' ? 'asc' : 'desc'; } else { currentSort.field = field; currentSort.order = 'desc'; } updateNetworkTable(); } }); } function updateNetworkTable() { if (!elements.networkTableBody) return; // Filter networks let filtered = Array.from(networks.values()); switch (currentFilter) { case 'hidden': filtered = filtered.filter(n => n.is_hidden); break; case 'open': filtered = filtered.filter(n => n.security === 'Open'); break; case 'strong': filtered = filtered.filter(n => n.rssi_current && n.rssi_current >= -60); break; case '2.4': filtered = filtered.filter(n => n.band === '2.4GHz'); break; case '5': filtered = filtered.filter(n => n.band === '5GHz'); break; } // Sort networks filtered.sort((a, b) => { let aVal, bVal; switch (currentSort.field) { case 'rssi': aVal = a.rssi_current || -100; bVal = b.rssi_current || -100; break; case 'channel': aVal = a.channel || 0; bVal = b.channel || 0; break; case 'essid': aVal = (a.essid || '').toLowerCase(); bVal = (b.essid || '').toLowerCase(); break; case 'clients': aVal = a.client_count || 0; bVal = b.client_count || 0; break; default: aVal = a.rssi_current || -100; bVal = b.rssi_current || -100; } if (currentSort.order === 'desc') { return bVal > aVal ? 1 : bVal < aVal ? -1 : 0; } else { return aVal > bVal ? 1 : aVal < bVal ? -1 : 0; } }); // Render table elements.networkTableBody.innerHTML = filtered.map(n => createNetworkRow(n)).join(''); } function createNetworkRow(network) { const rssi = network.rssi_current; const signalClass = rssi >= -50 ? 'signal-strong' : rssi >= -70 ? 'signal-medium' : rssi >= -85 ? 'signal-weak' : 'signal-very-weak'; const securityClass = network.security === 'Open' ? 'security-open' : network.security === 'WEP' ? 'security-wep' : network.security.includes('WPA3') ? 'security-wpa3' : 'security-wpa'; const hiddenBadge = network.is_hidden ? 'Hidden' : ''; const newBadge = network.is_new ? 'New' : ''; // Agent source badge const agentName = network._agent || 'Local'; const agentClass = agentName === 'Local' ? 'agent-local' : 'agent-remote'; return ` ${escapeHtml(network.display_name || network.essid || '[Hidden]')} ${hiddenBadge}${newBadge} ${escapeHtml(network.bssid)} ${network.channel || '-'} ${rssi !== null ? rssi : '-'} ${escapeHtml(network.security)} ${network.client_count || 0} ${escapeHtml(agentName)} `; } function updateNetworkRow(network) { const row = elements.networkTableBody?.querySelector(`tr[data-bssid="${network.bssid}"]`); if (row) { row.outerHTML = createNetworkRow(network); } else { // Add new row updateNetworkTable(); } } function selectNetwork(bssid) { selectedNetwork = bssid; // Update row selection elements.networkTableBody?.querySelectorAll('.wifi-network-row').forEach(row => { row.classList.toggle('selected', row.dataset.bssid === bssid); }); // Update detail panel updateDetailPanel(bssid); // Highlight on radar if (typeof WiFiProximityRadar !== 'undefined') { WiFiProximityRadar.highlightNetwork(bssid); } } // ========================================================================== // Detail Panel // ========================================================================== function updateDetailPanel(bssid) { if (!elements.detailDrawer) return; const network = networks.get(bssid); if (!network) { closeDetail(); return; } // Update drawer header if (elements.detailEssid) { elements.detailEssid.textContent = network.display_name || network.essid || '[Hidden SSID]'; } if (elements.detailBssid) { elements.detailBssid.textContent = network.bssid; } // Update detail stats if (elements.detailRssi) { elements.detailRssi.textContent = network.rssi_current ? `${network.rssi_current} dBm` : '--'; } if (elements.detailChannel) { elements.detailChannel.textContent = network.channel || '--'; } if (elements.detailBand) { elements.detailBand.textContent = network.band || '--'; } if (elements.detailSecurity) { elements.detailSecurity.textContent = network.security || '--'; } if (elements.detailCipher) { elements.detailCipher.textContent = network.cipher || '--'; } if (elements.detailVendor) { elements.detailVendor.textContent = network.vendor || 'Unknown'; } if (elements.detailClients) { elements.detailClients.textContent = network.client_count || '0'; } if (elements.detailFirstSeen) { elements.detailFirstSeen.textContent = formatTime(network.first_seen); } // Show the drawer elements.detailDrawer.classList.add('open'); // Fetch and display clients for this network fetchClientsForNetwork(network.bssid); } function closeDetail() { selectedNetwork = null; if (elements.detailDrawer) { elements.detailDrawer.classList.remove('open'); } elements.networkTableBody?.querySelectorAll('.wifi-network-row').forEach(row => { row.classList.remove('selected'); }); } // ========================================================================== // Client Display // ========================================================================== async function fetchClientsForNetwork(bssid) { if (!elements.detailClientList) return; try { const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local'; let response; if (isAgentMode) { // Route through agent proxy response = await fetch(`/controller/agents/${currentAgent}/wifi/v2/clients?bssid=${encodeURIComponent(bssid)}&associated=true`); } else { response = await fetch(`${CONFIG.apiBase}/clients?bssid=${encodeURIComponent(bssid)}&associated=true`); } if (!response.ok) { // Hide client list on error elements.detailClientList.style.display = 'none'; return; } const data = await response.json(); // Handle agent response format (may be nested in 'result') const result = isAgentMode && data.result ? data.result : data; const clientList = result.clients || []; if (clientList.length > 0) { renderClientList(clientList, bssid); elements.detailClientList.style.display = 'block'; } else { elements.detailClientList.style.display = 'none'; } } catch (error) { console.debug('[WiFiMode] Error fetching clients:', error); elements.detailClientList.style.display = 'none'; } } function renderClientList(clientList, bssid) { const container = elements.detailClientList?.querySelector('.wifi-client-list'); const countBadge = document.getElementById('wifiClientCountBadge'); if (!container) return; // Update count badge if (countBadge) { countBadge.textContent = clientList.length; } // Render client cards container.innerHTML = clientList.map(client => { const rssi = client.rssi_current; const signalClass = rssi >= -50 ? 'signal-strong' : rssi >= -70 ? 'signal-medium' : rssi >= -85 ? 'signal-weak' : 'signal-very-weak'; // Format last seen time const lastSeen = client.last_seen ? formatTime(client.last_seen) : '--'; // Build probed SSIDs badges let probesHtml = ''; if (client.probed_ssids && client.probed_ssids.length > 0) { const probes = client.probed_ssids.slice(0, 5); // Show max 5 probesHtml = `
${probes.map(ssid => `${escapeHtml(ssid)}`).join('')} ${client.probed_ssids.length > 5 ? `+${client.probed_ssids.length - 5}` : ''}
`; } return `
${escapeHtml(client.mac)} ${escapeHtml(client.vendor || 'Unknown vendor')} ${probesHtml}
${rssi !== null && rssi !== undefined ? rssi + ' dBm' : '--'} ${lastSeen}
`; }).join(''); } function updateClientInList(client) { // Check if this client belongs to the currently selected network if (!selectedNetwork || client.associated_bssid !== selectedNetwork) { return; } const container = elements.detailClientList?.querySelector('.wifi-client-list'); if (!container) return; const existingCard = container.querySelector(`[data-mac="${client.mac}"]`); if (existingCard) { // Update existing card's RSSI and last seen const rssiEl = existingCard.querySelector('.wifi-client-rssi'); const lastSeenEl = existingCard.querySelector('.wifi-client-lastseen'); if (rssiEl && client.rssi_current !== null && client.rssi_current !== undefined) { const rssi = client.rssi_current; const signalClass = rssi >= -50 ? 'signal-strong' : rssi >= -70 ? 'signal-medium' : rssi >= -85 ? 'signal-weak' : 'signal-very-weak'; rssiEl.textContent = rssi + ' dBm'; rssiEl.className = 'wifi-client-rssi ' + signalClass; } if (lastSeenEl && client.last_seen) { lastSeenEl.textContent = formatTime(client.last_seen); } } else { // New client for this network - re-fetch the full list fetchClientsForNetwork(selectedNetwork); } } // ========================================================================== // Statistics // ========================================================================== function updateStats() { const networksList = Array.from(networks.values()); // Update counts in status bar if (elements.networkCount) { elements.networkCount.textContent = networks.size; } if (elements.clientCount) { elements.clientCount.textContent = clients.size; } if (elements.hiddenCount) { const hidden = networksList.filter(n => n.is_hidden).length; elements.hiddenCount.textContent = hidden; } // Update security counts const securityCounts = { wpa3: 0, wpa2: 0, wep: 0, open: 0 }; networksList.forEach(n => { const sec = (n.security || '').toLowerCase(); if (sec.includes('wpa3')) securityCounts.wpa3++; else if (sec.includes('wpa2') || sec.includes('wpa')) securityCounts.wpa2++; else if (sec.includes('wep')) securityCounts.wep++; else if (sec === 'open' || sec === '') securityCounts.open++; }); if (elements.wpa3Count) elements.wpa3Count.textContent = securityCounts.wpa3; if (elements.wpa2Count) elements.wpa2Count.textContent = securityCounts.wpa2; if (elements.wepCount) elements.wepCount.textContent = securityCounts.wep; if (elements.openCount) elements.openCount.textContent = securityCounts.open; // Update zone summary const zoneCounts = { immediate: 0, near: 0, far: 0 }; networksList.forEach(n => { const rssi = n.rssi_current; if (rssi >= -50) zoneCounts.immediate++; else if (rssi >= -70) zoneCounts.near++; else zoneCounts.far++; }); if (elements.zoneImmediate) elements.zoneImmediate.textContent = zoneCounts.immediate; if (elements.zoneNear) elements.zoneNear.textContent = zoneCounts.near; if (elements.zoneFar) elements.zoneFar.textContent = zoneCounts.far; } // ========================================================================== // Proximity Radar // ========================================================================== function initProximityRadar() { if (!elements.proximityRadar) return; // Initialize radar component if (typeof ProximityRadar !== 'undefined') { ProximityRadar.init('wifiProximityRadar', { mode: 'wifi', size: 280, onDeviceClick: (bssid) => selectNetwork(bssid), }); } } function updateProximityRadar() { if (typeof ProximityRadar === 'undefined') return; // Convert networks to radar-compatible format const devices = Array.from(networks.values()).map(n => ({ device_key: n.bssid, device_id: n.bssid, name: n.essid || '[Hidden]', rssi_current: n.rssi_current, rssi_ema: n.rssi_ema, proximity_band: n.proximity_band, estimated_distance_m: n.estimated_distance_m, is_new: n.is_new, heuristic_flags: n.heuristic_flags || [], })); ProximityRadar.updateDevices(devices); } // ========================================================================== // Channel Chart // ========================================================================== function initChannelChart() { if (!elements.channelChart) return; // Initialize channel chart component if (typeof ChannelChart !== 'undefined') { ChannelChart.init('wifiChannelChart'); } // 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); } }); } } 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 updateChannelChart(band) { if (typeof ChannelChart === 'undefined') 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'; } // Recalculate channel stats from networks if needed if (channelStats.length === 0 && networks.size > 0) { channelStats = calculateChannelStats(); } // 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); ChannelChart.update(filteredStats, filteredRecs); } // ========================================================================== // Export // ========================================================================== async function exportData(format) { try { const response = await fetch(`${CONFIG.apiBase}/export?format=${format}&type=all`); if (!response.ok) throw new Error('Export failed'); const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `wifi_scan_${new Date().toISOString().slice(0, 19).replace(/[:-]/g, '')}.${format}`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (error) { console.error('[WiFiMode] Export error:', error); showError('Export failed: ' + error.message); } } // ========================================================================== // Utilities // ========================================================================== function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function formatTime(isoString) { if (!isoString) return '-'; const date = new Date(isoString); return date.toLocaleTimeString(); } function showError(message) { // Use global notification if available if (typeof showNotification === 'function') { showNotification('WiFi Error', message, 'error'); } else { console.error('[WiFiMode]', message); } } function showInfo(message) { if (typeof showNotification === 'function') { showNotification('WiFi', message, 'info'); } else { console.log('[WiFiMode]', message); } } // ========================================================================== // Agent Handling // ========================================================================== /** * Handle agent change - refresh interfaces and optionally clear data. * Called when user selects a different agent. */ function handleAgentChange() { const currentAgentId = typeof currentAgent !== 'undefined' ? currentAgent : 'local'; // Check if agent actually changed if (lastAgentId === currentAgentId) return; console.log('[WiFiMode] Agent changed from', lastAgentId, 'to', currentAgentId); // Stop UI polling only - don't stop the actual scan on the agent // The agent should continue running independently if (isScanning) { stopAgentDeepScanPolling(); if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } if (eventSource) { eventSource.close(); eventSource = null; } setScanning(false); } // Clear existing data when switching agents (unless "Show All" is enabled) if (!showAllAgentsMode) { clearData(); showInfo(`Switched to ${getCurrentAgentName()} - previous data cleared`); } // Refresh capabilities for new agent checkCapabilities(); // Check if new agent already has a scan running checkScanStatus(); lastAgentId = currentAgentId; } /** * Clear all collected data. */ function clearData() { networks.clear(); clients.clear(); probeRequests = []; channelStats = []; recommendations = []; updateNetworkTable(); updateStats(); updateProximityRadar(); updateChannelChart(); } /** * Toggle "Show All Agents" mode. * When enabled, displays combined WiFi results from all agents. */ function toggleShowAllAgents(enabled) { showAllAgentsMode = enabled; console.log('[WiFiMode] Show all agents mode:', enabled); if (enabled) { // If currently scanning, switch to multi-agent stream if (isScanning && eventSource) { eventSource.close(); startEventStream(); } showInfo('Showing WiFi networks from all agents'); } else { // Filter to current agent only filterToCurrentAgent(); } } /** * Filter networks to only show those from current agent. */ function filterToCurrentAgent() { const agentName = getCurrentAgentName(); const toRemove = []; networks.forEach((network, bssid) => { if (network._agent && network._agent !== agentName) { toRemove.push(bssid); } }); toRemove.forEach(bssid => networks.delete(bssid)); // Also filter clients const clientsToRemove = []; clients.forEach((client, mac) => { if (client._agent && client._agent !== agentName) { clientsToRemove.push(mac); } }); clientsToRemove.forEach(mac => clients.delete(mac)); updateNetworkTable(); updateStats(); updateProximityRadar(); } /** * Refresh WiFi interfaces from current agent. * Called when agent changes. */ async function refreshInterfaces() { await checkCapabilities(); } // ========================================================================== // Public API // ========================================================================== return { init, startQuickScan, startDeepScan, stopScan, selectNetwork, closeDetail, setFilter: setNetworkFilter, exportData, checkCapabilities, // Agent handling handleAgentChange, clearData, toggleShowAllAgents, refreshInterfaces, // Getters getNetworks: () => Array.from(networks.values()), getClients: () => Array.from(clients.values()), getProbes: () => [...probeRequests], isScanning: () => isScanning, getScanMode: () => scanMode, isShowAllAgents: () => showAllAgentsMode, // Callbacks onNetworkUpdate: (cb) => { onNetworkUpdate = cb; }, onClientUpdate: (cb) => { onClientUpdate = cb; }, onProbeRequest: (cb) => { onProbeRequest = cb; }, }; })(); // Auto-initialize when DOM is ready document.addEventListener('DOMContentLoaded', () => { // Only init if we're in WiFi mode if (typeof currentMode !== 'undefined' && currentMode === 'wifi') { WiFiMode.init(); } });