From 8945b99782e3e3189990547e38a20517922fe6eb Mon Sep 17 00:00:00 2001 From: josh Date: Sun, 29 Mar 2026 14:58:31 -0400 Subject: [PATCH] feat: power play indicator with live countdown clock Shows a red pill below the team rows when a PP is active, displaying the team on the power play and a ticking countdown. PP clock always resyncs from the API (no local anchoring) since 2-minute penalties are short enough that accuracy matters throughout. Removed the old inline PP text from team rows. Co-Authored-By: Claude Sonnet 4.6 --- app/static/script.js | 28 +++++++++++++++++++++++----- app/static/styles.css | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/app/static/script.js b/app/static/script.js index 8bbb3c9..9d48ecf 100644 --- a/app/static/script.js +++ b/app/static/script.js @@ -73,6 +73,7 @@ function renderLiveGame(game) { ${teamRow(game, 'Away', 'live')} ${teamRow(game, 'Home', 'live')} + ${ppIndicator(game)} ${hype} `; } @@ -112,12 +113,10 @@ function teamRow(game, side, state) { const logo = game[`${side} Logo`]; const score = game[`${side} Score`]; const sog = game[`${side} Shots`]; - const pp = game[`${side} Power Play`]; const record = game[`${side} Record`]; const sogHtml = (state === 'live' || state === 'final') && sog !== undefined ? `${sog} SOG` : ''; - const ppHtml = pp ? `${pp}` : ''; const right = state === 'pre' ? `${record}` @@ -128,12 +127,31 @@ function teamRow(game, side, state) {
${name} - ${sogHtml}${ppHtml} + ${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() { @@ -166,7 +184,7 @@ function secondsToTime(s) { function snapshotClocks(grid) { const snapshot = new Map(); grid.querySelectorAll('[data-game-key]').forEach(card => { - const badge = card.querySelector('[data-seconds][data-received-at]'); + 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); @@ -181,7 +199,7 @@ 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]'); + 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) { diff --git a/app/static/styles.css b/app/static/styles.css index 3fb1848..951ab88 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -209,6 +209,47 @@ main { white-space: nowrap; } +/* ── Power Play Indicator ───────────────────────── */ + +.pp-indicator { + display: flex; + align-items: center; + gap: 0.4rem; + margin-top: 0.5rem; + padding: 0.35rem 0.5rem; + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + border-radius: 6px; +} + +.pp-label { + font-size: 0.6rem; + font-weight: 700; + letter-spacing: 0.08em; + color: var(--red); + text-transform: uppercase; + flex-shrink: 0; +} + +.pp-team { + font-size: 0.72rem; + font-weight: 600; + color: var(--text); + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.pp-clock { + font-size: 0.72rem; + font-weight: 700; + color: var(--red); + flex-shrink: 0; + font-variant-numeric: tabular-nums; +} + /* ── Hype Meter ─────────────────────────────────── */ .hype-meter {