Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions public/live.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
let showOnlyFavorites = localStorage.getItem('live-favorites-only') === 'true';
let matrixMode = localStorage.getItem('live-matrix-mode') === 'true';
let matrixRain = localStorage.getItem('live-matrix-rain') === 'true';
const _savedSpeed = parseFloat(localStorage.getItem('live-vcr-speed'));
const _initialSpeed = [0.25, 0.5, 1, 2, 4, 8].includes(_savedSpeed) ? _savedSpeed : 1;
let rainCanvas = null, rainCtx = null, rainDrops = [], rainRAF = null;
const propagationBuffer = new Map(); // hash -> {timer, packets[]}
let _onResize = null;
Expand Down Expand Up @@ -393,10 +395,17 @@
if (VCR.replayTimer) { clearTimeout(VCR.replayTimer); VCR.replayTimer = null; }
}

function speedLabel(s) {
if (s === 0.25) return '¼x';
if (s === 0.5) return '½x';
return s + 'x';
}

function vcrSpeedCycle() {
const speeds = [1, 2, 4, 8];
const speeds = [0.25, 0.5, 1, 2, 4, 8];
const idx = speeds.indexOf(VCR.speed);
VCR.speed = speeds[(idx + 1) % speeds.length];
localStorage.setItem('live-vcr-speed', VCR.speed);
updateVCRUI();
// If replaying, restart with new speed
if (VCR.mode === 'REPLAY' && VCR.replayTimer) {
Expand Down Expand Up @@ -532,7 +541,7 @@
if (pauseBtn) { pauseBtn.textContent = '⏸'; pauseBtn.setAttribute('aria-label', 'Pause'); }
if (missedEl) missedEl.classList.add('hidden');
}
if (speedBtn) { speedBtn.textContent = VCR.speed + 'x'; speedBtn.setAttribute('aria-label', 'Speed ' + VCR.speed + 'x'); }
if (speedBtn) { speedBtn.textContent = speedLabel(VCR.speed); speedBtn.setAttribute('aria-label', 'Speed ' + speedLabel(VCR.speed)); }
updateVCRLcd();
}

Expand Down Expand Up @@ -1865,6 +1874,7 @@
window._liveFormatLiveTimestampHtml = formatLiveTimestampHtml;
window._liveResolveHopPositions = resolveHopPositions;
window._liveVcrSpeedCycle = vcrSpeedCycle;
window._liveSpeedLabel = speedLabel;
window._liveVcrPause = vcrPause;
window._liveVcrResumeLive = vcrResumeLive;
window._liveVcrSetMode = vcrSetMode;
Expand Down Expand Up @@ -2504,7 +2514,7 @@

const matrixGreen = '#00ff41';
const TRAIL_LEN = Math.min(6, bytes.length);
const DURATION_MS = 1100; // total hop duration
const DURATION_MS = 1100 / VCR.speed;
const CHAR_INTERVAL = 0.06; // spawn a char every 6% of progress
const charMarkers = [];
let nextCharAt = CHAR_INTERVAL;
Expand Down Expand Up @@ -2623,8 +2633,9 @@
return;
}
const elapsed = now - lastStep;
if (elapsed >= 33) {
const ticks = Math.min(Math.floor(elapsed / 33), 4);
const stepMs = 33 / VCR.speed;
if (elapsed >= stepMs) {
const ticks = Math.min(Math.floor(elapsed / stepMs), 4);
lastStep = now;
for (let t = 0; t < ticks && step < steps; t++) {
step++;
Expand Down Expand Up @@ -2947,7 +2958,7 @@
packetCount = 0; activeAnims = 0;
nodeActivity = {}; pktTimestamps = [];
feedDedup.clear();
VCR.buffer = []; VCR.playhead = -1; VCR.mode = 'LIVE'; VCR.missedCount = 0; VCR.speed = 1; VCR.replayGen = 0;
VCR.buffer = []; VCR.playhead = -1; VCR.mode = 'LIVE'; VCR.missedCount = 0; VCR.speed = _initialSpeed; VCR.replayGen = 0;
}

let _themeRefreshHandler = null;
Expand Down
13 changes: 5 additions & 8 deletions test-e2e-playwright.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,9 @@ async function run() {
// Test 5: Node detail loads (reuses nodes page from test 2)
await test('Node detail loads', async () => {
await page.waitForSelector('table tbody tr');
// Click first row
const firstRow = await page.$('table tbody tr');
assert(firstRow, 'No node rows found');
await firstRow.click();
// Use page.click() instead of an element handle to avoid detached-element races
// when the WebSocket auto-refresh re-renders the table between querySelector and click.
await page.click('table tbody tr');
// Wait for detail pane to appear
await page.waitForSelector('.node-detail');
const html = await page.content();
Expand All @@ -240,10 +239,8 @@ async function run() {
await test('Node side panel Details link navigates', async () => {
await page.goto(`${BASE}/#/nodes`, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('table tbody tr');
// Click first row to open side panel
const firstRow = await page.$('table tbody tr');
assert(firstRow, 'No node rows found');
await firstRow.click();
// Use page.click() to avoid detached-element race with WebSocket auto-refresh.
await page.click('table tbody tr');
await page.waitForSelector('.node-detail');
// Find the Details link in the side panel
const detailsLink = await page.$('#nodesRight a.btn-primary[href^="#/nodes/"]');
Expand Down
29 changes: 26 additions & 3 deletions test-live.js
Original file line number Diff line number Diff line change
Expand Up @@ -402,17 +402,40 @@ console.log('\n=== live.js: VCR state machine ===');
assert.strictEqual(VCR().mode, 'PAUSED', 'mode should stay PAUSED after second call');
});

test('vcrSpeedCycle cycles through 1,2,4,8', () => {
test('vcrSpeedCycle cycles through 0.25, 0.5, 1, 2, 4, 8 and wraps', () => {
vcrSetMode('LIVE');
VCR().speed = 1;
VCR().speed = 0.25;
vcrSpeedCycle();
assert.strictEqual(VCR().speed, 0.5);
vcrSpeedCycle();
assert.strictEqual(VCR().speed, 1);
vcrSpeedCycle();
assert.strictEqual(VCR().speed, 2);
vcrSpeedCycle();
assert.strictEqual(VCR().speed, 4);
vcrSpeedCycle();
assert.strictEqual(VCR().speed, 8);
vcrSpeedCycle();
assert.strictEqual(VCR().speed, 1); // wraps around
assert.strictEqual(VCR().speed, 0.25); // wraps around
});

test('vcrSpeedCycle saves speed to localStorage', () => {
VCR().speed = 1;
vcrSpeedCycle();
assert.strictEqual(ctx.localStorage.getItem('live-vcr-speed'), '2');
vcrSpeedCycle();
assert.strictEqual(ctx.localStorage.getItem('live-vcr-speed'), '4');
});

const speedLabel = ctx.window._liveSpeedLabel;
assert.ok(speedLabel, '_liveSpeedLabel must be exposed');

test('speedLabel returns fraction strings for sub-1x speeds', () => {
assert.strictEqual(speedLabel(0.25), '¼x');
assert.strictEqual(speedLabel(0.5), '½x');
assert.strictEqual(speedLabel(1), '1x');
assert.strictEqual(speedLabel(2), '2x');
assert.strictEqual(speedLabel(8), '8x');
});

const vcrResumeLive = ctx.window._liveVcrResumeLive;
Expand Down