diff --git a/docs/code-review-remediation-plan.md b/docs/code-review-remediation-plan.md index ddcb112..6c85ee2 100644 --- a/docs/code-review-remediation-plan.md +++ b/docs/code-review-remediation-plan.md @@ -1,7 +1,7 @@ # Code Review Remediation Plan > **Source:** Full-project code review of 2026-07-06 (code, tests, documentation). -> **Status:** Proposed — not yet validated. +> **Status:** Done — F1–F12 implemented and manually validated by the user. > **Convention:** One fix at a time, user validation between each, per AGENTS.md. > Each fix follows the standard Definition of Done (code, tests on Firefox + > Chromium, docs/decision-log updates where relevant, manual validation). diff --git a/docs/decisions-log.md b/docs/decisions-log.md index ef4b7b7..24d2279 100644 --- a/docs/decisions-log.md +++ b/docs/decisions-log.md @@ -2,6 +2,12 @@ Record of architectural and technical decisions made during development. +## 2026-07-22 — Manual "Check for updates" button, reusing the existing choice modal (Step 27) + +**Decision:** `useAppUpdate.js` now captures the service-worker `registration` via `useRegisterSW`'s `onRegisteredSW` callback and exposes `checkForUpdate()`, which calls `registration.update()` to force the browser to re-fetch the SW script instead of waiting for its own schedule. If the script changed, Workbox flips the existing `needRefresh` flag and `AppUpdateDialog` appears exactly as it does for a passively-detected update — same "Update now" / "Later" choice. `SettingsView.vue` gained an "Application" card with a "Check for updates" button; since the Service Worker API has no "no update found" event, a plain timeout heuristic shows a transient "You're up to date" message if `needRefresh` hasn't flipped a couple of seconds after the check. + +**Rationale:** The point of the manual button is only to force *detection* sooner (Step 24's passive check only runs on the browser's own schedule, roughly on navigation/reload) — it must not become a second code path that could apply an update without asking. Routing the found-update case through the same `needRefresh`/`AppUpdateDialog` flow guarantees updating stays a user decision (user decision 2026-07-21) whether the update was found automatically or via the manual check, with no new modal or auto-apply branch to keep in sync. + ## 2026-07-09 — Deleting an exercise/combo cleans session usage and warns about it (code review F4) **Decision:** `session_item.item_id` is polymorphic (exercise or combo) and carries no FK, so deleting an exercise or combo used in a session silently left orphaned `session_item` rows that the INNER JOIN in `sessionRepository.getItems` simply hid — the entry vanished from the session with no trace. Deletes in `exerciseRepository`/`comboRepository` now also remove the matching `session_item` rows (in one `db.batch` transaction), and the delete confirmations on the exercise/combo/session detail views show usage counts before the user confirms: sessions+combos using the exercise, sessions using the combo, collections containing the session (new repository count helpers, new `deleteConfirmUsage` i18n keys in EN/FR/DA). diff --git a/docs/plan.md b/docs/plan.md index 9d4b945..7409bd2 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -1,7 +1,7 @@ # TrainUs — Implementation Plan (v3) -> **Last updated:** 2026-07-09 -> **Status:** Step 24 implemented (app-update modal) — awaiting manual validation; Step 25 implemented (headset media controls) — awaiting manual validation; Step 26 implemented (voice announcements) — awaiting manual validation. Code-review remediation F1–F12 (see `docs/code-review-remediation-plan.md`) implemented — awaiting manual validation. +> **Last updated:** 2026-07-24 +> **Status:** Step 24 (app-update modal), Step 25 (headset media controls), Step 26 (voice announcements), and Step 27 (manual "Check for updates" button) all manually validated by the user. Code-review remediation F1–F12 (see `docs/code-review-remediation-plan.md`) manually validated by the user. ## Overview @@ -642,7 +642,7 @@ Iterative refinement of the audio cue timing introduced in Step 12, driven by de ### Step 16 — Single Deployment + Startup DB Migration -**Status:** Implemented — detailed plan in [`plan-step16-single-deployment-migration.md`](plan-step16-single-deployment-migration.md); awaiting manual validation by the user +**Status:** Done — detailed plan in [`plan-step16-single-deployment-migration.md`](plan-step16-single-deployment-migration.md); manually validated by the user Replaces the "versioned deployments with isolated data" model: a single deployment (latest version at site root), a stable OPFS database location (no version suffix), and a transactional migration step at app startup driven by the `RELEASES` registry. When a migration is pending, a blocking screen forces the user to download a backup before the update runs. No `SCHEMA_VERSION` bump (stays at 1). @@ -661,7 +661,7 @@ Replaces the "versioned deployments with isolated data" model: a single deployme ### Improvement Plan (post-v1.0 review) — `docs/improvement-plan.md` -**Status:** Implemented — awaiting manual validation by the user +**Status:** Done — manually validated by the user All items from the improvement plan have been implemented. See `docs/improvement-plan.md` for the original descriptions and DoDs. @@ -692,7 +692,7 @@ All items from the improvement plan have been implemented. See `docs/improvement ### Website — Hugo static site (`website/`) -**Status:** Steps 1–5 done (2026-06-14) — awaiting manual validation +**Status:** Steps 1–5 done (2026-06-14) — manually validated by the user Hugo 0.123.7 static site under `website/`. FR default at `/`, EN at `/en/`. Deployed to a subpath of the personal domain on Uberspace (manual deploy, no CI). No tutorials section. Blog lives on this project site. @@ -789,7 +789,7 @@ 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:** ✅ Implemented (2026-07-05) — awaiting manual validation by the user. Implemented as planned below; 7 Playwright tests passing on Firefox + Chromium (real SW update flow still to be validated manually on a deployed build). +**Status:** ✅ Done (2026-07-05) — manually validated by the user, including the real SW update flow on a deployed build. Implemented as planned below; 7 Playwright tests passing on Firefox + Chromium. **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.) @@ -827,7 +827,7 @@ Hugo 0.123.7 static site under `website/`. FR default at `/`, EN at `/en/`. Depl ### Step 25 — Headset / Media-Key Control of Session Execution -**Status:** ✅ Implemented (2026-07-05) — awaiting manual validation by the user. Implemented as planned below; 9 Playwright tests passing on Firefox + Chromium (real headset/lock-screen control still to be validated manually on a device). +**Status:** ✅ Done (2026-07-05) — manually validated by the user, including real headset/lock-screen control on a device. Implemented as planned below; 9 Playwright tests passing on Firefox + Chromium. **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`). @@ -869,7 +869,7 @@ Hugo 0.123.7 static site under `website/`. FR default at `/`, EN at `/en/`. Depl ### Step 26 — Voice Announcements (Web Speech API) -**Status:** ✅ Implemented (2026-07-06) — awaiting manual validation by the user. Implemented as planned below; 9 Playwright tests passing on Firefox + Chromium. Full regression suite (374 passed, 1 skipped) green on Firefox 2026-07-06. +**Status:** ✅ Done (2026-07-06) — manually validated by the user. Implemented as planned below; 9 Playwright tests passing on Firefox + Chromium. Full regression suite (374 passed, 1 skipped) green on Firefox 2026-07-06. **Goal:** in execution mode, a synthesized voice (SpeechSynthesis — `window.speechSynthesis`, no audio assets, supported by Firefox and Chromium) announces the exercise title at each step change. In TABATA and EMOM the _next_ exercise is pre-announced. The voice can be disabled in Settings → Sound. @@ -913,21 +913,32 @@ Hugo 0.123.7 static site under `website/`. FR default at `/`, EN at `/en/`. Depl --- -### Backlog — Manual "Check for updates" button in Settings +### Step 27 — Manual "Check for updates" button in Settings -**Status:** 🔲 To do — not yet planned in detail, not implemented +**Status:** ✅ Done (2026-07-22) — manually validated by the user. 5 Playwright tests passing on Firefox + Chromium. **Requested:** 2026-07-21, user request — the PWA doesn't seem to update automatically; add a manual update button in Settings. **Why:** Step 24 (`useAppUpdate.js`, `AppUpdateDialog.vue`) already shows an update prompt when the browser detects a new service worker, but that detection only runs on the browser's own schedule (roughly on navigation/reload) — if the PWA is left open, an update can go unnoticed for a long time. A manual trigger in Settings lets the user force a check on demand. -**Discussed approach (not yet implemented):** +#### Decisions -- `useAppUpdate.js` — capture the SW `registration` via `useRegisterSW`'s `onRegisteredSW` callback; add `checkForUpdate()` that calls `registration.update()` to force the browser to re-fetch the SW script. If it changed, the existing `needRefresh` flow fires and `AppUpdateDialog` appears as usual (same "Update now" / "Later" choice — user retains the option to defer even when an update is found). -- `SettingsView.vue` — new "Application" card: a "Check for updates" button, with a brief inline status message ("You're up to date") shown for a few seconds when a check finds nothing (via a timeout heuristic, since there's no direct "no update" event from the SW API). -- Explicitly out of scope: no periodic/background auto-check — checking stays a deliberate user action, and updating stays optional even when available (user decision 2026-07-21). +| Decision | Choice | Rationale | +| ----------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | +| Detection mechanism | `registration.update()` via the SW registration captured through `useRegisterSW`'s `onRegisteredSW` callback | Forces the browser to re-fetch the SW script now instead of waiting for its own schedule | +| Found-update path | Reuses the existing `needRefresh` flow and `AppUpdateDialog` unchanged | Same "Update now" / "Later" choice regardless of how the update was detected — no auto-apply | +| No-update feedback | Timeout heuristic in `SettingsView.vue`: if `needRefresh` hasn't flipped ~2s after the check, show "You're up to date" for ~3s | There is no direct "no update found" event from the Service Worker API | +| Scope | No periodic/background auto-check — checking stays a deliberate user action | User decision 2026-07-21: updating stays optional even when available, checking stays opt-in too | -**Not started:** no code, no tests, no i18n keys yet. +#### Files created / modified + +- `src/composables/useAppUpdate.js` — capture `registration` via `onRegisteredSW`; add `checkForUpdate()` (calls `registration.update()`, guarded by a dev-only `mockCheckFn` test hook) and `checkingForUpdate` state +- `src/views/SettingsView.vue` — new "Application" card: "Check for updates" button (disabled while checking) + transient "up to date" message +- `src/i18n/locales/{en,fr,da}.json` — `settings.application`, `settings.checkForUpdate`, `settings.upToDate` +- `tests/test_step27_check_for_updates.py` +- `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 `window.__appUpdateTest.mockCheckFn`) — button calls through to `registration.update()`; no update found shows the transient "up to date" message which then disappears; an update found via manual check still shows the "Update now" / "Later" modal (choice preserved, no auto-apply); the "up to date" message never appears when an update was found. Firefox + Chromium. --- diff --git a/src/composables/useAppUpdate.js b/src/composables/useAppUpdate.js index 4b6353f..4676138 100644 --- a/src/composables/useAppUpdate.js +++ b/src/composables/useAppUpdate.js @@ -4,10 +4,17 @@ import { useRegisterSW } from 'virtual:pwa-register/vue' // Singleton: one service-worker registration and one shared update state for // the whole app. With `registerType: 'prompt'` (vite.config.js) the new SW // precaches in the background and waits; activation is a user decision. -const { needRefresh, updateServiceWorker } = useRegisterSW() +let registration = null + +const { needRefresh, updateServiceWorker } = useRegisterSW({ + onRegisteredSW(_swScriptUrl, reg) { + registration = reg + }, +}) const updating = ref(false) const dismissed = ref(false) +const checkingForUpdate = ref(false) async function applyUpdate() { updating.value = true @@ -22,6 +29,20 @@ function dismiss() { dismissed.value = true } +async function checkForUpdate() { + checkingForUpdate.value = true + try { + // Forces the browser to re-fetch the SW script now instead of waiting for + // its own schedule. If it changed, Workbox flips `needRefresh` and the + // existing modal appears with its usual Update now / Later choice. + const check = + (import.meta.env.DEV && window.__appUpdateTest?.mockCheckFn) || (() => registration?.update()) + await check() + } finally { + checkingForUpdate.value = false + } +} + if (import.meta.env.DEV) { // Test hook: simulating a real SW update in Playwright would require two // production builds, so tests force the prompt and mock the update instead. @@ -30,9 +51,18 @@ if (import.meta.env.DEV) { needRefresh.value = true }, mockUpdateFn: null, + mockCheckFn: null, } } export function useAppUpdate() { - return { needRefresh, updating, dismissed, applyUpdate, dismiss } + return { + needRefresh, + updating, + dismissed, + checkingForUpdate, + applyUpdate, + dismiss, + checkForUpdate, + } } diff --git a/src/i18n/locales/da.json b/src/i18n/locales/da.json index d5c7305..3cdd51d 100644 --- a/src/i18n/locales/da.json +++ b/src/i18n/locales/da.json @@ -278,6 +278,9 @@ "soundVolume": "Lydstyrke", "soundTest": "Test", "voiceAnnouncements": "Stemmemeddelelser", + "application": "Applikation", + "checkForUpdate": "Søg efter opdateringer", + "upToDate": "Du har den nyeste version", "credits": "Kreditering", "manageJournal": "Administrer øvelseslogge", "manageJournalHint": "Slet øvelseslogposter inden for et datointerval.", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 9d5397e..5656069 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -278,6 +278,9 @@ "soundVolume": "Volume", "soundTest": "Test", "voiceAnnouncements": "Voice announcements", + "application": "Application", + "checkForUpdate": "Check for updates", + "upToDate": "You're up to date", "credits": "Credits", "manageJournal": "Manage Exercise Logs", "manageJournalHint": "Delete exercise log entries within a date range.", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 960f8bd..5559295 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -278,6 +278,9 @@ "soundVolume": "Volume", "soundTest": "Tester", "voiceAnnouncements": "Annonces vocales", + "application": "Application", + "checkForUpdate": "Vérifier les mises à jour", + "upToDate": "Vous êtes à jour", "credits": "Crédits", "manageJournal": "Gérer les journaux d'exercices", "manageJournalHint": "Supprimez les entrées du journal d'exercices sur une plage de dates.", diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue index 8a25027..a67bdf8 100644 --- a/src/views/SettingsView.vue +++ b/src/views/SettingsView.vue @@ -5,6 +5,7 @@ import { useSettings } from '../composables/useSettings.js' import { useAudioCues } from '../composables/useAudioCues.js' import { useBackupStatus } from '../composables/useBackupStatus.js' import { useBackupDownload } from '../composables/useBackupDownload.js' +import { useAppUpdate } from '../composables/useAppUpdate.js' import { useJsonExport } from '../composables/useJsonExport.js' import { useJsonImport } from '../composables/useJsonImport.js' import { suffixRepository } from '../db/repositories/suffixRepository.js' @@ -24,6 +25,7 @@ const { exportByFilter, exportAll } = useJsonExport() const { loadFile } = useJsonImport() const { markBackedUp, load: loadBackupStatus } = useBackupStatus() const { downloadBackup } = useBackupDownload() +const { needRefresh, checkingForUpdate, checkForUpdate } = useAppUpdate() const userName = ref('') const userUuid = ref('') @@ -41,6 +43,11 @@ const showCleanJournalDialog = ref(false) const initializeConfirmed = ref(false) const initializing = ref(false) +// App update check +const showUpToDate = ref(false) +let upToDateTimer = null +let hideUpToDateTimer = null + // Backup reminder const lastBackupAtRaw = ref(null) const backupReminderDays = ref('7') @@ -111,6 +118,24 @@ async function onVoiceEnabledChange() { await set('voice_enabled', String(voiceEnabled.value)) } +async function onCheckForUpdate() { + clearTimeout(upToDateTimer) + clearTimeout(hideUpToDateTimer) + showUpToDate.value = false + await checkForUpdate() + // There is no "no update found" event from the SW API, so fall back to a + // timeout: if the update prompt hasn't appeared shortly after the check, + // tell the user they're up to date. + upToDateTimer = setTimeout(() => { + if (!needRefresh.value) { + showUpToDate.value = true + hideUpToDateTimer = setTimeout(() => { + showUpToDate.value = false + }, 3000) + } + }, 2000) +} + function onSoundTest() { const vol = audioVolume.value / 100 playBip(vol) @@ -437,6 +462,28 @@ async function onBackupReminderDaysChange() { + +
+ {{ $t('settings.upToDate') }} +
+