mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-03-30 15:55:49 +00:00
VCR timeline time tooltip, real packet timestamps in feed, replay-from-packets button
- Timeline scrubber shows time on hover (tooltip follows cursor) - Feed items display actual packet timestamps, not render time - Packet detail panel has 'Replay on Live Map' button → navigates to live view and animates the packet
This commit is contained in:
@@ -544,3 +544,20 @@
|
||||
.vcr-btn { padding: 3px 6px; font-size: 0.7rem; }
|
||||
.vcr-scope-btn { font-size: 0.6rem; padding: 1px 4px; }
|
||||
}
|
||||
|
||||
/* Timeline time tooltip */
|
||||
.vcr-time-tooltip {
|
||||
position: absolute;
|
||||
top: -24px;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0,0,0,0.85);
|
||||
color: #e2e8f0;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
.vcr-time-tooltip.hidden { display: none; }
|
||||
|
||||
@@ -232,6 +232,7 @@
|
||||
const typeName = raw.type || pkt.payload_type_name || 'UNKNOWN';
|
||||
return {
|
||||
id: pkt.id, hash: pkt.hash,
|
||||
_ts: new Date(pkt.timestamp || pkt.created_at).getTime(),
|
||||
decoded: { header: { payloadTypeName: typeName }, payload: raw, path: { hops } },
|
||||
snr: pkt.snr, rssi: pkt.rssi, observer: pkt.observer_name
|
||||
};
|
||||
@@ -239,7 +240,8 @@
|
||||
|
||||
// Buffer a packet from WS
|
||||
function bufferPacket(pkt) {
|
||||
const entry = { ts: Date.now(), pkt };
|
||||
pkt._ts = Date.now();
|
||||
const entry = { ts: pkt._ts, pkt };
|
||||
VCR.buffer.push(entry);
|
||||
// Keep buffer capped at ~2000
|
||||
if (VCR.buffer.length > 2000) VCR.buffer.splice(0, 500);
|
||||
@@ -419,6 +421,7 @@
|
||||
<div class="vcr-timeline-container">
|
||||
<canvas id="vcrTimeline" class="vcr-timeline"></canvas>
|
||||
<div id="vcrPlayhead" class="vcr-playhead"></div>
|
||||
<div id="vcrTimeTooltip" class="vcr-time-tooltip hidden"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="vcrPrompt" class="vcr-prompt hidden"></div>
|
||||
@@ -519,6 +522,20 @@
|
||||
// Timeline click to scrub
|
||||
document.getElementById('vcrTimeline').addEventListener('click', handleTimelineClick);
|
||||
|
||||
// Timeline hover — show time tooltip
|
||||
const timelineEl = document.getElementById('vcrTimeline');
|
||||
const timeTooltip = document.getElementById('vcrTimeTooltip');
|
||||
timelineEl.addEventListener('mousemove', (e) => {
|
||||
const rect = timelineEl.getBoundingClientRect();
|
||||
const pct = (e.clientX - rect.left) / rect.width;
|
||||
const ts = Date.now() - VCR.timelineScope + pct * VCR.timelineScope;
|
||||
const d = new Date(ts);
|
||||
timeTooltip.textContent = d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
||||
timeTooltip.style.left = (e.clientX - rect.left) + 'px';
|
||||
timeTooltip.classList.remove('hidden');
|
||||
});
|
||||
timelineEl.addEventListener('mouseleave', () => { timeTooltip.classList.add('hidden'); });
|
||||
|
||||
// Refresh timeline periodically
|
||||
setInterval(updateTimeline, 5000);
|
||||
|
||||
@@ -538,6 +555,16 @@
|
||||
livePage.addEventListener('click', showNav);
|
||||
}
|
||||
showNav();
|
||||
|
||||
// Check for replay packet from packets page
|
||||
const replayData = sessionStorage.getItem('replay-packet');
|
||||
if (replayData) {
|
||||
sessionStorage.removeItem('replay-packet');
|
||||
try {
|
||||
const pkt = JSON.parse(replayData);
|
||||
setTimeout(() => animatePacket(pkt), 1500); // let map load first
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
function injectSVGFilters() {
|
||||
@@ -622,7 +649,8 @@
|
||||
const pkts = (data.packets || []).reverse();
|
||||
pkts.forEach((pkt, i) => {
|
||||
const livePkt = dbPacketToLive(pkt);
|
||||
const ts = new Date(pkt.timestamp || pkt.created_at).getTime();
|
||||
livePkt._ts = new Date(pkt.timestamp || pkt.created_at).getTime();
|
||||
const ts = livePkt._ts;
|
||||
VCR.buffer.push({ ts, pkt: livePkt });
|
||||
setTimeout(() => animatePacket(livePkt), i * 400);
|
||||
});
|
||||
@@ -902,7 +930,7 @@
|
||||
<span class="feed-type" style="color:${color}">${typeName}</span>
|
||||
${hopStr}
|
||||
<span class="feed-text">${escapeHtml(preview)}</span>
|
||||
<span class="feed-time">${new Date().toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'})}</span>
|
||||
<span class="feed-time">${new Date(pkt._ts || Date.now()).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'})}</span>
|
||||
`;
|
||||
item.addEventListener('click', () => showFeedCard(item, pkt, color));
|
||||
feed.prepend(item);
|
||||
|
||||
@@ -395,7 +395,25 @@
|
||||
<div class="hex-dump">${createColoredHexDump(pkt.raw_hex, ranges)}</div>` : ''}
|
||||
|
||||
${hasRawHex ? buildFieldTable(pkt, decoded, pathHops, ranges) : buildDecodedTable(decoded)}
|
||||
|
||||
<button class="replay-live-btn" title="Replay this packet on the live map">▶ Replay on Live Map</button>
|
||||
`;
|
||||
|
||||
// Wire up replay button
|
||||
const replayBtn = panel.querySelector('.replay-live-btn');
|
||||
if (replayBtn) {
|
||||
replayBtn.addEventListener('click', () => {
|
||||
// Store packet in sessionStorage for the live page to pick up
|
||||
const livePkt = {
|
||||
id: pkt.id, hash: pkt.hash,
|
||||
_ts: new Date(pkt.timestamp).getTime(),
|
||||
decoded: { header: { payloadTypeName: typeName }, payload: decoded, path: { hops: pathHops } },
|
||||
snr: pkt.snr, rssi: pkt.rssi, observer: pkt.observer_name
|
||||
};
|
||||
sessionStorage.setItem('replay-packet', JSON.stringify(livePkt));
|
||||
window.location.hash = '#/live';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
|
||||
@@ -1067,3 +1067,20 @@ button.ch-item.ch-item-encrypted .ch-badge { filter: grayscale(0.6); }
|
||||
.node-qr svg { max-width: 140px; border-radius: 4px; }
|
||||
[data-theme="dark"] .node-qr svg rect[fill="#ffffff"] { fill: var(--card-bg); }
|
||||
[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%;
|
||||
margin-top: 12px;
|
||||
padding: 8px 16px;
|
||||
background: rgba(168, 85, 247, 0.15);
|
||||
border: 1px solid rgba(168, 85, 247, 0.3);
|
||||
color: #c084fc;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.replay-live-btn:hover { background: rgba(168, 85, 247, 0.3); }
|
||||
|
||||
Reference in New Issue
Block a user