fix: smooth intermission clock by preserving local anchor across renders
All checks were successful
CI / Lint (push) Successful in 5s
CI / Test (push) Successful in 6s
CI / Build & Push (push) Successful in 16s

Snapshot the locally-computed clock state before each re-render and
restore it afterwards, so the API response doesn't cause a visible
jump. Only resync to the API value in the final 60 seconds, where
accuracy matters.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-29 14:55:48 -04:00
parent 96529c4705
commit 257e2151c8

View File

@@ -20,7 +20,14 @@ function updateScoreboard(data) {
const grid = document.getElementById(gridId); const grid = document.getElementById(gridId);
const hasGames = games && games.length > 0; const hasGames = games && games.length > 0;
section.classList.toggle('hidden', !hasGames); section.classList.toggle('hidden', !hasGames);
// Snapshot current clock state before blowing away the DOM
const clockSnapshot = snapshotClocks(grid);
grid.innerHTML = hasGames ? games.map(render).join('') : ''; grid.innerHTML = hasGames ? games.map(render).join('') : '';
// Restore smooth local anchors unless we're in the final 60s
if (hasGames) restoreClocks(grid, clockSnapshot);
} }
updateGauges(); updateGauges();
@@ -56,7 +63,7 @@ function renderLiveGame(game) {
</div>` : ''; </div>` : '';
return ` return `
<div class="game-box ${intermission ? 'game-box-intermission' : 'game-box-live'}"> <div class="game-box ${intermission ? 'game-box-intermission' : 'game-box-live'}" data-game-key="${game['Away Team']}|${game['Home Team']}">
<div class="card-header"> <div class="card-header">
<div class="badges"> <div class="badges">
${periodLabel} ${periodLabel}
@@ -139,6 +146,8 @@ function updateGauges() {
}); });
} }
const CLOCK_SYNC_THRESHOLD = 60; // seconds — only resync from API in final 60s
// ── Clock ───────────────────────────────────────────── // ── Clock ─────────────────────────────────────────────
function timeToSeconds(str) { function timeToSeconds(str) {
@@ -154,6 +163,35 @@ function secondsToTime(s) {
return `${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`; return `${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
} }
function snapshotClocks(grid) {
const snapshot = new Map();
grid.querySelectorAll('[data-game-key]').forEach(card => {
const badge = card.querySelector('[data-seconds][data-received-at]');
if (!badge) return;
const seconds = parseInt(badge.dataset.seconds, 10);
const receivedAt = parseInt(badge.dataset.receivedAt, 10);
const elapsed = Math.floor((Date.now() - receivedAt) / 1000);
const current = Math.max(0, seconds - elapsed);
snapshot.set(card.dataset.gameKey, { current, ts: Date.now() });
});
return snapshot;
}
function restoreClocks(grid, snapshot) {
grid.querySelectorAll('[data-game-key]').forEach(card => {
const prior = snapshot.get(card.dataset.gameKey);
if (!prior) return;
const badge = card.querySelector('[data-seconds][data-received-at]');
if (!badge) return;
// Only restore if we're outside the final sync window
if (prior.current > CLOCK_SYNC_THRESHOLD) {
badge.dataset.seconds = prior.current;
badge.dataset.receivedAt = prior.ts;
badge.textContent = secondsToTime(prior.current);
}
});
}
function tickClocks() { function tickClocks() {
const now = Date.now(); const now = Date.now();
document.querySelectorAll('[data-seconds][data-received-at]').forEach(el => { document.querySelectorAll('[data-seconds][data-received-at]').forEach(el => {