# Decisions Log 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. **Rationale:** Requested by the user as a small UX improvement — clearing the field previously required selecting and deleting the text manually, or toggling search off and back on (which also closes the bar). `@mousedown.prevent` was chosen over reordering the blur/clear logic because it's the standard idiom for "clear button inside a focused input" and avoids a state-machine edge case where blur could close the search bar before the field is cleared. ## 2026-06-16 — "Add a New Theme" how-to now covers all supported languages **Decision:** Fixed `website/content/developers/how-to-guides.{en,fr}.md`'s "Add a New Theme" recipe, which only had the new contributor add the theme's display label to `en.json` and `fr.json` — missing `da.json`, even though Danish is a real, shipped language (same gap just fixed elsewhere in "Add a New Language", see the entry above). Merged the two language-specific steps into one ("add the display label in every supported language"), added the `da.json` example, noted that `i18next`'s `fallbackLng: 'en'` (from `src/i18n/index.js`) means a missing label degrades to English silently rather than erroring, and added a step in the smoke-test checklist to verify the label in all three languages before considering the theme done. Renumbered the touch-point list and step headings accordingly (6 steps → 5). **Rationale:** A theme contributed by following the old recipe would ship with a Danish UI silently showing an English theme name — a real, easy-to-miss gap given `fallbackLng` doesn't surface it as an error. Caught by the user immediately after the equivalent fix to "Add a New Language". ## 2026-06-16 — Corrected language-count and COOP/COEP claims in developer docs **Decision:** Fixed two inaccuracies in `website/content/developers/how-to-guides.{en,fr}.md` and `technical.{en,fr}.md`, both inherited unchanged from the pre-split single-page `technical.en.md`: 1. "Add a New Language" used Danish (`da`) as the worked example for a language being added, with an `// ← new` comment — misleading, since `da` is already a real, shipped locale (`src/i18n/locales/da.json`, registered in `src/i18n/index.js`, and selectable today in Settings per `src/views/SettingsView.vue`). Reworked the recipe to use Spanish (`es`) as the example instead, and added a note up front that TrainUs already ships three languages (English, French, Danish), not just the EN/FR pair the project website itself emphasizes. Also corrected the Reference page's "Available settings" table, which still listed the language selector as "(English / Français)" only. 2. "Deploy TrainUs" stated the COOP/COEP headers **must** be present "or the browser blocks OPFS access" — contradicted by the same reference page's own §2 ("Does not require COOP/COEP headers") and by the R17 README correction (2026-06-12, see below). Reworded to match README's accurate framing: not strictly required by the `opfs-sahpool` VFS actually used, recommended as defense-in-depth, and the dev server already sets them as a precaution. **Rationale:** Both inaccuracies were latent in the original single-page `technical.en.md` and simply carried forward during the Diataxis split rather than introduced by it; the user caught them after reading the new how-to pages. Fixed in both languages since both were translated from the same (incorrect) source text. ## 2026-06-16 — French developer documentation ported to the EN Diataxis split **Decision:** Ported the four-page developer docs Diataxis split (see the "English developer documentation split Diataxis-style" entry below) to French, completing the deferred follow-up: `getting-started.fr.md` (tutorial, slug `demarrage-rapide`), `how-to-guides.fr.md` (how-to, slug `guides-pratiques`), `technical.fr.md` (reference — replaced the old "moved to EN" stub with the full translated content, title changed from "Documentation technique" to "Référence" and weight corrected from 5 to 3 to match the EN page's position, mirroring `user-guide/index.fr.md`'s "Référence" title), `understanding-the-architecture.fr.md` (explanation, slug `comprendre-l-architecture`). Basenames match the EN files for Hugo's translation pairing (confirmed via the language switcher rendering correctly on each page); French-friendly URLs come from `slug:` front matter, the same technique used for the EN user-docs port. Internal cross-page anchors (e.g. `technical.fr.md#1-architecture`, `how-to-guides.fr.md#déployer-trainus`) were verified against Hugo's actual generated heading IDs after a full `hugo --minify` build — no `REF_NOT_FOUND` and no broken anchors. **Rationale:** The English split was done first specifically because the project already had two real how-to documents in English; with that done and validated, finishing the French side closes out the deferred TODO in `docs/website-plan.md` §8.1 and brings developer docs to parity with user docs (both now fully bilingual, 4-page Diataxis splits). ## 2026-06-16 — English developer documentation split Diataxis-style **Decision:** Restructured the English developer documentation from a single page (`website/content/developers/technical.en.md`) into a four-page Diataxis split, mirroring the structure already used for user docs: `getting-started.en.md` (tutorial — new content: prerequisites, clone/install/dev, a one-line-per-folder tour of `src/`, running the Playwright suite, and one small guided edit to close the dev loop before sending the reader to the how-to page), `how-to-guides.en.md` (how-to — four recipes: "Add a New Language" and "Add a New Theme" ported from the pre-existing `docs/how-to-add-a-language.md` and `docs/how-to-add-a-theme.md`, plus two new recipes, "Deploy TrainUs" extracted from the old §7 and "Add a Database Migration" synthesized from the old §2/§6 narrative into actual steps), `technical.en.md` (reference — kept the filename/weight-3 slot, trimmed of rationale prose and the deploy header recipes, otherwise unchanged: tech stack, project structure, schema, ER diagram, composables, the full §3 feature-by-feature description, routing, i18n, versioning, build commands, Annex A), and `understanding-the-architecture.en.md` (explanation — new: why no backend, why SQLite runs in a Worker, why the `opfs-sahpool` VFS with no fallback, why the migration system is designed the way it is, plus a condensed pass over a handful of notable per-feature design decisions — the save dialog vs. native picker, partial logs on exit, asymmetric side-flip strategies, per-combo-type rounds calculation — and the content-width-cap rationale). The two superseded `docs/how-to-add-a-*.md` files were replaced with "moved to the website" stubs, matching the existing pattern for `docs/technical-documentation.md` / `docs/user-documentation.md` (whose own stub was also corrected — it still pointed at the pre-split `website/content/docs/technical.en.md` path). `docs/release-procedure.md` and `AGENTS.md`'s Definition of Done checklist had the same stale path and were updated to `website/content/developers/technical.en.md` / `website/content/docs/user-guide/index.en.md`. French (`technical.fr.md`) is **not** restructured in this step — the user explicitly asked to start with English, since the existing how-to content was already English-only. Porting the split to French is left for later, the reverse order of how the user docs split was done (FR first, then EN). **Rationale:** The user-docs Diataxis split improved discoverability enough that the same treatment was requested for developer docs. Reusing the two pre-existing, real how-to documents as the seed for the how-to page (rather than writing recipes from scratch) kept the content grounded in material the project owner had already written and validated, and matches the precedent of `docs/technical-documentation.md`/`docs/user-documentation.md` being retired into stubs once their content moved to the website. ## 2026-06-16 — English user documentation ported to the FR Diataxis split **Decision:** Restructured the English user documentation from a single page (`user-guide.en.md`) into the same four-page Diataxis split already used for French (see the 2026-06-16 "French user documentation split Diataxis-style" entry below): `demarrage-rapide/index.en.md` (tutorial — what TrainUs is, local-first principles and trade-offs, GUI overview, the two modes; also kept the browser-requirements and PWA-install note that used to open the old single-page guide, since that's tutorial-appropriate content), `guides-pratiques.en.md` (how-to — same six recipes as the FR page, written natively rather than translated literally), `user-guide/index.en.md` (reference — the existing EN content reordered to match the FR section order, with the EMOM/AMRAP/TABATA combo screenshots and a Markdown cheat-sheet appendix added, both previously FR-only), and `comprendre.en.md` (explanation — why local-first, JSON vs SQLite, import behaviour, timer logic). The reference page kept the `user-guide` bundle name (no language-specific renaming) so it still pairs as a Hugo translation with `user-guide/index.fr.md`; the other three pages also share their basename with the FR files for the same reason, but get an English-friendly rendered URL via `slug:` front matter (`/en/docs/getting-started/`, `/en/docs/how-to-guides/`, `/en/docs/understanding-trainus/`) instead of inheriting the French words. Screenshots are genuinely English-UI captures (not reused FR images) — generated via `scripts/screenshots/all_pages.py --lang en` and `scripts/screenshots/combo_timers.py --lang en`, which already supported a `--lang` flag added during the FR work specifically for this future EN port. **Rationale:** The FR rewrite's decision log explicitly deferred this ("English documentation is **not** restructured ... porting it to the same split is left for later"); the user asked for that follow-up now that they're happy with the FR result. Reusing the FR page structure (rather than re-deriving an EN-specific organization) keeps the two languages structurally interchangeable, which matters for the lang-switcher and for anyone maintaining both versions going forward. English slugs were chosen over French-derived ones because the rendered URL is user-facing and worth getting right, while the underlying filename is purely an internal pairing mechanism Hugo already required to match. ## 2026-06-16 — Corrected user-guide claims about delete cascade behaviour **Decision:** Fixed `website/content/docs/user-guide.en.md` and `website/content/docs/user-guide/index.fr.md`: the "Deleting a Session" section wrongly claimed execution history is "preserved" with a "missing session reference" after deleting a session — that described the `ON DELETE SET NULL` alternative considered (and rejected) in R15 of `docs/improvement-plan.md`, not the cascade behaviour actually shipped. Corrected both pages to state that deleting a session also permanently deletes its execution history, and that the confirmation dialog shows the affected count beforehand. Also added the equivalent missing detail to "Deleting an Exercise" (EN had no mention of history impact at all; FR already had a partial note, now aligned with the same confirm-count wording). No code changed — `src/db/schema.js` keeps `ON DELETE CASCADE` on `execution_log.session_id` and `execution_log_item.exercise_id`, confirmed still in effect via a live Playwright/Firefox check against the actual app. **Rationale:** R15 (`docs/improvement-plan.md`, implemented, see `docs/plan.md` R15 row) chose the "minimum" fix — count-aware confirm dialogs (`exercises.deleteConfirmWithHistory` / `sessions.deleteConfirmWithHistory`) — over the more invasive `SET NULL` schema change, but the user-guide was never updated to match and kept describing the rejected option. Leaving it as-is would mislead users into believing deleted sessions' history survives. --- ## 2026-06-16 — Separate user docs and developer docs into two Hugo sections **Decision:** Replaced the `audience: "user"|"contributor"` front matter tag (added earlier the same day, see entry below) with an actual structural split: contributor docs moved from `website/content/docs/` to a new `website/content/developers/` section (`technical.fr.md`, `technical.en.md`, unchanged content), each with its own top-level nav entry — "Documentation" / "Développeurs" (FR) and "Documentation" / "Developers" (EN) in `hugo.toml`. The `audience` field is now redundant (the section itself is the signal) and was removed from all six docs/developers pages. The shared list rendering (sort by weight, front-matter description, top-level heading preview) was extracted from `layouts/docs/list.html` into `layouts/partials/doc-list.html` so the new `layouts/developers/list.html` doesn't duplicate it. `content/docs/_index.*.md` descriptions were updated to drop the now-inaccurate "technical reference" mention; new `content/developers/_index.fr.md` / `_index.en.md` were added. **Rationale:** A front-matter tag with no visible effect doesn't actually separate the two audiences for a visitor — landing on `/docs/` still mixed user guides with technical/architecture content under one menu link. Two sections with distinct nav entries make the split obvious without requiring readers to know the tag exists. Nothing was deployed yet, so breaking the old `/docs/technical...` URLs has no cost. --- ## 2026-06-16 — French user documentation split Diataxis-style **Decision:** Rewrote the French user documentation (previously a stub redirecting to the English page) as four pages under `website/content/docs/`: `demarrage-rapide/` (tutorial — local-first principles, GUI overview, the two modes), `guides-pratiques.fr.md` (how-to — sending a session to a friend, importing JSON, backing up data, syncing two devices, Yoga/running tips), `user-guide/` (reference — kept this filename for continuity with `user-guide.en.md`; full screen-by-screen description, keyboard shortcuts, a Markdown cheat-sheet table), and `comprendre.fr.md` (explanation — why local-first, JSON vs SQLite, how EMOM/AMRAP/TABATA timers work). Added `audience: "user"` / `"contributor"` front matter on all docs pages as a foundation for future audience tagging (no visual badge built yet). The two pages with screenshots (`demarrage-rapide/`, `user-guide/`) are Hugo leaf bundles (`index.fr.md` + images alongside) so image references stay relative, avoiding the `baseURL` prefix bug that affects absolute `/static/...` paths. `website/layouts/docs/list.html` already sorts by `.Pages.ByWeight`, so the five FR docs pages order correctly without further changes. English documentation is **not** restructured — `user-guide.en.md` stays a single page; porting it to the same split is left for later. Screenshots were generated with `scripts/screenshots/all_pages.py` (fixed: it hung on the native `window.confirm()` shown when leaving an in-progress execution screen — added a `page.on("dialog", ...)` auto-accept handler; also added a `--lang en|fr` flag so screenshots can be captured in either UI language) and a new `scripts/screenshots/combo_timers.py` script (seeds an EMOM and a TABATA combo to capture the EMOM/TABATA running screens and the TABATA rest screen, none of which `all_pages.py` covers since it only seeds an AMRAP combo). **Rationale:** `docs/specification.md`'s "Réécriture Documentation" chapter asked for a Diataxis-inspired structure distinguishing contributor vs. user docs, and explicitly requested the two new how-to guides (backup, device sync) plus combo timer screenshots including the TABATA rest screen. Page bundles were chosen over `static/` to sidestep a known baseURL bug rather than fix it as part of an unrelated doc change. The reference page kept the `user-guide` filename (vs. renaming to something Diataxis-flavored) to preserve the Hugo translation pairing with `user-guide.en.md` and avoid touching `AGENTS.md`'s named deliverable. --- ## 2026-06-12 — Single deployment + startup DB migration (Step 16) **Decision:** Replaced the "versioned deployments with isolated data" model. The latest build is deployed alone at the site root, the OPFS database lives at a stable location (`.trainus` directory, no app-version suffix), and schema changes are applied by an automatic migration step at startup. The `RELEASES` registry (`src/db/releases.js`) is the single source of truth: `adaptScript` was renamed `migrationScript` (one SQL script per `schemaVersion` bump) and is shared by the startup migration and the restore-adaptation pipeline. Each migration runs in a transaction together with its `schema_version` stamp; on failure it rolls back and the app shows a blocking error. Before any migration runs, a blocking screen forces the user to download a backup — the Update button is enabled only after the download. A DB newer than the app (`dbVersion > SCHEMA_VERSION`) fails safe with a blocking `DB_NEWER_THAN_APP` error, data untouched. Existing `.trainus-x.y.z` directories are orphaned on purpose (nothing has shipped). `SCHEMA_VERSION` stays at 1. **Rationale:** OPFS is _origin_-scoped, not path-scoped, so versioned deployment folders never truly isolated data — they only added deployment complexity while orphaning user data on every release, even without a schema change. The restore pipeline already paid the migration-code cost; reusing its scripts for startup migration removes the manual backup/restore UX on updates. Transactions guarantee a failed migration leaves the DB at the previous version; the forced pre-migration backup covers semantic corruption. --- ## 2026-06-09 — License display: dialog instead of dedicated page **Decision:** Replaced the `/license` route and `LicenseView.vue` page with a stacked modal approach. The About dialog now shows the notice text inline (rendered as Markdown from `src/assets/notice.md?raw`) and an added **License: GNU AGPL v3** row. Clicking "View License" opens `LicenseDialog.vue` on top of the About dialog; closing it returns to About without any navigation. **Rationale:** A full page navigation broke the "About" context and was disproportionate for legal text. A stacked modal keeps the user in place, matches the existing modal pattern, and the full license text is still bundled offline via `src/assets/license.md?raw`. --- ## 2026-06-08 — License: GNU AGPL v3 **Decision:** The project is licensed under the **GNU Affero General Public License v3 (AGPL v3)**. The full license text is in `LICENSE.md` at the repository root and `src/assets/license.md` (bundled offline via Vite `?raw`). The copyright notice is in `notice.md` / `src/assets/notice.md`. Contact: trainus.steadfast012@passmail.net. **Rationale:** AGPL v3 ensures that any modified version of the app served over a network must also publish its source. Bundling both files into the JS build guarantees offline availability, consistent with the PWA-first philosophy. --- ## 2026-06-08 — About dialog added to Settings **Decision:** Added an **About TrainUs** card at the bottom of the Settings page. Clicking it opens `AboutDialog.vue`, a modal that displays the app version (`__APP_VERSION__`), the DB schema version (`SCHEMA_VERSION`), the author (David Florance), placeholder links for the repository and website, and a disabled "View License" button with a "License to be determined." note. **Rationale:** Centralises version and authorship information in a discoverable place without cluttering the main UI. Placeholder links and the disabled license button provide the correct structure now so they can be filled in once the repository is published and a license is chosen, requiring only a URL swap rather than a structural change. --- ## 2026-06-07 — EMOM repetitions semantics corrected **Decision:** For EMOM combo items in a session, `repetitions` now means **total duration in minutes** (not number of rounds). The queue builder computes `totalRounds = floor(repetitions / numExercises)`, so each exercise still occupies exactly one 60-second slot and the total execution time equals `repetitions` minutes. The session form now labels the field **Duration (min)** for EMOM items, consistent with AMRAP. **Rationale:** The previous behaviour (`totalRounds = repetitions`) made the total EMOM duration grow with the number of exercises in the combo, which was counter-intuitive and inconsistent with how AMRAP works. Users think in minutes when setting up a timed workout. The new formula keeps the interface label and the actual timer duration consistent. --- ## 2026-06-07 — Home page: history section, inline exercise summaries, HTML export **Decision:** - Increased recent sessions from 3 to 4. - Added a **History section** below recent sessions: all execution logs loaded at mount, grouped by calendar day (descending), filtered client-side by two date pickers (default last 30 days). No extra DB query on filter change. - Each session card (recent and history) now shows an inline compact summary of exercises performed (reps, weight, timing) and a **play button (▶)** to re-execute. - Added a **HTML export button** in the action bar (visible only when data exists) via `useHtmlExport.js`. The generated file is fully self-contained: app icon embedded as base64, inline CSS, no external dependencies. Play buttons, navigation links, and date pickers are excluded from the export. **Rationale:** The history section turns the home page into a lightweight training journal without a dedicated log view. Client-side filtering avoids extra DB queries on every date change. The HTML export provides a shareable, printable snapshot of activity that works in any browser without the app. --- ## 2026-06-02 — Add Danish locale **Decision:** Added Danish (`da`) as a supported UI language, with a full translation of all keys in `src/i18n/locales/da.json`, registered in `src/i18n/index.js`, and selectable via the Settings language dropdown. **Rationale:** Demonstrates the language addition workflow described in `docs/how-to-add-a-language.md` and broadens the app's reach to Danish speakers. --- ## 2026-06-02 — Test fixture strategy for import/export tests **Decision:** Static JSON files in `tests/fixtures/` for cross-user scenarios with fixed IDs; dynamic construction (via `page.evaluate`) for same-user roundtrip scenarios. **Rationale:** Cross-user tests require known, stable UUIDs so assertions can reference them directly — a static file is self-documenting and removes multi-line `json.dumps` blocks from test code. Same-user roundtrip tests need the live `user_uuid` from the browser DB at test time, so they must be built dynamically. Two fixture files: `cross_user_id_collision.json` (extracted from the existing combo FK test) and `cross_user_full.json` (exercises + combo + session with both exercise and combo items + collection + execution log, all referencing fixture UUIDs in the `facade00-*` range). --- ## 2026-05-31 — Add TrainUs theme **Decision:** Add a third theme ("TrainUs") that uses the app logo colours — blue `#1E7FC0` (navbar, primary button, input border) and teal `#12A892` (action bar, hover state, success colour) — on a light blue-tinted background. **Rationale:** Provides a branded colour experience beyond the generic light/dark pair, matching the visual identity already present in the SVG icon. --- ## 2026-05-26 — Step 10: PWA Finalization & Polish ### Service worker: vite-plugin-pwa (Workbox) over hand-rolled sw.js - **Context:** The original `public/sw.js` used `self.__APP_VERSION__` which Vite never substitutes in a plain static file, causing the cache name to always fall back to `1.0.0`. No precaching meant the app did not work offline. - **Decision:** Replace with `vite-plugin-pwa` + Workbox. `registerType: 'autoUpdate'` silently activates new SW versions. `maximumFileSizeToCacheInBytes: 10 MB` to cover `sqlite3.wasm`. `manifest: false` to keep our hand-authored `public/manifest.json`. - **Rationale:** Workbox handles content-hash-based cache busting automatically; the app shell and WASM binary are all precached at install time, enabling true offline support. ### Install prompt: composable singleton + action bar button - **Context:** The `beforeinstallprompt` event must be captured at page load, before it fires. - **Decision:** Module-level event listeners in `useInstallPrompt.js` (singleton ref `canInstall`). `InstallPrompt.vue` teleports a download icon + dismiss × into the action bar when `canInstall` is true; dismissed for the session only (no persistence needed). - **Rationale:** Singleton ensures the event is captured regardless of when the composable is called. Session-only dismiss avoids noisy storage writes; user will see it again on next visit if they haven't installed. ### Accessibility scope: targeted, not exhaustive - **Context:** "ARIA labels, keyboard nav, focus management" could mean auditing every component. - **Decision:** Focus on critical flows — navigation, all forms, execution stepper, modal dialogs. Every icon-only actionbar button across all views also gets `aria-label` (mechanical addition of 1 attribute per button). - **Rationale:** Screen reader coverage for the flows a user actually executes is more valuable than a theoretical full audit. Full ARIA audit deferred to Step 11. ## 2026-05-26 — Step 9: Statistics ### Chart library: Chart.js 4 + vue-chartjs 5 - **Context:** Plan specified Chart.js via vue-chartjs; needed Vue 3 Composition API support. - **Decision:** `chart.js@^4` + `vue-chartjs@^5`. Registered only the needed Chart.js components (tree-shaking) in `StatsView.vue` rather than globally. - **Rationale:** vue-chartjs 5 is the Vue 3-compatible major version; registering locally avoids side effects in other routes that don't use charts. ### Streak calculation: UTC midnight for date comparison - **Context:** SQLite `DATE(started_at)` extracts the UTC calendar date from ISO-8601 timestamps. JavaScript `new Date().setHours(0,0,0,0)` uses local time, creating a mismatch in non-UTC timezones. - **Decision:** Use `setUTCHours(0,0,0,0)` and `toISOString().slice(0,10)` for streak iteration. - **Rationale:** Ensures the JS streak walk and the SQLite date grouping use the same calendar boundary; avoids streak being under-reported by one day in timezones ahead of UTC. ### Volume metric: reps × weight - **Context:** Need a single "effort" number per session to chart over time. - **Decision:** Volume = `SUM(reps_done × weight_used)` for completed items. Bodyweight exercises (weight=0) contribute 0, so the chart only shows weight-bearing volume. - **Rationale:** Standard fitness volume definition; keeps the metric meaningful. Users can still track bodyweight frequency via the Frequency chart. ### Statistics export: deferred to Step 9b - **Context:** User requested statistics export be planned. - **Decision:** Defer CSV/summary export to Step 9b; not included in Step 9. - **Rationale:** Step 9 scope is already substantial; export logic is independent and can be added without touching existing stats code. ## 2026-03-10 — Initial Decisions ### Framework: Vue 3 + Vite - **Context:** App has complex CRUD forms, reactive i18n/theme, timers, drag-reorder, charts. - **Decision:** Vue 3 with Composition API over Vanilla JS. - **Rationale:** Vanilla JS would require reinventing reactivity and component lifecycle. Vue provides reactive data binding, composables, and a rich ecosystem while staying lightweight. ### i18n: i18next + i18next-vue - **Context:** Spec requires multi-language support, easy to translate. - **Decision:** Use i18next (open-source, MIT) rather than a custom solution. - **Rationale:** Proven library with JSON locale files, pluralization, interpolation, reactive Vue integration. ### Database: Official SQLite WASM + OPFS - **Context:** Spec requires SQLite with OPFS persistence, local-first. - **Decision:** Official SQLite WASM build with OPFS VFS. - **Rationale:** Best maintained, native OPFS support, Apache-licensed. ### App version strategy - **Context:** Need version at build time (OPFS namespacing, SW) and runtime (export metadata). - **Decision:** Source of truth in `package.json`, injected by Vite `define`, stored in DB `settings` on init. ### Single user per device - **Context:** Spec says one user per device. - **Decision:** No `users` table. UUID + name stored in `settings` key/value table. ### Charts: Chart.js via vue-chartjs - **Context:** Statistics page needs progression, volume, frequency charts. - **Decision:** Chart.js — good balance of features (~60KB gzipped) and ease of use. ### Routing: Hash mode - **Context:** App deployed as static files, no server-side routing control. - **Decision:** Vue Router with `createWebHashHistory()`. - **Rationale:** Hash routes (`#/home`) work without server configuration. ### ID collision on import: UUID prefix - **Context:** Users exchange data files; IDs may collide. - **Decision:** When imported entity ID exists and belongs to a different user (by UUID), prefix imported ID with source user's short UUID (first 8 chars). --- ## 2026-03-13 — Step 1: Database Layer ### OPFS VFS: SAH Pool over standard OPFS - **Context:** Two OPFS VFS options available: standard `opfs` VFS (requires sub-worker + SharedArrayBuffer) and `opfs-sahpool` (SyncAccessHandle pool). - **Decision:** Use `opfs-sahpool` VFS. - **Rationale:** Highest performance, works on all major browsers since March 2023, no COOP/COEP requirement (though we have them), simpler architecture (no sub-worker). Trade-off: no concurrent connections, which is fine for a single-user PWA. ### Worker-based architecture - **Context:** OPFS APIs are only available in Web Worker threads. - **Decision:** Run SQLite in a dedicated Web Worker (`src/db/worker.js`), communicate from main thread via `postMessage()` with Promise-based wrappers. - **Rationale:** Clean separation of concerns, non-blocking main thread, required by OPFS. ### Table naming: `collection` instead of `group` - **Context:** `GROUP` is a SQL reserved keyword. Originally named `grp`, but that was confusing. - **Decision:** Rename `grp` → `collection` and `group_session` → `collection_session`. - **Rationale:** `collection` clearly describes a set of sessions without SQL keyword conflicts. ### WASM file serving strategy - **Context:** The `sqlite3.wasm` binary must be accessible at a known URL for the worker to load it. - **Decision:** Copy to `public/` via a Vite plugin (`copySqliteWasm`), serve at `/sqlite3.wasm`. Added to `.gitignore`. - **Rationale:** Works identically in dev and production. No complex asset pipeline needed. ### Repository + composable pattern - **Context:** Need CRUD access from Vue components through an async worker-based DB. - **Decision:** Two layers — repositories (pure async CRUD) and composables (reactive `ref` wrappers). - **Rationale:** Repositories are reusable outside Vue (e.g., import/export). Composables provide reactive state for components. Clean separation per SOLID. ### No in-memory fallback — block app if OPFS unavailable - **Context:** Originally the worker fell back to `:memory:` if OPFS was unavailable, meaning data would be silently lost on browser close. - **Decision:** Remove the fallback. If OPFS init fails, propagate the error and display a full-screen error message asking the user to update their browser or exit private browsing. - **Rationale:** Silent data loss is worse than a clear error. OPFS is supported on all major browsers since March 2023 (Firefox 111, Chrome 102, Safari 16.4). The only realistic failure cases are private browsing mode or very outdated browsers. ### Loading screen during DB initialization - **Context:** Database initialization is async and may take a moment. - **Decision:** Block the app with a spinner overlay until the DB is ready. Three states: loading → ready → error. - **Rationale:** Prevents users from interacting with the app before data is available. ### Rename `asymmetric_mode` to `alternate` - **Context:** The `asymmetric_mode` TEXT field was vague. The intent is a boolean: should the exercise alternate sides at each repetition? - **Decision:** Replace `asymmetric_mode TEXT` with `alternate INTEGER NOT NULL DEFAULT 0` (boolean, 0/1). - **Rationale:** Simpler, clearer semantics. Boolean stored as INTEGER per SQLite convention. ### COMBO type: added NONE - **Context:** Combos can be SUPERSET, CIRCUIT, or EMOM, but standalone exercises (not grouped) also appear as session items. - **Decision:** Add `'NONE'` as a valid combo type. - **Rationale:** Allows a "combo" of a single exercise, simplifying session item management — every item is a combo reference. ### Session item repetitions - **Context:** A session may include the same combo/exercise multiple times (e.g., 3 rounds of a circuit). - **Decision:** Add `repetitions INTEGER NOT NULL DEFAULT 1` to `session_item`. - **Rationale:** Avoids duplicating rows for repeated items. Keeps data normalized. ### Home page: activity summary instead of collection list - **Context:** Originally the Home page was the collection list. - **Decision:** Home shows last 3 executed sessions and session counts for the last 7/30 days. - **Rationale:** More useful landing page — quick overview of recent activity. ### Collections get their own page - **Context:** Collections were displayed on the Home page. - **Decision:** Move collections to a dedicated route (`#/collections`) with its own nav link. - **Rationale:** Cleaner information architecture. Home focuses on activity summary. --- ## 2026-03-15 — Step 2: Settings ### Settings page: v-model for select elements - **Context:** Using `:value` binding on `` option selection. ### App shell: guard content behind DB readiness - **Context:** The `
` section (containing ``) was outside the `v-if/v-else` chain in `App.vue`, so views mounted before the DB was ready. This caused `onMounted` hooks in views to fail when calling DB queries. - **Decision:** Wrap both `