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') }} +

+
+
+
diff --git a/tests/test_step27_check_for_updates.py b/tests/test_step27_check_for_updates.py new file mode 100644 index 0000000..d2feda4 --- /dev/null +++ b/tests/test_step27_check_for_updates.py @@ -0,0 +1,121 @@ +""" +Backlog — Manual "Check for updates" button in Settings + +- test_check_for_update_button_visible: The button renders in the new Application card +- test_check_for_update_calls_registration_update: Clicking the button forces a re-check (registration.update()) +- test_check_for_update_shows_up_to_date_when_nothing_found: No update found -> a transient "up to date" message appears, then disappears +- test_check_for_update_found_still_offers_choice: A manual check that finds an update still shows the Update now / Later modal, not an auto-apply +- test_up_to_date_message_not_shown_when_update_found: The "up to date" heuristic does not fire when a check does find an update + +Uses the dev-only test hook `window.__appUpdateTest.mockCheckFn` (see +src/composables/useAppUpdate.js) as a stand-in for `registration.update()` — +simulating a real service-worker update in Playwright would require two +production builds (see Step 24). page.clock drives the "no update found" +timeout heuristic deterministically (as in Step 15/26). +""" +from playwright.sync_api import Page, expect + + +def _wait_for_db(page: Page, app_url: str): + page.goto(app_url) + page.wait_for_function( + "document.documentElement.getAttribute('data-db-ready') === 'true'", + timeout=15000, + ) + + +def _go_to_settings(page: Page, app_url: str): + """Navigate to Settings, waiting for DB and the dev test hook to be ready.""" + _wait_for_db(page, app_url) + page.click('a[href="#/settings"]') + page.wait_for_selector('[data-testid="settings-language"]') + page.wait_for_function("!!window.__appUpdateTest", timeout=10000) + + +def _mock_check_fn_no_update(page: Page): + """Mock registration.update() as a no-op: the check resolves but nothing changes.""" + page.evaluate("""() => { + window.__checkCalls = [] + window.__appUpdateTest.mockCheckFn = () => { + window.__checkCalls.push(true) + return Promise.resolve() + } + }""") + + +def _mock_check_fn_found_update(page: Page): + """Mock registration.update() as if it found a new SW: flips `needRefresh`.""" + page.evaluate("""() => { + window.__checkCalls = [] + window.__appUpdateTest.mockCheckFn = () => { + window.__checkCalls.push(true) + window.__appUpdateTest.trigger() + return Promise.resolve() + } + }""") + + +def test_check_for_update_button_visible(page: Page, app_url: str): + """The 'Check for updates' button renders in the new Application card.""" + _go_to_settings(page, app_url) + expect(page.locator('[data-testid="settings-check-for-update"]')).to_be_visible() + + +def test_check_for_update_calls_registration_update(page: Page, app_url: str): + """Clicking the button calls through to registration.update().""" + _go_to_settings(page, app_url) + _mock_check_fn_no_update(page) + + page.locator('[data-testid="settings-check-for-update"]').click() + + calls = page.evaluate("window.__checkCalls") + assert calls == [True], f"registration.update() should have been called, got {calls}" + + +def test_check_for_update_shows_up_to_date_when_nothing_found(page: Page, app_url: str): + """No update found: a transient 'up to date' message appears, then disappears.""" + page.clock.install() + _go_to_settings(page, app_url) + _mock_check_fn_no_update(page) + + up_to_date = page.locator('[data-testid="settings-up-to-date"]') + expect(up_to_date).not_to_be_visible() + + page.locator('[data-testid="settings-check-for-update"]').click() + expect(up_to_date).not_to_be_visible() # not shown immediately + + page.clock.run_for(2100) + page.wait_for_timeout(100) + expect(up_to_date).to_be_visible() + + page.clock.run_for(3100) + page.wait_for_timeout(100) + expect(up_to_date).not_to_be_visible() + + +def test_check_for_update_found_still_offers_choice(page: Page, app_url: str): + """A manual check that finds an update still shows Update now / Later — updating stays optional.""" + page.clock.install() + _go_to_settings(page, app_url) + _mock_check_fn_found_update(page) + + page.locator('[data-testid="settings-check-for-update"]').click() + + modal = page.locator('[data-testid="app-update-modal"]') + expect(modal).to_be_visible() + expect(page.locator('[data-testid="app-update-now"]')).to_be_visible() + expect(page.locator('[data-testid="app-update-later"]')).to_be_visible() + + +def test_up_to_date_message_not_shown_when_update_found(page: Page, app_url: str): + """The 'up to date' heuristic does not fire when a check does find an update.""" + page.clock.install() + _go_to_settings(page, app_url) + _mock_check_fn_found_update(page) + + page.locator('[data-testid="settings-check-for-update"]').click() + expect(page.locator('[data-testid="app-update-modal"]')).to_be_visible() + + page.clock.run_for(2100) + page.wait_for_timeout(100) + expect(page.locator('[data-testid="settings-up-to-date"]')).not_to_be_visible() diff --git a/website/content/developers/technical.en.md b/website/content/developers/technical.en.md index 37b4006..13d9ba8 100644 --- a/website/content/developers/technical.en.md +++ b/website/content/developers/technical.en.md @@ -1102,6 +1102,7 @@ The service worker was originally registered with `registerType: 'autoUpdate'`: - `needRefresh` — ref from `useRegisterSW`, true when a new SW is installed and waiting - `applyUpdate()` — sets `updating`, then calls `updateServiceWorker(true)`; the page reloads when the new SW takes control, so `updating` is never reset - `dismiss()` — session-only dismissal (plain ref, same pattern as `InstallPrompt`); the modal reappears on the next app load if the update is still pending +- `checkForUpdate()` / `checkingForUpdate` — see "Manual check" below **Component:** `AppUpdateDialog.vue`, mounted once in `App.vue`'s DB-ready branch. Two states: @@ -1112,7 +1113,9 @@ The modal is suppressed while the current route is the execution view (`route.na **Testing:** simulating a real SW update in Playwright would require two production builds, so `useAppUpdate.js` exposes a dev-only hook (`window.__appUpdateTest`, guarded by `import.meta.env.DEV`) that lets tests force `needRefresh` and mock the update call. The real update flow (deploy old build → deploy new build → prompt → update → reload) is validated manually. -**Key files:** `vite.config.js`, `src/composables/useAppUpdate.js`, `src/components/AppUpdateDialog.vue`, `src/App.vue` +**Manual check (Step 27):** the browser only re-fetches the SW script on its own schedule (roughly on navigation/reload), so a PWA left open for a long time can miss an update notification for a while. `useRegisterSW`'s `onRegisteredSW(swScriptUrl, registration)` callback captures the live `registration` into a module-level variable; `checkForUpdate()` calls `registration.update()` to force that re-fetch on demand, setting `checkingForUpdate` around the call. If the script changed, Workbox flips `needRefresh` exactly as it would from a passive check, so `AppUpdateDialog` appears with its usual Update now / Later choice — there is no separate "found via manual check" code path, so a found update is never applied without asking. Settings → **Application** exposes a "Check for updates" button wired to `checkForUpdate()`. Since the Service Worker API has no "no update found" event, `SettingsView.vue` uses a timeout heuristic: if `needRefresh` hasn't flipped ~2s after the check, a transient "You're up to date" message is shown for ~3s. Testing reuses the same dev-only hook, extended with `mockCheckFn` to stand in for `registration.update()`. + +**Key files:** `vite.config.js`, `src/composables/useAppUpdate.js`, `src/components/AppUpdateDialog.vue`, `src/App.vue`, `src/views/SettingsView.vue` --- diff --git a/website/content/docs/user-guide/index.en.md b/website/content/docs/user-guide/index.en.md index a96b5b2..232530d 100644 --- a/website/content/docs/user-guide/index.en.md +++ b/website/content/docs/user-guide/index.en.md @@ -648,6 +648,8 @@ When a new version is available, TrainUs shows an **Update available** dialog. C If an update changes the database structure, TrainUs shows a one-time update screen at startup. As a safety measure, it asks you to **download a backup** of your data first; the **Update** button becomes available once the backup has been saved. The update then runs in a few seconds and the app continues with all your data in place. If anything were to go wrong, your data is left unchanged and you hold the backup you just downloaded. +If you keep the app open for a long time, the browser's own update check can be slow to notice a new version. Settings → **Application** has a **Check for updates** button to force a check on demand: if an update is found, the same **Update available** dialog appears with the same **Update now** / **Later** choice; if nothing is found, a brief "You're up to date" message is shown. Checking is always something you choose to do — TrainUs never checks for updates in the background — and updating stays your choice even when a check does find one. + The regular backup reminder remains your day-to-day safety net. In the Database section, you can also see: diff --git a/website/content/docs/user-guide/index.fr.md b/website/content/docs/user-guide/index.fr.md index 083185a..e85cba4 100644 --- a/website/content/docs/user-guide/index.fr.md +++ b/website/content/docs/user-guide/index.fr.md @@ -650,6 +650,8 @@ Quand une nouvelle version est disponible, TrainUs affiche une boîte de dialogu Si une mise à jour change la structure de la base de données, TrainUs affiche un écran de mise à jour unique au démarrage. Par sécurité, il vous demande d'abord de **télécharger une sauvegarde** de vos données ; le bouton **Mettre à jour** devient disponible une fois la sauvegarde enregistrée. La mise à jour s'exécute alors en quelques secondes et l'application continue avec toutes vos données en place. Si quoi que ce soit devait mal se passer, vos données restent inchangées et vous disposez de la sauvegarde que vous venez de télécharger. +Si vous laissez l'application ouverte longtemps, la vérification automatique du navigateur peut mettre du temps à détecter une nouvelle version. Réglages → **Application** propose un bouton **Vérifier les mises à jour** pour forcer une vérification à la demande : si une mise à jour est trouvée, la même boîte de dialogue **Mise à jour disponible** s'affiche, avec le même choix **Mettre à jour** / **Plus tard** ; sinon, un bref message « Vous êtes à jour » apparaît. La vérification est toujours une action volontaire — TrainUs ne vérifie jamais en arrière-plan — et la mise à jour reste votre choix même lorsqu'une vérification en trouve une. + Le rappel de sauvegarde habituel reste votre filet de sécurité au quotidien. Dans la section base de données, vous pouvez aussi voir :