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
+102 -1
View File
@@ -1,5 +1,5 @@
import app.games
from tests.conftest import make_game
from tests.conftest import make_game, make_playoff_game
from app.games import (
_get_man_advantage,
calculate_game_importance,
@@ -745,6 +745,107 @@ class TestGetComebackBonus:
assert result == 70 # 50*1.0 + 20
class TestPlayoffEnrichment:
_FULL_STANDINGS = {
"league_sequence": 16,
"league_l10_sequence": 16,
"division_abbrev": "ATL",
"conference_abbrev": "E",
"games_played": 40,
"wildcard_sequence": 16,
}
def test_regular_season_game_has_empty_playoff_fields(self, mocker):
mocker.patch(
"app.games.get_team_standings", return_value=self._FULL_STANDINGS
)
result = parse_games({"games": [make_game()]})
g = result[0]
assert g["Is Playoff"] is False
assert g["Pinned"] is False
assert g["Playoff OT"] is False
assert g["Series Blurb"] == ""
assert g["Series Badges"] == []
def test_playoff_game_gets_series_fields(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value={"league_sequence": 0, "league_l10_sequence": 0},
)
game = make_playoff_game(top_wins=2, bottom_wins=1, round_num=1)
result = parse_games({"games": [game]})
g = result[0]
assert g["Is Playoff"] is True
assert g["Pinned"] is False
assert "Game 4" in g["Series Blurb"]
assert "R1" in g["Series Badges"]
def test_game7_is_pinned(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value={"league_sequence": 0, "league_l10_sequence": 0},
)
game = make_playoff_game(top_wins=3, bottom_wins=3)
result = parse_games({"games": [game]})
assert result[0]["Pinned"] is True
assert "GAME 7" in result[0]["Series Badges"]
def test_pinned_game_sorts_first(self, mocker):
mocker.patch(
"app.games.get_team_standings", return_value=self._FULL_STANDINGS
)
# Low-priority Game 7 PRE should sort above a high-priority non-playoff LIVE
g7_pre = make_playoff_game(
top_wins=3,
bottom_wins=3,
game_state="FUT",
period=0,
seconds_remaining=1200,
start_time_utc="2026-04-20T23:00:00Z",
home_name="Kings",
away_name="Oilers",
)
hype_live = make_game(
game_state="LIVE",
home_name="Rangers",
away_name="Devils",
home_score=2,
away_score=2,
period=3,
seconds_remaining=60,
)
result = parse_games({"games": [hype_live, g7_pre]})
assert result[0]["Home Team"] == "Kings"
assert result[0]["Pinned"] is True
def test_playoff_ot_flagged(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value={"league_sequence": 0, "league_l10_sequence": 0},
)
game = make_playoff_game(
top_wins=1,
bottom_wins=1,
period=4,
seconds_remaining=600,
game_state="LIVE",
)
result = parse_games({"games": [game]})
assert result[0]["Playoff OT"] is True
assert result[0]["OT Label"] == "OT"
def test_regular_season_ot_not_flagged_as_playoff_ot(self, mocker):
mocker.patch(
"app.games.get_team_standings", return_value=self._FULL_STANDINGS
)
game = make_game(
game_state="LIVE", period=4, seconds_remaining=180, game_type=2
)
result = parse_games({"games": [game]})
assert result[0]["Playoff OT"] is False
assert result[0]["OT Label"] == ""
class TestCalculateGameImportance:
def _standings(
self,