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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`/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

opened this

/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 at GET /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 /offline at 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 /offline and / 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: offline is now reserved, so no owner can take the /offline path. If someone already holds that name, they would collide with it.
  • Wiring: I added /offline to the route marks and to the no-prefetch list in src/web/asset.ts, and wrote docs/features/offline.md.
  • Tests: test/offline-page.test.ts fetches /offline and checks the size, the time element, the security headers and the absence of script. It also checks that sw.js names /offline and that the asset branch never calls the offline fallback. In test/offline.test.ts I changed one assertion from /__offline to /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.

Closes #12324

7 earlier comments · 1 event
commented

/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 at GET /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 /offline at 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 /offline and / 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: offline is now reserved, so no owner can take the /offline path. If someone already holds that name, they would collide with it.
  • Wiring: I added /offline to the route marks and to the no-prefetch list in src/web/asset.ts, and wrote docs/features/offline.md.
  • Tests: test/offline-page.test.ts fetches /offline and checks the size, the time element, the security headers and the absence of script. It also checks that sw.js names /offline and that the asset branch never calls the offline fallback. In test/offline.test.ts I changed one assertion from /__offline to /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.

commented

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.

commented

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:

  • /offline now sends default-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.js in test/forge-looks.test.ts, test/design-audit.test.ts and test/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.

commented

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:

  • /offline page: 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.ts is 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.

Changes requested — before the latest push

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).

commented

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: /offline now inlines the forge's own --bg, --fg and --muted-fg values, taken from THEMES.light and THEMES.dark in src/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. The sw.ts comment on the inline fallback now says it is only for when the forge was unreachable at install.
  • Docs: docs/features/offline.md now 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.ts now runs the real swSource against stubbed self, caches and fetch, 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.

commented

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.

commented

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 on app_env.value_plain and secret_start.value_updated_at. Both come from migrations on main (for example 20261114070100-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.

commented

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

  • /offline page: 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.ts fetches /offline at install and keeps the server's own copy. A failed navigation with nothing else cached gets that copy instead of the inline fallback. Tests in test/offline-page.test.ts cover install, the fallback and the page itself.
  • No-polling guard: test/no-polling.test.ts forbids refreshing pages. I allowlisted src/web/pages/offline.ts and skipped /offline in the router walk, because the forge is down when that page shows and there is no /events stream 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 core still lists app_env.value_plain and secret_start.value_updated_at. They come from migrations on main, 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.

Approved

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs