From 25fc2924e0b33f5defdaf376dabb6dc82c15d571 Mon Sep 17 00:00:00 2001 From: you Date: Thu, 19 Mar 2026 07:50:17 +0000 Subject: [PATCH] Time-travel map: filter nodes by replay timestamp When scrubbing to a past time, only nodes that had advertised before that time appear on the map. Nodes that hadn't been seen yet are hidden. - Added 'before' param to /api/nodes (filters first_seen <= before) - loadNodes(beforeTs) accepts optional timestamp filter - clearNodeMarkers() removes all markers and data - vcrReplayFromTs clears and reloads nodes for replay time - vcrResumeLive reloads all nodes (no filter) --- public/live.js | 23 +++++++++++++++++++++-- server.js | 3 ++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/public/live.js b/public/live.js index 3a26abcb..0c6b77e6 100644 --- a/public/live.js +++ b/public/live.js @@ -92,6 +92,9 @@ VCR.dragPct = null; VCR.scrubTs = null; vcrSetMode('LIVE'); + // Reload all nodes (no time filter) + clearNodeMarkers(); + loadNodes(); const prompt = document.getElementById('vcrPrompt'); if (prompt) prompt.classList.add('hidden'); } @@ -115,6 +118,11 @@ const fetchTo = new Date(targetTs + 300000).toISOString(); // 5 min window stopReplay(); vcrSetMode('REPLAY'); + + // Reload map nodes to match the replay time + clearNodeMarkers(); + loadNodes(targetTs); + fetch(`/api/packets?limit=200&grouped=false&since=${encodeURIComponent(fetchFrom)}&until=${encodeURIComponent(fetchTo)}`) .then(r => r.json()) .then(data => { @@ -818,9 +826,12 @@ }, 2000); } - async function loadNodes() { + async function loadNodes(beforeTs) { try { - const resp = await fetch('/api/nodes?limit=500'); + const url = beforeTs + ? `/api/nodes?limit=500&before=${encodeURIComponent(new Date(beforeTs).toISOString())}` + : '/api/nodes?limit=500'; + const resp = await fetch(url); const nodes = await resp.json(); const list = Array.isArray(nodes) ? nodes : (nodes.nodes || []); list.forEach(n => { @@ -833,6 +844,14 @@ } catch (e) { console.error('Failed to load nodes:', e); } } + function clearNodeMarkers() { + for (const [key, marker] of Object.entries(nodeMarkers)) { + if (map) map.removeLayer(marker); + } + nodeMarkers = {}; + nodeData = {}; + } + function addNodeMarker(n) { if (nodeMarkers[n.public_key]) return nodeMarkers[n.public_key]; const color = ROLE_COLORS[n.role] || ROLE_COLORS.unknown; diff --git a/server.js b/server.js index a4f83f61..b7f7c9ac 100644 --- a/server.js +++ b/server.js @@ -455,13 +455,14 @@ app.post('/api/packets', (req, res) => { }); app.get('/api/nodes', (req, res) => { - const { limit = 50, offset = 0, role, region, lastHeard, sortBy = 'lastSeen', search } = req.query; + const { limit = 50, offset = 0, role, region, lastHeard, sortBy = 'lastSeen', search, before } = req.query; let where = []; let params = {}; if (role) { where.push('role = @role'); params.role = role; } if (search) { where.push('name LIKE @search'); params.search = `%${search}%`; } + if (before) { where.push('first_seen <= @before'); params.before = before; } if (lastHeard) { const durations = { '1h': 3600000, '6h': 21600000, '24h': 86400000, '7d': 604800000, '30d': 2592000000 }; const ms = durations[lastHeard];