mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
perf: add destroy() lifecycle to all mode modules to prevent resource leaks
Mode modules were leaking EventSource connections, setInterval timers, and setTimeout timers on every mode switch, causing progressive browser sluggishness. Added destroy() to 8 modules missing it (meshtastic, bluetooth, wifi, bt_locate, sstv, sstv-general, websdr, spy-stations) and centralized all destroy calls in switchMode() via a moduleDestroyMap that cleanly tears down only the previous mode. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1909,7 +1909,42 @@ const BtLocate = (function() {
|
||||
handleDetection,
|
||||
invalidateMap,
|
||||
fetchPairedIrks,
|
||||
destroy,
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroy — close SSE stream and clear all timers for clean mode switching.
|
||||
*/
|
||||
function destroy() {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
if (durationTimer) {
|
||||
clearInterval(durationTimer);
|
||||
durationTimer = null;
|
||||
}
|
||||
if (mapStabilizeTimer) {
|
||||
clearInterval(mapStabilizeTimer);
|
||||
mapStabilizeTimer = null;
|
||||
}
|
||||
if (queuedDetectionTimer) {
|
||||
clearTimeout(queuedDetectionTimer);
|
||||
queuedDetectionTimer = null;
|
||||
}
|
||||
if (crosshairResetTimer) {
|
||||
clearTimeout(crosshairResetTimer);
|
||||
crosshairResetTimer = null;
|
||||
}
|
||||
if (beepTimer) {
|
||||
clearInterval(beepTimer);
|
||||
beepTimer = null;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
window.BtLocate = BtLocate;
|
||||
|
||||
@@ -117,13 +117,13 @@ const Meshtastic = (function() {
|
||||
Settings.createTileLayer().addTo(meshMap);
|
||||
Settings.registerMap(meshMap);
|
||||
} else {
|
||||
L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OSM</a> © <a href="https://carto.com/">CARTO</a>',
|
||||
maxZoom: 19,
|
||||
subdomains: 'abcd',
|
||||
className: 'tile-layer-cyan'
|
||||
}).addTo(meshMap);
|
||||
}
|
||||
L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OSM</a> © <a href="https://carto.com/">CARTO</a>',
|
||||
maxZoom: 19,
|
||||
subdomains: 'abcd',
|
||||
className: 'tile-layer-cyan'
|
||||
}).addTo(meshMap);
|
||||
}
|
||||
|
||||
// Handle resize
|
||||
setTimeout(() => {
|
||||
@@ -401,10 +401,10 @@ const Meshtastic = (function() {
|
||||
|
||||
// Position is nested in the response
|
||||
const pos = info.position;
|
||||
if (pos && pos.latitude !== undefined && pos.latitude !== null && pos.longitude !== undefined && pos.longitude !== null) {
|
||||
if (posRow) posRow.style.display = 'flex';
|
||||
if (posEl) posEl.textContent = `${pos.latitude.toFixed(5)}, ${pos.longitude.toFixed(5)}`;
|
||||
} else {
|
||||
if (pos && pos.latitude !== undefined && pos.latitude !== null && pos.longitude !== undefined && pos.longitude !== null) {
|
||||
if (posRow) posRow.style.display = 'flex';
|
||||
if (posEl) posEl.textContent = `${pos.latitude.toFixed(5)}, ${pos.longitude.toFixed(5)}`;
|
||||
} else {
|
||||
if (posRow) posRow.style.display = 'none';
|
||||
}
|
||||
}
|
||||
@@ -2295,7 +2295,8 @@ const Meshtastic = (function() {
|
||||
// Store & Forward
|
||||
showStoreForwardModal,
|
||||
requestStoreForward,
|
||||
closeStoreForwardModal
|
||||
closeStoreForwardModal,
|
||||
destroy
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2306,6 +2307,13 @@ const Meshtastic = (function() {
|
||||
setTimeout(() => meshMap.invalidateSize(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy — tear down SSE, timers, and event listeners for clean mode switching.
|
||||
*/
|
||||
function destroy() {
|
||||
stopStream();
|
||||
}
|
||||
})();
|
||||
|
||||
// Initialize when DOM is ready (will be called by selectMode)
|
||||
|
||||
@@ -515,6 +515,13 @@ const SpyStations = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy — no-op placeholder for consistent lifecycle interface.
|
||||
*/
|
||||
function destroy() {
|
||||
// SpyStations has no background timers or streams to clean up.
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
init,
|
||||
@@ -524,7 +531,8 @@ const SpyStations = (function() {
|
||||
showDetails,
|
||||
closeDetails,
|
||||
showHelp,
|
||||
closeHelp
|
||||
closeHelp,
|
||||
destroy
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
@@ -858,6 +858,13 @@ const SSTVGeneral = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy — close SSE stream and stop scope animation for clean mode switching.
|
||||
*/
|
||||
function destroy() {
|
||||
stopStream();
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
init,
|
||||
@@ -869,6 +876,7 @@ const SSTVGeneral = (function() {
|
||||
deleteImage,
|
||||
deleteAllImages,
|
||||
downloadImage,
|
||||
selectPreset
|
||||
selectPreset,
|
||||
destroy
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -12,12 +12,12 @@ const SSTV = (function() {
|
||||
let progress = 0;
|
||||
let issMap = null;
|
||||
let issMarker = null;
|
||||
let issTrackLine = null;
|
||||
let issPosition = null;
|
||||
let issUpdateInterval = null;
|
||||
let countdownInterval = null;
|
||||
let nextPassData = null;
|
||||
let pendingMapInvalidate = false;
|
||||
let issTrackLine = null;
|
||||
let issPosition = null;
|
||||
let issUpdateInterval = null;
|
||||
let countdownInterval = null;
|
||||
let nextPassData = null;
|
||||
let pendingMapInvalidate = false;
|
||||
|
||||
// ISS frequency
|
||||
const ISS_FREQ = 145.800;
|
||||
@@ -38,31 +38,31 @@ const SSTV = (function() {
|
||||
/**
|
||||
* Initialize the SSTV mode
|
||||
*/
|
||||
function init() {
|
||||
checkStatus();
|
||||
loadImages();
|
||||
loadLocationInputs();
|
||||
loadIssSchedule();
|
||||
initMap();
|
||||
startIssTracking();
|
||||
startCountdown();
|
||||
// Ensure Leaflet recomputes dimensions after the SSTV pane becomes visible.
|
||||
setTimeout(() => invalidateMap(), 80);
|
||||
setTimeout(() => invalidateMap(), 260);
|
||||
}
|
||||
|
||||
function isMapContainerVisible() {
|
||||
if (!issMap || typeof issMap.getContainer !== 'function') return false;
|
||||
const container = issMap.getContainer();
|
||||
if (!container) return false;
|
||||
if (container.offsetWidth <= 0 || container.offsetHeight <= 0) return false;
|
||||
if (container.style && container.style.display === 'none') return false;
|
||||
if (typeof window.getComputedStyle === 'function') {
|
||||
const style = window.getComputedStyle(container);
|
||||
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function init() {
|
||||
checkStatus();
|
||||
loadImages();
|
||||
loadLocationInputs();
|
||||
loadIssSchedule();
|
||||
initMap();
|
||||
startIssTracking();
|
||||
startCountdown();
|
||||
// Ensure Leaflet recomputes dimensions after the SSTV pane becomes visible.
|
||||
setTimeout(() => invalidateMap(), 80);
|
||||
setTimeout(() => invalidateMap(), 260);
|
||||
}
|
||||
|
||||
function isMapContainerVisible() {
|
||||
if (!issMap || typeof issMap.getContainer !== 'function') return false;
|
||||
const container = issMap.getContainer();
|
||||
if (!container) return false;
|
||||
if (container.offsetWidth <= 0 || container.offsetHeight <= 0) return false;
|
||||
if (container.style && container.style.display === 'none') return false;
|
||||
if (typeof window.getComputedStyle === 'function') {
|
||||
const style = window.getComputedStyle(container);
|
||||
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load location into input fields
|
||||
@@ -189,9 +189,9 @@ const SSTV = (function() {
|
||||
/**
|
||||
* Initialize Leaflet map for ISS tracking
|
||||
*/
|
||||
async function initMap() {
|
||||
const mapContainer = document.getElementById('sstvIssMap');
|
||||
if (!mapContainer || issMap) return;
|
||||
async function initMap() {
|
||||
const mapContainer = document.getElementById('sstvIssMap');
|
||||
if (!mapContainer || issMap) return;
|
||||
|
||||
// Create map
|
||||
issMap = L.map('sstvIssMap', {
|
||||
@@ -231,21 +231,21 @@ const SSTV = (function() {
|
||||
issMarker = L.marker([0, 0], { icon: issIcon }).addTo(issMap);
|
||||
|
||||
// Create ground track line
|
||||
issTrackLine = L.polyline([], {
|
||||
color: '#00d4ff',
|
||||
weight: 2,
|
||||
opacity: 0.6,
|
||||
dashArray: '5, 5'
|
||||
}).addTo(issMap);
|
||||
|
||||
issMap.on('resize moveend zoomend', () => {
|
||||
if (pendingMapInvalidate) invalidateMap();
|
||||
});
|
||||
|
||||
// Initial layout passes for first-time mode load.
|
||||
setTimeout(() => invalidateMap(), 40);
|
||||
setTimeout(() => invalidateMap(), 180);
|
||||
}
|
||||
issTrackLine = L.polyline([], {
|
||||
color: '#00d4ff',
|
||||
weight: 2,
|
||||
opacity: 0.6,
|
||||
dashArray: '5, 5'
|
||||
}).addTo(issMap);
|
||||
|
||||
issMap.on('resize moveend zoomend', () => {
|
||||
if (pendingMapInvalidate) invalidateMap();
|
||||
});
|
||||
|
||||
// Initial layout passes for first-time mode load.
|
||||
setTimeout(() => invalidateMap(), 40);
|
||||
setTimeout(() => invalidateMap(), 180);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start ISS position tracking
|
||||
@@ -454,9 +454,9 @@ const SSTV = (function() {
|
||||
/**
|
||||
* Update map with ISS position
|
||||
*/
|
||||
function updateMap() {
|
||||
if (!issMap || !issPosition) return;
|
||||
if (pendingMapInvalidate) invalidateMap();
|
||||
function updateMap() {
|
||||
if (!issMap || !issPosition) return;
|
||||
if (pendingMapInvalidate) invalidateMap();
|
||||
|
||||
const lat = issPosition.lat;
|
||||
const lon = issPosition.lon;
|
||||
@@ -516,13 +516,13 @@ const SSTV = (function() {
|
||||
issTrackLine.setLatLngs(segments.length > 0 ? segments : []);
|
||||
}
|
||||
|
||||
// Pan map to follow ISS only when the map pane is currently renderable.
|
||||
if (isMapContainerVisible()) {
|
||||
issMap.panTo([lat, lon], { animate: true, duration: 0.5 });
|
||||
} else {
|
||||
pendingMapInvalidate = true;
|
||||
}
|
||||
}
|
||||
// Pan map to follow ISS only when the map pane is currently renderable.
|
||||
if (isMapContainerVisible()) {
|
||||
issMap.panTo([lat, lon], { animate: true, duration: 0.5 });
|
||||
} else {
|
||||
pendingMapInvalidate = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check current decoder status
|
||||
@@ -1335,27 +1335,27 @@ const SSTV = (function() {
|
||||
/**
|
||||
* Show status message
|
||||
*/
|
||||
function showStatusMessage(message, type) {
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification('SSTV', message);
|
||||
} else {
|
||||
console.log(`[SSTV ${type}] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate ISS map size after pane/layout changes.
|
||||
*/
|
||||
function invalidateMap() {
|
||||
if (!issMap) return false;
|
||||
if (!isMapContainerVisible()) {
|
||||
pendingMapInvalidate = true;
|
||||
return false;
|
||||
}
|
||||
issMap.invalidateSize({ pan: false, animate: false });
|
||||
pendingMapInvalidate = false;
|
||||
return true;
|
||||
}
|
||||
function showStatusMessage(message, type) {
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification('SSTV', message);
|
||||
} else {
|
||||
console.log(`[SSTV ${type}] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate ISS map size after pane/layout changes.
|
||||
*/
|
||||
function invalidateMap() {
|
||||
if (!issMap) return false;
|
||||
if (!isMapContainerVisible()) {
|
||||
pendingMapInvalidate = true;
|
||||
return false;
|
||||
}
|
||||
issMap.invalidateSize({ pan: false, animate: false });
|
||||
pendingMapInvalidate = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
@@ -1370,12 +1370,25 @@ const SSTV = (function() {
|
||||
deleteAllImages,
|
||||
downloadImage,
|
||||
useGPS,
|
||||
updateTLE,
|
||||
stopIssTracking,
|
||||
stopCountdown,
|
||||
invalidateMap
|
||||
};
|
||||
})();
|
||||
updateTLE,
|
||||
stopIssTracking,
|
||||
stopCountdown,
|
||||
invalidateMap,
|
||||
destroy
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroy — close SSE stream and clear ISS tracking/countdown timers for clean mode switching.
|
||||
*/
|
||||
function destroy() {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
stopIssTracking();
|
||||
stopCountdown();
|
||||
}
|
||||
})();
|
||||
|
||||
// Initialize when DOM is ready (will be called by selectMode)
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
@@ -1005,6 +1005,15 @@ function escapeHtmlWebsdr(str) {
|
||||
|
||||
// ============== EXPORTS ==============
|
||||
|
||||
/**
|
||||
* Destroy — disconnect audio and clear S-meter timer for clean mode switching.
|
||||
*/
|
||||
function destroyWebSDR() {
|
||||
disconnectFromReceiver();
|
||||
}
|
||||
|
||||
const WebSDR = { destroy: destroyWebSDR };
|
||||
|
||||
window.initWebSDR = initWebSDR;
|
||||
window.searchReceivers = searchReceivers;
|
||||
window.selectReceiver = selectReceiver;
|
||||
@@ -1015,3 +1024,4 @@ window.disconnectFromReceiver = disconnectFromReceiver;
|
||||
window.tuneKiwi = tuneKiwi;
|
||||
window.tuneFromBar = tuneFromBar;
|
||||
window.setKiwiVolume = setKiwiVolume;
|
||||
window.WebSDR = WebSDR;
|
||||
|
||||
@@ -28,9 +28,9 @@ const WiFiMode = (function() {
|
||||
maxProbes: 1000,
|
||||
};
|
||||
|
||||
// ==========================================================================
|
||||
// Agent Support
|
||||
// ==========================================================================
|
||||
// ==========================================================================
|
||||
// Agent Support
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Get the API base URL, routing through agent proxy if agent is selected.
|
||||
@@ -59,49 +59,49 @@ const WiFiMode = (function() {
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
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
|
||||
@@ -120,23 +120,23 @@ const WiFiMode = (function() {
|
||||
let channelStats = [];
|
||||
let recommendations = [];
|
||||
|
||||
// UI state
|
||||
let selectedNetwork = null;
|
||||
let currentFilter = 'all';
|
||||
let currentSort = { field: 'rssi', order: 'desc' };
|
||||
let renderFramePending = false;
|
||||
const pendingRender = {
|
||||
table: false,
|
||||
stats: false,
|
||||
radar: false,
|
||||
chart: false,
|
||||
detail: false,
|
||||
};
|
||||
const listenersBound = {
|
||||
scanTabs: false,
|
||||
filters: false,
|
||||
sort: false,
|
||||
};
|
||||
// UI state
|
||||
let selectedNetwork = null;
|
||||
let currentFilter = 'all';
|
||||
let currentSort = { field: 'rssi', order: 'desc' };
|
||||
let renderFramePending = false;
|
||||
const pendingRender = {
|
||||
table: false,
|
||||
stats: false,
|
||||
radar: false,
|
||||
chart: false,
|
||||
detail: false,
|
||||
};
|
||||
const listenersBound = {
|
||||
scanTabs: false,
|
||||
filters: false,
|
||||
sort: false,
|
||||
};
|
||||
|
||||
// Agent state
|
||||
let showAllAgentsMode = false; // Show combined results from all agents
|
||||
@@ -165,11 +165,11 @@ const WiFiMode = (function() {
|
||||
|
||||
// Initialize components
|
||||
initScanModeTabs();
|
||||
initNetworkFilters();
|
||||
initSortControls();
|
||||
initProximityRadar();
|
||||
initChannelChart();
|
||||
scheduleRender({ table: true, stats: true, radar: true, chart: true });
|
||||
initNetworkFilters();
|
||||
initSortControls();
|
||||
initProximityRadar();
|
||||
initChannelChart();
|
||||
scheduleRender({ table: true, stats: true, radar: true, chart: true });
|
||||
|
||||
// Check if already scanning
|
||||
checkScanStatus();
|
||||
@@ -378,16 +378,16 @@ const WiFiMode = (function() {
|
||||
// Scan Mode Tabs
|
||||
// ==========================================================================
|
||||
|
||||
function initScanModeTabs() {
|
||||
if (listenersBound.scanTabs) return;
|
||||
if (elements.scanModeQuick) {
|
||||
elements.scanModeQuick.addEventListener('click', () => setScanMode('quick'));
|
||||
}
|
||||
if (elements.scanModeDeep) {
|
||||
elements.scanModeDeep.addEventListener('click', () => setScanMode('deep'));
|
||||
}
|
||||
listenersBound.scanTabs = true;
|
||||
}
|
||||
function initScanModeTabs() {
|
||||
if (listenersBound.scanTabs) return;
|
||||
if (elements.scanModeQuick) {
|
||||
elements.scanModeQuick.addEventListener('click', () => setScanMode('quick'));
|
||||
}
|
||||
if (elements.scanModeDeep) {
|
||||
elements.scanModeDeep.addEventListener('click', () => setScanMode('deep'));
|
||||
}
|
||||
listenersBound.scanTabs = true;
|
||||
}
|
||||
|
||||
function setScanMode(mode) {
|
||||
scanMode = mode;
|
||||
@@ -511,10 +511,10 @@ const WiFiMode = (function() {
|
||||
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';
|
||||
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) {
|
||||
@@ -523,25 +523,25 @@ const WiFiMode = (function() {
|
||||
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,
|
||||
}),
|
||||
});
|
||||
}
|
||||
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();
|
||||
@@ -572,8 +572,8 @@ const WiFiMode = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
async function stopScan() {
|
||||
console.log('[WiFiMode] Stopping scan...');
|
||||
async function stopScan() {
|
||||
console.log('[WiFiMode] Stopping scan...');
|
||||
|
||||
// Stop polling
|
||||
if (pollTimer) {
|
||||
@@ -585,41 +585,41 @@ const WiFiMode = (function() {
|
||||
stopAgentDeepScanPolling();
|
||||
|
||||
// Close event stream
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
|
||||
// Update UI immediately so mode transitions are responsive even if the
|
||||
// backend needs extra time to terminate subprocesses.
|
||||
setScanning(false);
|
||||
|
||||
// Stop scan on server (local or agent)
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
const timeoutMs = isAgentMode ? 8000 : 2200;
|
||||
const controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
||||
const timeoutId = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
||||
|
||||
try {
|
||||
if (isAgentMode) {
|
||||
await fetch(`/controller/agents/${currentAgent}/wifi/stop`, {
|
||||
method: 'POST',
|
||||
...(controller ? { signal: controller.signal } : {}),
|
||||
});
|
||||
} else if (scanMode === 'deep') {
|
||||
await fetch(`${CONFIG.apiBase}/scan/stop`, {
|
||||
method: 'POST',
|
||||
...(controller ? { signal: controller.signal } : {}),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[WiFiMode] Error stopping scan:', error);
|
||||
} finally {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
|
||||
// Update UI immediately so mode transitions are responsive even if the
|
||||
// backend needs extra time to terminate subprocesses.
|
||||
setScanning(false);
|
||||
|
||||
// Stop scan on server (local or agent)
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
const timeoutMs = isAgentMode ? 8000 : 2200;
|
||||
const controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
||||
const timeoutId = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
||||
|
||||
try {
|
||||
if (isAgentMode) {
|
||||
await fetch(`/controller/agents/${currentAgent}/wifi/stop`, {
|
||||
method: 'POST',
|
||||
...(controller ? { signal: controller.signal } : {}),
|
||||
});
|
||||
} else if (scanMode === 'deep') {
|
||||
await fetch(`${CONFIG.apiBase}/scan/stop`, {
|
||||
method: 'POST',
|
||||
...(controller ? { signal: controller.signal } : {}),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[WiFiMode] Error stopping scan:', error);
|
||||
} finally {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setScanning(scanning, mode = null) {
|
||||
isScanning = scanning;
|
||||
@@ -713,10 +713,10 @@ const WiFiMode = (function() {
|
||||
}, CONFIG.pollInterval);
|
||||
}
|
||||
|
||||
function processQuickScanResult(result) {
|
||||
// Update networks
|
||||
result.access_points.forEach(ap => {
|
||||
networks.set(ap.bssid, ap);
|
||||
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)
|
||||
@@ -724,12 +724,12 @@ const WiFiMode = (function() {
|
||||
recommendations = result.recommendations || [];
|
||||
|
||||
// If no channel stats from API, calculate from networks
|
||||
if (channelStats.length === 0 && networks.size > 0) {
|
||||
channelStats = calculateChannelStats();
|
||||
}
|
||||
|
||||
// Update UI
|
||||
scheduleRender({ table: true, stats: true, radar: true, chart: true });
|
||||
if (channelStats.length === 0 && networks.size > 0) {
|
||||
channelStats = calculateChannelStats();
|
||||
}
|
||||
|
||||
// Update UI
|
||||
scheduleRender({ table: true, stats: true, radar: true, chart: true });
|
||||
|
||||
// Callbacks
|
||||
result.access_points.forEach(ap => {
|
||||
@@ -938,25 +938,25 @@ const WiFiMode = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleNetworkUpdate(network) {
|
||||
networks.set(network.bssid, network);
|
||||
scheduleRender({
|
||||
table: true,
|
||||
stats: true,
|
||||
radar: true,
|
||||
chart: true,
|
||||
detail: selectedNetwork === network.bssid,
|
||||
});
|
||||
|
||||
if (onNetworkUpdate) onNetworkUpdate(network);
|
||||
}
|
||||
|
||||
function handleClientUpdate(client) {
|
||||
clients.set(client.mac, client);
|
||||
scheduleRender({ stats: true });
|
||||
|
||||
// Update client display if this client belongs to the selected network
|
||||
updateClientInList(client);
|
||||
function handleNetworkUpdate(network) {
|
||||
networks.set(network.bssid, network);
|
||||
scheduleRender({
|
||||
table: true,
|
||||
stats: true,
|
||||
radar: true,
|
||||
chart: true,
|
||||
detail: selectedNetwork === network.bssid,
|
||||
});
|
||||
|
||||
if (onNetworkUpdate) onNetworkUpdate(network);
|
||||
}
|
||||
|
||||
function handleClientUpdate(client) {
|
||||
clients.set(client.mac, client);
|
||||
scheduleRender({ stats: true });
|
||||
|
||||
// Update client display if this client belongs to the selected network
|
||||
updateClientInList(client);
|
||||
|
||||
if (onClientUpdate) onClientUpdate(client);
|
||||
}
|
||||
@@ -970,37 +970,37 @@ const WiFiMode = (function() {
|
||||
if (onProbeRequest) onProbeRequest(probe);
|
||||
}
|
||||
|
||||
function handleHiddenRevealed(bssid, revealedSsid) {
|
||||
const network = networks.get(bssid);
|
||||
if (network) {
|
||||
network.revealed_essid = revealedSsid;
|
||||
network.display_name = `${revealedSsid} (revealed)`;
|
||||
scheduleRender({
|
||||
table: true,
|
||||
detail: selectedNetwork === bssid,
|
||||
});
|
||||
|
||||
// Show notification
|
||||
showInfo(`Hidden SSID revealed: ${revealedSsid}`);
|
||||
}
|
||||
}
|
||||
function handleHiddenRevealed(bssid, revealedSsid) {
|
||||
const network = networks.get(bssid);
|
||||
if (network) {
|
||||
network.revealed_essid = revealedSsid;
|
||||
network.display_name = `${revealedSsid} (revealed)`;
|
||||
scheduleRender({
|
||||
table: true,
|
||||
detail: selectedNetwork === bssid,
|
||||
});
|
||||
|
||||
// Show notification
|
||||
showInfo(`Hidden SSID revealed: ${revealedSsid}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Network Table
|
||||
// ==========================================================================
|
||||
|
||||
function initNetworkFilters() {
|
||||
if (listenersBound.filters) return;
|
||||
if (!elements.networkFilters) return;
|
||||
|
||||
elements.networkFilters.addEventListener('click', (e) => {
|
||||
if (e.target.matches('.wifi-filter-btn')) {
|
||||
const filter = e.target.dataset.filter;
|
||||
setNetworkFilter(filter);
|
||||
}
|
||||
});
|
||||
listenersBound.filters = true;
|
||||
}
|
||||
function initNetworkFilters() {
|
||||
if (listenersBound.filters) return;
|
||||
if (!elements.networkFilters) return;
|
||||
|
||||
elements.networkFilters.addEventListener('click', (e) => {
|
||||
if (e.target.matches('.wifi-filter-btn')) {
|
||||
const filter = e.target.dataset.filter;
|
||||
setNetworkFilter(filter);
|
||||
}
|
||||
});
|
||||
listenersBound.filters = true;
|
||||
}
|
||||
|
||||
function setNetworkFilter(filter) {
|
||||
currentFilter = filter;
|
||||
@@ -1015,11 +1015,11 @@ const WiFiMode = (function() {
|
||||
updateNetworkTable();
|
||||
}
|
||||
|
||||
function initSortControls() {
|
||||
if (listenersBound.sort) return;
|
||||
if (!elements.networkTable) return;
|
||||
|
||||
elements.networkTable.addEventListener('click', (e) => {
|
||||
function initSortControls() {
|
||||
if (listenersBound.sort) return;
|
||||
if (!elements.networkTable) return;
|
||||
|
||||
elements.networkTable.addEventListener('click', (e) => {
|
||||
const th = e.target.closest('th[data-sort]');
|
||||
if (th) {
|
||||
const field = th.dataset.sort;
|
||||
@@ -1029,54 +1029,54 @@ const WiFiMode = (function() {
|
||||
currentSort.field = field;
|
||||
currentSort.order = 'desc';
|
||||
}
|
||||
updateNetworkTable();
|
||||
}
|
||||
});
|
||||
|
||||
if (elements.networkTableBody) {
|
||||
elements.networkTableBody.addEventListener('click', (e) => {
|
||||
const row = e.target.closest('tr[data-bssid]');
|
||||
if (!row) return;
|
||||
selectNetwork(row.dataset.bssid);
|
||||
});
|
||||
}
|
||||
listenersBound.sort = true;
|
||||
}
|
||||
|
||||
function scheduleRender(flags = {}) {
|
||||
pendingRender.table = pendingRender.table || Boolean(flags.table);
|
||||
pendingRender.stats = pendingRender.stats || Boolean(flags.stats);
|
||||
pendingRender.radar = pendingRender.radar || Boolean(flags.radar);
|
||||
pendingRender.chart = pendingRender.chart || Boolean(flags.chart);
|
||||
pendingRender.detail = pendingRender.detail || Boolean(flags.detail);
|
||||
|
||||
if (renderFramePending) return;
|
||||
renderFramePending = true;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
renderFramePending = false;
|
||||
|
||||
if (pendingRender.table) updateNetworkTable();
|
||||
if (pendingRender.stats) updateStats();
|
||||
if (pendingRender.radar) updateProximityRadar();
|
||||
if (pendingRender.chart) updateChannelChart();
|
||||
if (pendingRender.detail && selectedNetwork) {
|
||||
updateDetailPanel(selectedNetwork, { refreshClients: false });
|
||||
}
|
||||
|
||||
pendingRender.table = false;
|
||||
pendingRender.stats = false;
|
||||
pendingRender.radar = false;
|
||||
pendingRender.chart = false;
|
||||
pendingRender.detail = false;
|
||||
});
|
||||
}
|
||||
|
||||
function updateNetworkTable() {
|
||||
if (!elements.networkTableBody) return;
|
||||
|
||||
// Filter networks
|
||||
let filtered = Array.from(networks.values());
|
||||
updateNetworkTable();
|
||||
}
|
||||
});
|
||||
|
||||
if (elements.networkTableBody) {
|
||||
elements.networkTableBody.addEventListener('click', (e) => {
|
||||
const row = e.target.closest('tr[data-bssid]');
|
||||
if (!row) return;
|
||||
selectNetwork(row.dataset.bssid);
|
||||
});
|
||||
}
|
||||
listenersBound.sort = true;
|
||||
}
|
||||
|
||||
function scheduleRender(flags = {}) {
|
||||
pendingRender.table = pendingRender.table || Boolean(flags.table);
|
||||
pendingRender.stats = pendingRender.stats || Boolean(flags.stats);
|
||||
pendingRender.radar = pendingRender.radar || Boolean(flags.radar);
|
||||
pendingRender.chart = pendingRender.chart || Boolean(flags.chart);
|
||||
pendingRender.detail = pendingRender.detail || Boolean(flags.detail);
|
||||
|
||||
if (renderFramePending) return;
|
||||
renderFramePending = true;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
renderFramePending = false;
|
||||
|
||||
if (pendingRender.table) updateNetworkTable();
|
||||
if (pendingRender.stats) updateStats();
|
||||
if (pendingRender.radar) updateProximityRadar();
|
||||
if (pendingRender.chart) updateChannelChart();
|
||||
if (pendingRender.detail && selectedNetwork) {
|
||||
updateDetailPanel(selectedNetwork, { refreshClients: false });
|
||||
}
|
||||
|
||||
pendingRender.table = false;
|
||||
pendingRender.stats = false;
|
||||
pendingRender.radar = false;
|
||||
pendingRender.chart = false;
|
||||
pendingRender.detail = false;
|
||||
});
|
||||
}
|
||||
|
||||
function updateNetworkTable() {
|
||||
if (!elements.networkTableBody) return;
|
||||
|
||||
// Filter networks
|
||||
let filtered = Array.from(networks.values());
|
||||
|
||||
switch (currentFilter) {
|
||||
case 'hidden':
|
||||
@@ -1126,44 +1126,44 @@ const WiFiMode = (function() {
|
||||
return bVal > aVal ? 1 : bVal < aVal ? -1 : 0;
|
||||
} else {
|
||||
return aVal > bVal ? 1 : aVal < bVal ? -1 : 0;
|
||||
}
|
||||
});
|
||||
|
||||
if (filtered.length === 0) {
|
||||
let message = 'Start scanning to discover networks';
|
||||
let type = 'empty';
|
||||
if (isScanning) {
|
||||
message = 'Scanning for networks...';
|
||||
type = 'loading';
|
||||
} else if (networks.size > 0) {
|
||||
message = 'No networks match current filters';
|
||||
}
|
||||
if (typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(elements.networkTableBody, {
|
||||
type,
|
||||
message,
|
||||
columns: 7,
|
||||
});
|
||||
} else {
|
||||
elements.networkTableBody.innerHTML = `<tr class="wifi-network-placeholder"><td colspan="7"><div class="placeholder-text">${escapeHtml(message)}</div></td></tr>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Render table
|
||||
elements.networkTableBody.innerHTML = filtered.map(n => createNetworkRow(n)).join('');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function createNetworkRow(network) {
|
||||
const rssi = network.rssi_current;
|
||||
const security = network.security || 'Unknown';
|
||||
const signalClass = rssi >= -50 ? 'signal-strong' :
|
||||
rssi >= -70 ? 'signal-medium' :
|
||||
rssi >= -85 ? 'signal-weak' : 'signal-very-weak';
|
||||
|
||||
const securityClass = security === 'Open' ? 'security-open' :
|
||||
security === 'WEP' ? 'security-wep' :
|
||||
security.includes('WPA3') ? 'security-wpa3' : 'security-wpa';
|
||||
if (filtered.length === 0) {
|
||||
let message = 'Start scanning to discover networks';
|
||||
let type = 'empty';
|
||||
if (isScanning) {
|
||||
message = 'Scanning for networks...';
|
||||
type = 'loading';
|
||||
} else if (networks.size > 0) {
|
||||
message = 'No networks match current filters';
|
||||
}
|
||||
if (typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(elements.networkTableBody, {
|
||||
type,
|
||||
message,
|
||||
columns: 7,
|
||||
});
|
||||
} else {
|
||||
elements.networkTableBody.innerHTML = `<tr class="wifi-network-placeholder"><td colspan="7"><div class="placeholder-text">${escapeHtml(message)}</div></td></tr>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Render table
|
||||
elements.networkTableBody.innerHTML = filtered.map(n => createNetworkRow(n)).join('');
|
||||
}
|
||||
|
||||
function createNetworkRow(network) {
|
||||
const rssi = network.rssi_current;
|
||||
const security = network.security || 'Unknown';
|
||||
const signalClass = rssi >= -50 ? 'signal-strong' :
|
||||
rssi >= -70 ? 'signal-medium' :
|
||||
rssi >= -85 ? 'signal-weak' : 'signal-very-weak';
|
||||
|
||||
const securityClass = security === 'Open' ? 'security-open' :
|
||||
security === 'WEP' ? 'security-wep' :
|
||||
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>' : '';
|
||||
@@ -1172,25 +1172,25 @@ const WiFiMode = (function() {
|
||||
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)}"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
data-keyboard-activate="true"
|
||||
aria-label="Select network ${escapeHtml(network.display_name || network.essid || '[Hidden]')}">
|
||||
<td class="col-essid">
|
||||
<span class="essid">${escapeHtml(network.display_name || network.essid || '[Hidden]')}</span>
|
||||
${hiddenBadge}${newBadge}
|
||||
</td>
|
||||
return `
|
||||
<tr class="wifi-network-row ${network.bssid === selectedNetwork ? 'selected' : ''}"
|
||||
data-bssid="${escapeHtml(network.bssid)}"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
data-keyboard-activate="true"
|
||||
aria-label="Select network ${escapeHtml(network.display_name || network.essid || '[Hidden]')}">
|
||||
<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(security)}</span>
|
||||
</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(security)}</span>
|
||||
</td>
|
||||
<td class="col-clients">${network.client_count || 0}</td>
|
||||
<td class="col-agent">
|
||||
<span class="agent-badge ${agentClass}">${escapeHtml(agentName)}</span>
|
||||
@@ -1199,12 +1199,12 @@ const WiFiMode = (function() {
|
||||
`;
|
||||
}
|
||||
|
||||
function updateNetworkRow(network) {
|
||||
scheduleRender({
|
||||
table: true,
|
||||
detail: selectedNetwork === network.bssid,
|
||||
});
|
||||
}
|
||||
function updateNetworkRow(network) {
|
||||
scheduleRender({
|
||||
table: true,
|
||||
detail: selectedNetwork === network.bssid,
|
||||
});
|
||||
}
|
||||
|
||||
function selectNetwork(bssid) {
|
||||
selectedNetwork = bssid;
|
||||
@@ -1227,9 +1227,9 @@ const WiFiMode = (function() {
|
||||
// Detail Panel
|
||||
// ==========================================================================
|
||||
|
||||
function updateDetailPanel(bssid, options = {}) {
|
||||
const { refreshClients = true } = options;
|
||||
if (!elements.detailDrawer) return;
|
||||
function updateDetailPanel(bssid, options = {}) {
|
||||
const { refreshClients = true } = options;
|
||||
if (!elements.detailDrawer) return;
|
||||
|
||||
const network = networks.get(bssid);
|
||||
if (!network) {
|
||||
@@ -1274,11 +1274,11 @@ const WiFiMode = (function() {
|
||||
// Show the drawer
|
||||
elements.detailDrawer.classList.add('open');
|
||||
|
||||
// Fetch and display clients for this network
|
||||
if (refreshClients) {
|
||||
fetchClientsForNetwork(network.bssid);
|
||||
}
|
||||
}
|
||||
// Fetch and display clients for this network
|
||||
if (refreshClients) {
|
||||
fetchClientsForNetwork(network.bssid);
|
||||
}
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
selectedNetwork = null;
|
||||
@@ -1294,18 +1294,18 @@ const WiFiMode = (function() {
|
||||
// Client Display
|
||||
// ==========================================================================
|
||||
|
||||
async function fetchClientsForNetwork(bssid) {
|
||||
if (!elements.detailClientList) return;
|
||||
const listContainer = elements.detailClientList.querySelector('.wifi-client-list');
|
||||
|
||||
if (listContainer && typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(listContainer, { type: 'loading', message: 'Loading clients...' });
|
||||
elements.detailClientList.style.display = 'block';
|
||||
}
|
||||
|
||||
try {
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
let response;
|
||||
async function fetchClientsForNetwork(bssid) {
|
||||
if (!elements.detailClientList) return;
|
||||
const listContainer = elements.detailClientList.querySelector('.wifi-client-list');
|
||||
|
||||
if (listContainer && typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(listContainer, { type: 'loading', message: 'Loading clients...' });
|
||||
elements.detailClientList.style.display = 'block';
|
||||
}
|
||||
|
||||
try {
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
let response;
|
||||
|
||||
if (isAgentMode) {
|
||||
// Route through agent proxy
|
||||
@@ -1314,44 +1314,44 @@ const WiFiMode = (function() {
|
||||
response = await fetch(`${CONFIG.apiBase}/clients?bssid=${encodeURIComponent(bssid)}&associated=true`);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
if (listContainer && typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(listContainer, { type: 'empty', message: 'Client list unavailable' });
|
||||
elements.detailClientList.style.display = 'block';
|
||||
} else {
|
||||
elements.detailClientList.style.display = 'none';
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!response.ok) {
|
||||
if (listContainer && typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(listContainer, { type: 'empty', message: 'Client list unavailable' });
|
||||
elements.detailClientList.style.display = 'block';
|
||||
} else {
|
||||
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 {
|
||||
const countBadge = document.getElementById('wifiClientCountBadge');
|
||||
if (countBadge) countBadge.textContent = '0';
|
||||
if (listContainer && typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(listContainer, { type: 'empty', message: 'No associated clients' });
|
||||
elements.detailClientList.style.display = 'block';
|
||||
} else {
|
||||
elements.detailClientList.style.display = 'none';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug('[WiFiMode] Error fetching clients:', error);
|
||||
if (listContainer && typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(listContainer, { type: 'empty', message: 'Client list unavailable' });
|
||||
elements.detailClientList.style.display = 'block';
|
||||
} else {
|
||||
elements.detailClientList.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (clientList.length > 0) {
|
||||
renderClientList(clientList, bssid);
|
||||
elements.detailClientList.style.display = 'block';
|
||||
} else {
|
||||
const countBadge = document.getElementById('wifiClientCountBadge');
|
||||
if (countBadge) countBadge.textContent = '0';
|
||||
if (listContainer && typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(listContainer, { type: 'empty', message: 'No associated clients' });
|
||||
elements.detailClientList.style.display = 'block';
|
||||
} else {
|
||||
elements.detailClientList.style.display = 'none';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug('[WiFiMode] Error fetching clients:', error);
|
||||
if (listContainer && typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(listContainer, { type: 'empty', message: 'Client list unavailable' });
|
||||
elements.detailClientList.style.display = 'block';
|
||||
} else {
|
||||
elements.detailClientList.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderClientList(clientList, bssid) {
|
||||
const container = elements.detailClientList?.querySelector('.wifi-client-list');
|
||||
@@ -1708,16 +1708,16 @@ const WiFiMode = (function() {
|
||||
/**
|
||||
* Clear all collected data.
|
||||
*/
|
||||
function clearData() {
|
||||
networks.clear();
|
||||
clients.clear();
|
||||
probeRequests = [];
|
||||
channelStats = [];
|
||||
recommendations = [];
|
||||
if (selectedNetwork) {
|
||||
closeDetail();
|
||||
}
|
||||
scheduleRender({ table: true, stats: true, radar: true, chart: true });
|
||||
function clearData() {
|
||||
networks.clear();
|
||||
clients.clear();
|
||||
probeRequests = [];
|
||||
channelStats = [];
|
||||
recommendations = [];
|
||||
if (selectedNetwork) {
|
||||
closeDetail();
|
||||
}
|
||||
scheduleRender({ table: true, stats: true, radar: true, chart: true });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1763,12 +1763,12 @@ const WiFiMode = (function() {
|
||||
clientsToRemove.push(mac);
|
||||
}
|
||||
});
|
||||
clientsToRemove.forEach(mac => clients.delete(mac));
|
||||
if (selectedNetwork && !networks.has(selectedNetwork)) {
|
||||
closeDetail();
|
||||
}
|
||||
scheduleRender({ table: true, stats: true, radar: true, chart: true });
|
||||
}
|
||||
clientsToRemove.forEach(mac => clients.delete(mac));
|
||||
if (selectedNetwork && !networks.has(selectedNetwork)) {
|
||||
closeDetail();
|
||||
}
|
||||
scheduleRender({ table: true, stats: true, radar: true, chart: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh WiFi interfaces from current agent.
|
||||
@@ -1811,7 +1811,28 @@ const WiFiMode = (function() {
|
||||
onNetworkUpdate: (cb) => { onNetworkUpdate = cb; },
|
||||
onClientUpdate: (cb) => { onClientUpdate = cb; },
|
||||
onProbeRequest: (cb) => { onProbeRequest = cb; },
|
||||
|
||||
// Lifecycle
|
||||
destroy,
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroy — close SSE stream and clear polling timers for clean mode switching.
|
||||
*/
|
||||
function destroy() {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
if (agentPollTimer) {
|
||||
clearInterval(agentPollTimer);
|
||||
agentPollTimer = null;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
|
||||
@@ -4140,12 +4140,27 @@
|
||||
const stopPhaseMs = Math.round(performance.now() - stopPhaseStartMs);
|
||||
await styleReadyPromise;
|
||||
|
||||
// Clean up SubGHz SSE connection when leaving the mode
|
||||
if (typeof SubGhz !== 'undefined' && currentMode === 'subghz' && mode !== 'subghz') {
|
||||
SubGhz.destroy();
|
||||
}
|
||||
if (typeof MorseMode !== 'undefined' && currentMode === 'morse' && mode !== 'morse' && typeof MorseMode.destroy === 'function') {
|
||||
MorseMode.destroy();
|
||||
// Generic module cleanup — destroy previous mode's timers, SSE, etc.
|
||||
const moduleDestroyMap = {
|
||||
subghz: () => typeof SubGhz !== 'undefined' && SubGhz.destroy(),
|
||||
morse: () => typeof MorseMode !== 'undefined' && MorseMode.destroy?.(),
|
||||
spaceweather: () => typeof SpaceWeather !== 'undefined' && SpaceWeather.destroy?.(),
|
||||
weathersat: () => typeof WeatherSat !== 'undefined' && WeatherSat.suspend?.(),
|
||||
wefax: () => typeof WeFax !== 'undefined' && WeFax.destroy?.(),
|
||||
system: () => typeof SystemHealth !== 'undefined' && SystemHealth.destroy?.(),
|
||||
waterfall: () => typeof Waterfall !== 'undefined' && Waterfall.destroy?.(),
|
||||
gps: () => typeof GPS !== 'undefined' && GPS.destroy?.(),
|
||||
meshtastic: () => typeof Meshtastic !== 'undefined' && Meshtastic.destroy?.(),
|
||||
bluetooth: () => typeof BluetoothMode !== 'undefined' && BluetoothMode.destroy?.(),
|
||||
wifi: () => typeof WiFiMode !== 'undefined' && WiFiMode.destroy?.(),
|
||||
bt_locate: () => typeof BtLocate !== 'undefined' && BtLocate.destroy?.(),
|
||||
sstv: () => typeof SSTV !== 'undefined' && SSTV.destroy?.(),
|
||||
sstv_general: () => typeof SSTVGeneral !== 'undefined' && SSTVGeneral.destroy?.(),
|
||||
websdr: () => typeof WebSDR !== 'undefined' && WebSDR.destroy?.(),
|
||||
spystations: () => typeof SpyStations !== 'undefined' && SpyStations.destroy?.(),
|
||||
};
|
||||
if (previousMode && previousMode !== mode && moduleDestroyMap[previousMode]) {
|
||||
try { moduleDestroyMap[previousMode](); } catch(e) { console.warn(`[switchMode] destroy ${previousMode} failed:`, e); }
|
||||
}
|
||||
|
||||
currentMode = mode;
|
||||
@@ -4301,25 +4316,7 @@
|
||||
refreshTscmDevices();
|
||||
}
|
||||
|
||||
// Initialize/destroy Space Weather mode
|
||||
if (mode !== 'spaceweather') {
|
||||
if (typeof SpaceWeather !== 'undefined' && SpaceWeather.destroy) SpaceWeather.destroy();
|
||||
}
|
||||
|
||||
// Suspend Weather Satellite background timers/streams when leaving the mode
|
||||
if (mode !== 'weathersat') {
|
||||
if (typeof WeatherSat !== 'undefined' && WeatherSat.suspend) WeatherSat.suspend();
|
||||
}
|
||||
|
||||
// Suspend WeFax background streams when leaving the mode
|
||||
if (mode !== 'wefax') {
|
||||
if (typeof WeFax !== 'undefined' && WeFax.destroy) WeFax.destroy();
|
||||
}
|
||||
|
||||
// Disconnect System Health SSE when leaving the mode
|
||||
if (mode !== 'system') {
|
||||
if (typeof SystemHealth !== 'undefined' && SystemHealth.destroy) SystemHealth.destroy();
|
||||
}
|
||||
// Module destroy is now handled by moduleDestroyMap above.
|
||||
|
||||
// Show/hide Device Intelligence for modes that use it (not for satellite/aircraft/tscm)
|
||||
const reconBtn = document.getElementById('reconBtn');
|
||||
@@ -4460,10 +4457,7 @@
|
||||
SystemHealth.init();
|
||||
}
|
||||
|
||||
// Destroy Waterfall WebSocket when leaving SDR receiver modes
|
||||
if (mode !== 'waterfall' && typeof Waterfall !== 'undefined' && Waterfall.destroy) {
|
||||
Promise.resolve(Waterfall.destroy()).catch(() => {});
|
||||
}
|
||||
// Waterfall destroy is now handled by moduleDestroyMap above.
|
||||
|
||||
const totalMs = Math.round(performance.now() - switchStartMs);
|
||||
console.info(
|
||||
|
||||
Reference in New Issue
Block a user