OverviewFilesCommitsIssuesDocsReleasesPeers

favela-surface commits / 3f6d9c3

0.0.63: suppress a refusal for a bundle older than the one running
boot-plan.js is React Native and ships in the APK; this page ships in the BUNDLE and arrives over OTA. A shell too old to suppress a stale refusal keeps sending one — on the handset that pinned 'this update cannot be applied — staged bundle previously failed at boot' to every page while a NEWER bundle was running. Waiting for an APK release to clear a banner is precisely the trap this work exists to escape, so the page applies the rule too. A refusal naming a bundle NEWER than the running one is still shown: a new release that cannot run is worth knowing about. Suite 201/201. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3f6d9c364327d306a904dafc8cb5ad0f5a0968c4
by plankatron <fleeky@gmail.com> · 2026-09-11T02:04:26.000Z

Changed files (3) +36 -3

lib/pages.js +11 -2
……@@ -86,8 +86,17 @@ export function updateWaiting (u) {
8686 */
8787export function updateBlockedReason (u) {
8888 if (!u || !u.staged) return null
89 if (u.refused && u.refused.hash === u.staged) return u.refused.reason || 'refused by the shell'
90 return null
89 if (!u.refused || u.refused.hash !== u.staged) return null
90 // ⛔ SUPPRESS A REFUSAL FOR A BUNDLE OLDER THAN THE ONE RUNNING, page-side as well as shell-side.
91 // boot-plan.js is React Native and ships in the APK; this page ships in the BUNDLE and arrives over
92 // OTA. A shell too old to suppress a stale refusal keeps sending one, and on the handset that pinned
93 // "this update cannot be applied — staged bundle previously failed at boot" to every page while a
94 // NEWER bundle was running. Waiting for an APK to clear a banner is the trap this work exists to
95 // escape, so the rule is applied wherever it can be.
96 const staged = Number(u.stagedBuiltAt) || 0
97 const running = Number(u.builtAt) || 0
98 if (staged && running && staged <= running) return null
99 return u.refused.reason || 'refused by the shell'
91100}
92101
93102export function withUpdateBanner (html, update) {
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "favela-surface",
3 "version": "0.0.62",
3 "version": "0.0.63",
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 +24 -0
……@@ -762,3 +762,27 @@ test('a blocked update gets a line that does NOT offer a restart', (t) => {
762762 t.ok(html.includes('cannot be applied'), 'says so')
763763 t.absent(html.includes('restart to apply'), 'and never offers the button that does nothing')
764764})
765
766test('a refusal for a bundle older than the RUNNING one is suppressed page-side too', (t) => {
767 // ⛔ WHY BOTH SIDES CHECK. boot-plan.js is React Native — it ships in the APK. The page ships in the
768 // BUNDLE and arrives over OTA. So a shell too old to suppress a stale refusal keeps sending one, and
769 // on the handset that left "this update cannot be applied — staged bundle previously failed at boot"
770 // pinned to every page while running a NEWER bundle. Waiting for an APK to fix a banner is exactly
771 // the trap this whole effort exists to escape, so the page applies the rule as well.
772 const u = {
773 staged: 'a'.repeat(64), running: 'b'.repeat(64),
774 stagedBuiltAt: 1000, builtAt: 9000,
775 refused: { hash: 'a'.repeat(64), reason: 'staged bundle previously failed to boot' }
776 }
777 t.is(updateBlockedReason(u), null, 'older than what is running — nothing to say')
778 t.absent(updateWaiting(u))
779})
780
781test('a refusal for a NEWER bundle is still shown', (t) => {
782 const u = {
783 staged: 'a'.repeat(64), running: 'b'.repeat(64),
784 stagedBuiltAt: 9000, builtAt: 1000,
785 refused: { hash: 'a'.repeat(64), reason: 'staged bundle previously failed to boot' }
786 }
787 t.ok(updateBlockedReason(u).includes('failed to boot'), 'a new release that cannot run is worth saying')
788})