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.
This commit is contained in:
you
2026-03-19 06:19:35 +00:00
parent 600f24248f
commit dcfd4db318
2 changed files with 45 additions and 8 deletions
+37 -8
View File
@@ -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');
+8
View File
@@ -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' });