feat: stop reloading the page out from under people - #41
Merged
Conversation
"we just need to make the page not refresh and use ajax/realtime event updates and logging and status bars or spinners and shit. its hard to tell when i should wait or take action." There is no client router and there should not be one: the pages are server-rendered and the project commits to working without JavaScript. What there was instead was `window.location.reload()` — fired 1.5s after any job finished, twice more after uploads — and a "Analysis is running. Refresh for progress." link for the gaps in between. Four hard reloads and an instruction to do a fifth by hand. That is what made it impossible to tell whether to wait or to click. It also raced the user: a click landing before a reload arrived with no athlete loaded, and the server minted another one. Seven duplicate athletes came from that race, so this and the identify fix are the same bug seen from two ends. `client/live.ts` fetches the URL the user is already on and swaps only the regions marked `data-live` — the footage list, the athlete table, the suggested moments. The server stays the single source of truth, no-JavaScript keeps working unchanged, nothing scrolls, and the islands that own state are outside every region: the job log keeps its EventSource, the identify grid keeps a half-finished selection. `#moment-review` sits inside one deliberately, because it re-reads its data from an attribute, and is re-mounted after a swap. Identifying an athlete no longer navigates at all. It reports what happened in place, refreshes its own grid, and lets the rest of the page catch up. apps/web/src/liveregions.test.ts pins the invariant that makes this safe: the job log, the identify grid and the uploader are outside every swappable region. Move one inside and the test fails rather than the feed dying silently in production. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was actually there
No client router — I checked, zero hits for
pushState/popstate. The app is Hono SSR plus five islands (upload,jobs,identify,overlay,review), navigation is full page loads, and mutations are form POST → 302.The only "liveness" was:
jobs.tsx:109—window.location.reload()1.5s after any job completesupload.tsx:306and:405— two more reloadspages.tsx— "Analysis is running. Refresh for progress."Four hard reloads and an instruction to do a fifth by hand. That's the whole reason it's unclear whether to wait or act. It's also the same bug as #40 from the other end: a click landing before a reload arrived with no athlete loaded, so the server minted another one. The reloads were racing the clicks.
Approach: not a router
A SPA router would fight the explicit commitment in
actions.tsthat pages "must keep working without JavaScript". Instead,client/live.tsfetches the URL the user is already on and swaps only the regions markeddata-live— footage list, athlete table, suggested moments.Islands that own state stay outside every region: the job log keeps its
EventSource, the identify grid keeps a half-made selection.#moment-reviewis inside one deliberately — it re-reads from adata-momentsattribute and is re-mounted after a swap.Identifying an athlete no longer navigates at all: it reports in place, refreshes its own grid, and lets the page catch up. The "Refresh for progress" line is now
no-js-onlyand hidden once the bundle runs.Test
apps/web/src/liveregions.test.tspins the invariant that makes this safe: job log, identify grid and uploader are outside every swappable region. Move one inside and the test fails, rather than the analysis feed dying silently in production.576 tests pass, 8 skipped. Lint and build clean.
Note
This is one of three from the same report. The other two — jersey/team in the picker, and ball detection recall — are separate branches.
🤖 Generated with Claude Code