Nothing tells you the cockpit on your phone is running an old build
- Dominant language
- TypeScript
- Stars
- 1
- Forks
- 0
- Avg merge
- 1h 21m
- Merged PRs (30d)
- 41
Description
## Evidence
A fix for #61 was committed at `17:00:41`, built and deployed immediately after. The developer photographed the phone at **5:01pm** and the fix was not there — because the page had been open since **3:52pm** and was still executing the bundle it loaded then. Two more hours of screenshots, all of the same stale build, all reasonably read as "the fix did not ship".
It had shipped. Verified against the public URL after the fact:
```
$ curl -s https://bench-cockpit.web.app/app.js | grep -c phone-only -> 1
$ curl -s https://bench-cockpit.web.app/styles.css | grep -n phone-only
1470:#composer-send.phone-only { display: none; }
3497: #composer-send.phone-only {
```
And the deployed stylesheet renders the control correctly at 390x844 — screenshot taken from `https://bench-cockpit.web.app/styles.css` against the real composer markup.
## Why it happens
`src/client/sw.ts` is network-first, so this is not a stale cache being served — a reload gets the new bundle. The problem is that **nothing prompts a reload**. On a desktop the tab gets closed a few times a day. On a phone it does not: the cockpit is a home-screen PWA that sits in the background for hours, and the developer has no reason to suspect the JavaScript in front of them is old.
The service worker already calls `skipWaiting()` (`sw.ts:60`) and `clients.claim()` (`sw.ts:70`), so a new worker takes control of open pages immediately — but taking control does not re-execute the page's already-loaded JavaScript. The app never finds out.
The cost is not cosmetic. It burned a full round trip of developer time, and every future one risks the same: a fix is deployed, the phone shows the old behaviour, and the only way to tell the difference between "not deployed", "does not work", and "not reloaded" is to go and diff the live bundle by hand.
## Acceptance criteria
- [ ] When a newer build becomes available, the open cockpit says so
- [ ] Reloading onto it takes one tap, and the affordance does not appear when the build is current
- [ ] Anything unsent in the composer is not lost by taking the offer
- [ ] It is not modal and does not interrupt reading a decision — this is an offer, not a demand
- [ ] Nothing about it is visible on a build that is up to date, at any width
- [ ] `pnpm typecheck` and `pnpm test` clean against the #50 baseline of 4 known failures
## Out of scope
- Automatic reloading without asking. Losing a half-typed brief to a background deploy would be worse than the problem.
- Any change to the network-first fetch policy in `sw.ts` — it is correct.
- Versioning or release notes surfaced in the UI.
## Verification
```
pnpm typecheck
pnpm test # 4 failures expected: dispatch-modal, model-costs, model-picker, settings-ui (#50)
```
Manual: open the cockpit, deploy a change, and confirm the open page offers the update without being reloaded by hand.
## Related
- #61 (the fix that appeared not to ship)
- #55 (proving the phone against a real daemon)
Contributor guide
Research direction
Start with src/client/sw.ts, especially skipWaiting() at line 60 and clients.claim() at line 70, then trace how the open cockpit can detect a newly controlled worker without disrupting the composer. Run pnpm typecheck and pnpm test against the #50 baseline. Done means an unobtrusive one-tap update offer appears only for newer builds, preserves unsent text, and passes the stated manual verification.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100