Releases
1.9.1
For you
- During a game, the game page carries a live card: a two color ribbon that traces the win probability from our locked kickoff call to the game's current state, quarter by quarter, and a ticker of every scoring change with the model's number beside it whenever the live model was speaking. The ticker states score changes only, never invented play descriptions, because the live feed knows scores, not plays. After the final the card stays put as the game's record, and the projection card beneath it now says plainly that its numbers are the pre kickoff call.
- A game page opened before kickoff now notices the game starting: the live connection decision is re checked every minute instead of decided once at page load, and a connection that goes quiet mid game falls back to polling within three minutes instead of waiting for the network to admit the drop.
- Nothing changes on the site. This is a private, internal-only addition: a new server lets the team query the published forecasts and rankings directly, not a feature anyone visiting the site sees.
- Coming back to the site is faster: the styles and fonts your browser already saw are trusted for a year instead of being re-checked every four hours, because their filenames guarantee they cannot have changed.
- The numbers behind the forecasts refresh themselves twice a day. The rosters, injuries and results the models read now advance automatically every morning and again in the late afternoon instead of waiting for someone to run the update by hand, so afternoon injury news reaches the forecasts the same evening on most days, and the inputs stop depending on anyone remembering to run the update.
- The call you saw before kickoff is the call that stays up. A between-runs refresh that happened to start minutes before a kickoff could replace a game's published forecast with a fresh one computed after the game began; now the last pre-kickoff call is the one that stands.
- A flexed game's card shows its real kickoff. The corrected time from the scoreboard source now reaches the published card instead of the old slot nflverse still lists, and the freeze that keeps a published call standing after kickoff fires at the game's true start rather than the stale one.
Under the hood
- The live card's ribbon geometry and ticker derivation are pure modules (`site/src/lib/live-ribbon.ts`, `site/src/lib/live-ticker.ts`) with unit tests, and the two rail decisions behind the self updating fix, the window recheck and the silent socket timeout, are pure functions in `site/src/lib/facts.ts` beside the existing socket drop decision. The game detail page's small live probability row and the never wired `LiveGame.astro` status element are removed; both jobs moved into the card. Team colors in the ribbon resolve the same `--team-<ABBR>` custom properties every other team colored surface reads, never a hex written by the card.
- A third Worker, `workers/mcp/`, serves seven read-only tools against the published research artifacts and live state through the Model Context Protocol, reachable only from one authorized laptop. Two independent checks guard it: a Cloudflare Access application at the custom domain, and the Worker's own verification of the resulting token before it touches any binding. `workers_dev` stays off, so the Worker has exactly one hostname.
- `make worker-test` now runs the new MCP server suite alongside the live Worker's and the cadence Worker's, so its tests sit inside Gate 1's existing worker job.
- A new Cache-Control rule in `site/public/_headers` marks `/_astro/*` (Astro's content-hashed CSS and JS) and `/fonts/*` as public, max-age=31536000, immutable. Production today serves both with max-age=14400, must-revalidate, cf-cache-status REVALIDATED: five to six needless revalidation round trips per return visit for names that can never point at different bytes. Fonts are not content-hashed, so a changed font ships as a new filename (pjs-400-v2.woff2), never reused in place. The HTML's max-age=0 stays untouched: forecasts update weekly and staleness is a truthfulness problem, not a performance one.
- A twice-daily GitHub Actions workflow (`.github/workflows/scheduled-ingest.yml`) runs `sporacle ingest` at 11:17 UTC, after nflverse's overnight update and before the midday refresh cadence, and again at 21:04 UTC, so the afternoon injury-report window lands in the master ahead of the 21:30 UTC evening refresh when the run starts on time; both minutes sit off the top of the hour, which GitHub documents as the crowded schedule slot most likely to be delayed or dropped. Each run then archives the day's depth chart snapshot with `sporacle archive-depth-charts` and verifies the result with `sporacle readiness --probe-source`, where exit 0 is the only go. A failed run notifies the user who created the workflow, subject to that user's own Actions notification settings, and leaves the previous master untouched, because the ingest refuses to upload a partial result. A final dead-man step pings a healthchecks.io check on every fully green run, so a run that never starts, which sends no failure email at all, raises an external alarm when the pings stop arriving. The schedule arms only when the file reaches `main`; the job checks out `dev` so the code it runs is current. `docs/runbooks/SCHEDULED-INGEST.md` owns the detail, and the Week 1 manual ingest runbook becomes the diagnostic playbook for red runs.
- The card build handed the kickoff resolver rows off a game_id index, and set_index drops that column, so the resolver's in-row id lookup came back empty and the schedule fact's kickoff override was silently skipped for exactly the flexed games it exists to correct. The resolver now takes the game id explicitly from its caller, and a wiring-level regression test drives a real run with an override present, asserting both the published kickoff and the freeze reading the true time.
- The refresh's kicked-off freeze reads the clock at publish time instead of run start. A cadence run measures about three minutes in the container, so a refresh or reprice that started shortly before a kickoff judged that game not yet kicked, froze nothing for it, and published a fresh forecast of a live game (invariant 3). One clock read now feeds the freeze, its guard and the artifact's generated_at, so the three cannot disagree about the same instant, and a regression test drives the whole run through the trigger-before-kick, finish-after-kick ordering. Moving the clock exposed a second hole: a game with no readable kickoff carries the card build's own instant as its displayed kickoff_utc, and against the publish-time clock that invented stamp always read as already kicked off, freezing the card forever or failing every refresh until a full run rescued it. Freeze decisions now judge each game on the kickoff the build could actually read, which for such a game is nothing at all, so an invented stamp stays a display value and never decides a freeze.