async function fetchScoreboardData() { try { const res = await fetch('/scoreboard'); if (!res.ok) throw new Error(res.status); updateScoreboard(await res.json()); } catch (e) { console.error('Failed to fetch scoreboard data:', e); } } function updateScoreboard(data) { const sections = [ { sectionId: 'live-section', gridId: 'live-games-section', games: data.live_games, render: renderLiveGame }, { sectionId: 'intermission-section', gridId: 'intermission-games-section', games: data.intermission_games, render: renderLiveGame }, { sectionId: 'pre-section', gridId: 'pre-games-section', games: data.pre_games, render: renderPreGame }, { sectionId: 'final-section', gridId: 'final-games-section', games: data.final_games, render: renderFinalGame }, ]; for (const { sectionId, gridId, games, render } of sections) { const section = document.getElementById(sectionId); const grid = document.getElementById(gridId); const hasGames = games && games.length > 0; 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('') : ''; // Restore smooth local anchors unless we're in the final 60s if (hasGames) restoreClocks(grid, clockSnapshot); } updateGauges(); } // ── Renderers ──────────────────────────────────────── function renderLiveGame(game) { const intermission = game['Intermission']; const period = game['Period']; const time = game['Time Remaining']; const running = game['Time Running']; const periodLabel = intermission ? `${intermissionLabel(period)}` : `${ordinalPeriod(period)}`; const dot = running ? `` : ''; // Tick the clock locally when the clock is running or during intermission const shouldTick = running || intermission; const rawSeconds = timeToSeconds(time); const clockAttrs = shouldTick ? `data-seconds="${rawSeconds}" data-received-at="${Date.now()}"` : ''; const hype = !intermission ? `
Hype Meter
` : ''; return `
${periodLabel} ${time}
${dot}
${teamRow(game, 'Away', 'live')} ${teamRow(game, 'Home', 'live')} ${ppIndicator(game)} ${hype}
`; } function renderPreGame(game) { return `
${game['Start Time']}
${teamRow(game, 'Away', 'pre')} ${teamRow(game, 'Home', 'pre')}
`; } function renderFinalGame(game) { const labels = { REG: 'Final', OT: 'Final/OT', SO: 'Final/SO' }; const label = labels[game['Last Period Type']] ?? 'Final'; return `
${label}
${teamRow(game, 'Away', 'final')} ${teamRow(game, 'Home', 'final')}
`; } // ── Team Row ───────────────────────────────────────── function teamRow(game, side, state) { const name = game[`${side} Team`]; const logo = game[`${side} Logo`]; const score = game[`${side} Score`]; const sog = game[`${side} Shots`]; const record = game[`${side} Record`]; const sogHtml = (state === 'live' || state === 'final') && sog !== undefined ? `${sog} SOG` : ''; const right = state === 'pre' ? `${record}` : `${score}`; return `
${name} ${sogHtml}
${right}
`; } function ppIndicator(game) { const awayPP = game['Away Power Play']; const homePP = game['Home Power Play']; const pp = awayPP || homePP; if (!pp) return ''; const team = awayPP ? game['Away Team'] : game['Home Team']; const timeStr = pp.replace('PP ', ''); const seconds = timeToSeconds(timeStr); const attrs = `data-seconds="${seconds}" data-received-at="${Date.now()}" data-pp-clock`; return `
PP ${team} ${timeStr}
`; } // ── Gauge ──────────────────────────────────────────── function updateGauges() { document.querySelectorAll('.gauge').forEach(el => { const score = Math.min(1000, Math.max(0, parseInt(el.dataset.score, 10))); el.style.width = `${(score / 1000) * 100}%`; el.style.backgroundColor = score <= 350 ? '#4a90e2' : score <= 650 ? '#f97316' : '#ef4444'; }); } const CLOCK_SYNC_THRESHOLD = 60; // seconds — only resync from API in final 60s // ── Clock ───────────────────────────────────────────── function timeToSeconds(str) { if (!str || str === 'END') return 0; const [m, s] = str.split(':').map(Number); return m * 60 + s; } function secondsToTime(s) { if (s <= 0) return 'END'; const m = Math.floor(s / 60); const sec = s % 60; 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]:not([data-pp-clock])'); 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]:not([data-pp-clock])'); 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() { const now = Date.now(); document.querySelectorAll('[data-seconds][data-received-at]').forEach(el => { const seconds = parseInt(el.dataset.seconds, 10); const receivedAt = parseInt(el.dataset.receivedAt, 10); const elapsed = Math.floor((now - receivedAt) / 1000); el.textContent = secondsToTime(Math.max(0, seconds - elapsed)); }); } // ── Helpers ────────────────────────────────────────── function ordinalPeriod(period) { return ['1st', '2nd', '3rd', 'OT'][period - 1] ?? 'SO'; } function intermissionLabel(period) { return ['1st Int', '2nd Int', '3rd Int'][period - 1] ?? 'Int'; } // ── Init ───────────────────────────────────────────── function autoRefresh() { fetchScoreboardData(); setTimeout(autoRefresh, 5000); } window.addEventListener('load', () => { autoRefresh(); setInterval(tickClocks, 1000); if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').catch(err => { console.warn('Service worker registration failed:', err); }); } });