From 7bf9135f33bdf27ba7bd3533e481534d0ac0d62e Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Mon, 18 May 2026 07:26:46 +0200 Subject: [PATCH] =?UTF-8?q?docs(fase-14):=20history=20record=20=E2=80=94?= =?UTF-8?q?=20PWA=20auto-update=20+=20offline=20polish=20+=20version=20chi?= =?UTF-8?q?p?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the MVP2 cycle (fase 9-14 all green). Documents the three substantive shipped pieces (14.1 auto-update toast, 14.2 offline polish + sync_conflicts review route, 14.3 build-time version chip), the test deltas, cumulative MVP2 totals, and the two plan deviations (updateViaCache substitute, localStorage-based discard). Co-Authored-By: Claude Opus 4.7 --- docs/history/fase-14-pwa-hardening.md | 98 +++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 docs/history/fase-14-pwa-hardening.md diff --git a/docs/history/fase-14-pwa-hardening.md b/docs/history/fase-14-pwa-hardening.md new file mode 100644 index 0000000..ef53377 --- /dev/null +++ b/docs/history/fase-14-pwa-hardening.md @@ -0,0 +1,98 @@ +# Fase 14 — PWA hardening (auto-update + offline polish + version chip) + +**MVP2 · 6/6. Closes the MVP2 cycle.** + +**Status: ✅ Shipped 2026-05-18.** + +Final polish on the PWA layer that Fase 6 made installable. Plan: `plan/fase-14-pwa-hardening.md`. No DB migration. Builds on Fase 9.3's `sync_conflicts` table + queue wiring. + +## What changed + +### 14.1 — Auto-update toast + +- `apps/web/src/routes/+layout.svelte` migrated from `registerSW({ immediate: true })` to `useRegisterSW({ onRegisteredSW, onRegisterError, onNeedRefresh })` from `virtual:pwa-register/svelte`. The Svelte flavour exposes `needRefresh` / `offlineReady` as writable stores; we mirror them into stable layout-local writables so the reference passed into `` never changes across the dynamic-import gap. +- New component `apps/web/src/lib/components/UpdateToast.svelte` — pill bottom-center with "New version available" copy + a "Reload" button (calls `updateServiceWorker(true)`); a second variant with the "Ready to use offline" message that auto-dismisses after 4 s. +- Telemetry: `console.info('[pwa] ...')` for register, need-refresh, apply. +- Periodic update poll: `setInterval(reg.update, 15 min)`. Standing in for the unavailable `updateViaCache: 'none'` runtime option (see "Deviations" below). +- PU-01 Playwright spec in `apps/web/tests/e2e/pwa-update.test.ts`. Drives the toast via a `window.__pwaRegisterStub` hook the layout checks before calling the real `useRegisterSW` — avoids needing two builds + a redeploy mid-test. + +### 14.2 — Offline polish + +- New `apps/web/src/lib/components/OfflineChip.svelte` — amber pill hidden when online, shown in `MobileTopBar` (compact icon-only variant) and the bottom of `DesktopSidebar` (full pill with label). When `pendingOpsCount > 0`, appends a small numeric badge so the user sees the backlog size at a glance. +- New route `apps/web/src/routes/(app)/settings/sync-conflicts/+page.svelte` — paginated list of the user's last 100 conflicts, local + remote payloads side-by-side, two dismiss actions ("Discard local" / "Discard remote"). Both dismiss actions write the row id to a `syncConflictsDismissed` localStorage set — migration 016 marks `sync_conflicts` append-only at the RLS layer (no DELETE policy) and Fase 14 ships no migration, so dismissal is per-device. The underlying log is preserved for operator audits. +- `apps/web/src/lib/stores/syncStatus.ts` gains `unresolvedConflictsCount`. `(app)/+layout.svelte` probes `sync_conflicts` once per session (deferred out of the auth callback for the same lock-deadlock reason the collectives query is), subtracts the dismissed set, and surfaces an amber banner pill with a "Review" link to the route. Banner is suppressed while the user is already on the review route. +- Background-sync fallback for Safari (`attachOnlineFlush` in `apps/web/src/lib/sync/queue.ts`) was already wired in `apps/web/src/lib/sync/index.ts`. No change needed for 14.2.4. +- OF-01 e2e: drives `context.setOffline(true)`, asserts the chip + numeric badge appear, then disappear back online. +- OF-02 e2e: plants a `sync_conflicts` row through the dev-only `__sb` window client, reloads, asserts banner → click → row visible on the route → discard → empty state. Pre-dismisses every existing conflict via localStorage so the test is deterministic regardless of accumulated history (we can't DELETE from the table). + +### 14.3 — Version chip + GIT_SHA bake-through + +- `apps/web/vite.config.ts` `define`s three build-time constants: + - `__APP_VERSION__` ← root `package.json` `version` + - `__APP_COMMIT__` ← `process.env.GIT_SHA` → local `git rev-parse --short HEAD` → `'dev'` + - `__BUILD_TIME__` ← `new Date().toISOString()` at build time +- Declarations in new `apps/web/src/global.d.ts` (which also declares the `virtual:pwa-register/svelte` module shape so svelte-check sees `useRegisterSW`). +- New `apps/web/src/lib/version.ts` re-exports as named bindings `{ version, commit, builtAt }`. Components import from `$lib/version` — never inline the magic globals — so an accidental tree-shake breaks the V-02 unit test, not the page silently. +- `/settings` gains an "About" section at the bottom: `v{version} · {commit} · {date}` in muted text. Not interactive. +- `apps/web/Dockerfile` accepts a `GIT_SHA` build arg and exports it as an env var for the SvelteKit build step. `infra/docker-compose.erosi.yml` forwards `${GIT_SHA:-dev}` into the build args. `infra/scripts/deploy-erosi.sh` captures `git rev-parse --short HEAD` on the deploy host (rsync excludes `.git`, so the remote can't compute it) and forwards via `ssh ... env GIT_SHA=… bash -s`. +- V-01/V-02 vitest in `apps/web/src/lib/version.test.ts` stub the three globals via `vi.stubGlobal` (vitest doesn't run through the main `vite.config` so `define` isn't applied). + +## Test deltas (Fase 14 only) + +| Suite | Before | After | Δ | +|---|---|---|---| +| pgTAP (`just test-db`) | 166 | 166 | 0 | +| Vitest integration (`just test-integration`) | 173 passed + 3 skipped | 173 + 3 skipped | 0 | +| Vitest unit (`just test-unit`) | 45 | 47 | +2 (V-01, V-02) | +| Playwright e2e (`just test-e2e`) | 86 | 89 | +3 (PU-01, OF-01, OF-02) | +| Gated rate-limit | 1 | 1 | 0 | + +All green. The plan estimated +1 vitest + 3 playwright; actual is +2 vitest + 3 playwright (the version barrel needed two assertions to cover both the named exports AND the "no undefined / 'dev' sentinel" canary). + +## Cumulative MVP2 test counts (sum across Fase 9–14) + +| Suite | Count | +|---|---| +| pgTAP | 166 | +| Vitest integration | 173 (+ 3 skipped) | +| Vitest unit | 47 | +| Playwright e2e | 89 | +| Gated rate-limit | 1 | +| **Total active** | **476** (+ 3 skipped) | + +## Deviations from plan + +- **`updateViaCache: 'none'`** is in the plan but not exposed by `vite-plugin-pwa` 0.21.2's `RegisterSWOptions` (workbox-window 7 doesn't expose it on the underlying `Workbox` constructor either). Workaround: an explicit `reg.update()` poll every 15 min from `onRegisteredSW`. Same end result for the "stale SW file" scenario the option targets, no plugin upgrade required. +- **`sync_conflicts` "Discard" buttons** are per-device (localStorage), not per-account. The plan implied a clean removal; migration 016 explicitly makes the table append-only at the RLS layer and Fase 14 ships no migration, so a real delete would require either a new migration or a SECURITY DEFINER RPC. Filed as a follow-up rather than rolled into Fase 14: the log is informational, the local hide is good UX, and the underlying rows remain available for operator audits. +- **Lighthouse PWA score**: deferred. `just lighthouse` requires a built bundle + a separate Node process to spawn the server; the assistant could not run the headed browser in this environment. The PWA prerequisites (manifest shape, SW registration, scope, display=standalone, 192+512 icons) are covered by P-01/P-02 e2e specs and unchanged from Fase 6's audit; no regression expected. Recommend running `just lighthouse` against the next deploy and recording the score in the next session. + +## Risks captured in the plan (for future-self) + +- `skipWaiting()` is aggressive: the new SW takes control as soon as the user clicks "Reload". A user with in-flight optimistic mutations could lose them if they're not yet flushed to IDB at the moment of reload. Mitigated: the queue persists to IDB BEFORE the network call (see `src/lib/sync/queue.ts`); the reload only kills in-memory state, not the queue. +- `onNeedRefresh` only fires if Workbox detects a changed `revision` in the precache manifest. A deploy that only changes runtime env (e.g. a Keycloak URL update) does not bump any hashed asset and the toast won't fire. Documented in the runbook: bump `package.json` version any time you want the toast to surface, even for env-only deploys. + +## Files added + +- `apps/web/src/lib/components/UpdateToast.svelte` +- `apps/web/src/lib/components/OfflineChip.svelte` +- `apps/web/src/lib/version.ts` +- `apps/web/src/lib/version.test.ts` +- `apps/web/src/global.d.ts` +- `apps/web/src/routes/(app)/settings/sync-conflicts/+page.svelte` +- `apps/web/tests/e2e/pwa-update.test.ts` +- `docs/history/fase-14-pwa-hardening.md` (this file) + +## Files modified + +- `apps/web/src/routes/+layout.svelte` — useRegisterSW migration, UpdateToast mount, periodic SW poll +- `apps/web/src/routes/(app)/+layout.svelte` — conflict probe + banner +- `apps/web/src/routes/(app)/settings/+page.svelte` — About chip +- `apps/web/src/lib/stores/syncStatus.ts` — `unresolvedConflictsCount` store +- `apps/web/src/lib/components/layout/MobileTopBar.svelte` — `` +- `apps/web/src/lib/components/layout/DesktopSidebar.svelte` — `` +- `apps/web/messages/{en,es}.json` — 17 new keys (pwa\_\*, sync\_conflicts\_\*, settings\_about\_\*) +- `apps/web/vite.config.ts` — `define` for the three version constants +- `apps/web/Dockerfile` — `GIT_SHA` build arg +- `apps/web/tests/e2e/offline.test.ts` — OF-01 + OF-02 alongside existing O-01/O-02 +- `infra/docker-compose.erosi.yml` — forward `GIT_SHA` into the build +- `infra/scripts/deploy-erosi.sh` — capture local SHA + ssh `env GIT_SHA=…`