feat: make scoreboard playoff-aware with banner, bracket, and series drill-down
CI / Lint (push) Failing after 8s
CI / Test (push) Has been skipped
CI / Build & Push (push) Has been skipped

Turn a regular-season-looking Tuesday into a full playoff experience:

- Playoff banner with round + day + series + elimination counts, gold/silver
  Cup theme toggled by body.playoff-mode
- Series context on each playoff card: round chip, series score, stake badges
  (GAME 7, CLINCHER, PIVOTAL), and one-line blurb
- Game 7s pin to a new Spotlight section above Live
- Playoff OT renders with SUDDEN DEATH badge and pulsing gold border
- Client-side OT notifications via bell button in the banner
- New /series/<id> drill-down with headline, next-game, and game-by-game history
- New /bracket page: 7-column desktop grid, accordion on mobile
- Day N banner count auto-anchors on first playoff scoreboard hit
- SQLite cache for bracket + per-series schedules, stale-on-failure up to 24h

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-19 12:47:31 -04:00
parent e0db8f0859
commit ebe770fecd
21 changed files with 3163 additions and 31 deletions
+49
View File
@@ -19,6 +19,8 @@ def make_game(
game_type=2,
situation=None,
series_status=None,
home_abbrev="TOR",
away_abbrev="BOS",
):
clock = {
"timeRemaining": f"{seconds_remaining // 60:02d}:{seconds_remaining % 60:02d}",
@@ -33,6 +35,7 @@ def make_game(
"clock": clock,
"homeTeam": {
"name": {"default": home_name},
"abbrev": home_abbrev,
"score": home_score,
"sog": 15,
"logo": "https://example.com/home.png",
@@ -40,6 +43,7 @@ def make_game(
},
"awayTeam": {
"name": {"default": away_name},
"abbrev": away_abbrev,
"score": away_score,
"sog": 12,
"logo": "https://example.com/away.png",
@@ -52,6 +56,49 @@ def make_game(
}
def make_playoff_game(
top_wins=0,
bottom_wins=0,
round_num=1,
series_letter="A",
top_abbrev="TOR",
bottom_abbrev="BOS",
top_is_home=True,
game_state="LIVE",
**kwargs,
):
"""Convenience wrapper around make_game for playoff fixtures.
`top_is_home` controls which side of the matchup hosts this game, so tests
for the blurb copy (leader vs. trailer names) don't have to juggle raw dicts.
"""
series_status = {
"round": round_num,
"topSeedWins": top_wins,
"bottomSeedWins": bottom_wins,
"seriesLetter": series_letter,
"topSeedTeamAbbrev": top_abbrev,
"bottomSeedTeamAbbrev": bottom_abbrev,
}
if top_is_home:
home_abbrev, away_abbrev = top_abbrev, bottom_abbrev
home_name, away_name = "Top Seeds", "Bottom Seeds"
else:
home_abbrev, away_abbrev = bottom_abbrev, top_abbrev
home_name, away_name = "Bottom Seeds", "Top Seeds"
return make_game(
game_state=game_state,
game_type=3,
series_status=series_status,
home_abbrev=kwargs.pop("home_abbrev", home_abbrev),
away_abbrev=kwargs.pop("away_abbrev", away_abbrev),
home_name=kwargs.pop("home_name", home_name),
away_name=kwargs.pop("away_name", away_name),
**kwargs,
)
LIVE_GAME = make_game()
PRE_GAME = make_game(
game_state="FUT", home_score=0, away_score=0, period=0, seconds_remaining=1200
@@ -89,9 +136,11 @@ def flask_client(tmp_path, monkeypatch):
# Patch module-level path constants so no reloads are needed
import app.routes as routes
import app.games as games
import app.playoff_cache as playoff_cache
monkeypatch.setattr(routes, "SCOREBOARD_DATA_FILE", str(scoreboard_file))
monkeypatch.setattr(games, "DB_PATH", str(db_path))
monkeypatch.setattr(playoff_cache, "DB_PATH", str(db_path))
from app import app as flask_app