feat(app): tell the truth about freshness, keep the tabs in reach, and let a phone leave - #14
Conversation
…d let a phone leave Five things Fredrik found testing on his own box. The freshness band read "no reading yet" for a whole session while every number on screen moved. The app was asking after three source ids it had been compiled with — the simulator's — and a real box names its sources after its own drivers, so all three came back unknown and unknown ranks worse than dead. The ids now come from the dictionary the box sends, which is what the rule about never hand-writing a name shared with the box is for. The simulator was the only box in the world that matched. Fixing that opened worse holes than it closed, and closing those is most of this change. A carrier was claimed the moment its socket opened, so a launch from cache showed a two-hour-old house under "Live via encrypted relay" with the age suppressed. Answering hello moved the box's clock without sending a reading, so a box that was booting, wedged or refusing this device looked live — and it re-hellos every five seconds, so it re-armed forever. An empty source list read as healthy, because the worst-of scan starts at live and never enters its loop. A cache restore counted as an arriving frame. So: a carrier is claimed once it has delivered a reading, entering the stream is the first one, a box that names no source is judged by the sources it does report, and a stream that has gone quiet stops being live so the age returns to the screen. The tab bar sat below the fold on any view taller than the screen: the shell was min-height, so it grew past the viewport instead of scrolling inside it. The price chart took 57% of a phone before the timeline began, and history drew its series in a way Fredrik called hackig. Both are the box's own components, changed upstream and re-vendored rather than forked. And a phone can leave. Signing out clears what is on the device; it does not tell the box to forget it, because the app has no message that says so and a sign-out that only worked online would not be one. The screen says both halves and sends you to the box for the other. The order is the design: the pointer, then the rows, then the key — and never the key while a row it sealed is still there, because that is a house on screen that can never be opened again and no amount of reloading fixes it. Two screens stopped contradicting each other. The band told a phone whose access had been withdrawn that it could not reach its box, while the screen below said the owner had removed it. And "keeps trying on its own" was a promise nothing kept when no carrier had been built at all — ConnectError already carries the sentence saying what to do, and the app was throwing it away. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
ftw-webapp | 8c444a9 | Aug 06 2026, 09:44 AM |
The instanceof needed ConnectError, which meant importing connect.ts on the launch path — and the test that mocks that module does not export the class, so every run raised an unhandled rejection while all 475 tests passed. Green locally only because I filtered the summary down to the lines I expected. The help string is the only part being used, so read it structurally. That also keeps the carrier stack out of the entry chunk, which is what the dynamic import was for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b86690a2d3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } catch (err) { | ||
| failures.push(err) | ||
| } |
There was a problem hiding this comment.
Preserve the site row after any projection clear fails
When any store clear fails, this catch records the error but continues deleting later stores, including sites; a failure clearing snapshot, or a failure clearing the final keys store after sites succeeded, therefore makes forgetEverything() reject after the rendezvous secret and pinned box key are already gone. The recovery path in App.svelte then calls openFeed(), but connectToSite() requires that site row and returns not_paired, so the screen's claim that the home is still present and working is false and the user must physically rescan the box. Stop clearing subsequent stores after the first failure, or otherwise keep the site row until the operation can complete.
Useful? React with 👍 / 👎.
| stop(): void { | ||
| if (this.#timer) clearTimeout(this.#timer) | ||
| this.#timer = null | ||
| this.#pending = null | ||
| this.#stopped = true |
There was a problem hiding this comment.
Wait for active snapshot writes before clearing storage
When #flush() has already removed a snapshot from #pending and is awaiting sealing or IndexedDB, stop() clears only the timer and pending slot; the active saveSnapshot() continues and can execute its final snapshot put after forgetEverything() has cleared that store. This can make sign-out report success while a sealed household snapshot is written back, especially when a timer or visibilitychange flush overlaps the sign-out. Track and await or invalidate the in-flight write before clearing the databases.
Useful? React with 👍 / 👎.
Five things Fredrik found testing on his own box, and the holes that fixing the first one opened.
Needs srcfl/ftw#838, merged — the vendored chart cites its commit.
"No reading yet" on a box that was answering
The band read
Live via encrypted relay · no reading yetfor a whole session while every number on screen moved.The app was asking after three source ids it had been compiled with — the simulator's
meter.p1,inverter.sungrow,battery.sungrow. A real box builds its dictionary from its running drivers, so on Fredrik's box every id is a driver name and none of the three exists. Unknown ranks worse than dead, so all three came backnever.This is CLAUDE.md's "never hand-write a name shared with the box", exactly. The ids now come from the dictionary the box sends. It could not have been caught against the simulator — it is the only box in the world that matched.
Fixing that opened worse holes than it closed
Most of this PR is closing them. Each was reproduced, and each has a test that fails when its fix is reverted.
liveand never enters its loop. A box that names no source for anything drawn had every driver down and the band said all clear.So: a carrier is claimed once it has delivered a reading; entering the stream is the first reading (the snapshot carries the same uptime hello just announced); a box that names no source is judged by the sources it does report; and a stream that has gone quiet stops being live, which is what puts the age back on screen.
The tab bar below the fold
The shell was
min-height: 100dvh, so it grew past the viewport rather than scrolling inside it, and the nav went with it. Measured on Plan at 375 px: document scroll 1826 px → 0, nav bottom edge 2638 px → 812 px.Charts
The price chart took 57 % of a phone before the timeline began; now 45 %. History was "lite hackig i sin form". Both are the box's own components — changed upstream and re-vendored, not forked.
A phone can leave
Signing out clears what is on the device. It does not tell the box to forget it: the app has no message that says so, and a sign-out that only worked online would not be one — a phone is handed on exactly where its box is not. The screen says both halves and sends you to the box for the other.
The order is the design: the pointer, then the rows, then the key — and never the key while a row it sealed is still there. That combination is a house on screen that can never be opened again: the next launch mints a fresh key, unsealing throws where nothing catches it, and the app paints nothing. A phone that reloading cannot fix, in an app whose principle is that reloading is never the fix.
Two screens that contradicted each other
The band told a phone whose access had been withdrawn that it could not reach its box, while the screen directly below said the owner had removed it. The box was perfectly reachable; it had made a decision about the phone.
And "keeps trying on its own" was a promise nothing kept when no carrier had been built at all.
ConnectErroralready carries the sentence saying what to do instead, and the app was throwing it away.Review notes
boxFingerprintduplicatesfingerprintOfin Pair.svelte;Chart.sveltecarries raw hex fallbacks against the design-role convention; the band/line switch flips on a 20 px width change;openFeed's dynamic imports can land after a sign-out.🤖 Generated with Claude Code