OverviewFilesCommitsIssuesDocsReleasesPeers

favela-surface commits / 7ca7d56

0.0.55: a staged update was invisible on exactly the devices that had one
⛔ MY BUG, AND IT MADE "CHECK NOW" LOOK BROKEN. Both the banner and the settings section asked `staged && running && staged !== running`. `running` comes from `shell.bundle`, which the shell sets from `chooseBundle`'s `hash` — and that is NULL whenever the app booted the APK's OWN bundle, which is the normal state of every fresh install. So a handset checked, downloaded, staged correctly, and then rendered "Up to date" with no banner. Every layer did its job and the aggregate was a lie — which is the shape hyper-social #68 §1 already records: "Every layer reported success truthfully about its own responsibility and the aggregate was false." MEASURED, not reasoned: a real boot produced {"ota":false,"running":null,"staged":"c47428d0…","version":"0.0.6"} with 0.0.6 genuinely downloaded and staged on disk. `updateWaiting()` is the rule, extracted so it is testable: anything staged while `running` is unknown IS waiting, because we are running the asset and the staged bundle is by construction not it. When `running` IS known the comparison is the real one — staged and identical means the node re-verified what it is already running, which is the ordinary case and not an update. ⚠ `ota:false` above is correct for a laptop with no RN shell — the buttons are gated on the shell's real capability. On a device `worklet.js` writes `shell.json` at :188 BEFORE `worklet.start()` at :195, so there is no race and `ota` is true. Checked rather than assumed, because a write-after-start would have hidden the buttons permanently. 188/188 tests, 602/602 asserts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7ca7d56092a720291bc7cc02bf5632339aa0ee56
by plankatron <fleeky@gmail.com> · 2026-09-10T16:02:48.000Z

Changed files (3) +28 -3

lib/pages.js +20 -2
……@@ -38,9 +38,27 @@ export function withTheme (html, theme) {
3838 * only the shell knows whether it can; a banner that offered a button the shell cannot honour would
3939 * be the same defect `parseShell` keeps separate flags to avoid.
4040 */
41/**
42 * Is a downloaded update actually waiting to be applied?
43 *
44 * ⛔ `running === null` MEANS "WE BOOTED THE APK'S OWN BUNDLE", and it is the normal state of a
45 * freshly installed app — `chooseBundle` returns `hash: null` for the asset, so the shell has no
46 * hash to report. Requiring `running` to be set therefore made the answer NO on exactly the devices
47 * that most need it: the phone staged an update correctly and then rendered "Up to date", with no
48 * banner, which is what "check now does nothing" looked like from the outside.
49 *
50 * Anything staged while we are running the asset is by construction not what booted, so it IS
51 * waiting. When `running` IS known, the comparison is the real one — staged and identical means the
52 * node re-verified what it is already running, which is the ordinary case and not an update.
53 */
54export function updateWaiting (u) {
55 if (!u || !u.staged) return false
56 return u.running ? u.staged !== u.running : true
57}
58
4159export function withUpdateBanner (html, update) {
4260 const u = update || null
43 if (!u || !u.staged || !u.running || u.staged === u.running) return html
61 if (!u || !updateWaiting(u)) return html
4462 const bar = '<div class="update-bar"><a href="/settings">An update is ready — restart to apply</a></div>'
4563 // After the opening <body>, so it is the first thing in the document and cannot be pushed below
4664 // the fold by whatever the page happens to render first.
……@@ -1511,7 +1529,7 @@ export function updateSettings (u) {
15111529 // ⛔ STAGED AND DIFFERENT is the only state that means "an update is waiting". Staged and IDENTICAL
15121530 // is the ordinary case — the node re-verified the bundle it is already running — and calling that
15131531 // "update available" is what makes a restart button lie.
1514 const waiting = u.staged && u.running && u.staged !== u.running
1532 const waiting = updateWaiting(u)
15151533 const body = waiting
15161534 ? '<p class="warn">An update is downloaded and will apply on the next start.</p>' +
15171535 '<p class="who">running ' + short(u.running) + ' &rarr; staged ' + short(u.staged) + '</p>' +
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "favela-surface",
3 "version": "0.0.54",
3 "version": "0.0.55",
44 "description": "The favela renderer \u2014 server-side HTML over the daemon's socket. No storage, no keys, no swarm.",
55 "type": "module",
66 "main": "index.js",
test/pages.test.js +7 -0
……@@ -595,6 +595,13 @@ test('★ the banner appears only when a DIFFERENT bundle is staged', (t) => {
595595 t.absent(withUpdateBanner(page, null).includes('update-bar'), 'no shell, no banner')
596596 t.absent(withUpdateBanner(page, { running: 'aaaa', staged: null }).includes('update-bar'),
597597 'nothing staged is not an update')
598
599 // ⛔ THE CASE THAT MADE THIS USELESS ON A PHONE. `running` is null whenever the app booted the
600 // APK's OWN bundle — `chooseBundle` has no hash for the asset — which is the normal state of a
601 // fresh install. Requiring `running` meant a handset staged an update correctly and then rendered
602 // "Up to date", with no banner. Reported as "check now does nothing", and it was this.
603 t.ok(withUpdateBanner(page, { running: null, staged: 'bbbb' }).includes('update-bar'),
604 '★ staged while running the APK asset IS an update waiting')
598605})
599606
600607test('the banner sits at the top of the document, not wherever a page happens to put it', (t) => {