From dcfd4db318a89d16eb96b46143f18ecf1da30013 Mon Sep 17 00:00:00 2001 From: you Date: Thu, 19 Mar 2026 06:19:35 +0000 Subject: [PATCH] Fix timeline scrubber: fetch historical timestamps from DB Timeline sparkline was only showing packets from the current browser session (WS buffer). Now fetches timestamps from DB via lightweight /api/packets/timestamps endpoint, so 6h/12h/24h scopes actually show historical activity density. --- public/live.js | 45 +++++++++++++++++++++++++++++++++++++-------- server.js | 8 ++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/public/live.js b/public/live.js index 052321c2..51deb1a0 100644 --- a/public/live.js +++ b/public/live.js @@ -23,6 +23,8 @@ speed: 1, // replay speed: 1, 2, 4, 8 replayTimer: null, timelineScope: 3600000, // 1h default ms + timelineTimestamps: [], // historical timestamps from DB for sparkline + timelineFetchedScope: 0, // last fetched scope to avoid redundant fetches }; const ROLE_COLORS = { @@ -260,6 +262,20 @@ // === Timeline === + async function fetchTimelineTimestamps() { + const scopeMs = VCR.timelineScope; + if (scopeMs === VCR.timelineFetchedScope) return; + const since = new Date(Date.now() - scopeMs).toISOString(); + try { + const resp = await fetch(`/api/packets/timestamps?since=${encodeURIComponent(since)}`); + if (resp.ok) { + const timestamps = await resp.json(); // array of ISO strings + VCR.timelineTimestamps = timestamps.map(t => new Date(t).getTime()); + VCR.timelineFetchedScope = scopeMs; + } + } catch(e) { /* ignore */ } + } + function updateTimeline() { const canvas = document.getElementById('vcrTimeline'); if (!canvas) return; @@ -272,19 +288,27 @@ ctx.clearRect(0, 0, cw, ch); - if (VCR.buffer.length === 0) return; - const now = Date.now(); const scopeMs = VCR.timelineScope; const startTs = now - scopeMs; + // Merge historical DB timestamps with live buffer timestamps + const allTimestamps = []; + VCR.timelineTimestamps.forEach(ts => { + if (ts >= startTs) allTimestamps.push(ts); + }); + VCR.buffer.forEach(entry => { + if (entry.ts >= startTs) allTimestamps.push(entry.ts); + }); + + if (allTimestamps.length === 0) return; + // Draw density sparkline const buckets = 100; const counts = new Array(buckets).fill(0); let maxCount = 0; - VCR.buffer.forEach(entry => { - if (entry.ts < startTs) return; - const bucket = Math.floor((entry.ts - startTs) / scopeMs * buckets); + allTimestamps.forEach(ts => { + const bucket = Math.floor((ts - startTs) / scopeMs * buckets); if (bucket >= 0 && bucket < buckets) { counts[bucket]++; if (counts[bucket] > maxCount) maxCount = counts[bucket]; @@ -516,7 +540,7 @@ document.querySelectorAll('.vcr-scope-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); VCR.timelineScope = parseInt(btn.dataset.scope); - updateTimeline(); + fetchTimelineTimestamps().then(() => updateTimeline()); }); }); @@ -537,8 +561,13 @@ }); timelineEl.addEventListener('mouseleave', () => { timeTooltip.classList.add('hidden'); }); - // Refresh timeline periodically - setInterval(updateTimeline, 5000); + // Fetch historical timestamps for timeline, then start refresh + fetchTimelineTimestamps().then(() => updateTimeline()); + setInterval(() => { + // Re-fetch if scope changed or periodically to pick up new data + VCR.timelineFetchedScope = 0; // force refetch + fetchTimelineTimestamps().then(() => updateTimeline()); + }, 30000); // Auto-hide nav const topNav = document.querySelector('.top-nav'); diff --git a/server.js b/server.js index 240b5880..e8c88558 100644 --- a/server.js +++ b/server.js @@ -310,6 +310,14 @@ app.get('/api/packets', (req, res) => { res.json({ packets, total }); }); +// Lightweight endpoint: just timestamps for timeline sparkline +app.get('/api/packets/timestamps', (req, res) => { + const { since } = req.query; + if (!since) return res.status(400).json({ error: 'since required' }); + const rows = db.db.prepare('SELECT timestamp FROM packets WHERE timestamp > ? ORDER BY timestamp ASC').all(since); + res.json(rows.map(r => r.timestamp)); +}); + app.get('/api/packets/:id', (req, res) => { const packet = db.getPacket(Number(req.params.id)); if (!packet) return res.status(404).json({ error: 'Not found' });