feat: auto-remove stale devices after 60s timeout

- Add deviceLastSeen tracking for all WiFi and BT devices
- Add cleanupStaleDevices() that runs every 10 seconds
- Remove devices not seen for 60 seconds (STALE_DEVICE_TIMEOUT_MS)
- Works regardless of live tracking state
- Updates lastSeen in all scan paths: manual, WS, polling, node switch

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
User
2026-02-01 17:32:05 +01:00
parent 9c9f27e55f
commit 322c53d513

View File

@@ -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 = '<div style="color:#888;padding:1rem;">No scans yet. Click "New Scan" to start.</div>';
@@ -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: [],