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:
@@ -64,6 +64,11 @@ let deviceDistanceHistory = {};
|
|||||||
let deviceMissCount = {};
|
let deviceMissCount = {};
|
||||||
const MAX_MISSED_SCANS = 5; // Remove device after this many consecutive misses (~20s with 4s interval)
|
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
|
// Calculate mean of array
|
||||||
function mean(arr) {
|
function mean(arr) {
|
||||||
if (arr.length === 0) return 0;
|
if (arr.length === 0) return 0;
|
||||||
@@ -142,6 +147,81 @@ function filterScannerDevices(data) {
|
|||||||
return 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)
|
// Device positions for hit detection (radar view)
|
||||||
let devicePositions = [];
|
let devicePositions = [];
|
||||||
|
|
||||||
@@ -170,6 +250,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
loadAutoScanStatus();
|
loadAutoScanStatus();
|
||||||
loadDevicePositions(); // Load saved manual positions
|
loadDevicePositions(); // Load saved manual positions
|
||||||
loadTrilateratedPositions(); // Load multi-scanner trilaterated positions
|
loadTrilateratedPositions(); // Load multi-scanner trilaterated positions
|
||||||
|
startStaleDeviceCleanup(); // Auto-remove devices not seen for 60s
|
||||||
|
|
||||||
// Initialize 3D map as default view
|
// Initialize 3D map as default view
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -336,6 +417,7 @@ async function switchNode(nodeId) {
|
|||||||
|
|
||||||
if (latestResp.ok) {
|
if (latestResp.ok) {
|
||||||
scanData = filterScannerDevices(await latestResp.json());
|
scanData = filterScannerDevices(await latestResp.json());
|
||||||
|
markAllDevicesSeen();
|
||||||
if (floorsResp.ok) {
|
if (floorsResp.ok) {
|
||||||
const floorsData = await floorsResp.json();
|
const floorsData = await floorsResp.json();
|
||||||
updateDeviceFloors(floorsData);
|
updateDeviceFloors(floorsData);
|
||||||
@@ -434,6 +516,7 @@ function handleWebSocketScanUpdate(data) {
|
|||||||
|
|
||||||
// Reset miss count - device was detected
|
// Reset miss count - device was detected
|
||||||
deviceMissCount[newDev.address] = 0;
|
deviceMissCount[newDev.address] = 0;
|
||||||
|
updateDeviceLastSeen(newDev.address);
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
// Update RSSI and estimated distance, preserve custom values
|
// Update RSSI and estimated distance, preserve custom values
|
||||||
@@ -465,6 +548,7 @@ function handleWebSocketScanUpdate(data) {
|
|||||||
// Clean up tracking data for removed device
|
// Clean up tracking data for removed device
|
||||||
delete deviceMissCount[dev.address];
|
delete deviceMissCount[dev.address];
|
||||||
delete deviceDistanceHistory[dev.address];
|
delete deviceDistanceHistory[dev.address];
|
||||||
|
delete deviceLastSeen[dev.address];
|
||||||
// Clear trail if showing
|
// Clear trail if showing
|
||||||
if (deviceTrails[dev.address]) {
|
if (deviceTrails[dev.address]) {
|
||||||
clearDeviceTrail(dev.address);
|
clearDeviceTrail(dev.address);
|
||||||
@@ -483,6 +567,7 @@ function handleWebSocketScanUpdate(data) {
|
|||||||
isDeviceMoving(dev.address, dev.estimated_distance_m);
|
isDeviceMoving(dev.address, dev.estimated_distance_m);
|
||||||
dev.is_moving = false;
|
dev.is_moving = false;
|
||||||
deviceMissCount[dev.address] = 0;
|
deviceMissCount[dev.address] = 0;
|
||||||
|
updateDeviceLastSeen(dev.address);
|
||||||
});
|
});
|
||||||
scanData = {
|
scanData = {
|
||||||
wifi_networks: [],
|
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
|
// Load latest scan
|
||||||
async function loadLatestScan() {
|
async function loadLatestScan() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/latest');
|
const response = await fetch('/api/latest');
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
scanData = filterScannerDevices(await response.json());
|
scanData = filterScannerDevices(await response.json());
|
||||||
|
markAllDevicesSeen();
|
||||||
updateUI();
|
updateUI();
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('wifi-list').innerHTML = '<div style="color:#888;padding:1rem;">No scans yet. Click "New Scan" to start.</div>';
|
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) {
|
if (response.ok) {
|
||||||
scanData = filterScannerDevices(await response.json());
|
scanData = filterScannerDevices(await response.json());
|
||||||
|
markAllDevicesSeen();
|
||||||
updateUI();
|
updateUI();
|
||||||
status.textContent = `Scanned at ${new Date().toLocaleTimeString()}`;
|
status.textContent = `Scanned at ${new Date().toLocaleTimeString()}`;
|
||||||
} else {
|
} else {
|
||||||
@@ -2448,6 +2547,7 @@ async function performLiveBTScan() {
|
|||||||
|
|
||||||
// Reset miss count - device was detected
|
// Reset miss count - device was detected
|
||||||
deviceMissCount[newDev.address] = 0;
|
deviceMissCount[newDev.address] = 0;
|
||||||
|
updateDeviceLastSeen(newDev.address);
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
// Update RSSI and estimated distance, preserve custom values
|
// Update RSSI and estimated distance, preserve custom values
|
||||||
@@ -2479,6 +2579,7 @@ async function performLiveBTScan() {
|
|||||||
// Clean up tracking data for removed device
|
// Clean up tracking data for removed device
|
||||||
delete deviceMissCount[dev.address];
|
delete deviceMissCount[dev.address];
|
||||||
delete deviceDistanceHistory[dev.address];
|
delete deviceDistanceHistory[dev.address];
|
||||||
|
delete deviceLastSeen[dev.address];
|
||||||
// Clear trail if showing
|
// Clear trail if showing
|
||||||
if (deviceTrails[dev.address]) {
|
if (deviceTrails[dev.address]) {
|
||||||
clearDeviceTrail(dev.address);
|
clearDeviceTrail(dev.address);
|
||||||
@@ -2497,6 +2598,7 @@ async function performLiveBTScan() {
|
|||||||
isDeviceMoving(dev.address, dev.estimated_distance_m);
|
isDeviceMoving(dev.address, dev.estimated_distance_m);
|
||||||
dev.is_moving = false;
|
dev.is_moving = false;
|
||||||
deviceMissCount[dev.address] = 0;
|
deviceMissCount[dev.address] = 0;
|
||||||
|
updateDeviceLastSeen(dev.address);
|
||||||
});
|
});
|
||||||
scanData = {
|
scanData = {
|
||||||
wifi_networks: [],
|
wifi_networks: [],
|
||||||
|
|||||||
Reference in New Issue
Block a user