Files
intercept/static/js/modes/wifi.js

1703 lines
63 KiB
JavaScript

/**
* 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;
}
function getChannelPresetList(preset) {
switch (preset) {
case '2.4-common':
return '1,6,11';
case '2.4-all':
return '1,2,3,4,5,6,7,8,9,10,11,12,13';
case '5-low':
return '36,40,44,48';
case '5-mid':
return '52,56,60,64';
case '5-high':
return '149,153,157,161,165';
default:
return '';
}
}
function buildChannelConfig() {
const preset = document.getElementById('wifiChannelPreset')?.value || '';
const listInput = document.getElementById('wifiChannelList')?.value || '';
const singleInput = document.getElementById('wifiChannel')?.value || '';
const listValue = listInput.trim();
const presetValue = getChannelPresetList(preset);
const channels = listValue || presetValue || '';
const channel = channels ? null : (singleInput.trim() ? parseInt(singleInput.trim()) : null);
return {
channels: channels || null,
channel: Number.isFinite(channel) ? channel : null,
};
}
// ==========================================================================
// 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 = `
<div class="wifi-capability-warning">
<strong>WiFi scanning not available</strong>
<ul>
${capabilities.issues.map(i => `<li>${escapeHtml(i)}</li>`).join('')}
</ul>
</div>
`;
} 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 = `
<div class="wifi-capability-info">
Available modes: ${modes.join(', ')}
${capabilities.preferred_quick_tool ? ` (using ${capabilities.preferred_quick_tool})` : ''}
</div>
`;
if (capabilities.issues.length > 0) {
html += `
<div class="wifi-capability-warning" style="margin-top: 8px;">
<small>${capabilities.issues.join('. ')}</small>
</div>
`;
}
}
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 = `
<div class="wifi-capability-error">${escapeHtml(message)}</div>
`;
elements.capabilityStatus.style.display = 'block';
}
function populateInterfaceSelect() {
if (!elements.interfaceSelect || !capabilities) return;
elements.interfaceSelect.innerHTML = '';
if (capabilities.interfaces.length === 0) {
elements.interfaceSelect.innerHTML = '<option value="">No interfaces found</option>';
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 channelConfig = buildChannelConfig();
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: channelConfig.channel,
channels: channelConfig.channels,
}),
});
} 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: channelConfig.channel,
channels: channelConfig.channels,
}),
});
}
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 ? '<span class="badge badge-hidden">Hidden</span>' : '';
const newBadge = network.is_new ? '<span class="badge badge-new">New</span>' : '';
// Agent source badge
const agentName = network._agent || 'Local';
const agentClass = agentName === 'Local' ? 'agent-local' : 'agent-remote';
return `
<tr class="wifi-network-row ${network.bssid === selectedNetwork ? 'selected' : ''}"
data-bssid="${escapeHtml(network.bssid)}"
onclick="WiFiMode.selectNetwork('${escapeHtml(network.bssid)}')">
<td class="col-essid">
<span class="essid">${escapeHtml(network.display_name || network.essid || '[Hidden]')}</span>
${hiddenBadge}${newBadge}
</td>
<td class="col-bssid"><code>${escapeHtml(network.bssid)}</code></td>
<td class="col-channel">${network.channel || '-'}</td>
<td class="col-rssi">
<span class="rssi-value ${signalClass}">${rssi !== null ? rssi : '-'}</span>
</td>
<td class="col-security">
<span class="security-badge ${securityClass}">${escapeHtml(network.security)}</span>
</td>
<td class="col-clients">${network.client_count || 0}</td>
<td class="col-agent">
<span class="agent-badge ${agentClass}">${escapeHtml(agentName)}</span>
</td>
</tr>
`;
}
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 = `
<div class="wifi-client-probes">
${probes.map(ssid => `<span class="wifi-client-probe-badge">${escapeHtml(ssid)}</span>`).join('')}
${client.probed_ssids.length > 5 ? `<span class="wifi-client-probe-badge">+${client.probed_ssids.length - 5}</span>` : ''}
</div>
`;
}
return `
<div class="wifi-client-card" data-mac="${escapeHtml(client.mac)}">
<div class="wifi-client-identity">
<span class="wifi-client-mac">${escapeHtml(client.mac)}</span>
<span class="wifi-client-vendor">${escapeHtml(client.vendor || 'Unknown vendor')}</span>
${probesHtml}
</div>
<div class="wifi-client-signal">
<span class="wifi-client-rssi ${signalClass}">${rssi !== null && rssi !== undefined ? rssi + ' dBm' : '--'}</span>
<span class="wifi-client-lastseen">${lastSeen}</span>
</div>
</div>
`;
}).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();
}
});