feat(offline): app-shell precache service worker (vite-plugin-pwa)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 1m11s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m43s

Migrate the former install-only public/sw.js to a custom (injectManifest)
service worker built from ui/src/sw.ts by vite-plugin-pwa. It precaches the
app shell + hashed assets (Workbox) so an installed web PWA cold-launches with
no network, and falls in-scope navigations back to the precached shell (the
hash router resolves the route client-side). This is the C1 prerequisite for a
usable offline mode: without it a cold offline launch cannot load the bundle.

- ui/src/sw.ts: skipWaiting + clientsClaim + cleanupOutdatedCaches +
  precacheAndRoute(__WB_MANIFEST) + a NavigationRoute fallback to index.html,
  deny-listing the RPC path and /_gm. The Connect stream and API POSTs are
  never precached nor intercepted.
- vite.config.ts: VitePWA(injectManifest); manifest:false (we ship our own),
  injectRegister:false (registration stays manual + web-only in pwa.svelte.ts),
  disabled in the mock build (Playwright unperturbed); landing + polyfills
  excluded from the precache.
- Removed public/sw.js; the registration path (dist/sw.js at /app/) is unchanged.
- New dev deps: vite-plugin-pwa + workbox-{core,precaching,routing}.

Docs: ARCHITECTURE + gateway/README. Offline cold-launch is contour-verified
(the mock e2e harness disables the SW).
This commit is contained in:
Ilia Denisov
2026-07-06 11:06:59 +02:00
parent 8abd8b2ae6
commit 1a95a5f2cb
8 changed files with 3282 additions and 88 deletions
+9 -6
View File
@@ -1238,12 +1238,15 @@ separate site; the SPA shell is `noindex`, and the landing always boots in Russi
browser-language detection — crawlers render with arbitrary languages). The favicon set,
`og-image.png` and `robots.txt` ship unhashed from `ui/public/` (generated by
`assets/icons/`, same tile design as the VK loader). On the web (`/app/`) the SPA is an installable **PWA**:
`manifest.webmanifest` and an install-only **service worker** (`sw.js`) ship unhashed from
`ui/public/`, and the client registers the worker **web-only** — never inside a Mini App or the
mock build. The worker intercepts only top-level navigations (network-first with a cached-shell
fallback), leaving `/assets/*` and the Connect stream untouched; it exists to satisfy Chromium's
installability requirement (a registered SW, needed for install on Android) and is the single
growth point for the opt-in **offline mode** (in progress): a deliberate, device-scoped Settings
`manifest.webmanifest` ships unhashed from `ui/public/`, while the **service worker** (`sw.js`) is
built from `ui/src/sw.ts` by **vite-plugin-pwa** (injectManifest strategy) — the client registers it
**web-only**, never inside a Mini App or the mock build (the plugin is disabled there entirely). It
**precaches the app shell and the hashed assets** (Workbox) so an installed PWA cold-launches with
no network, and falls every in-scope navigation back to the precached shell (the hash router
resolves the route client-side); the landing page and the conditional polyfill bundle are excluded,
and the Connect stream and runtime API POSTs are never precached nor intercepted, so the live app is
never served stale. This satisfies Chromium's installability requirement (a registered SW, needed
for install on Android) and powers the opt-in **offline mode** (in progress): a deliberate, device-scoped Settings
toggle — distinct from the transient gateway-reachability signal — that tints the header blue with
an *Offline* chip and confines play to on-device `vs_ai` games. To have data ready before the
switch, the **Profile advertises the current dictionary version per variant** (`dict_versions`,