# Journey: Offline warm-cache continuity and bounded write replay

Oshun's PWA supports useful offline continuity after an online warm-up; it does
not provide a cold-install copy of every customer route or a universal offline
write queue. The service worker pre-caches the welcome/download shell, warms a
declared authenticated-route set, caches navigation responses, and permits
offline API reads only for specific Nisaba endpoints. Arete uses the explicit
background-sync queue. Other local-first flows may retain browser state without
ever entering that queue. This journey tests those exact boundaries.

## Personas

- **Returning PWA member** — has already loaded the authenticated shell and
  selected routes online.
- **Offline reader** — revisits an already fetched navigation or allowed Nisaba
  response.
- **Offline writer** — performs an Arete action that explicitly posts to the
  service-worker queue.
- **Recovery tester** — restores connectivity and verifies the concrete server
  side effect, not just the disappearance of an offline banner.

## Pre-conditions

- Install or run the PWA with the service worker controlling the page.
- Sign in online before testing protected routes. The manifest start URL is
  `/?surface=pwa`, while `/` itself is intentionally not install-pre-cached
  because it participates in redirect/session resolution.
- Visit the exact pages and data that must work offline. This is a warm-cache
  journey, not a fresh-install/cold-cache claim.
- Use a supported browser with service worker, Cache Storage, and IndexedDB.
- For replay verification, choose an action whose implementation sends
  `OSHUN_QUEUE_ACTION`; Arete has named coverage for this seam.

## Steps

### 1. Warm the authenticated shell

`PwaBootstrap` asks the service worker to warm the allowlisted shell routes with
serial fetches as soon as the component initializes. The warming is not deferred
through `requestIdleCallback`. Navigation requests use network-first caching for
the declared exact/prefix routes; the install cache itself contains `/welcome`,
`/welcome/download`, and static assets rather than the complete customer app.

### 2. Warm page-specific data

Fetch the items needed during the offline period. API reads are network-only by
default. The service worker makes explicit exceptions for Nisaba daily-passage
and notebook endpoints and retains those responses for the configured cache
window. There is no **Keep offline** control; eligibility and prior fetch are
what matter.

Activity is not in the API cache allowlist. A cached activity HTML navigation
can still load, but that is not proof that its BFF data is available offline.
Page-local stores or honest fallbacks may determine what the user sees.

### 3. Go offline and inspect the shell

When the browser fires its `offline` event, `OfflineBanner` renders an assertive
`role="alert"`. It performs an initial health check and responds to browser
online/offline events; it does not poll continuously. **Retry** uses a
five-second abort timeout and a health sentinel to avoid treating a captive
portal page as recovery.

The banner is not automatically wired to the reduced-motion preference, so do
not claim its animation is disabled merely because the OS preference is set.

### 4. Distinguish cached navigation from cached data

Open only routes warmed in step 1 and confirm the honest offline surface. The
service worker can mark a cached home document for `PwaOfflineFallback`, after
which hydrated local content may take over. Repeat with one warmed Nisaba
allowlist response and one non-allowlisted API to prove the boundary.

### 5. Queue an explicitly supported write

Perform the covered Arete offline action. The page posts `OSHUN_QUEUE_ACTION`;
the service worker stores it in the `oshun-sync-queue` IndexedDB database and
can request background sync. Inspect pending count/state rather than assuming
every localStorage write joins this queue.

Library saves are optimistic and browser local first; failed server pushes are
swallowed and are not posted to the service-worker queue. Tara reflections use
`tara:reflection:<sitting.id>` localStorage and currently have no replay
consumer. Their local confirmation must not say that background sync will
necessarily deliver them.

### 6. Reconnect and verify the side effect

Restore a healthy network, trigger retry or allow the browser sync event, and
verify the Arete record through its BFF/read surface. `pwa_sync_queued` is a
real client analytics event for pending actions. The audited service worker does
not emit a matching `pwa_sync_complete` analytics event; replay completion is
observable through state/read-back and logs.

## Post-conditions

- Warm shell routes and explicitly allowed cached data are identified by exact
  scope.
- At least one explicitly queued Arete action is verified after server
  read-back.
- Browser-only library and Tara state are not described as queued writes.
- Banner recovery is tied to a successful health sentinel, not merely a browser
  `online` event.
- No storage-quota warning is claimed: cache trimming removes older keys but
  exposes no quota hint to the member.

## Failure modes

- **Cold cache** — a route or data response was never fetched while online.
- **Protected shell without session** — a cached document cannot manufacture a
  valid authenticated session.
- **API outside allowlist** — navigation loads but required live data remains
  unavailable.
- **Captive portal** — `online` fires while the health sentinel fails; the
  banner must remain.
- **Unsupported queued-write claim** — a localStorage update is mistaken for an
  IndexedDB background-sync entry.
- **Replay authorization failure** — the queued request no longer has usable
  credentials and requires explicit recovery.
- **Motion/accessibility mismatch** — banner animation is assumed to obey a
  preference it does not currently read.

## E2E coverage

- [`apps/oshun/web/e2e/pwa-install-update-offline.spec.ts`](../../apps/oshun/web/e2e/pwa-install-update-offline.spec.ts)
  covers installation, update, fallback, and offline shell behavior.
- [`apps/oshun/web/e2e/offline-shell-routes.spec.ts`](../../apps/oshun/web/e2e/offline-shell-routes.spec.ts)
  proves the navigation-cache scope.
- [`apps/oshun/web/e2e/offline-background-sync.spec.ts`](../../apps/oshun/web/e2e/offline-background-sync.spec.ts)
  verifies the explicit Arete queue and replay contract.
- [`apps/oshun/web/e2e/offline-write-deepening.spec.ts`](../../apps/oshun/web/e2e/offline-write-deepening.spec.ts)
  covers additional offline-write states and negative boundaries.
- Coverage is **deep for the declared shell and Arete queue**, but partial for
  product-wide offline continuity because most APIs and several local-first
  writes are intentionally outside that contract.

## Per-view files touched

- [`shell/03-pwa-behavior.md`](../shell/03-pwa-behavior.md) — service-worker and
  offline banner behavior.
- [`customer/01-entry/welcome-download.md`](../customer/01-entry/welcome-download.md)
  — install-pre-cached entry surface.
- [`customer/04-arete/arete.md`](../customer/04-arete/arete.md) — explicit
  replay example.
- [`customer/07-nisaba/nisaba.md`](../customer/07-nisaba/nisaba.md) — bounded
  cached API reads.
- [`customer/03-tara/tara.md`](../customer/03-tara/tara.md) — local reflection
  counterexample.

## Cross-references

- [`install-as-pwa.md`](./install-as-pwa.md) — installation and update
  lifecycle.
- [`first-tara-sit.md`](./first-tara-sit.md) — Tara completion/reflection
  boundaries.
- [`library-save-collection-share.md`](./library-save-collection-share.md) —
  optimistic library state.
- Code: `apps/oshun/web/public/sw.js`,
  `apps/oshun/web/src/components/pwa/PwaBootstrap.tsx`, and
  `apps/oshun/web/src/components/pwa/OfflineBanner.tsx`.

## Open questions

- Which additional API reads are safe, useful, and privacy-appropriate to add to
  the explicit offline allowlist?
- Should Library and Tara adopt the service-worker queue or change their copy to
  describe local-only retention?
- How should the UI expose exact cached inventory, age, size, and eviction?
- Should reduced-motion state be passed into `OfflineBanner`?
- What recovery UI should handle expired credentials or permanently rejected
  queued writes?
