What a player actually experiences
The entry form, live game links, accounts, and the two other player-facing views (bracket comparisons, historical simulation) — plus the two "APIs" this system actually has, and how account email works today.
"API" shows up twice in this system, in opposite directions: one is data the app consumes from the outside world, the other is a small API the app itself exposes.
| Outbound — consuming ESPN | Inbound — the worker API | |
|---|---|---|
| Direction | This app is the client | This app is the server |
| Lives in | app/espn.py | app/blueprints/worker_api.py |
| What it does | Pulls the tournament field, team rosters and season records, per-player scoring averages, and full game box scores — all from ESPN's public REST endpoints | Lets the Spark queue's worker process claim a pending job and post progress/results back, over HTTP, without holding a database credential |
app/espn.py is the more mature of the two: it fetches team info and rosters, resolves each player's season points-per-game from ESPN's athlete-stats API, and pulls full box scores per game — explicitly excluding NCAA-tournament-specific data from the season-schedule fetch (games are fetched without seasontype=3) so regular-season analytics never accidentally include tournament games. The inbound worker API is the newer piece, built as part of Spark Queue v2 and proven working end to end in dev — a real job ran through the full queue-and-poll loop against real MySQL and a real local Ollama with correct event ordering. What's still open is making it live on the production host (mTLS at nginx, flipping SPARK_ENABLED), not the API itself. It's the reusable substrate the eventual agentic phase (tool calls running on the Spark) will also use.
Submitting a bracket means making 22 separate picks across 5 groups without losing track of where you are — the entry form's whole design is keeping that manageable.
3/6 that updates the instant a checkbox is toggled — and once a group hits its max, its remaining unchecked boxes simply disable themselves, so it's not possible to over-pick a group.Anywhere a game, player, or team shows up in the app, it links straight back to that exact ESPN page — so nothing in a standing or a bracket asks you to just trust it.
None of this is proxied through the app's own routes — the links are built directly from the same ESPN IDs the scoring pipeline already uses (see How It's Built §6), so a link and the score next to it are always describing the same underlying ESPN record.
Account lifecycle is intentionally plain: register, log in, submit, edit until locked, reset a password if needed.
Two account emails go through Amazon SES: an entry-confirmation email after you submit a bracket, and the password-reset link above.
Pick any two entries and see them projected side by side, not just as two separate numbers.
The comparison view reuses the exact same round-by-round projection logic as the single-entry visual bracket (see How It's Built §7) — it doesn't duplicate that logic, it calls the same shared projection function twice, once per entry, and then colors each slot by whether entry A picked that team, entry B did, both did (a genuine collision), or neither did. Eliminated teams still show struck through, exactly as they do on a single bracket.
Before trusting the scoring engine with a live, once-a-year tournament, it needs to be tested against a tournament that already happened.
The historical-year workflow re-points the active tournament year at a past season and re-runs the exact same import-and-score pipeline against real ESPN data from that year — same tables, same scoring logic, just scoped by year rather than fenced off in separate tables. That reuse is deliberate: it means the historical mode is testing the actual production scoring path, not a simulated stand-in for it. A separate, unrelated setting (daily simulation mode) exists for a different kind of test — replaying single days of the regular season rather than a full historical tournament year — and the two don't share any wiring. Because both modes work by pointing the same "active year" setting at a different value, running one deliberately means the current live tournament year is not the active one for that session — an admin has to switch back explicitly when historical testing is done.