# Shell: PWA behavior

Source: `apps/oshun/web/public/manifest.json`, `apps/oshun/web/public/sw.js`,
`apps/oshun/web/src/components/PwaBootstrap.tsx`,
`apps/oshun/web/src/components/PwaInstallPrompt.tsx`,
`apps/oshun/web/src/components/PwaInstallTriggerButton.tsx`,
`apps/oshun/web/src/components/PwaUpdatePrompt.tsx`,
`apps/oshun/web/src/components/PwaOfflineFallback.tsx`,
`apps/oshun/web/src/components/OfflineBanner.tsx`,
`apps/oshun/web/src/lib/pwa-runtime.ts`,
`apps/oshun/web/src/lib/pwa-service-worker.ts`,
`apps/oshun/web/src/lib/offline-shell-routes.ts`, and
`apps/oshun/web/src/lib/pwa-recent-content.ts`.

The PWA story - install, offline, update, sync, relaunch, and push routing - is
the spine of "PWA" in the product name. Walk this whenever the manifest,
`sw.js`, or `PwaBootstrap` changes.

## Manifest

`/manifest.json`

- [x] `name` and `short_name` are `OSHUN`.
- [x] `description` names the one-shell/six-domain product promise.
- [x] `id` is `/`, `start_url` is `/?surface=pwa`, and `scope` is `/`.
- [x] `display` is `standalone`; `display_override` is
      `["window-controls-overlay","standalone"]`.
- [x] `theme_color` and `background_color` are `#f1ebdd`.
- [x] Categories include health, education, lifestyle, and productivity.
- [x] Icons include 192 and 512 pixel `any` and `maskable` PNGs.
- [x] Screenshots include two narrow 390x844 and two wide 1440x960 assets.
- [x] Shortcuts cover Tara, Veritas, Nyx, Arete, Nisaba, and Metis.
- [x] Install assets are resolved by `pwa-smoke.spec.ts`.

## Install prompt

`PwaInstallPrompt.tsx` + `PwaInstallTriggerButton.tsx`

- [x] Eligibility is event-driven: the custom prompt appears when the browser
      fires `beforeinstallprompt`.
- [x] The component calls `preventDefault()`, stores the deferred browser event,
      and renders `[data-pwa-prompt][data-pwa-state="available"]`.
- [x] The manual `/welcome/download` trigger dispatches
      `oshun-trigger-pwa-install`, which calls the saved prompt event.
- [x] Accepted prompt outcome records install state and renders the installed
      guidance state.
- [x] Explicit dismiss or dismissed browser outcome writes
      `oshun_pwa_install_dismissed`; reloads suppress the prompt during the
      14-day cooldown.
- [x] Standalone runtime detection records install state from
      `(display-mode: standalone)` and suppresses the available prompt.
- [x] iPhone/iPad Safari is documented through the manual Share -> Add to Home
      Screen copy on `/welcome/download`; Safari does not expose
      `beforeinstallprompt`.
- [x] There is no V1 app-side "N qualifying interactions" threshold; the browser
      decides when `beforeinstallprompt` fires.

## Service worker lifecycle

`sw.js` + `PwaBootstrap.tsx` + `lib/pwa-service-worker.ts`

- [x] `registerServiceWorker()` registers `/sw.js` at scope `/` on mount when
      the browser exposes `navigator.serviceWorker`.
- [x] Registration failures are caught, logged, and reset the one-shot guard so
      a later mount can retry.
- [x] With service workers blocked by the browser, the authenticated shell still
      renders instead of white-screening.
- [x] A waiting worker surfaces `PwaUpdatePrompt` only after cookie consent is
      resolved.
- [x] The update prompt distinguishes standard routes from reading routes and
      uses route-specific copy.
- [x] "Refresh now" posts `OSHUN_SKIP_WAITING`; controllerchange emits
      `pwa_update_applied` and reloads.
- [x] "Later" / "Keep reading" dismisses only the current prompt token; a cold
      launch with the same waiting worker re-prompts.
- [x] `OSHUN_SW_UPDATE` messages move the prompt to the ready state.
- [x] A lost skip-waiting response surfaces the manual reload escape before the
      silent fallback reload.
- [x] Multi-tab update application reloads each open route once and preserves
      route identity.
- [x] `activate` deletes stale `oshun-*` caches while preserving current version
      and foreign caches.

## Cache strategies

`sw.js` defines static, runtime, BFF, and media caches with
`CACHE_VERSION = 'v7'`.

- [x] Static assets include the app shell, public install routes, manifest,
      icons, and screenshots.
- [x] Runtime navigation fallback uses cached shell HTML for supported offline
      shell paths.
- [x] BFF responses use stale-while-revalidate with the general BFF max-age
      policy.
- [x] Nisaba offline reads use `NISABA_OFFLINE_BFF_MAX_AGE_MS` (24 hours) for
      offline-pinned scholarly content.
- [x] Media uses `MEDIA_CACHE` with `MAX_MEDIA_ENTRIES = 200`; runtime cache
      eviction uses `MAX_RUNTIME_ENTRIES = 100`.
- [x] Auth/session paths are denied from cache and fail closed while offline.
- [x] Cache misses on offline deep navigations render the static service-worker
      fallback and hydrate into `PwaOfflineFallback` when the client is
      available.

## Offline behavior

`OfflineBanner.tsx`, `PwaOfflineFallback.tsx`, and `offline-shell-routes.ts`

- [x] `navigator.onLine === false` or a failed genuine `/healthz` probe surfaces
      `<OfflineBanner>`.
- [x] Captive-portal `200 text/html` health responses are rejected and keep the
      banner visible.
- [x] A genuine BFF health payload clears the banner on retry/online.
- [x] Warmed Home, Explore, Activity, and Library shells stay navigable offline.
- [x] Warmable prefixes include `/library`, `/domains`, `/d`, `/app`, and
      `/workspace`.
- [x] Cached BFF and Nisaba offline records render when within their freshness
      windows.
- [x] Uncached reads render `PwaOfflineFallback` with retry, cached shell
      shortcuts, and recent cached records.
- [x] Recent content is persisted by `persistPwaRecentContent` for authenticated
      route visits.
- [x] Offline local edits and queueable writes show explicit local/sync-pending
      states; they do not silently fail.

## Background sync

- [x] `queueBackgroundAction()` posts `OSHUN_QUEUE_ACTION` to the controlling
      service worker.
- [x] `sw.js` persists queued actions in IndexedDB `oshun-sync-queue` /
      `pending-actions`.
- [x] Chromium Background Sync dispatch replays queued actions in order.
- [x] Successful replay posts `OSHUN_SYNC_COMPLETE`, updates queue count, writes
      the live BFF, and drains IndexedDB.
- [x] Queue status posts `OSHUN_SYNC_QUEUE_STATUS`, which drives
      `pwa_sync_queued` telemetry.
- [x] A pending queue count renders the update prompt's queued-write note so the
      user knows refresh will preserve held writes.

## Relaunch target

`lib/pwa-runtime.ts`

- `OSHUN_PWA_RELAUNCH_TARGET_KEY`
- `OSHUN_PWA_RELAUNCH_TARGET_COOKIE`
- `persistPwaRelaunchTarget`
- `readPwaRelaunchTarget`
- `resolveStandaloneLaunchTarget`

- [x] Authenticated route changes persist a sanitized relaunch target in
      localStorage and the `oshun-pwa-relaunch` cookie.
- [x] Signed-in standalone launches from `/?surface=pwa` restore the saved
      target.
- [x] Anonymous standalone launches route to `/welcome` with the saved redirect
      preserved.
- [x] Expired or unauthenticated protected relaunches remain under the auth
      redirect contract.
- [x] Sign-out/session-clear removes the relaunch target and asks the service
      worker to clear member-owned caches and queued writes.

## Offline shell warmup

`lib/offline-shell-routes.ts` + `lib/pwa-recent-content.ts`

- [x] `buildOfflineShellWarmRoutes()` always includes Home, Explore, Activity,
      and Library.
- [x] Current and recent warmable routes are normalized with
      `sanitizeRedirectPath`.
- [x] `?surface=pwa` is stripped before offline route normalization.
- [x] `warmOfflineShellRoutes()` waits for service-worker readiness/controller
      when possible, then fetches normalized HTML routes with `x-oshun-surface`.
- [x] Warmup failures are best-effort and collapse to zero warmed routes instead
      of breaking the shell.

## Standalone PWA detection

- [x] `(display-mode: standalone)` is detected.
- [x] iOS `navigator.standalone === true` is detected.
- [x] `getBrowserSurface()` returns `pwa` only for standalone runtime, otherwise
      `web`.
- [x] `document.documentElement.dataset.oshunSurface` tracks the current browser
      surface.
- [x] Standalone launch restoration is session-safe and route-safe.

## Push notifications

`sw.js`, profile notification settings, and device-token BFF routes.

- [x] Profile notification settings register this browser through the Push API
      boundary and POST a stringified web-push subscription as a `web-push`
      token.
- [x] Unregister DELETE carries the same subscription token.
- [x] Registration failure leaves the row in error state with retry available.
- [x] Missing VAPID config reports unavailable and hides the register action.
- [x] `sw.js` `push` calls `showNotification()` with title, body, icon, badge,
      tag, vibration, and action data.
- [x] `notificationclick` closes the notification, focuses an existing OSHUN
      window and posts `OSHUN_NOTIFICATION_TAP`, or cold-opens the target URL.

## States

- [x] First visit in browser: public welcome shell, manifest, mobile metadata,
      and service-worker asset resolve.
- [x] Browser install prompt available: custom install banner renders after
      `beforeinstallprompt`.
- [x] Installed home-screen launch: standalone detection and relaunch target are
      honored.
- [x] Update available in foreground: `PwaUpdatePrompt` is visible after
      consent.
- [x] Update deferred: route remains stable and prompt can re-surface next
      launch.
- [x] Update applied: prompt clears, route/scroll are preserved where
      applicable, and stale caches are purged.
- [x] Offline cached page: cached shell renders and banner is visible.
- [x] Offline uncached page: service-worker fallback and hydrated
      `PwaOfflineFallback` render.
- [x] Offline write attempt: action is queued or kept locally with user-visible
      pending copy.
- [x] Push tap: existing window focus or cold-open route is exercised at the
      service-worker runtime layer.

## Telemetry to verify

- [x] `pwa_update_applied` for refresh-now and silent cold-start paths.
- [x] `pwa_sync_queued` for service-worker queue status.
- [x] Install state is recorded in `oshun.pwa.install-state`; install analytics
      names are not currently the E2E assertion boundary.
- [x] Offline banner/fallback assertions use visible DOM and service-worker
      marker contracts.
- [x] Relaunch target assertions use URL restoration and auth redirect behavior.

## Coverage

- [`apps/oshun/web/e2e/pwa-smoke.spec.ts`](../../apps/oshun/web/e2e/pwa-smoke.spec.ts)
  - manifest/assets, SW asset, mobile metadata, download/Safari install copy,
    install prompt trigger, standalone relaunch, and reading-route update flow.
- [`apps/oshun/web/e2e/pwa-install-update-offline.spec.ts`](../../apps/oshun/web/e2e/pwa-install-update-offline.spec.ts)
  - install dismissal cooldown, standard update deferral, update consent gate,
    offline recent-content fallback, and uncached deep-route fallback controls.
- [`apps/oshun/web/e2e/pwa-lifecycle-deepening.spec.ts`](../../apps/oshun/web/e2e/pwa-lifecycle-deepening.spec.ts)
  - apply handshake, multi-tab update, real cache purge, silent cold start,
    in-flight writes, queued-write update note, offline banner, and
    reduced-motion spinner.
- [`apps/oshun/web/e2e/pwa-failure-modes.spec.ts`](../../apps/oshun/web/e2e/pwa-failure-modes.spec.ts)
  - SW blocked fallback, update/offline co-mount, captive portal, stuck apply,
    and reload-loop guard.
- [`apps/oshun/web/e2e/offline-shell-routes.spec.ts`](../../apps/oshun/web/e2e/offline-shell-routes.spec.ts),
  [`offline-write-deepening.spec.ts`](../../apps/oshun/web/e2e/offline-write-deepening.spec.ts),
  and
  [`offline-background-sync.spec.ts`](../../apps/oshun/web/e2e/offline-background-sync.spec.ts)
  - offline shell availability, real service-worker fallback, health probes,
    local edits, sync hints, and Background Sync replay.
- [`apps/oshun/web/e2e/push-registration.spec.ts`](../../apps/oshun/web/e2e/push-registration.spec.ts)
  and
  [`apps/oshun/web/e2e/nyx-service-worker-notification.spec.ts`](../../apps/oshun/web/e2e/nyx-service-worker-notification.spec.ts)
  - push device registration and service-worker push/click routing.

## Known boundaries

- [x] OS-level install/uninstall and iOS home-screen confirmation are outside
      Playwright; tests assert the browser/PWA boundary and manual copy.
- [x] Cross-browser storage-quota exhaustion is not deterministic enough for an
      E2E gate; cache eviction policies are covered through targeted runtime and
      unit/policy checks.
- [x] Lighthouse PWA score thresholds are not currently a release gate; the
      manifest, install, SW, offline, relaunch, update, sync, and push contracts
      above are the automated signoff.

## Cross-references

- [01-app-shell.md](./01-app-shell.md) - where `PwaBootstrap` mounts.
- [02-routing-layouts.md](./02-routing-layouts.md) - SW navigation matching and
  auth redirect behavior.
- [05-notifications.md](./05-notifications.md) - notification center and push
  preferences.
- Journey: [journeys/install-as-pwa.md](../journeys/install-as-pwa.md)
- Architecture:
  [`V1/ARCHITECTURE.md`](../../V1/ARCHITECTURE.md#customer-web--appsoshunweb)
- Feature spec: [`V1/features.md`](../../V1/features.md)
