Files
NHL-Scoreboard/app/routes.py
T
josh a88e2edef0
CI / Lint (push) Successful in 5s
CI / Test (push) Successful in 9s
CI / Build & Push (push) Successful in 19s
fix: anchor Day N to each round's first game instead of lazy first sighting
The banner read "Day 1 of ~60" on day 2 of the playoffs because the old
anchor recorded whatever date we first polled a playoff game as Day 1.
Now round start dates come from /v1/schedule/playoff-series, so Day N
is authoritative and resets at each round boundary. Drops the noisy
"of ~60" denominator.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 13:03:08 -04:00

125 lines
3.6 KiB
Python

import json
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_for_round,
fetch_series,
get_bracket,
parse_series_id,
refresh_bracket,
)
from app.series_view import build_series_view
from datetime import datetime
from zoneinfo import ZoneInfo
_EASTERN = ZoneInfo("America/New_York")
def _max_playoff_round(raw_games):
max_round = 0
for g in raw_games or []:
if g.get("gameType") != 3:
continue
r = (g.get("seriesStatus") or {}).get("round") or 0
if r > max_round:
max_round = r
return max_round or None
@app.route("/manifest.json")
def manifest():
return send_from_directory(app.static_folder, "manifest.json")
@app.route("/sw.js")
def service_worker():
response = send_from_directory(app.static_folder, "sw.js")
response.headers["Service-Worker-Allowed"] = "/"
response.headers["Cache-Control"] = "no-cache"
return response
@app.route("/favicon.ico")
def favicon():
return send_from_directory(
app.static_folder, "icon-32x32.png", mimetype="image/png"
)
@app.route("/")
def index():
return render_template("index.html")
@app.route("/scoreboard")
def get_scoreboard():
try:
with open(SCOREBOARD_DATA_FILE, "r") as json_file:
scoreboard_data = json.load(json_file)
except FileNotFoundError:
return jsonify({"error": "Failed to retrieve scoreboard data. File not found."})
except json.JSONDecodeError:
return jsonify(
{"error": "Failed to retrieve scoreboard data. Invalid JSON format."}
)
if scoreboard_data:
raw_games = scoreboard_data.get("games", [])
games = parse_games(scoreboard_data)
max_round = _max_playoff_round(raw_games)
n = day_n_for_round(max_round) if max_round else None
meta = today_meta(raw_games, day_n=n)
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 remaining
if g["Game State"] == "LIVE" and not g["Intermission"]
],
"intermission_games": [
g
for g in remaining
if g["Game State"] == "LIVE" and g["Intermission"]
],
"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)