Rewrite scrubber: simple pause on release, no async fetch

Previous approach tried to fetch packets from DB on scrub release,
causing race conditions, rubber-banding, and stale state. Replaced
with dead-simple approach:
- Drag: moves playhead visually + updates clock
- Release: pauses at that position (VCR.scrubTs holds timestamp)
- No async fetch, no replay on release
- updateTimelinePlayhead uses scrubTs to hold position
- Click LIVE to resume
This commit is contained in:
you committed 2026-03-19 07:21:00 +00:00
1 parent e19f2cd6d1
commit f3fc2d4c11
1 file changed
+17 -35
+17 -35
View File
@@ -90,6 +90,7 @@
VCR.missedCount = 0;
VCR.scrubEnd = null;
VCR.dragPct = null;
VCR.scrubTs = null;
vcrSetMode('LIVE');
const prompt = document.getElementById('vcrPrompt');
if (prompt) prompt.classList.add('hidden');
@@ -360,6 +361,9 @@
let x;
if (VCR.mode === 'LIVE') {
x = cw;
} else if (VCR.scrubTs != null) {
// Scrubbed to a specific time — hold there
x = ((VCR.scrubTs - startTs) / scopeMs) * cw;
} else if (VCR.playhead >= 0 && VCR.playhead < VCR.buffer.length) {
const playTs = VCR.buffer[VCR.playhead].ts;
x = ((playTs - startTs) / scopeMs) * cw;
@@ -594,49 +598,26 @@
VCR.dragPct = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
const playheadEl = document.getElementById('vcrPlayhead');
if (playheadEl) playheadEl.style.left = (VCR.dragPct * rect.width) + 'px';
// Update VCR clock to show drag target time
const now = VCR.frozenNow || Date.now();
const targetTs = now - VCR.timelineScope + VCR.dragPct * VCR.timelineScope;
updateVCRClock(targetTs);
}
function scrubCommit() {
VCR.frozenNow = Date.now(); // freeze timeline at scrub moment
const targetTs = VCR.frozenNow - VCR.timelineScope + VCR.dragPct * VCR.timelineScope;
function scrubRelease() {
VCR.dragging = false;
VCR.frozenNow = Date.now();
stopReplay();
vcrSetMode('REPLAY');
const fetchFrom = new Date(targetTs - 30000).toISOString(); // 30s before target
fetch(`/api/packets?limit=200&grouped=false&since=${encodeURIComponent(fetchFrom)}`)
.then(r => r.json())
.then(data => {
const pkts = (data.packets || []).reverse(); // oldest first
const existingIds = new Set(VCR.buffer.map(b => b.pkt.id).filter(Boolean));
const newEntries = pkts.filter(p => !existingIds.has(p.id)).map(p => ({
ts: new Date(p.timestamp || p.created_at).getTime(),
pkt: dbPacketToLive(p)
}));
if (newEntries.length) {
VCR.buffer = [...newEntries, ...VCR.buffer].sort((a,b) => a.ts - b.ts);
}
// Find closest entry to target timestamp
let closest = 0, minDist = Infinity;
VCR.buffer.forEach((entry, i) => {
const dist = Math.abs(entry.ts - targetTs);
if (dist < minDist) { minDist = dist; closest = i; }
});
VCR.playhead = closest;
// Only replay ~50 packets from scrub point, not entire buffer to end
VCR.scrubEnd = Math.min(closest + 50, VCR.buffer.length);
VCR.dragging = false;
startReplay();
})
.catch(() => { VCR.dragging = false; });
// Store the target timestamp so playhead stays put
VCR.scrubTs = VCR.frozenNow - VCR.timelineScope + VCR.dragPct * VCR.timelineScope;
vcrSetMode('PAUSED');
updateVCRClock(VCR.scrubTs);
}
timelineEl.addEventListener('mousedown', (e) => {
VCR.dragging = true;
VCR.scrubTs = null;
stopReplay();
if (!VCR.frozenNow) VCR.frozenNow = Date.now();
scrubVisual(e.clientX);
e.preventDefault();
});
@@ -646,12 +627,13 @@
});
document.addEventListener('mouseup', () => {
if (!VCR.dragging) return;
scrubCommit(); // keeps VCR.dragging=true until fetch completes
scrubRelease();
});
// Touch support
timelineEl.addEventListener('touchstart', (e) => {
VCR.dragging = true;
VCR.scrubTs = null;
stopReplay();
if (!VCR.frozenNow) VCR.frozenNow = Date.now();
scrubVisual(e.touches[0].clientX);
e.preventDefault();
}, { passive: false });
@@ -661,7 +643,7 @@
});
timelineEl.addEventListener('touchend', () => {
if (!VCR.dragging) return;
scrubCommit(); // keeps VCR.dragging=true until fetch completes
scrubRelease();
});
// Fetch historical timestamps for timeline, then start refresh