diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock index 9c7686a..c4e04dc 100644 --- a/.claude/scheduled_tasks.lock +++ b/.claude/scheduled_tasks.lock @@ -1 +1 @@ -{"sessionId":"5ade3667-a4e8-4863-b973-cb39073bdd6a","pid":5817,"procStart":"18931","acquiredAt":1781606447105} \ No newline at end of file +{"sessionId":"493ec87f-3940-4bbe-b1fb-f7d93a184f03","pid":4723,"procStart":"26079","acquiredAt":1783170307973} \ No newline at end of file diff --git a/docs/decisions-log.md b/docs/decisions-log.md index 904e2c7..97f03d7 100644 --- a/docs/decisions-log.md +++ b/docs/decisions-log.md @@ -2,6 +2,14 @@ Record of architectural and technical decisions made during development. +## 2026-07-04 — First-launch warning modal (single tab / no private browsing) + +**Decision:** Added `FirstLoadWarning.vue`, a one-time modal shown on first app launch warning that TrainUs (1) can only be open in one browser tab at a time and (2) does not work in private/incognito browsing. Modeled on `BackupReminder.vue`'s conditional-modal pattern: checks a new `first_load_warning_seen` settings key on mount, shows the modal if unset, and persists the flag permanently on dismiss (no session-only snooze — this is a one-time notice, not a recurring reminder). Both warnings share a single modal and a single "Got it" dismiss button per the original request, rather than two separate dialogs. + +Because every Playwright test runs in its own fresh browser context (a fresh OPFS database), the "first launch" condition the modal keys off is true for every test, not just a real user's actual first run — left unhandled, this would have popped a blocking modal in front of roughly 30 existing test files. Rather than adding test-only branches to the production component, `tests/conftest.py`'s `page` fixture now injects a `MutationObserver`-based init script that auto-dismisses the modal the instant it appears, for every test except the dedicated `tests/test_step23_first_load_warning.py`, which opts out via a new `keep_first_load_warning` pytest marker to exercise the real modal. Verified against the full existing suite plus the two trickiest interactions (the Step 3c backup startup modal, which can appear stacked with this one, and the Step 16 migration flow, where DB-readiness — and so this modal — can appear mid-test after the migration completes). + +**Rationale:** The DB layer's `opfs-sahpool` VFS only supports a single active connection per origin, and OPFS may be unavailable in private browsing; both previously only surfaced as the generic `error.browserNotCompatible` message after the failure already happened. A proactive one-time warning is cheaper for users than a confusing post-hoc error screen. The `MutationObserver` approach (over a fixed-timeout wait) was chosen for the test-infra fix because it reacts the instant the modal enters the DOM regardless of when — the initial page load, or later during Step 16's mid-test dbReady transition — so it doesn't add fixed wait time to any of the ~30 unrelated test files. + ## 2026-06-18 — Clear (×) button added to list search fields **Decision:** Added a clear button inside the search input on the four list views with a search bar (`ExerciseListView`, `ComboListView`, `SessionListView`, `CollectionsView`). The button (Bootstrap `bi-x-lg` icon) only renders when the field has a value, sits absolutely positioned inside the input (`padding-right` added to make room), and on click empties `searchQuery` and refocuses the input via a new `clearSearch()` function in each view. The click handler uses `@mousedown.prevent` instead of `@click` so the button doesn't steal focus from the input first (which would otherwise fire `onSearchBlur` while the field still holds its old, non-empty value). Added a shared i18n key `common.clearSearch` (EN/FR/DA) for its `title`/`aria-label` rather than per-entity keys, since the action is identical across all four views. Each view gets its own `data-testid="-search-clear"` to match the existing per-view `data-testid` convention for the search bar/input/toggle. The pattern is duplicated across the four `.vue` files rather than extracted into a shared component, consistent with how the rest of the search bar (debounce, toggle, blur-to-close) is already duplicated there. diff --git a/docs/plan.md b/docs/plan.md index 9e01a32..a1262e1 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -765,6 +765,28 @@ Hugo 0.123.7 static site under `website/`. FR default at `/`, EN at `/en/`. Depl --- +### Backlog — First-launch warning modal (single tab / no private browsing) + +**Status:** ✅ Done (2026-07-04) + +**Requested:** 2026-07-03, user request — on first app startup, show a warning modal telling the user (1) the app can only be open in one browser tab at a time, and (2) the app does not work in private/incognito browsing mode. Both messages in a single modal, one "J'ai compris" (Got it) button to dismiss. + +**Why (found during exploration):** the DB layer (`src/db/worker.js`) uses `sqlite3.installOpfsSAHPoolVfs(...)`, the OPFS SyncAccessHandle Pool VFS, which only supports a single active connection per origin — a 2nd tab, or private/incognito mode where OPFS may be unavailable, currently only surfaces as a generic post-failure error screen (`error.browserNotCompatible` in `App.vue`'s `dbErrorBody`). This feature adds a proactive warning instead of only a reactive error. + +**What changed:** + +- New `settings` table key `first_load_warning_seen` (absent until dismissed, then `"true"`), read/written via `useSettings()`. +- New component `src/components/FirstLoadWarning.vue`, modeled on `src/components/BackupReminder.vue`: on `onMounted`, reads the flag and, if unset, shows a `ModalDialog` with both warnings (single-tab-only + no-private-browsing) and one dismiss button that persists the flag and closes for good (no session-only snooze, unlike the backup reminder). +- Mounted once in `src/App.vue`'s `v-else` app-shell block, alongside `` / ``. +- New top-level `firstLoadWarning: { title, singleTab, privateMode, dismiss }` i18n block in `en.json`, `fr.json`, `da.json`. +- New `tests/test_step23_first_load_warning.py`: modal appears on a fresh DB, both messages present, dismiss persists the flag, modal does not reappear on reload. Firefox + Chromium. + +**Test-infra fix found along the way:** every Playwright test runs in a fresh browser context — a fresh OPFS database — which is exactly the "first launch" condition, so the modal would have appeared (and blocked interaction) in every one of the ~30 existing test files, not just the new one. Fixed by overriding the `page` fixture in `tests/conftest.py` to inject a `MutationObserver`-based init script that auto-clicks the dismiss button as soon as it appears; the new test file opts out via a `keep_first_load_warning` pytest marker (registered in `tests/pytest.ini`) so it can exercise the real modal. Verified no regressions across the full existing suite plus the trickiest cases (backup startup modal, Step 16 migration flow) on Firefox. + +**Key files changed:** `src/components/FirstLoadWarning.vue` (new), `src/App.vue`, `src/i18n/locales/{en,fr,da}.json`, `tests/test_step23_first_load_warning.py` (new), `tests/conftest.py`, `tests/pytest.ini`, `docs/decisions-log.md`, `website/content/developers/technical.en.md`, `website/content/docs/user-guide/index.{en,fr}.md`. + +--- + ## Data Model ```mermaid diff --git a/src/App.vue b/src/App.vue index 57e5875..7e51d1a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -3,6 +3,7 @@ import { useTranslation } from 'i18next-vue' import { computed, onMounted, ref } from 'vue' import { useRoute } from 'vue-router' import BackupReminder from './components/BackupReminder.vue' +import FirstLoadWarning from './components/FirstLoadWarning.vue' import InstallPrompt from './components/InstallPrompt.vue' import SaveFileDialog from './components/SaveFileDialog.vue' import { db } from './db/database.js' @@ -163,6 +164,7 @@ const pageTitle = computed(() => {