What a player actually experiences

Player Experience

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.

01Two APIs, two directions

"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 ESPNInbound — the worker API
DirectionThis app is the clientThis app is the server
Lives inapp/espn.pyapp/blueprints/worker_api.py
What it doesPulls the tournament field, team rosters and season records, per-player scoring averages, and full game box scores — all from ESPN's public REST endpointsLets 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.

02The entry form

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.

  1. Tabbed by pick group. One Bootstrap tab per group (seeds 1–4, 5–8, 9–12, 13–16, and coaches), each showing a live counter like 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.
  2. Sub-tabs by seed. Inside a group, players are further split into pill-tabs by seed (or, for coaches, by seed range) — each pill carries its own small pick count, so it's easy to see at a glance which seed you still need to pick from.
  3. Eliminated players are handled, not hidden. A player from a team that's lost its play-in game is visibly flagged and can't be newly picked — but if you already had that player on your roster before they were eliminated, they stay visible and you can still un-pick them; you just can't add them back.
  4. Tabs flag their own state. A tab turns red if the server rejected that group on a previous submit, and separately colors as "partial" or "complete" live as you pick — so problems are visible without having to open every tab to check.
Where validation actually happens: the in-browser counters and colors are there to guide you, but they don't block submission by themselves — the one thing that does intercept submission client-side is a confirmation popup if you've picked a still-undecided play-in team, asking you to confirm you understand that pick could still be eliminated. The real enforcement of "did you pick exactly the right number in every group" happens server-side on submit, which is what actually decides whether the entry is accepted.

03Live games, linked back to ESPN

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.

  • Every scored game badge on an entry, prediction, or comparison view links to that specific ESPN box score by game ID.
  • Every player name links to their ESPN player page by ESPN player ID; every team name or logo links to their ESPN team page the same way.
  • The day's games grid colors each game by its live ESPN state — in progress, final, or not yet started — and links each one straight to its ESPN box score.

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.

04Accounts

Account lifecycle is intentionally plain: register, log in, submit, edit until locked, reset a password if needed.

  1. Register with name, email, and cell number (both unique); the password is hashed, and registering logs you in immediately.
  2. Log in / log out — rate-limited to guard against repeated guessing.
  3. Submit or edit an entry — editing is blocked once the admin locks entries, and one account can only edit its own entries (checked on every request, not just hidden in the UI).
  4. Forgot password — generates a one-time reset link, valid 24 hours, and always reports "an email was sent" either way, so the response itself can't be used to check whether an email address is registered.

05Email

Two account emails go through Amazon SES: an entry-confirmation email after you submit a bracket, and the password-reset link above.

Current status, honestly: SES is not yet configured in production, so neither email currently sends there — the password-reset flow itself still works end to end (the link is generated and accepted correctly), the email just isn't delivered until SES credentials are set up. This is a tracked, known gap, not a bug.

06Bracket comparisons

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.

07Historical data fetching

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.