Files
Zeyi (Rice) Fan ef8aba3af0 refactor(web): retire the PWA service worker and update prompt (#4617)
## Related issue

N/A — `Refactor / chore`.

## Summary

The PWA landed as one squashed PR (`b6976c1b2`, #116) whose headline was
installability. #116 was authored around mid-June, when "installable Omnigent on
mobile" was an open problem; it merged 2026-06-30, by which point the iOS shell
had shipped (#965, 2026-06-22) and the Android shell landed the next day
(#1604/#1704). The native shells took over the installed-app story while the PR
was in flight, and the PWA was never re-evaluated.

What was left was load-bearing for one thing only — the "new version → Reload"
prompt — and inert for everything else:

- `web/src` had zero uses of `navigator.serviceWorker`, `caches.*`,
  `BroadcastChannel`, `pushManager`, `backgroundSync` or `setAppBadge`.
  Notifications deliberately bypass the worker
  (`web/src/lib/browserNotifications.ts`) and badges go through `nativeBridge.ts`.
- `version.json` was emitted, precached, and read by nobody.
- Installability was unadvertised (no `beforeinstallprompt`) and unmeasured (no
  `display-mode` checks), so the worker's one cache entry existed only to satisfy
  Chrome's "non-empty fetch handler" install heuristic.

Web Push (#1751, P2) is the only thing that would need a worker again, and a push
worker needs different handlers, VAPID keys and server infra — the retired file
is not useful groundwork.

ELI5: the service worker was a doorbell that only rang to say "the app has been
updated". Nothing else used it, and three native apps now do the "install
Omnigent" job it was built for, so the doorbell and its wiring come out.

A worker already registered in a browser stays registered after we stop shipping
one, so `sw.js` becomes a tombstone that removes itself:

```
deploy 0.10.0
      │
      ▼
browser fetches /sw.js (no-cache)  →  installs tombstone  →  parks in `waiting`
      │
      ├─ old tab still runs old JS, shows its own update banner one last time
      │     user clicks Reload → SKIP_WAITING → activate
      │                                          ├─ purge omnigent-pwa-* caches
      │                                          └─ registration.unregister()
      │                                                → tab reloads, PWA-free
      └─ or all tabs close → activate on next visit → same cleanup, no prompt
```

Deliberately no `skipWaiting()` on install, so nobody's agent session is
interrupted by an unprompted reload. The purge matches the retired worker's exact
cache-name shape, `/^omnigent-pwa-[0-9a-f]{8}$/` — it only ever created
`omnigent-pwa-${(hash >>> 0).toString(16).padStart(8, "0")}` — rather than
clearing Cache Storage wholesale or trusting a bare prefix, so a tombstone
lingering in some browser cannot delete a future feature's caches even if that
feature reuses the prefix.

`registration.unregister()` leaves no persistent browser state, so registering a
worker at `/sw.js` again later is clean. Two things are kept for that reason:
the `no-cache` header for `sw.js` in `app.py` (so a cached tombstone can never
shadow a future worker) and the embed-island guard that forbids shipping any
service worker into a host origin.

Tombstone deletion is targeted at **0.11.0** (marked `@deprecated` in
`web/sw-src/sw.js` and in the vite plugin).

Not in this PR: `emptyOutDir: true` deletes old hashed chunks on deploy, the app
lazy-loads most routes, and there is no `ErrorBoundary` anywhere in `web/src`, so
a tab left open across a deploy can white-screen on navigation to a lazy route.
The prompt was a proactive nudge, never a guard — it never prevented the 404. The
gap pre-dates this change (it already applied to anyone who dismissed the banner)
and the fix (ErrorBoundary + reload on failed dynamic import) is independent of
the PWA, so it is filed separately.

## Test Plan

- `pnpm --filter web run type-check`, `run lint`, `run build` — clean; build
  output contains `sw.js` only, with no `manifest.webmanifest`, no
  `version.json` and no `pwa-*.png` (`apple-touch-icon.png` / `favicon.svg`
  retained).
- `uv run pytest tests/server/integration/test_app.py::test_web_ui_serves_service_worker_uncached`
  — passes.
- `pnpm exec vitest run src/components/UpdateBanner.test.tsx` — 5 passed;
  confirms the similarly-named Electron desktop update banner is untouched.
- Exercised the rewritten build guard against the real build output, plus eight
  negative cases, to prove it is not vacuous: a worker that calls `respondWith`,
  an unscoped cache purge, a *bare-prefix* cache filter, a worker that never
  unregisters, a stale `__BUILD_VERSION__` token, a re-emitted manifest, a
  re-emitted `version.json`, and a missing `sw.js` are each rejected.
- Round-tripped the anchored cache pattern against the fingerprints the retired
  worker could produce (uint32 min, max and typical values all render as 8
  lowercase hex chars) to confirm the tightened filter still purges every legacy
  cache name, while leaving unrelated names in the same namespace alone.
- `uv run pre-commit run` — all hooks pass.

## Demo

N/A — the only visible effect is the absence of the update banner.

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change

## Test coverage

- [ ] Unit tests added / updated
- [x] Integration tests added / updated
- [x] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable

## Coverage notes

`tests/e2e_ui/test_pwa_e2e.py` is deleted (it asserted live PWA behaviour) and
`conftest._assert_pwa_build` is replaced by
`_assert_service_worker_tombstone`, which now enforces the *dangerous*
direction: the worker must unregister itself, must intercept nothing, must not
purge caches it does not own, and the manifest/version sentinel must be gone.
`tests/e2e_ui/test_pwa_build.py` is renamed to `test_embed_service_worker.py` and
kept — "the embed island ships no service worker" outlives the PWA.

Manual verification covered the parts a test cannot: the emitted build output was
inspected by hand, and the guard was run against both the real output and seven
mutated inputs (listed in the Test Plan) to confirm each regression is caught.
The deleted unit tests covered only the removed components.

## Changelog

Removed the "A new version of Omnigent is available" prompt and browser PWA
install support; the desktop and mobile apps remain the installable clients.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
2026-08-11 15:55:02 -07:00
..