Fix VCR scrubber rubber-band, compact replay button, fix channel links

VCR: playhead now stays where you drag it — updateTimelinePlayhead()
skips during drag (VCR.dragging flag). Always update playhead visually
via direct DOM during scrub instead of relying on buffer position calc.

Packets: replay button compact, inline next to View Route button.
Analytics: channel links now navigate to specific channel hash.
This commit is contained in:
you
2026-03-19 06:49:11 +00:00
parent d36c206a8e
commit 5ee08f1fa5
4 changed files with 32 additions and 31 deletions
+1 -1
View File
@@ -566,7 +566,7 @@
<table class="analytics-table">
<thead><tr><th>Channel</th><th>Hash</th><th>Messages</th><th>Unique Senders</th><th>Last Activity</th><th>Decrypted</th></tr></thead>
<tbody>
${ch.channels.map(c => `<tr class="clickable-row" onclick="location.hash='#/channels'">
${ch.channels.map(c => `<tr class="clickable-row" onclick="location.hash='#/channels?ch=${c.hash}'">
<td><strong>${esc(c.name || 'Unknown')}</strong></td>
<td class="mono">${c.hash}</td>
<td>${c.messages}</td>
+17 -19
View File
@@ -330,6 +330,7 @@
}
function updateTimelinePlayhead() {
if (VCR.dragging) return; // don't fight the user's drag
const canvas = document.getElementById('vcrTimeline');
if (!canvas) return;
const ctx = canvas.getContext('2d');
@@ -574,24 +575,22 @@
timelineEl.addEventListener('mouseleave', () => { timeTooltip.classList.add('hidden'); });
// Drag scrubbing on timeline
let dragging = false;
VCR.dragging = false;
function scrubToX(clientX, isFinal) {
const rect = timelineEl.getBoundingClientRect();
const pct = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
const now = Date.now();
const targetTs = now - VCR.timelineScope + pct * VCR.timelineScope;
// Always move playhead visually during drag
const playheadEl = document.getElementById('vcrPlayhead');
if (playheadEl) {
playheadEl.style.left = (pct * rect.width) + 'px';
}
// If buffer is empty or target is before buffer start, fetch from DB on release
if (VCR.buffer.length === 0 || targetTs < VCR.buffer[0].ts - 5000) {
if (isFinal) {
vcrRewind(now - targetTs);
} else {
// Update playhead position visually during drag even without buffer
const playheadEl = document.getElementById('vcrPlayhead');
if (playheadEl) {
playheadEl.style.left = (pct * rect.width) + 'px';
}
}
if (isFinal) vcrRewind(now - targetTs);
return;
}
@@ -605,38 +604,37 @@
stopReplay();
VCR.playhead = closest;
vcrSetMode('REPLAY');
updateTimelinePlayhead();
updateVCRUI();
}
timelineEl.addEventListener('mousedown', (e) => {
dragging = true;
VCR.dragging = true;
scrubToX(e.clientX, false);
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!dragging) return;
if (!VCR.dragging) return;
scrubToX(e.clientX, false);
});
document.addEventListener('mouseup', (e) => {
if (!dragging) return;
dragging = false;
if (!VCR.dragging) return;
VCR.dragging = false;
scrubToX(e.clientX, true);
if (VCR.mode === 'REPLAY') startReplay();
});
// Touch support
timelineEl.addEventListener('touchstart', (e) => {
dragging = true;
VCR.dragging = true;
scrubToX(e.touches[0].clientX, false);
e.preventDefault();
}, { passive: false });
timelineEl.addEventListener('touchmove', (e) => {
if (!dragging) return;
if (!VCR.dragging) return;
scrubToX(e.touches[0].clientX, false);
});
timelineEl.addEventListener('touchend', (e) => {
if (!dragging) return;
if (!VCR.dragging) return;
const lastTouch = e.changedTouches[0];
dragging = false;
VCR.dragging = false;
scrubToX(lastTouch.clientX, true);
if (VCR.mode === 'REPLAY') startReplay();
});
+4 -2
View File
@@ -474,8 +474,10 @@
<dt>Timestamp</dt><dd>${pkt.timestamp}</dd>
<dt>Path</dt><dd>${pathHops.length ? renderPath(pathHops) : ''}</dd>
</dl>
${pathHops.length ? `<button class="detail-map-link" id="viewRouteBtn">🗺️ View route on map</button>` : ''}
<button class="replay-live-btn" title="Replay this packet on the live map"> Replay on Live Map</button>
<div class="detail-actions">
${pathHops.length ? `<button class="detail-map-link" id="viewRouteBtn">🗺️ View route on map</button>` : ''}
<button class="replay-live-btn" title="Replay this packet on the live map"> Replay</button>
</div>
${hasRawHex ? `<div class="hex-legend">${buildHexLegend(ranges)}</div>
<div class="hex-dump">${createColoredHexDump(pkt.raw_hex, ranges)}</div>` : ''}
+10 -9
View File
@@ -1069,15 +1069,17 @@ button.ch-item.ch-item-encrypted .ch-badge { filter: grayscale(0.6); }
[data-theme="dark"] .node-qr svg rect[fill="#000000"] { fill: var(--text); }
/* Replay on Live Map button in packet detail */
.replay-live-btn {
display: block;
width: 100%;
.detail-actions {
display: flex;
gap: 8px;
margin-top: 12px;
padding: 8px 16px;
}
.replay-live-btn {
padding: 5px 12px;
background: rgba(168, 85, 247, 0.15);
border: 1px solid rgba(168, 85, 247, 0.3);
color: #c084fc;
font-size: 0.85rem;
font-size: 0.78rem;
font-weight: 600;
border-radius: 6px;
cursor: pointer;
@@ -1120,16 +1122,15 @@ button.ch-item.ch-item-encrypted .ch-badge { filter: grayscale(0.6); }
/* Detail map link */
.detail-map-link {
display: inline-block;
margin: 8px 0;
padding: 6px 12px;
padding: 5px 12px;
background: rgba(245, 158, 11, 0.12);
border: 1px solid rgba(245, 158, 11, 0.25);
color: #fbbf24;
border-radius: 6px;
font-size: 0.82rem;
font-size: 0.78rem;
font-weight: 600;
text-decoration: none;
cursor: pointer;
transition: background 0.15s;
}
.detail-map-link:hover { background: rgba(245, 158, 11, 0.25); }