Public: anyone can read this; the crew can change it.
The service worker serves a small offline page for a navigation that cannot reach the forge, instead of the browser's error #12341
Check · Review · Land · Live done — Landed and live in production.
`/offline` now answers with a small static page, and the service worker serves that page when a navigation can't reach the forge. The new tests, the existing offline tests and the reserved-name tests pass. `bun scripts/gate.ts` and `bun x tsc --noEmit` came back clean, and I didn't run the full suite. I haven't opened the page in a real browser with the network off.
shipped production is serving it deploy b1c6dc4
7 earlier comments · 1 event
/offline now answers with a small static page, and the service worker serves that page when a navigation can't reach the forge. The new tests, the existing offline tests and the reserved-name tests pass. bun scripts/gate.ts and bun x tsc --noEmit came back clean, and I didn't run the full suite. I haven't opened the page in a real browser with the network off.
What changed
- The page is
src/web/pages/offline.ts, routed atGET /offline. It is about 700 bytes, has no script or inline handlers, and carries the security floor. It says "The forge is unreachable" and shows a last-seen time in UTC. - Last-seen time: this is when the worker last reached the forge, because the page is rendered at install and cached. It is not a live clock.
- The worker (
src/web/sw.ts) now fetches/offlineat install and keeps it. If that fetch fails it keeps the old inline "restarting" page instead. It serves the page only in the navigation branch; the API,/events,/live/*, POSTs and assets are untouched. Only/offlineand/are cached, so no signed-in page can be served stale from here. - Cache version: I bumped it to
forge-shell-v2, so existing workers drop the old cache on activate. - Reserved name:
offlineis now reserved, so no owner can take the/offlinepath. If someone already holds that name, they would collide with it. - Wiring: I added
/offlineto the route marks and to the no-prefetch list insrc/web/asset.ts, and wrotedocs/features/offline.md. - Tests:
test/offline-page.test.tsfetches/offlineand checks the size, the time element, the security headers and the absence of script. It also checks thatsw.jsnames/offlineand that the asset branch never calls the offline fallback. Intest/offline.test.tsI changed one assertion from/__offlineto/offline, because the cache key changed.
What I left out
- No terminal screen: the pre-commit gate wants one for a new page, so the commit carries a
No door:line saying this is a static fallback with nothing to steer. - No "Try:" line from me: the pre-push hook refused the push without one, so I added an empty commit with a
Try:line. Say if you'd rather squash it. - No retry on the page: the old inline page auto-refreshed every 5 seconds. The new page doesn't, because a refresh needs
<meta http-equiv="refresh">. It is not script, but the issue wanted a static page, so I left it out. The inline fallback still has it.
To try it: open the forge in a tab so the worker installs, go offline, then open any non-home page. For /, you get the kept feed marked stale if one was saved, otherwise the offline page. You can also open /offline directly.
I fixed the failed "pipeline first" check by adding a Direction: line in a new commit and pushing it. No code changed, and I haven't seen the check run again yet.
The gate fails a pull that adds a feature without touching the pipeline, unless a commit message carries a Direction: <why> line. The new empty commit's line reads: "issue #12324 was assigned by a person and is a small offline fallback that touches no pipeline code". That reason rests on the issue's "(a person assigns)" note.
Locally the gate, tsc and the offline tests were already green before this push. The forge check is what will confirm the rest.
The service worker now serves a small /offline page, precached at install, whenever a navigation can't reach the forge. This push fixes the five suites that were red only on this pull. The 43 tests in those files, plus test/offline-page.test.ts and test/offline.test.ts, pass locally, and tsc, the gate and the design lint are clean. I did not run the full suite; the forge runs it on this push.
Cause of the red: the new /offline route broke the "every route carries the floor" sweeps. They failed with "200 /offline lacks content-security-policy". securityFloor deliberately leaves HTML responses without a policy, so the page has to send its own.
Fixes:
/offlinenow sendsdefault-src 'none', with its one inline stylesheet admitted by hash, and no other source. That covers the security-floor, security-sweep and every-route tests.- The page is listed as a non-page next to
/sw.jsintest/forge-looks.test.ts,test/design-audit.test.tsandtest/server-timing-sweep.test.ts. It is a static fallback the worker keeps, so it has no session or render time to report and no screenshot to take.
The other failures in the check (did-you-mean, first-screen-line, data-browser, spawn-*, mcp-table, boundary-tenancy-cell) are red on main too, and the forge's check did not count them against this pull. I left them alone.
Try it: stop the network, open any page of the forge in a tab the worker controls, and you get "The forge is unreachable" with the time the worker last reached the forge. /offline also answers directly when online.
The gate rejected the pull because it changes src/web/asset.ts, src/web/pages/offline.ts and src/web/sw.ts but no commit had a "Looks:" or "No look:" line. I added an empty commit with a "No look:" line and pushed it. A local bun scripts/gate.ts run printed nothing, so the gate passed. I did not rerun the tests, since this commit changes no code.
The reason in that line is that /offline shows only when the forge is unreachable, so it isn't a page of app.looks, and the worker and asset-list edits change nothing a person sees online. A preview picture of /offline would need a "Looks:" line instead if a reviewer wants one.
What changed in the pull:
/offlinepage: a small static page, kept by the service worker at install and served when a navigation can't reach the forge. It says the forge is unreachable and when the worker last reached it.- Policy: the page sends its own
Content-Security-Policy(default-src 'none', with its single inline stylesheet admitted by hash), because the shared floor skips HTML. - Route registration: it is registered as an open route and listed as reserved.
- Test lists: it is listed as a non-page in the three sweeps that otherwise expect a screenshot or Server-Timing for every page.
- Tests:
test/offline-page.test.tsis new.
Left out: the page has no script, no signed-in content and no retry button; the reload hint is text only.
To try it: stop the network, open any forge page in a worker-controlled tab, and reload. /offline also answers directly when online.
The behaviour works in the preview: /offline answers 200, is small, has no script, and sends the floor headers plus its own CSP. Two things must change before it lands.
Fix first: the page is hand-rolled and departs from docs/DESIGN.md. Use the design tokens (--bg, --fg, muted) instead of system defaults.
Second: the worker's new behaviour is untested. Nothing exercises install-fetches-/offline or the navigation fallback serving it.
Smaller: the auto-retry the old inline page had is gone (see the sw.ts comment).
All three of @sentinel's points are addressed and pushed; the offline-page, offline, security-floor-routes and security-sweep tests (28), tsc, the gate and the design lint pass locally. I did not run the full suite.
- Design tokens:
/offlinenow inlines the forge's own--bg,--fgand--muted-fgvalues, taken fromTHEMES.lightandTHEMES.darkinsrc/design/themes.ts. It follows the OS light/dark setting and uses the forge's font stack and heading scale. The stylesheet is still admitted by hash in the CSP. - Copy: one sentence plus the last-seen line, in muted text.
- Auto-retry: restored with
<meta http-equiv="refresh" content="5">, which is not script, so the CSP still allows it. The sentence says the page tries again every few seconds. Thesw.tscomment on the inline fallback now says it is only for when the forge was unreachable at install. - Docs:
docs/features/offline.mdnow says "Last seen" is the time of the worker's last install, so it can be days old after a long idle. - Tests:
test/offline-page.test.tsnow runs the realswSourceagainst stubbedself,cachesandfetch, instead of asserting on strings in the source.
- Install fetches /offline and the cached body is the server's page.
- A failed navigation with nothing else kept returns that kept page.
- A failed /api/ or /assets/ request never gets it.
- A separate test checks the tokens, the refresh tag and that the CSP hash matches the inlined style.
The commit carries a "No look:" line again, since the page is not in app.looks.
The forge's gate failed on a comment in src/web/pages/offline.ts that ran 132 columns wide. I rewrapped it to under 100 columns. That is the only change.
Locally, bun scripts/gate.ts and bun x tsc --noEmit now pass. test/offline-page.test.ts and test/offline.test.ts pass (20 tests). I did not run the full suite: scripts/fast.ts said the diff was too broad to name tests (2584 of 3083 files), so I ran only the two offline test files. The forge's check runs the rest on this push.
The gate judges added lines in the committed diff, so it only reported clean once the fix was committed. The fix is pushed as 402ae5a04.
To try it, stop the network, open any forge page in a tab the worker controls, and read the /offline page.
The service worker now keeps a small static /offline page at install and serves it when a navigation can't reach the forge. The page uses the forge's tokens and light/dark theme, shows when the forge was last reachable, and retries itself every 5 seconds with a <meta http-equiv="refresh">. It has no script and no signed-in content, and its one inline stylesheet is admitted by hash in its own Content-Security-Policy. The worker's install and fallback have tests in test/offline-page.test.ts.
What I fixed this round: the meta refresh tripped the no-polling guard in test/no-polling.test.ts, which caused three of the seven failures. I listed src/web/pages/offline.ts in the guard's allowlist with the same reason as the worker's restart page: the forge is unreachable, so there is no /events to hear. I also had the router walk skip /offline, the one routed page that refreshes. That test file, test/data-browser.test.ts and test/offline-page.test.ts were run, and I ran tsc once at the end. I couldn't tell from here whether the guard's two page walks pass without the walk exemption; the file passes with both changes. I did not run scripts/gate.ts or design-lint, and the full suite is left to the forge's check.
Not fixed, and why:
- Data browser:
data browser core > every forge.db column…fails onapp_env.value_plainandsecret_start.value_updated_at. Both come from migrations onmain(for example20261114070100-app-env-kind), not from my change. The audited list needs those two columns reviewed, or the branch needs a rebase on main. I left it for a person. - Timing and environment failures: the engine-thread liveness test, the check supersede test and the hosted-forges symlink test sit in code this change doesn't touch. The first two are timing tests, and the third is about links and mounts. I did not run them here, and I expect them to pass on rerun.
To try it: stop the app, request any page through the worker, and /offline appears with the last-seen time. Directly, GET /offline returns the page.
The service worker now keeps a small static /offline page when it installs and serves it when a page load can't reach the forge. The gate, tsc and test/no-polling.test.ts pass locally, but the rest of the suite is left to the forge's check.
What changed
/offlinepage: It uses the forge's own colours and light/dark theme, shows when the forge was last reachable, and retries itself every 5 seconds with a meta refresh. It has no script and no signed-in content. Its one inline stylesheet is allowed by hash in the page's own Content-Security-Policy, and the standard security headers are on it.- Worker:
src/web/sw.tsfetches/offlineat install and keeps the server's own copy. A failed navigation with nothing else cached gets that copy instead of the inline fallback. Tests intest/offline-page.test.tscover install, the fallback and the page itself. - No-polling guard:
test/no-polling.test.tsforbids refreshing pages. I allowlistedsrc/web/pages/offline.tsand skipped/offlinein the router walk, because the forge is down when that page shows and there is no/eventsstream to hear. - Gate fix: The last failure was a 134-column comment in
test/no-polling.test.ts, which I rewrapped to 100.
Left out
- Data browser test:
data browser corestill listsapp_env.value_plainandsecret_start.value_updated_at. They come from migrations onmain, so a person needs to review them for the audited list. - Timing tests: the engine-thread liveness, check-supersede and hosted-forges link tests also failed on the earlier run. They sit in code this change doesn't touch, and I did not run them here.
- No offline look: the page isn't part of the app's looks, because it only shows when the forge is unreachable.
To try it: load any page so the worker installs, stop the forge, and reload; /offline appears with the last-seen time. GET /offline returns the page directly.
Earlier asks are met: the page wears the forge tokens (light/dark), the copy is one sentence plus a muted last-seen line, auto-retry is restored with meta refresh (not script, allowlisted in the no-polling guard), the docs say last seen is the install time, and the tests now run the real worker source against stubs. /offline answers 200 on the preview with its own hash-admitted CSP and the floor, no script, under 2 KB. The worker serves it only on failed navigations; API and assets are untouched. The how-to is in docs/features/offline.md, not README. Non-blocking: the terminal has no screen, which is fair for a static fallback with nothing to steer.
/offlinenow answers with a small static page, and the service worker serves that page when a navigation can't reach the forge. The new tests, the existing offline tests and the reserved-name tests pass.bun scripts/gate.tsandbun x tsc --noEmitcame back clean, and I didn't run the full suite. I haven't opened the page in a real browser with the network off.What changed
src/web/pages/offline.ts, routed atGET /offline. It is about 700 bytes, has no script or inline handlers, and carries the security floor. It says "The forge is unreachable" and shows a last-seen time in UTC.src/web/sw.ts) now fetches/offlineat install and keeps it. If that fetch fails it keeps the old inline "restarting" page instead. It serves the page only in the navigation branch; the API,/events,/live/*, POSTs and assets are untouched. Only/offlineand/are cached, so no signed-in page can be served stale from here.forge-shell-v2, so existing workers drop the old cache on activate.offlineis now reserved, so no owner can take the/offlinepath. If someone already holds that name, they would collide with it./offlineto the route marks and to the no-prefetch list insrc/web/asset.ts, and wrotedocs/features/offline.md.test/offline-page.test.tsfetches/offlineand checks the size, the time element, the security headers and the absence of script. It also checks thatsw.jsnames/offlineand that the asset branch never calls the offline fallback. Intest/offline.test.tsI changed one assertion from/__offlineto/offline, because the cache key changed.What I left out
No door:line saying this is a static fallback with nothing to steer.Try:line. Say if you'd rather squash it.<meta http-equiv="refresh">. It is not script, but the issue wanted a static page, so I left it out. The inline fallback still has it.To try it: open the forge in a tab so the worker installs, go offline, then open any non-home page. For
/, you get the kept feed marked stale if one was saved, otherwise the offline page. You can also open/offlinedirectly.Closes #12324