diff --git a/src/rf_mapper/web/static/js/app.js b/src/rf_mapper/web/static/js/app.js index 5a0c67c..85ae17e 100644 --- a/src/rf_mapper/web/static/js/app.js +++ b/src/rf_mapper/web/static/js/app.js @@ -64,6 +64,11 @@ let deviceDistanceHistory = {}; let deviceMissCount = {}; const MAX_MISSED_SCANS = 5; // Remove device after this many consecutive misses (~20s with 4s interval) +// Track last seen timestamp per device for timeout-based removal +let deviceLastSeen = {}; // { address: timestamp_ms } +const STALE_DEVICE_TIMEOUT_MS = 60000; // Remove devices not seen for 60 seconds +let staleDeviceCleanupInterval = null; + // Calculate mean of array function mean(arr) { if (arr.length === 0) return 0; @@ -142,6 +147,81 @@ function filterScannerDevices(data) { return data; } +// Update last seen timestamp for a device +function updateDeviceLastSeen(address) { + deviceLastSeen[address] = Date.now(); +} + +// Clean up stale devices that haven't been seen recently +function cleanupStaleDevices() { + if (!scanData) return; + + const now = Date.now(); + let removedCount = 0; + + // Clean up Bluetooth devices + if (scanData.bluetooth_devices) { + const before = scanData.bluetooth_devices.length; + scanData.bluetooth_devices = scanData.bluetooth_devices.filter(dev => { + const lastSeen = deviceLastSeen[dev.address]; + if (!lastSeen || (now - lastSeen) > STALE_DEVICE_TIMEOUT_MS) { + // Clean up tracking data + delete deviceMissCount[dev.address]; + delete deviceDistanceHistory[dev.address]; + delete deviceLastSeen[dev.address]; + if (deviceTrails[dev.address]) { + clearDeviceTrail(dev.address); + } + console.log(`[Stale] Removed BT ${dev.name || dev.address} (timeout)`); + return false; + } + return true; + }); + removedCount += before - scanData.bluetooth_devices.length; + } + + // Clean up WiFi networks + if (scanData.wifi_networks) { + const before = scanData.wifi_networks.length; + scanData.wifi_networks = scanData.wifi_networks.filter(net => { + const id = net.bssid || net.ssid; + const lastSeen = deviceLastSeen[id]; + if (!lastSeen || (now - lastSeen) > STALE_DEVICE_TIMEOUT_MS) { + delete deviceLastSeen[id]; + console.log(`[Stale] Removed WiFi ${net.ssid || net.bssid} (timeout)`); + return false; + } + return true; + }); + removedCount += before - scanData.wifi_networks.length; + } + + // Update UI if devices were removed + if (removedCount > 0) { + document.getElementById('wifi-count').textContent = scanData.wifi_networks?.length || 0; + document.getElementById('bt-count').textContent = scanData.bluetooth_devices?.length || 0; + document.getElementById('bt-list-count').textContent = scanData.bluetooth_devices?.length || 0; + drawRadar(); + update3DMarkers(); + updateMapMarkers(); + } +} + +// Start stale device cleanup interval +function startStaleDeviceCleanup() { + if (staleDeviceCleanupInterval) return; + staleDeviceCleanupInterval = setInterval(cleanupStaleDevices, 10000); // Check every 10 seconds + console.log('[Cleanup] Stale device cleanup started (timeout: ' + (STALE_DEVICE_TIMEOUT_MS / 1000) + 's)'); +} + +// Stop stale device cleanup interval +function stopStaleDeviceCleanup() { + if (staleDeviceCleanupInterval) { + clearInterval(staleDeviceCleanupInterval); + staleDeviceCleanupInterval = null; + } +} + // Device positions for hit detection (radar view) let devicePositions = []; @@ -170,6 +250,7 @@ document.addEventListener('DOMContentLoaded', () => { loadAutoScanStatus(); loadDevicePositions(); // Load saved manual positions loadTrilateratedPositions(); // Load multi-scanner trilaterated positions + startStaleDeviceCleanup(); // Auto-remove devices not seen for 60s // Initialize 3D map as default view setTimeout(() => { @@ -336,6 +417,7 @@ async function switchNode(nodeId) { if (latestResp.ok) { scanData = filterScannerDevices(await latestResp.json()); + markAllDevicesSeen(); if (floorsResp.ok) { const floorsData = await floorsResp.json(); updateDeviceFloors(floorsData); @@ -434,6 +516,7 @@ function handleWebSocketScanUpdate(data) { // Reset miss count - device was detected deviceMissCount[newDev.address] = 0; + updateDeviceLastSeen(newDev.address); if (existing) { // Update RSSI and estimated distance, preserve custom values @@ -465,6 +548,7 @@ function handleWebSocketScanUpdate(data) { // Clean up tracking data for removed device delete deviceMissCount[dev.address]; delete deviceDistanceHistory[dev.address]; + delete deviceLastSeen[dev.address]; // Clear trail if showing if (deviceTrails[dev.address]) { clearDeviceTrail(dev.address); @@ -483,6 +567,7 @@ function handleWebSocketScanUpdate(data) { isDeviceMoving(dev.address, dev.estimated_distance_m); dev.is_moving = false; deviceMissCount[dev.address] = 0; + updateDeviceLastSeen(dev.address); }); scanData = { wifi_networks: [], @@ -609,12 +694,25 @@ function setView(view) { } } +// Mark all devices in scanData as seen now +function markAllDevicesSeen() { + if (!scanData) return; + const now = Date.now(); + (scanData.wifi_networks || []).forEach(net => { + deviceLastSeen[net.bssid || net.ssid] = now; + }); + (scanData.bluetooth_devices || []).forEach(dev => { + deviceLastSeen[dev.address] = now; + }); +} + // Load latest scan async function loadLatestScan() { try { const response = await fetch('/api/latest'); if (response.ok) { scanData = filterScannerDevices(await response.json()); + markAllDevicesSeen(); updateUI(); } else { document.getElementById('wifi-list').innerHTML = '
No scans yet. Click "New Scan" to start.
'; @@ -651,6 +749,7 @@ async function triggerScan() { if (response.ok) { scanData = filterScannerDevices(await response.json()); + markAllDevicesSeen(); updateUI(); status.textContent = `Scanned at ${new Date().toLocaleTimeString()}`; } else { @@ -2448,6 +2547,7 @@ async function performLiveBTScan() { // Reset miss count - device was detected deviceMissCount[newDev.address] = 0; + updateDeviceLastSeen(newDev.address); if (existing) { // Update RSSI and estimated distance, preserve custom values @@ -2479,6 +2579,7 @@ async function performLiveBTScan() { // Clean up tracking data for removed device delete deviceMissCount[dev.address]; delete deviceDistanceHistory[dev.address]; + delete deviceLastSeen[dev.address]; // Clear trail if showing if (deviceTrails[dev.address]) { clearDeviceTrail(dev.address); @@ -2497,6 +2598,7 @@ async function performLiveBTScan() { isDeviceMoving(dev.address, dev.estimated_distance_m); dev.is_moving = false; deviceMissCount[dev.address] = 0; + updateDeviceLastSeen(dev.address); }); scanData = { wifi_networks: [],