7d1649d278
Per-file content-hash versioning on every /static reference, immutable cache headers on versioned URLs, no-cache on HTML, auto-bumped service worker cache name with stale-while-revalidate for assets, and a controllerchange listener that silently reloads the page when a new SW takes control. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
135 lines
3.9 KiB
Python
135 lines
3.9 KiB
Python
import json
|
|
|
|
from flask import abort, make_response, render_template, jsonify, send_from_directory
|
|
|
|
from app import APP_VERSION, app, static_v
|
|
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():
|
|
precache = [
|
|
"/",
|
|
static_v("styles.css"),
|
|
static_v("script.js"),
|
|
static_v("icon-192x192.png"),
|
|
static_v("icon-512x512.png"),
|
|
"/manifest.json",
|
|
]
|
|
body = render_template("sw.js.j2", app_version=APP_VERSION, precache=precache)
|
|
response = make_response(body)
|
|
response.headers["Content-Type"] = "application/javascript; charset=utf-8"
|
|
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)
|