diff --git a/docs/plan.md b/docs/plan.md index a1262e1..fcbc0ee 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -1,7 +1,7 @@ # TrainUs — Implementation Plan (v3) -> **Last updated:** 2026-06-09 -> **Status:** Step 15 complete — all steps done; GNU AGPL v3 license + notice added; license shown via stacked modal in About dialog +> **Last updated:** 2026-07-04 +> **Status:** Steps 24–26 planned (app-update modal, headset media controls, voice announcements) — ready for implementation, one step at a time with user validation between steps ## Overview @@ -787,6 +787,132 @@ Hugo 0.123.7 static site under `website/`. FR default at `/`, EN at `/en/`. Depl --- +### Step 24 — App Update Modal (no more silent freeze) + +**Status:** 🔲 Planned (2026-07-04) + +**Problem:** the service worker is registered with `registerType: 'autoUpdate'` (`vite.config.js`). When a new version is deployed, the new SW downloads the full precache (including `sqlite3.wasm`, ~10 MB), takes control and reloads — the user experiences an unexplained freeze, even when no DB migration is involved. (The DB migration screen from Step 16 only covers schema changes, not app-code updates.) + +**Goal:** the user is informed that an update is available, chooses when to apply it, and sees an explicit "updating…" state instead of a freeze. + +#### Decisions + +| Decision | Choice | Rationale | +| --------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| Update strategy | Switch VitePWA to `registerType: 'prompt'` | The new SW installs (precaches) in the background but waits; activation becomes a user decision | +| UI | Modal via existing `ModalDialog.vue`: "Update now" / "Later" | Reuse existing modal component (KISS) | +| "Update now" | Show non-dismissible spinner state, call `updateServiceWorker(true)` | The page reloads when the new SW takes control; the spinner explains the wait | +| "Later" | Dismiss for the current session; modal reappears on next app load | Same session-dismiss pattern as `InstallPrompt.vue`; no persisted flag needed | +| During execution mode | Do not show the modal on `#/sessions/:id/execute`; defer until leaving | Never interrupt a workout with an update prompt | +| E2E testing | Dev-only test hook to force `needRefresh` | Simulating a real SW update in Playwright requires two builds; the real flow is validated manually | + +#### Files to create / modify + +**New:** + +- `src/composables/useAppUpdate.js` — singleton wrapping `useRegisterSW` from `virtual:pwa-register/vue`: exposes `needRefresh`, `updating`, `applyUpdate()` (sets `updating`, calls `updateServiceWorker(true)`), `dismiss()` (session-only). Guards a dev-only hook `window.__appUpdateTest = { trigger() }` behind `import.meta.env.DEV` so Playwright can force the prompt. +- `src/components/AppUpdateDialog.vue` — modal with two states: (1) choice — title, short body ("A new version of TrainUs is ready"), buttons **Update now** / **Later**; (2) updating — spinner + "Updating…" text, no buttons, not dismissible (no Escape/backdrop close). Hidden while the current route is the execution view; shows automatically once the user navigates away. +- `tests/test_step24_app_update.py` + +**Modified:** + +- `vite.config.js` — `registerType: 'autoUpdate'` → `'prompt'` +- `src/App.vue` — mount `` once in the app-shell block (alongside `` / `` / ``) +- `src/i18n/locales/en.json`, `fr.json`, `da.json` — new `appUpdate: { title, body, updateNow, later, updating }` block +- `docs/decisions-log.md`, `website/content/developers/technical.en.md`, `website/content/docs/user-guide/index.{en,fr}.md` + +**Verification:** Playwright tests (via the dev test hook) — modal appears when an update is pending; "Later" closes it and it does not reappear within the session; "Update now" switches to the spinner state and calls the (mocked) update function; spinner state ignores Escape; modal is suppressed on the execution route and appears after navigating away. Real SW update flow validated manually (deploy old build → deploy new build → prompt → update → reload). Firefox + Chromium. + +--- + +### Step 25 — Headset / Media-Key Control of Session Execution + +**Status:** 🔲 Planned (2026-07-04) + +**Goal:** during session execution, headphone buttons (and OS media keys / lock-screen controls) drive the workout: play/pause, next, previous, stop — via the **Media Session API** (`navigator.mediaSession`). + +**Key constraint:** browsers only route hardware media keys to a page that is actively playing audio through a media element — Web Audio beeps do not qualify. The standard workaround is a short **silent looping `