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
+55 -5
View File
@@ -1,10 +1,25 @@
import json
from flask import render_template, jsonify, send_from_directory
from flask import abort, render_template, jsonify, send_from_directory
from app import app
from app.config import SCOREBOARD_DATA_FILE
from app.games import parse_games
from app.playoff import today_meta
from app.bracket_view import build_bracket_view
from app.playoff_cache import (
day_n as compute_day_n,
fetch_series,
get_bracket,
parse_series_id,
record_start_date_if_missing,
refresh_bracket,
)
from app.series_view import build_series_view
from datetime import datetime
from zoneinfo import ZoneInfo
_EASTERN = ZoneInfo("America/New_York")
@app.route("/manifest.json")
@@ -45,20 +60,55 @@ def get_scoreboard():
)
if scoreboard_data:
raw_games = scoreboard_data.get("games", [])
record_start_date_if_missing(raw_games)
games = parse_games(scoreboard_data)
n, total = compute_day_n()
meta = today_meta(raw_games, day_n=n, day_total=total)
pinned = [g for g in games if g.get("Pinned")]
remaining = [g for g in games if not g.get("Pinned")]
return jsonify(
{
"meta": meta,
"pinned_games": pinned,
"live_games": [
g
for g in games
for g in remaining
if g["Game State"] == "LIVE" and not g["Intermission"]
],
"intermission_games": [
g for g in games if g["Game State"] == "LIVE" and g["Intermission"]
g
for g in remaining
if g["Game State"] == "LIVE" and g["Intermission"]
],
"pre_games": [g for g in games if g["Game State"] == "PRE"],
"final_games": [g for g in games if g["Game State"] == "FINAL"],
"pre_games": [g for g in remaining if g["Game State"] == "PRE"],
"final_games": [g for g in remaining if g["Game State"] == "FINAL"],
}
)
else:
return jsonify({"error": "Failed to retrieve scoreboard data"})
@app.route("/series/<series_id>")
def series_detail(series_id):
if parse_series_id(series_id) is None:
abort(404)
payload = fetch_series(series_id)
if payload is None:
abort(404)
view = build_series_view(series_id, payload)
return render_template("series.html", series=view)
@app.route("/bracket")
def bracket():
year = datetime.now(_EASTERN).year
payload, fetched_at = get_bracket(year)
if payload is None:
payload = refresh_bracket(year)
if payload is None:
abort(404)
view = build_bracket_view(year, payload, fetched_at=fetched_at)
return render_template("bracket.html", bracket=view)