Add last manual check update date
This commit is contained in:
parent
b325415e6a
commit
63f2f2ba24
|
|
@ -27,6 +27,7 @@ A local-first PWA fitness training application. Plan, track, and execute your wo
|
|||
npm install
|
||||
npm run dev # dev server at http://localhost:5173
|
||||
npm run lint # ESLint (src/)
|
||||
npm run lint:fix # ESLint (src/), auto-fix
|
||||
npm run format # Prettier (auto-fix)
|
||||
npm run format:check # Prettier (check only)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2,11 +2,17 @@
|
|||
|
||||
Record of architectural and technical decisions made during development.
|
||||
|
||||
## 2026-07-24 — Check Updates section moved before Credits; shows last manual check date
|
||||
|
||||
**Decision:** Renamed the Settings "Application" card to **Check Updates** and moved it to sit directly above **Credits** (previously between Sound and the restore-overlay/report block). Added a `last_update_check_at` settings key (ISO-8601 UTC, in-place addition) stamped by `SettingsView.onCheckForUpdate()` right after `checkForUpdate()` resolves; the section displays it via `formatDateTime` ("Last manual check: {{when}}") or a "Never checked manually" fallback.
|
||||
|
||||
**Rationale:** User request (2026-07-24). The timestamp intentionally only reflects manual clicks — the Service Worker API has no event for the browser's own background SW checks, so there is nothing reliable to hook there; tracking manual checks only was confirmed with the user rather than guessing at the browser's schedule.
|
||||
|
||||
## 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.
|
||||
**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)
|
||||
|
||||
|
|
|
|||
20
docs/plan.md
20
docs/plan.md
|
|
@ -915,7 +915,7 @@ Hugo 0.123.7 static site under `website/`. FR default at `/`, EN at `/en/`. Depl
|
|||
|
||||
### Step 27 — Manual "Check for updates" button in Settings
|
||||
|
||||
**Status:** ✅ Done (2026-07-22) — manually validated by the user. 5 Playwright tests passing on Firefox + Chromium.
|
||||
**Status:** ✅ Done (2026-07-22) — manually validated by the user. 5 Playwright tests passing on Firefox + Chromium. Follow-up (2026-07-24): section renamed to "Check Updates" and moved just above Credits; now also shows the last manual check date. 7 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.
|
||||
|
||||
|
|
@ -923,22 +923,22 @@ Hugo 0.123.7 static site under `website/`. FR default at `/`, EN at `/en/`. Depl
|
|||
|
||||
#### Decisions
|
||||
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
#### 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`
|
||||
- `src/views/SettingsView.vue` — "Check Updates" section (moved just above Credits, 2026-07-24 follow-up): "Check for updates" button (disabled while checking), transient "up to date" message, and the last manual check date read/written via the `last_update_check_at` settings key (in-place addition, no schema bump)
|
||||
- `src/i18n/locales/{en,fr,da}.json` — `settings.checkUpdates`, `settings.checkForUpdate`, `settings.upToDate`, `settings.lastManualCheck`, `settings.neverCheckedManually`
|
||||
- `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.
|
||||
**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; last-check date shows "never checked manually" on a fresh DB and updates + persists across reload after a click. Firefox + Chromium.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -278,9 +278,11 @@
|
|||
"soundVolume": "Lydstyrke",
|
||||
"soundTest": "Test",
|
||||
"voiceAnnouncements": "Stemmemeddelelser",
|
||||
"application": "Applikation",
|
||||
"checkUpdates": "Søg efter opdateringer",
|
||||
"checkForUpdate": "Søg efter opdateringer",
|
||||
"upToDate": "Du har den nyeste version",
|
||||
"lastManualCheck": "Seneste manuelle søgning: {{when}}",
|
||||
"neverCheckedManually": "Aldrig søgt manuelt",
|
||||
"credits": "Kreditering",
|
||||
"manageJournal": "Administrer øvelseslogge",
|
||||
"manageJournalHint": "Slet øvelseslogposter inden for et datointerval.",
|
||||
|
|
|
|||
|
|
@ -278,9 +278,11 @@
|
|||
"soundVolume": "Volume",
|
||||
"soundTest": "Test",
|
||||
"voiceAnnouncements": "Voice announcements",
|
||||
"application": "Application",
|
||||
"checkUpdates": "Check Updates",
|
||||
"checkForUpdate": "Check for updates",
|
||||
"upToDate": "You're up to date",
|
||||
"lastManualCheck": "Last manual check: {{when}}",
|
||||
"neverCheckedManually": "Never checked manually",
|
||||
"credits": "Credits",
|
||||
"manageJournal": "Manage Exercise Logs",
|
||||
"manageJournalHint": "Delete exercise log entries within a date range.",
|
||||
|
|
|
|||
|
|
@ -278,9 +278,11 @@
|
|||
"soundVolume": "Volume",
|
||||
"soundTest": "Tester",
|
||||
"voiceAnnouncements": "Annonces vocales",
|
||||
"application": "Application",
|
||||
"checkUpdates": "Vérifier les mises à jour",
|
||||
"checkForUpdate": "Vérifier les mises à jour",
|
||||
"upToDate": "Vous êtes à jour",
|
||||
"lastManualCheck": "Dernière vérification manuelle : {{when}}",
|
||||
"neverCheckedManually": "Jamais vérifié manuellement",
|
||||
"credits": "Crédits",
|
||||
"manageJournal": "Gérer les journaux d'exercices",
|
||||
"manageJournalHint": "Supprimez les entrées du journal d'exercices sur une plage de dates.",
|
||||
|
|
|
|||
|
|
@ -47,6 +47,12 @@ const initializing = ref(false)
|
|||
const showUpToDate = ref(false)
|
||||
let upToDateTimer = null
|
||||
let hideUpToDateTimer = null
|
||||
const lastUpdateCheckAtRaw = ref(null)
|
||||
|
||||
const lastUpdateCheckDisplay = computed(() => {
|
||||
if (!lastUpdateCheckAtRaw.value) return ''
|
||||
return formatDateTime(lastUpdateCheckAtRaw.value, language.value)
|
||||
})
|
||||
|
||||
// Backup reminder
|
||||
const lastBackupAtRaw = ref(null)
|
||||
|
|
@ -82,6 +88,8 @@ onMounted(async () => {
|
|||
audioVolume.value = isNaN(parsed) ? 50 : parsed
|
||||
const voiceStr = await get('voice_enabled')
|
||||
voiceEnabled.value = voiceStr === null ? true : voiceStr === 'true'
|
||||
const lastUpdateCheckStr = await get('last_update_check_at')
|
||||
lastUpdateCheckAtRaw.value = lastUpdateCheckStr ? new Date(lastUpdateCheckStr) : null
|
||||
await loadSuffixes()
|
||||
await loadBackupInfo()
|
||||
await loadBackupStatus()
|
||||
|
|
@ -123,6 +131,9 @@ async function onCheckForUpdate() {
|
|||
clearTimeout(hideUpToDateTimer)
|
||||
showUpToDate.value = false
|
||||
await checkForUpdate()
|
||||
const now = new Date()
|
||||
await set('last_update_check_at', now.toISOString())
|
||||
lastUpdateCheckAtRaw.value = now
|
||||
// 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.
|
||||
|
|
@ -462,28 +473,6 @@ async function onBackupReminderDaysChange() {
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Application -->
|
||||
<div class="card setting-card">
|
||||
<label class="setting-label">
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
{{ $t('settings.application') }}
|
||||
</label>
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
data-testid="settings-check-for-update"
|
||||
:disabled="checkingForUpdate"
|
||||
@click="onCheckForUpdate"
|
||||
>
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
{{ $t('settings.checkForUpdate') }}
|
||||
</button>
|
||||
<p v-if="showUpToDate" class="text-muted" data-testid="settings-up-to-date">
|
||||
{{ $t('settings.upToDate') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Restore overlay (spinner) -->
|
||||
<div v-if="restoring" class="restore-overlay" data-testid="restore-spinner">
|
||||
<div class="restore-spinner">
|
||||
|
|
@ -796,6 +785,35 @@ async function onBackupReminderDaysChange() {
|
|||
@close="onImportClose"
|
||||
/>
|
||||
|
||||
<!-- Check Updates -->
|
||||
<div class="card setting-card">
|
||||
<label class="setting-label">
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
{{ $t('settings.checkUpdates') }}
|
||||
</label>
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
data-testid="settings-check-for-update"
|
||||
:disabled="checkingForUpdate"
|
||||
@click="onCheckForUpdate"
|
||||
>
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
{{ $t('settings.checkForUpdate') }}
|
||||
</button>
|
||||
<p v-if="showUpToDate" class="text-muted" data-testid="settings-up-to-date">
|
||||
{{ $t('settings.upToDate') }}
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-muted" data-testid="settings-last-update-check">
|
||||
{{
|
||||
lastUpdateCheckDisplay
|
||||
? $t('settings.lastManualCheck', { when: lastUpdateCheckDisplay })
|
||||
: $t('settings.neverCheckedManually')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Credits -->
|
||||
<div class="card setting-card">
|
||||
<label class="setting-label">
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
"""
|
||||
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_button_visible: The button renders in the "Check Updates" section
|
||||
- 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
|
||||
- test_last_check_date_shown_never_checked_initially: Fresh DB shows "never checked manually"
|
||||
- test_last_check_date_updates_after_click_and_persists: Clicking the button stamps the last-checked date, which survives a reload
|
||||
|
||||
Uses the dev-only test hook `window.__appUpdateTest.mockCheckFn` (see
|
||||
src/composables/useAppUpdate.js) as a stand-in for `registration.update()` —
|
||||
|
|
@ -13,6 +15,8 @@ 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).
|
||||
"""
|
||||
import re
|
||||
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
|
||||
|
|
@ -119,3 +123,27 @@ def test_up_to_date_message_not_shown_when_update_found(page: Page, app_url: str
|
|||
page.clock.run_for(2100)
|
||||
page.wait_for_timeout(100)
|
||||
expect(page.locator('[data-testid="settings-up-to-date"]')).not_to_be_visible()
|
||||
|
||||
|
||||
def test_last_check_date_shown_never_checked_initially(page: Page, app_url: str):
|
||||
"""A fresh DB has never been checked manually, so the fallback text is shown."""
|
||||
_go_to_settings(page, app_url)
|
||||
last_check = page.locator('[data-testid="settings-last-update-check"]')
|
||||
expect(last_check).to_be_visible()
|
||||
expect(last_check).to_have_text("Never checked manually")
|
||||
|
||||
|
||||
def test_last_check_date_updates_after_click_and_persists(page: Page, app_url: str):
|
||||
"""Clicking the button stamps a last-checked date, which survives a reload."""
|
||||
_go_to_settings(page, app_url)
|
||||
_mock_check_fn_no_update(page)
|
||||
|
||||
last_check = page.locator('[data-testid="settings-last-update-check"]')
|
||||
expect(last_check).to_have_text("Never checked manually")
|
||||
|
||||
page.locator('[data-testid="settings-check-for-update"]').click()
|
||||
expect(last_check).to_have_text(re.compile(r"^Last manual check: "))
|
||||
|
||||
page.reload()
|
||||
page.wait_for_selector('[data-testid="settings-language"]')
|
||||
expect(last_check).to_have_text(re.compile(r"^Last manual check: "))
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: "Why use AI"
|
||||
title: 'Why use AI'
|
||||
date: 2026-06-20
|
||||
description: "Why I used AI so heavily to build TrainUs"
|
||||
description: 'Why I used AI so heavily to build TrainUs'
|
||||
draft: false
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: "AI: the method"
|
||||
title: 'AI: the method'
|
||||
date: 2026-07-21
|
||||
description: "How I worked with AI to build TrainUs: the method"
|
||||
description: 'How I worked with AI to build TrainUs: the method'
|
||||
draft: false
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: "IA : la méthode"
|
||||
title: 'IA : la méthode'
|
||||
date: 2026-07-21
|
||||
description: "Comment j'ai travaillé avec l'IA pour créer TrainUs : la méthode"
|
||||
draft: false
|
||||
|
|
@ -11,7 +11,7 @@ Parce que oui, suivre une méthode ça aide ! Du coup j'ai adopté peu ou prou l
|
|||
|
||||
## L'architecture
|
||||
|
||||
Je vous ai dit que j'étais du genre feignant sur l'architecture, mais soyons honnêtes : les décisions structurantes, c'est moi qui les ai prises, et dès le départ. Une application 100 % locale, sans serveur, et qui fonctionne entièrement hors ligne : les données restent sur l'appareil de l'utilisateur, dans une base embarquée directement dans le navigateur. Pas de compte, pas de cloud, pas de backend à maintenir — et une séance qui ne dépend pas de la qualité du réseau dans la salle de sport.
|
||||
Je vous ai dit que j'étais du genre feignant sur l'architecture, mais soyons honnêtes : les décisions structurantes, c'est moi qui les ai prises, et dès le départ. Une application 100 % locale, sans serveur, et qui fonctionne entièrement hors ligne : les données restent sur l'appareil de l'utilisateur, dans une base embarquée directement dans le navigateur. Pas de compte, pas de cloud, pas de backend à maintenir — et une séance qui ne dépend pas de la qualité du réseau dans la salle de sport.
|
||||
|
||||
Là où je délègue, c'est l'architecture de détail : le découpage en composants et composables, la couche d'accès aux données, l'organisation du code. L'IA propose dans un cadre. À la racine du projet, un fichier de règles (`AGENTS.md`) que l'IA lit à chaque session : KISS, SOLID, DRY, changements minimaux plutôt que gros refactoring. Et surtout : un plan étape par étape, validé par moi **avant** d'écrire la moindre ligne de code, et une validation manuelle entre chaque étape (bon là faut que je revois, souvent j'ai peu de chose à redire donc à voir comment "moins" valider). Pas de "vas-y, fais toute l'appli". Une feature à la fois. D'ailleurs je prépare souvent le texte de mes features en amont de mes sessions de travail - en mode offline ;-) .
|
||||
|
||||
|
|
|
|||
|
|
@ -1113,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.
|
||||
|
||||
**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()`.
|
||||
**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 has a **Check Updates** section with 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()`.
|
||||
|
||||
**Last-checked date:** the section also shows when the button was last clicked ("Never checked manually" until then), independent of the update outcome. `SettingsView.vue` stores the timestamp under the `last_update_check_at` settings key (ISO-8601 UTC, in-place addition — no schema bump) immediately after `checkForUpdate()` resolves, and formats it with `formatDateTime`. This only reflects manual clicks: the Service Worker API exposes no event for the browser's own background checks, so there is nothing to hook for those.
|
||||
|
||||
**Key files:** `vite.config.js`, `src/composables/useAppUpdate.js`, `src/components/AppUpdateDialog.vue`, `src/App.vue`, `src/views/SettingsView.vue`
|
||||
|
||||
|
|
|
|||
|
|
@ -1019,6 +1019,36 @@ Les trois fonctions de téléchargement de bas niveau (`downloadJson` dans `json
|
|||
|
||||
---
|
||||
|
||||
### 3.13 Invite de mise à jour de l'application
|
||||
|
||||
Le service worker était initialement enregistré avec `registerType: 'autoUpdate'` : lors du déploiement d'une nouvelle version, le nouveau SW téléchargeait tout le précache (y compris les ~8 Mo de `sqlite3.wasm`), prenait le contrôle et rechargeait la page — l'utilisateur vivait un gel inexpliqué. (L'écran de migration du §2 ne couvre que les changements de schéma de base de données, pas les mises à jour du code de l'application.)
|
||||
|
||||
`registerType` est maintenant `'prompt'` : le nouveau SW s'installe (précache) toujours en arrière-plan, mais attend. L'activation devient une décision de l'utilisateur, présentée via une boîte de dialogue.
|
||||
|
||||
**Composable :** `useAppUpdate.js` — un singleton au niveau module encapsulant `useRegisterSW` de `virtual:pwa-register/vue` (importer le module virtuel remplace aussi le script d'enregistrement auto-injecté par le plugin). Il expose :
|
||||
|
||||
- `needRefresh` — ref de `useRegisterSW`, vraie quand un nouveau SW est installé et en attente
|
||||
- `applyUpdate()` — active `updating`, puis appelle `updateServiceWorker(true)` ; la page se recharge quand le nouveau SW prend le contrôle, donc `updating` n'est jamais réinitialisée
|
||||
- `dismiss()` — rejet valable pour la session uniquement (ref simple, même pattern que `InstallPrompt`) ; la boîte de dialogue réapparaît au prochain chargement de l'application si la mise à jour est toujours en attente
|
||||
- `checkForUpdate()` / `checkingForUpdate` — voir « Vérification manuelle » ci-dessous
|
||||
|
||||
**Composant :** `AppUpdateDialog.vue`, monté une seule fois dans la branche « BD prête » d'`App.vue`. Deux états :
|
||||
|
||||
1. **Choix** — titre, court texte, boutons **Mettre à jour** / **Plus tard**.
|
||||
2. **Mise à jour en cours** — spinner + texte « Mise à jour en cours… », non fermable : `ModalDialog` reçoit aucun titre (ce qui supprime le bouton de fermeture de l'en-tête) et l'événement `close` est ignoré tant que `updating` est vrai (Échap et clic hors modale sont absorbés).
|
||||
|
||||
La boîte de dialogue est masquée tant que la route courante est l'écran d'exécution (`route.name === 'session-execute'`) afin qu'une invite de mise à jour n'interrompe jamais un entraînement ; elle s'affiche automatiquement dès que l'utilisateur quitte cet écran.
|
||||
|
||||
**Tests :** simuler une vraie mise à jour de SW dans Playwright nécessiterait deux builds de production, donc `useAppUpdate.js` expose un point d'ancrage réservé au développement (`window.__appUpdateTest`, protégé par `import.meta.env.DEV`) qui permet aux tests de forcer `needRefresh` et de simuler l'appel de mise à jour. Le flux réel de mise à jour (déployer l'ancien build → déployer le nouveau → invite → mise à jour → rechargement) est validé manuellement.
|
||||
|
||||
**Vérification manuelle (étape 27) :** le navigateur ne re-télécharge le script du SW que selon son propre calendrier (à peu près à la navigation/au rechargement), donc une PWA laissée ouverte longtemps peut manquer une notification de mise à jour pendant un moment. Le callback `onRegisteredSW(swScriptUrl, registration)` de `useRegisterSW` capture le `registration` actif dans une variable au niveau module ; `checkForUpdate()` appelle `registration.update()` pour forcer ce re-téléchargement à la demande, en activant `checkingForUpdate` autour de l'appel. Si le script a changé, Workbox bascule `needRefresh` exactement comme lors d'une détection passive, donc `AppUpdateDialog` apparaît avec son choix habituel Mettre à jour / Plus tard — il n'y a pas de chemin de code séparé « trouvé via vérification manuelle », donc une mise à jour trouvée n'est jamais appliquée sans demander. Réglages possède une section **Vérifier les mises à jour** avec un bouton « Vérifier les mises à jour » relié à `checkForUpdate()`. Comme l'API Service Worker n'a pas d'événement « aucune mise à jour trouvée », `SettingsView.vue` utilise une heuristique par délai : si `needRefresh` n'a pas basculé ~2 s après la vérification, un message transitoire « Vous êtes à jour » s'affiche pendant ~3 s. Les tests réutilisent le même point d'ancrage de développement, étendu avec `mockCheckFn` pour simuler `registration.update()`.
|
||||
|
||||
**Date de dernière vérification :** la section affiche aussi la date du dernier clic sur le bouton (« Jamais vérifié manuellement » tant qu'il n'a pas été utilisé), indépendamment du résultat de la vérification. `SettingsView.vue` stocke l'horodatage sous la clé de réglage `last_update_check_at` (ISO-8601 UTC, ajout en place — pas de changement de version de schéma) immédiatement après la résolution de `checkForUpdate()`, et le formate avec `formatDateTime`. Cela ne reflète que les clics manuels : l'API Service Worker n'expose aucun événement pour les propres vérifications en arrière-plan du navigateur, il n'y a donc rien à observer pour celles-ci.
|
||||
|
||||
**Fichiers clés :** `vite.config.js`, `src/composables/useAppUpdate.js`, `src/components/AppUpdateDialog.vue`, `src/App.vue`, `src/views/SettingsView.vue`
|
||||
|
||||
---
|
||||
|
||||
## 4. Routage
|
||||
|
||||
Toutes les routes utilisent le mode hash (`createWebHashHistory`). Le chemin racine `/` redirige vers `/home`.
|
||||
|
|
|
|||
|
|
@ -648,7 +648,7 @@ 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.
|
||||
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 → **Check Updates** 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. The date of your last manual check is shown below the button ("Never checked manually" until you use it for the first time). 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -650,7 +650,7 @@ 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.
|
||||
Si vous laissez l'application ouverte longtemps, la vérification automatique du navigateur peut mettre du temps à détecter une nouvelle version. Réglages → **Vérifier les mises à jour** 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 date de votre dernière vérification manuelle est affichée sous le bouton (« Jamais vérifié manuellement » tant que vous ne l'avez pas utilisé). 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.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue