# Design — Working terminal refresh + past/upcoming scores **Date:** 2026-07-08 **Status:** approved (pending spec review) ## Context The first-class surfaces of this project are the **terminal TUI** (`tui/dashboard.py`, Textual/Python, launched via the `dashboard` command → `uv run`) and the **morning paper** (`morning-paper` script → `POST /api/edition` → puppeteer renders `/print` to a PDF). The Next.js webapp at `:4317` is effectively the backend: it serves `/api/*` (data) and `/print` (paper source). The interactive webapp UI is not a priority. Two problems motivate this work: 1. **TUI refresh doesn't work.** `fetch_sources()` calls bare `GET /api/sources`, which returns cached snapshots only (`currentState`). The server supports `?fresh=1` (refresh stale sources) and `?force=1` (refresh all), but the TUI never sends them — so pressing `r` just re-reads the same cache. Root cause: missing query param. 2. **No previous-day scores, and no live updates.** `lib/sources/modules/sports.ts` fetches only today → `+daysAhead` (never yesterday). `lib/edition-data.ts:70` already *wants* `yesterday..today` for the paper but the data was never fetched. The sports source also refreshes every 600s, so live scores never move in the TUI within a game. ### Data source finding The user's separate `sportsball` TUI (`~/Developer/Home/sportsball`, Go/Bubble Tea) keeps live scores fresh. Investigation: it pulls the **same** ESPN public API the dashboard already uses — `https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}/scoreboard` (`internal/espn/client.go:19`). There is no better data to sync to, and sportsball has no API or persisted score store (config.json holds only favorites) — nothing to sync *from*. So we **borrow its techniques**, staying independent: - **Flat ~15s poll while live** (`commands.go:62`) — "live scores feel fresh without hammering ESPN"; one scoreboard call per league per tick is cheap. - **Per-league past/future windows** (`internal/model/league.go:46-51`), tuned to each sport's cadence: | League | ESPN path (`sport/league`) | back | fwd | |---|---|---|---| | World Cup | `soccer/fifa.world` | 7 | 7 | | MLB | `baseball/mlb` | 2 | 3 | | NBA | `basketball/nba` | 3 | 4 | | WNBA | `basketball/wnba` | 3 | 4 | | NHL | `hockey/nhl` | 3 | 4 | | NFL | `football/nfl` | 8 | 8 | - **Favorites** live in `~/.config/sportsball/config.json` as `favorites: [{league, id, abbr, name}]`. We reuse these so the user doesn't re-enter teams. ## Decisions - Scores window: **per-league** (sportsball catalog), superseding an earlier flat "yesterday-only". Better for weekly sports (NFL needs ~8 days back to show the last game). - Layout: **FINAL / TODAY / UPCOMING** sections, grouped by league within each. Shared shape across TUI and paper. - **Paper shows FINAL + TODAY only** (no UPCOMING). - Live cadence: force-refresh **sports only** every **60s while a game is live**; otherwise dormant (normal cadence). No overnight hammering. - Favorites sync: **once, on TUI open** — not per fetch. Persisted to the sports source config in the DB, so the 6am paper LaunchAgent uses the last-synced favorites. ## Changes ### 1. TUI refresh (`tui/dashboard.py`) - `fetch_sources(force=False)`: request `/api/sources?force=1` on manual `r`, `?fresh=1` on the periodic timer. The server already does the upstream re-fetch behind those params. ### 2. Sports module + schema (`lib/sources/modules/sports.ts`, `lib/schemas/sources/sports.ts`) - Add a **league catalog** map: ESPN `sport/league` path + `back`/`forward` days, mirrored from sportsball's `league.go` (table above). - Fetch each configured league across `now-back … now+forward` (via the existing per-date `collect()`), replacing the flat `daysAhead` / proposed `daysBack`. - Payload shape (`league, status, state, home, away, homeScore, awayScore, startTime`) is unchanged; only the date range widens. `state` already distinguishes `pre`/`in`/`post`. - Config still supports explicit `leagues` (all games) + `teams` (favorite filters); windows come from the catalog keyed by league path. ### 3. Favorites sync on TUI open (`tui/dashboard.py`) - In `on_mount` (after the server is confirmed up): read `~/.config/sportsball/config.json`; map each favorite via a small league-key→ESPN-path dict (`mlb`→`baseball/mlb`, `nba`→`basketball/nba`, `wnba`→`basketball/wnba`, `nhl`→`hockey/nhl`, `nfl`→`football/nfl`, `worldcup`→`soccer/fifa.world`) into dashboard `teams` entries (`{league: , team: }`), then `PATCH /api/sources/{sportsId}` once with the updated config. - Skip silently if the file is absent or the server is unreachable. Leave `leagues` untouched. - The per-league window catalog stays only in the TS module; the TUI mirrors just the 6-entry path map (small, stable). ### 4. Shared bucketing — FINAL / TODAY / UPCOMING - `state=="post"` → **FINAL** (show score); `state=="in"` or (`pre` starting today) → **TODAY** (live games highlighted); `pre` starting after today → **UPCOMING**. Group by league within each section, chronological. ### 5. TUI `render_sports` + live poll (`tui/dashboard.py`) - Replace the current `live/today/upcoming` buckets (which mislabel finals) with FINAL/TODAY/UPCOMING per §4. - Track the sports source id and `_sports_live` (any game `state=="in"`). A 60s timer: when `_sports_live`, `POST /api/sources/{id}/refresh` (force-refresh sports only), then re-render just the Scores panel from the response. Dormant otherwise. ### 6. Paper (`app/print/page.tsx`) - Replace the raw group-by-league dump with FINAL + TODAY sections (§4 bucketing), **no UPCOMING**. `lib/edition-data.ts:70` already clips sports to `startYesterday..endToday` — leave as-is. ## Out of scope - Webapp `SportsBody` (`components/cards/CardBodies.tsx`) — untouched; the wider payload flows through harmlessly (yesterday finals land in its existing "Live/Final" bucket). - No coupling to the sportsball binary; no export code added there. ## Verification - `pnpm typecheck`. - Run `dashboard`, press `r`: snapshots update (not cache); during a live game, scores tick ~every 60s; FINAL shows yesterday's results, UPCOMING shows future games. - Confirm favorites appeared: check the sports source config after opening the TUI. - Run `morning-paper` (no `--print`) and eyeball the PDF's Scores section: FINAL + TODAY only.