▍ humdrum codex / soft

Add design: working terminal refresh + past/upcoming scores

8efd8313ee58d70a6d69f9c343e0be353f23abe1
humdrum <me@humdrum.me> · 2026-07-08 15:30

parent fb1e47ac

Add design: working terminal refresh + past/upcoming scores

Fix TUI refresh (missing fresh/force param), add per-league score
windows + sportsball favorites sync, FINAL/TODAY/UPCOMING bucketing
in TUI and paper, 60s live poll during games.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

1 files changed

docs/superpowers/specs/2026-07-08-terminal-refresh-and-scores-design.md +118 −0
@@ -0,0 +1,118 @@
+# 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: <espnPath>, team: <abbr or name>}`), 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.