71 KiB
| title | description | weight | date |
|---|---|---|---|
| Référence | Architecture, schéma de base de données, fonctionnalités, routage, i18n, versionnement et commandes de build — la référence technique complète. | 3 | 2026-06-16 |
Cette page décrit chaque partie de TrainUs en détail : pile technique, schéma, routes, composables. Pour une première prise en main guidée, voir [Démarrage rapide]({{< relref "getting-started.fr.md" >}}) ; pour des tâches concrètes, voir [Guides pratiques]({{< relref "how-to-guides.fr.md" >}}) ; pour le « pourquoi » des choix, voir [Comprendre l'architecture]({{< relref "understanding-the-architecture.fr.md" >}}).
Paternité IA. La grande majorité du code source de ce projet a été écrite par des assistants IA (principalement Claude). L'auteur humain a dirigé l'architecture, relu chaque modification, guidé l'implémentation et pris toutes les décisions de conception et de produit — mais si l'auteur, c'est celui qui tape les lignes, ce crédit revient en grande partie à l'IA. Cela est indiqué ici par souci de transparence, et non comme une mise en garde : le code a été relu et validé à chaque étape.
1. Architecture
TrainUs est une Progressive Web Application (PWA) qui tourne entièrement dans le navigateur — il n'y a pas de serveur backend, et toutes les données sont stockées localement via SQLite WASM avec une persistance OPFS (Origin Private File System). L'application fonctionne hors ligne après le premier chargement et peut être installée sur l'écran d'accueil. Voir [Comprendre l'architecture]({{< relref "understanding-the-architecture.fr.md" >}}) pour le pourquoi de ces choix.
Pile technique
| Couche | Technologie |
|---|---|
| Framework UI | Vue 3 (Composition API) |
| Build | Vite |
| Routage | Vue Router (mode hash) |
| i18n | i18next + i18next-vue |
| Base de données | SQLite WASM + OPFS |
| Graphiques | Chart.js 4 + vue-chartjs 5 |
| Icônes | Bootstrap Icons |
| Markdown | marked + DOMPurify |
| PWA | vite-plugin-pwa + Workbox |
| Formatage | Prettier |
| Tests | Playwright (Python) |
| Licence | GNU AGPL v3 |
Structure du projet
trainUs/
├── index.html # Point d'entrée
├── package.json # Dépendances & scripts npm
├── vite.config.js # Configuration Vite + PWA + CORS
├── public/
│ ├── manifest.json # Manifeste PWA (écrit à la main)
│ ├── icon-192.png # Icône PWA 192×192
│ ├── icon-512.png # Icône PWA 512×512
│ └── sqlite3-opfs-async-proxy.js # Proxy OPFS async pour le worker SQLite
├── src/
│ ├── main.js # Démarrage de l'app (init DB, restauration des réglages)
│ ├── App.vue # Composant racine (layout général, navigation)
│ ├── router/
│ │ └── index.js # Vue Router — les 21 routes
│ ├── views/ # Composants de page (un par route)
│ ├── components/ # Composants UI réutilisables
│ ├── composables/ # Composables Vue (données réactives & logique)
│ ├── db/
│ │ ├── database.js # Gestionnaire de DB (pont vers le worker)
│ │ ├── worker.js # Web Worker (SQLite WASM + OPFS)
│ │ ├── schema.js # DDL + SCHEMA_VERSION + MIGRATIONS
│ │ ├── releases.js # Registre des releases pour la compatibilité de restauration
│ │ └── repositories/ # Couche d'accès aux données, par entité
│ ├── i18n/
│ │ ├── index.js # Initialisation i18next
│ │ └── locales/
│ │ ├── en.json # Traductions anglaises
│ │ └── fr.json # Traductions françaises
│ ├── styles/
│ │ ├── variables.css # Variables CSS personnalisées (thèmes clair/sombre)
│ │ └── layout.css # Mise en page globale, navigation, barre d'action
│ └── utils/ # Fonctions utilitaires pures (validation, dateFormatter, markdown, …)
├── docs/ # Toute la documentation
└── tests/ # Suite de tests Playwright en Python
Architecture thread principal / Worker
SQLite tourne dans un Web Worker pour accéder aux API OPFS, indisponibles sur le thread principal.
Thread principal Thread Worker
┌──────────────────────┐ ┌──────────────────┐
│ database.js │──postMsg()──▶│ worker.js │
│ exec / run / │◀──postMsg()──│ sqlite3 WASM │
│ selectAll / │ │ stockage OPFS │
│ exportDatabase / │ └──────────────────┘
│ importDatabase │
├──────────────────────┤
│ repositories/ │ CRUD asynchrone pur, par entité
├──────────────────────┤
│ composables/ │ Wrappers réactifs pour Vue
└──────────────────────┘
La communication est entièrement basée sur des Promises : database.js enveloppe chaque postMessage() dans une Promise résolue par un ID de réponse correspondant.
En-têtes COOP / COEP
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Voir [Guides pratiques → Déployer TrainUs]({{< relref "how-to-guides.fr.md" >}}#déployer-trainus) pour la configuration côté serveur, et [Comprendre l'architecture]({{< relref "understanding-the-architecture.fr.md" >}}) pour savoir pourquoi le VFS utilisé ici n'en a pas strictement besoin.
Design responsive
| Seuil | Mise en page |
|---|---|
| > 700 px | Menu latéral fixe (liens avec libellés) |
| ≤ 700 px | Barre de navigation en bas (icônes + libellés) |
| ≤ 400 px | Navigation en bas, icônes seules |
Thèmes
Les thèmes clair et sombre sont implémentés via des variables CSS personnalisées sur :root / [data-theme="dark"]. Le thème est conservé dans la table settings et appliqué avant le premier rendu (pas de flash).
2. Couche base de données
VFS : OPFS SyncAccessHandle Pool
L'application utilise le VFS opfs-sahpool :
- E/S synchrones via
FileSystemSyncAccessHandle - Fonctionne sur tous les navigateurs majeurs depuis mars 2023 (Firefox 111+, Chrome 102+, Safari 16.4+)
- Ne nécessite pas les en-têtes COOP/COEP (même si nous les avons)
- Connexion unique seulement (suffisant pour une PWA mono-utilisateur)
Si OPFS n'est pas disponible (navigation privée, navigateur ancien), l'application affiche une erreur en plein écran. Il n'y a pas de repli en mémoire. Voir [Comprendre l'architecture]({{< relref "understanding-the-architecture.fr.md" >}}) pour savoir pourquoi ce VFS a été choisi et pourquoi il n'y a pas de repli.
Schéma
12 tables, toutes définies avec CREATE TABLE IF NOT EXISTS (idempotent). Le SCHEMA_VERSION actuel est 1.
| Table | Rôle |
|---|---|
schema_version |
Suivi des migrations (version + applied_at) |
settings |
Configuration clé-valeur (user_uuid, nom, thème, …) |
exercise |
Définitions d'exercices (title UNIQUE) |
combo |
Définitions de combos (title UNIQUE) |
combo_exercise |
Jonction combo ↔ exercice (position, reps, poids) |
session |
Définitions de séances (title UNIQUE) |
session_item |
Jonction séance ↔ élément (type, position, reps, poids) |
collection |
Collections (label UNIQUE) |
collection_session |
Jonction collection ↔ séance (position) |
execution_log |
Journaux d'exécution (started_at, finished_at) |
execution_log_item |
Résultats d'exécution par exercice (reps, poids, côté, …) |
suffix |
Registre des utilisateurs externes, pour désambiguïser l'import |
Diagramme entité-relation
erDiagram
SETTINGS {
text key PK
text value
}
EXERCISE {
text id PK
text title UK
text description
boolean asymmetric
boolean alternate
text image_urls
text video_urls
integer default_reps
real default_weight
text created_by
}
COMBO {
text id PK
text title UK
text description
text type
text timer_config
text created_by
}
COMBO_EXERCISE {
text combo_id FK
text exercise_id FK
integer position
integer default_reps
real default_weight
}
SESSION {
text id PK
text title UK
text description
text created_by
}
SESSION_ITEM {
text session_id FK
text item_id
text item_type
integer position
integer repetitions
real weight
}
COLLECTION {
text id PK
text label UK
text created_by
}
COLLECTION_SESSION {
text collection_id FK
text session_id FK
integer position
}
EXECUTION_LOG {
text id PK
text session_id FK
datetime started_at
datetime finished_at
}
EXECUTION_LOG_ITEM {
text id PK
text log_id FK
text exercise_id FK
text combo_id
integer round_number
integer reps_done
real weight_used
text side
boolean completed
datetime timestamp
}
SUFFIX {
text user_uuid PK
text user_name
text suffix UK
}
COMBO ||--|{ COMBO_EXERCISE : contains
EXERCISE ||--o{ COMBO_EXERCISE : "used in"
SESSION ||--|{ SESSION_ITEM : contains
COLLECTION ||--|{ COLLECTION_SESSION : contains
SESSION ||--o{ COLLECTION_SESSION : "used in"
SESSION ||--o{ EXECUTION_LOG : "executed as"
EXECUTION_LOG ||--|{ EXECUTION_LOG_ITEM : records
EXERCISE ||--o{ EXECUTION_LOG_ITEM : tracks
Détails de colonnes notables :
session_item.item_type:"exercise"ou"combo"— utilisé pour faire un JOIN avec la bonne table pour récupérer les titresexecution_log_item.combo_id/round_number: NULL pour les exercices autonomes ; renseignés pour les exercices exécutés au sein d'un combo (pas de contrainte FK — le combo peut être supprimé après exécution)- Clés de
settings:user_uuid,user_name,app_version,language,theme,db_created_at,last_backup_at,backup_reminder_days
Contraintes UNIQUE
L'unicité des titres/libellés est imposée au niveau de la base sur exercise.title, combo.title, session.title, collection.label. Les violations remontent comme des erreurs UNIQUE constraint failed, gérées par les repositories.
Table suffix
Stocke la correspondance entre les UUID d'utilisateurs externes et les suffixes qui leur sont attribués. Utilisée pendant l'import pour désambiguïser les titres provenant d'autres utilisateurs (voir §3.9).
Système de migration
Au démarrage, database.js envoie un message getSchemaStatus au worker, qui détecte l'état de la base sans modifier les données existantes :
- Base neuve (pas de ligne dans
schema_version) : exécuteSCHEMA_SQLet estampilleSCHEMA_VERSION. - À jour (
dbVersion === SCHEMA_VERSION) : continue normalement. - Plus récente que l'application (
dbVersion > SCHEMA_VERSION) : erreur bloquante (DB_NEWER_THAN_APP), données intactes. - Plus ancienne que l'application (
dbVersion < SCHEMA_VERSION) : l'application s'arrête sur un écran de migration bloquant. L'utilisateur doit d'abord télécharger une sauvegarde ; ce n'est qu'alors que le bouton Mettre à jour s'active, ce qui envoierunMigrationsau worker.
runMigrations applique les scripts de migration en attente, fournis par getMigrationScripts(dbVersion, SCHEMA_VERSION) dans src/db/releases.js. Le script de chaque release s'exécute dans une transaction avec son INSERT INTO schema_version (version) VALUES (N) ; en cas d'erreur, la transaction est annulée, la base reste à la version précédente, et une erreur bloquante DB_MIGRATION_FAILED est affichée (l'utilisateur dispose de la sauvegarde téléchargée juste avant).
flowchart TD
A[Démarrage app : db.init] --> B[Worker : ouvre le dossier OPFS pool .trainus, ouvre trainus.db]
B --> C{Table schema_version\nexiste avec des lignes ?}
C -- non : base neuve --> D[Exécute SCHEMA_SQL\nINSERT schema_version = SCHEMA_VERSION]
C -- oui --> E{dbVersion vs SCHEMA_VERSION}
E -- égal --> F[À jour — continue]
E -- "dbVersion > app" --> G[Erreur bloquante\nBase intacte]
E -- "dbVersion < app" --> H[Écran de migration bloquant\nBase intacte, app en pause]
H --> H2[Utilisateur clique Télécharger la sauvegarde\nexportDatabase + formatBackupFilename\nmise à jour de last_backup_at]
H2 --> H3[Bouton Mettre à jour activé\nutilisateur clique Mettre à jour]
H3 --> I[Pour chaque release N de dbVersion+1 à SCHEMA_VERSION :\nBEGIN ; migrationScript ; INSERT schema_version N ; COMMIT]
I -- erreur --> J[ROLLBACK\nErreur bloquante\nBase toujours à la version précédente + utilisateur a la sauvegarde]
I -- ok --> F
D --> K[_initDefaults : lignes settings]
F --> K
K --> L[Application prête]
Les changements de schéma en place (ajouter des colonnes directement dans SCHEMA_SQL) sont acceptables avant toute publication auprès des utilisateurs. Une augmentation de SCHEMA_VERSION est réservée aux changements de schéma faits après qu'une version a été déployée — à ce moment, la release embarque un migrationScript dans RELEASES (voir §6). Voir [Guides pratiques → Ajouter une migration de base de données]({{< relref "how-to-guides.fr.md" >}}#ajouter-une-migration-de-base-de-données) pour la recette pas à pas.
Pattern Repository
Chaque entité a un repository dédié dans src/db/repositories/ fournissant les méthodes standards : create, getById, getAll, update, delete, search. La gestion des tables de jonction est intégrée au repository parent (par exemple setExercises() dans comboRepository).
Les repositories n'utilisent que database.exec(), database.run() et database.selectAll() — aucune construction spécifique à SQLite hors de cette couche.
Composables Vue
Les wrappers réactifs dans src/composables/ exposent un état basé sur des ref (items, loading, error) et des méthodes asynchrones, à destination des composants Vue. Les composants n'importent jamais les repositories directement ; ils passent par les composables.
| Composable | Enveloppe |
|---|---|
useExercises |
exerciseRepository |
useCombos |
comboRepository |
useSessions |
sessionRepository |
useCollections |
collectionRepository |
useExecutionLogs |
executionLogRepository |
useSettings |
settingsRepository |
useStats |
Plusieurs repositories (agrégats en lecture seule) |
useSessionExecution |
executionLogRepository + logique de file |
useJsonExport |
Tous les repositories + jsonEnvelope.js |
useJsonImport |
Tous les repositories + suffixRepository |
useBackupStatus |
settingsRepository |
useInstallPrompt |
Événement beforeinstallprompt (API navigateur) |
useHtmlExport |
Rendu de données pur — aucun appel à un repository |
3. Fonctionnalités
3.1 Réglages
Vue : src/views/SettingsView.vue — #/settings
Restauration au démarrage : au lancement de l'application (src/main.js), après l'initialisation de la base et avant le premier rendu, language et theme sont lus depuis settings et appliqués. Cela évite tout flash de valeurs par défaut.
Réglages disponibles
| Réglage | Contrôle | Clé de persistance |
|---|---|---|
| Langue | <select> (English / Français / Dansk) |
settings.language |
| Nom d'utilisateur | Champ texte (requis, non vide) | settings.user_name |
| Identifiant utilisateur | Affichage UUID en lecture seule | settings.user_uuid |
| Thème | <select> (Clair / Sombre) |
settings.theme |
| Volume sonore | Curseur (0–100 %) + bouton Tester | settings.audio_volume |
| Télécharger la DB | Bouton → télécharge un .sqlite3 |
— |
| Restaurer la DB | Sélecteur de fichier → pipeline de validation | — |
| Initialiser la DB | Bouton + case à cocher obligatoire | — |
| Intervalle de rappel de sauvegarde | Champ numérique (1–365, défaut 7) | settings.backup_reminder_days |
| Exporter des données | Bouton → boîte de filtre par entité | — |
| Importer des données | Bouton → sélecteur de fichier | — |
| Gestion des suffixes | Liste des suffixes enregistrés | table suffix |
| Crédits | Bouton → fenêtre modale (attributions icônes/bibliothèques) | — |
| À propos | Bouton → fenêtre modale (version, auteur) | — |
Le volume sonore est détaillé en § 3.7 Signaux sonores.
Télécharger la base de données
Appelle sqlite3_js_db_export() dans le worker. Le fichier SQLite brut est téléchargé sous le nom trainus-{version}-backup-YYYYMMDD-HHhMMmSSs.sqlite3 et last_backup_at est mis à jour en cas de succès.
Restaurer la base de données
Pipeline en plusieurs étapes, exécuté dans le worker :
flowchart TD
A[Utilisateur clique Restaurer] --> B[Sélecteur de fichier]
B --> C[Boîte de confirmation]
C -->|Annuler| Z[Abandon]
C -->|OK| D[Affiche le spinner]
D --> E[Désérialise dans une DB :memory:]
E -->|Fichier invalide| F[Erreur : INVALID_FILE]
E -->|Valide| G[Vérifie les tables requises]
G -->|Manquantes| H[Erreur : INCOMPLETE_DB]
G -->|Présentes| I[Lit les versions de schéma]
I --> J[Vérifie la compatibilité]
J -->|Incompatible| K[Erreur : INCOMPATIBLE_VERSION]
J -->|Compatible| L{Adaptation nécessaire ?}
L -->|Oui| M[Exécute les scripts d'adaptation]
M -->|Échec| N[Erreur : ADAPT_FAILED]
M -->|OK| O[Copie table par table dans une transaction]
L -->|Non| O
O -->|Échec| P[Erreur : RESTORE_FAILED]
O -->|OK| Q[Carte récapitulative de succès]
La copie table par table ne transfère que les colonnes présentes dans les deux schémas. L'opération entière s'exécute dans une transaction ; en cas d'échec, elle est annulée. Après succès, les réglages (langue, thème) sont réappliqués sur place, sans rechargement de page.
La compatibilité des versions est gérée par src/db/releases.js : un tableau RELEASES où chaque entrée déclare schemaVersion, appVersion, dbBreak, et un migrationScript optionnel. checkCompatibility() parcourt cet historique pour déterminer si un chemin de migration existe. Les mêmes scripts migrationScript pilotent à la fois l'adaptation de restauration et la migration au démarrage (§ Système de migration).
Initialiser la base de données
Supprime toutes les données de toutes les tables, dans l'ordre sûr pour les clés étrangères (enfants en premier), puis appelle _initDefaults() pour régénérer user_uuid, user_name, app_version, db_created_at. La page se recharge pour refléter le nouvel état. Une case de confirmation obligatoire empêche tout usage accidentel.
Fichiers clés :
src/views/SettingsView.vue, src/db/database.js, src/db/worker.js, src/db/releases.js, src/db/repositories/settingsRepository.js
Fenêtre « Crédits »
Une fenêtre modale ouverte depuis la carte Crédits de la page Réglages, attribuant les ressources et bibliothèques tierces :
| Élément | Licence | Source |
|---|---|---|
| Icône de l'app (Mighty Force) | CC BY 3.0 | game-icons.net, par Delapouite |
| Bootstrap Icons | MIT | icons.getbootstrap.com |
| Bibliothèques tierces | — | Vue, Vue Router, Vite, SQLite WASM, Chart.js, vue-chartjs, i18next, i18next-vue, Prettier, vite-plugin-pwa, marked, DOMPurify |
| Outils de test | — | Playwright, pytest |
Les noms et liens des icônes/bibliothèques sont écrits en dur dans le composant ; le texte d'attribution provient de l'espace de noms i18n credits.*.
Composant : src/components/CreditsDialog.vue
Fenêtre « À propos »
Une fenêtre modale ouverte depuis la carte À propos de TrainUs en bas de la page Réglages. Elle affiche :
| Champ | Source |
|---|---|
| Version de l'application | __APP_VERSION__ (variable globale Vite issue de package.json) |
| Version de la base de données | SCHEMA_VERSION (depuis src/db/schema.js) |
| Créé par | Texte fixe : David Florance |
| Dépôt | Lien provisoire (href="#") |
| Site web | Lien provisoire (href="#") |
| Voir la licence | Bouton désactivé, avec la note « Licence à déterminer. » |
Composant : src/components/AboutDialog.vue
Les clés i18n sont sous l'espace de noms about.* dans tous les fichiers de locale.
3.2 Gestion des exercices
Vues : ExerciseListView, ExerciseFormView, ExerciseDetailView
Routes : #/exercises, #/exercises/new, #/exercises/:id, #/exercises/:id/edit
Champs du formulaire
| Champ | Validation |
|---|---|
| Titre | Requis, UNIQUE (au niveau base) |
| Description | Optionnelle ; rendue en Markdown dans la vue détail (marked + DOMPurify) |
| Asymétrique | Booléen ; révèle la sous-option Alternance |
| Alternance | Bouton radio : alterner par répétition ou par série |
| Répétitions par défaut | ≥ 0 |
| Poids par défaut | ≥ 0 |
| URLs d'images | Chaque URL doit être une URL HTTP(S) valide |
| URLs de vidéos | Chaque URL doit être une URL HTTP(S) valide |
ExerciseFormView est partagée entre les modes création et édition, distingués via route.name (exercise-new vs exercise-edit).
Barre d'action (pattern Teleport)
Les vues injectent des boutons dans la barre d'action partagée via <Teleport to="#actionbar-actions" defer>. L'attribut defer (Vue 3.5+) résout la cible au cycle de rendu suivant, ce qui évite une condition de course avec le shell conditionné par v-else.
Utilitaire de validation
src/utils/validation.js fournit des validateurs réutilisables, qui renvoient des clés i18n :
validateRequired(value)→'validation.required'validateMin(value, min)→'validation.minValue'validateUrl(value)→'validation.invalidUrl'
Recherche
Barre de recherche activable, avec une temporisation de 250ms → exerciseRepository.search() (SQL LIKE %query%). Un bouton d'effacement (×) s'affiche dans le champ dès qu'il contient une valeur ; il vide le champ et lui redonne le focus. Vider le champ (via ce bouton, manuellement, ou en désactivant la recherche) appelle fetchAll(). Le même schéma (barre de recherche, temporisation, bouton d'effacement) est dupliqué dans ExerciseListView, ComboListView, SessionListView et CollectionsView plutôt que factorisé dans un composable partagé.
Intégration des médias
- Images : affichées dans une grille responsive, avec chargement différé. Un gestionnaire
@errormasque les images cassées. - YouTube : les ID de vidéo sont extraits des formats
youtube.com/watch?v=etyoutu.be/, et intégrés viayoutube-nocookie.com. - Autres URLs : affichées comme des liens cliquables.
Fichiers clés : src/views/ExerciseListView.vue, src/views/ExerciseFormView.vue, src/views/ExerciseDetailView.vue, src/utils/validation.js, src/utils/markdown.js, src/utils/youtube.js
3.3 Gestion des combos
Vues : ComboListView, CombosView, ComboFormView, ComboDetailView
Routes : #/combos, #/combos/new, #/combos/:id, #/combos/:id/edit
Un combo est un groupe ordonné d'exercices avec un format minuté optionnel (EMOM, AMRAP, TABATA, ou NONE pour de simples supersets).
Champs du formulaire
| Champ | Notes |
|---|---|
| Titre | Requis, UNIQUE |
| Description | Optionnelle ; rendue en Markdown dans la vue détail |
| Type | NONE / EMOM / AMRAP / TABATA |
| Configuration du minuteur | Durée (min) pour EMOM/AMRAP ; nombre de tours pour TABATA ; stocké en JSON |
| Exercices | Liste ordonnée avec, par exercice, répétitions (≥ 1) et poids (≥ 0) |
Chaque exercice du combo a ses propres default_reps et default_weight, stockés dans la table de jonction combo_exercise. Ces valeurs surchargent les valeurs par défaut de l'exercice lorsque le combo est utilisé dans une séance.
Ordonnancement des exercices
Les boutons monter/descendre ajustent la position. comboRepository.setExercises() remplace en une seule opération toute la table de jonction d'un combo (suppression + réinsertion).
Forme du retour de getExercises()
{
;(exerciseId, exerciseTitle, position, defaultReps, defaultWeight, asymmetric, alternate)
}
asymmetric et alternate sont inclus pour que le mode exécution puisse déterminer la gestion du côté sans requête supplémentaire.
Fichiers clés : src/views/ComboListView.vue, src/views/ComboFormView.vue, src/views/ComboDetailView.vue, src/db/repositories/comboRepository.js
3.4 Gestion des séances
Vues : SessionListView, SessionsView, SessionFormView, SessionDetailView, SessionExecuteView
Routes : #/sessions, #/sessions/new, #/sessions/:id, #/sessions/:id/edit, #/sessions/:id/execute
Une séance est une liste ordonnée d'éléments — chaque élément est soit un exercice, soit un combo, avec des répétitions et un poids propres à l'élément.
Éléments de séance
session_item utilise une paire polymorphe item_id + item_type :
item_type IN ('exercise', 'combo')
sessionRepository.getItems() exécute deux requêtes séparées (une avec JOIN sur exercise, une avec JOIN sur combo) puis fusionne les résultats triés par position. SQLite ne supporte pas les JOIN conditionnels entre différentes tables dans une seule requête.
Champs du formulaire
| Champ | Notes |
|---|---|
| Titre | Requis, UNIQUE |
| Description | Optionnelle ; rendue en Markdown dans la vue détail |
| Éléments | Liste ordonnée ; chaque élément a des répétitions (≥ 1) et un poids (≥ 0) |
Le sélecteur de type d'élément (exercice vs combo) contrôle quel menu déroulant est rempli. Les éléments peuvent être déplacés vers le haut/bas ou supprimés.
Sémantique des répétitions
Pour un élément de séance qui est un combo, repetitions = le nombre de passages complets dans la liste d'exercices du combo (pas les répétitions individuelles par exercice).
Fichiers clés : src/views/SessionListView.vue, src/views/SessionFormView.vue, src/views/SessionDetailView.vue, src/db/repositories/sessionRepository.js
3.5 Gestion des collections
Vues : CollectionsView, CollectionFormView, CollectionDetailView
Routes : #/collections, #/collections/new, #/collections/:id, #/collections/:id/edit
Une collection est un groupe ordonné de séances — utile pour organiser des programmes hebdomadaires ou des phases d'entraînement.
Champs du formulaire
| Champ | Notes |
|---|---|
| Libellé | Requis, UNIQUE |
| Séances | Liste ordonnée ; chaque séance ne peut apparaître qu'une fois |
collectionRepository.getAll() inclut un champ sessionCount, via une requête COUNT sur collection_session — ce qui évite de charger toutes les données de séance pour la vue liste.
Fichiers clés : src/views/CollectionsView.vue, src/views/CollectionFormView.vue, src/views/CollectionDetailView.vue, src/db/repositories/collectionRepository.js
3.6 Page d'accueil
Vue : HomeView — #/home
Page d'accueil avec un aperçu rapide de l'activité d'entraînement récente.
| Widget | Source de données |
|---|---|
| Séances des 7 derniers jours | executionLogRepository.getCountSince(isoDate) |
| Séances des 30 derniers jours | executionLogRepository.getCountSince(isoDate) |
| Les 4 séances les plus récentes | executionLogRepository.getRecentWithItems(4) (JOIN avec session et ses éléments) |
| Historique par jour (filtré) | executionLogRepository.getAllWithItems() — filtré côté client par plage de dates |
Séances récentes
Les 4 séances les plus récemment exécutées sont affichées sous forme de cartes. Chaque carte affiche le titre de la séance, la date d'exécution (formatée selon la locale via dateFormatter.js), la durée totale (quand finished_at est renseigné), un bouton lecture (▶) pour relancer la séance, et un résumé compact intégré de chaque exercice/combo réalisé, avec les répétitions, le poids et la durée par élément réellement effectués.
Le résumé intégré regroupe les éléments via les fonctions groupItems() / enrichItemsWithTiming() / comboRounds(), définies dans HomeView. Le minutage par élément est dérivé des écarts entre execution_log_item.timestamp consécutifs, relativement à execution_log.started_at.
Section historique
Tous les journaux d'exécution sont chargés une seule fois au montage via getAllWithItems(), puis filtrés côté client selon la plage de dates active (par défaut : les 30 derniers jours). Le résultat est regroupé par jour calendaire (du plus récent au plus ancien) via une computed Vue. Chaque groupe de jour a un en-tête et une carte compacte par séance, elle aussi avec durée, bouton lecture et résumé intégré des éléments.
Deux sélecteurs <input type="date"> (de / à) permettent d'ajuster la plage de façon interactive, sans aller-retour réseau.
Export HTML
Un bouton de la barre d'action (bi-filetype-html) ne s'affiche que si au moins un journal d'exécution existe. Cliquer dessus appelle useHtmlExport.exportHomeAsHtml(), qui :
- Récupère
/icon.svget l'encode en URIdata:base64 (TextEncoder+btoa). - Construit un document HTML entièrement autonome à partir de l'état réactif courant — cartes de statistiques, séances récentes, et historique filtré selon la plage de dates active.
- Retire les boutons lecture, les liens de navigation et les sélecteurs de date du résultat (export en lecture seule).
- Télécharge le fichier sous le nom
trainus-home-YYYY-MM-DD.html.
Fichiers clés : src/views/HomeView.vue, src/composables/useHtmlExport.js, src/db/repositories/executionLogRepository.js, src/utils/dateFormatter.js
3.7 Mode exécution
Vue : SessionExecuteView — #/sessions/:id/execute
Composable : useSessionExecution
Le mode exécution déroule une séance exercice par exercice, en enregistrant les répétitions, le poids et le côté réellement effectués.
Modèle de file d'exécution
Au lancement de l'exécution, useSessionExecution construit une file linéaire à plat d'objets « étape » à partir des éléments de la séance :
- Un élément exercice → 1 étape
- Un élément combo →
totalRounds × Métapes, oùtotalRoundsdépend du type de combo :
| Type de combo | Signification de repetitions |
Formule de totalRounds |
|---|---|---|
| NONE/TABATA | Nombre de tours complets | repetitions |
| EMOM | Durée totale (min) | floor(repetitions / M) — chaque exercice = un créneau d'1 min |
| AMRAP | Durée totale (min) | 1 — un seul passage est mis en file ; l'utilisateur répète librement |
Éléments de séance :
1. Exercice A ×3 reps → 1 étape
2. Combo « HIIT » ×2 tours (2 exercices) → 4 étapes (NONE/TABATA)
Tour 1 : Pompes ×10, Squat ×15
Tour 2 : Pompes ×10, Squat ×15
3. Exercice B ×5 reps → 1 étape
File (à plat) :
[0] Exercice A
[1] Pompes — HIIT, Tour 1/2
[2] Squat — HIIT, Tour 1/2
↓ avance automatique (pas le dernier tour)
[3] Pompes — HIIT, Tour 2/2
[4] Squat — HIIT, Tour 2/2
↓ écran de dernier tour : [Continuer →] [+ Un tour de plus]
[5] Exercice B
Pour un combo EMOM avec 2 exercices et repetitions = 6 (= durée de 6 minutes) : totalRounds = floor(6/2) = 3, ce qui produit 6 étapes d'une minute chacune.
Chaque étape porte : exerciseId, exerciseTitle, prefillReps, prefillWeight, prefillSide, comboId?, comboTitle?, roundNumber?, totalRounds?, asymmetric, alternate, isFinalComboStep, comboExercises.
Le drapeau isFinalComboStep
Mis à true uniquement sur le dernier exercice du dernier tour d'un combo. Quand l'utilisateur clique sur Terminé sur cette étape, le composable affiche l'écran de dernier tour au lieu d'avancer automatiquement.
Logique de pré-remplissage
À l'initialisation, executionLogRepository.getLastItemsBySession(sessionId) récupère le execution_log le plus récent pour la séance et renvoie une correspondance exerciseId → {reps_done, weight_used, side}. Les étapes utilisent cette correspondance pour le pré-remplissage, et retombent sur les valeurs par défaut de l'élément de séance s'il n'existe aucun journal antérieur.
« + Un tour de plus »
Quand l'utilisateur clique sur + Un tour de plus, le composable :
- Récupère les étapes complétées pour le dernier tour en cours (répétitions/poids réellement utilisés)
- Insère un nouveau lot d'étapes à
currentIndex + 1dans la file - Marque
isFinalComboStep = truesur la dernière nouvelle étape - Avance
currentIndexjusqu'à la première nouvelle étape
Gestion du côté pour les exercices asymétriques
Si asymmetric = true et alternate = true, le côté change à chaque étape du même exercice. Si alternate = false, il change après chaque série complète. La valeur finale du côté est enregistrée dans execution_log_item.side.
Journal partiel en cas de sortie
Cliquer sur ✕ (sortir) enregistre toutes les étapes complétées dans execution_log et omet les étapes non terminées. Le journal est marqué avec finished_at = null pour indiquer une exécution partielle. Les éléments complétés restent comptabilisés dans les statistiques.
Bascule du détail de l'exercice
Le titre de l'exercice dans la carte de l'étape en cours est un bouton à bascule. Cliquer dessus :
- Déplie un panneau de détail intégré (description rendue en Markdown, images, liens vidéo).
- Met en pause le chronomètre actif — mais seulement s'il tournait au moment du clic (le drapeau
detailPausedTimerle retient).
Replier le panneau ne relance le chronomètre que si detailPausedTimer est vrai, laissant intacts les chronomètres mis en pause manuellement.
Le panneau de détail est chargé à la demande, au premier dépliage, via exerciseRepository.getById(step.exerciseId). Les bascules suivantes au sein de la même étape réutilisent le detailExercise mis en cache. Un watch(currentStep, …) réinitialise showDetail, detailExercise et detailPausedTimer à chaque avancée d'étape.
Pour la détection de l'état de connexion (afficher ou non les liens vidéo), SessionExecuteView utilise useOnlineStatus(). L'extraction de l'ID YouTube utilise l'utilitaire partagé src/utils/youtube.js (la même fonction que dans ExerciseDetailView).
Disposition de l'interface
┌─────────────────────────────────────────┐
│ Titre de la séance [✕ sortir] │
│ ████████████░░░░░ 3 / 6 étapes │
├─────────────────────────────────────────┤
│ ▶ MAINTENANT │
│ ┌───────────────────────────────────┐ │
│ │ Pompes ˅ │ │ ← le titre est un bouton bascule
│ │ ┌─────────────────────────────┐ │ │ ← panneau de détail (si déplié)
│ │ │ Gardez le corps droit… │ │ │
│ │ │ [image] [image] │ │ │
│ │ └─────────────────────────────┘ │ │
│ │ Combo : HIIT — Tour 2 sur 2 │ │
│ │ Reps : [ 10 ] │ │
│ │ Poids : [ 0 ] kg │ │
│ │ Côté : DROIT ↔ │ │ ← asymétrique uniquement
│ │ [ ✓ Terminé ] │ │
│ └───────────────────────────────────┘ │
│ ↓ SUIVANT │
│ Squat — 15 reps (HIIT, Tour 2) │
└─────────────────────────────────────────┘
Signaux sonores
src/composables/useAudioCues.js fournit deux fonctions construites sur la Web Audio API :
| Fonction | Fréquence | Durée | Rôle |
|---|---|---|---|
playBip(volume) |
440 Hz | 0.1 s | Tic discret — décompte, mi-parcours |
playBIP(volume) |
880 Hz | 0.3 s | Signal fort — changement de phase, fin du temps |
AudioContext est créé de façon différée, au premier usage (singleton). Le volume est lu depuis settings.audio_volume (0–100) et converti en multiplicateur 0.0–1.0.
La logique des sons vit dans useSessionExecution sous forme de trois fonctions privées :
_emomSounds(remaining)— déclencheplayBipà r = 30, 2, 1 (EMOM uniquement)._amrapLastMinuteSounds(remaining)— déclencheplayBipà r = 30, 2, 1 (dernière minute de l'AMRAP)._tabataCountdown(remaining, totalTime)— déclencheplayBipàfloor(totalTime / 2), r = 2, et r = 1.
Logique du minuteur AMRAP dans _maybeStartAmrapTimer :
r > 60 :
changement de minute écoulée → playBip (tic par minute)
r === round(duration / 2) → playBIP (signal mi-temps ; prioritaire sur le tic minute)
r ≤ 60 :
_amrapLastMinuteSounds(r)
r = 0 (expiration) :
playBIP (signal de fin de temps)
La séquence bip, bip, BIP (r = 2, r = 1, r = 0/expiration) est espacée régulièrement d'une seconde, car onTick se déclenche à r = 2 et r = 1, et onExpire se déclenche à r = 0 — chacun exactement à un tic de minuteur d'écart.
Fichiers clés : src/views/SessionExecuteView.vue, src/composables/useSessionExecution.js, src/composables/useAudioCues.js, src/db/repositories/executionLogRepository.js, src/utils/youtube.js
3.8 Statistiques
Vue : StatsView — #/stats
Composable : useStats
Bibliothèques : Chart.js 4 + vue-chartjs 5, enregistrées localement dans StatsView.vue (pas globalement, pour éviter des effets de bord sur les autres routes).
Filtre de plage de dates
Quatre onglets contrôlent la période affichée : 7j, 30j (par défaut), 3m, Tout.
Cartes de résumé
| Carte | Requête |
|---|---|
| Séances | Nombre de lignes execution_log sur la période |
| Exercices réalisés | Nombre de lignes execution_log_item (complétées) sur la période |
| Série de jours | Jours calendaires consécutifs (jusqu'à aujourd'hui) avec au moins un journal ; toujours calculée sur tout l'historique |
La série utilise setUTCHours(0,0,0,0) pour comparer les dates et correspondre à l'extraction UTC DATE(started_at) de SQLite.
Graphiques
| Graphique | Type | Métrique |
|---|---|---|
| Fréquence | Barres | Séances par semaine ou par mois (au choix) |
| Volume | Barres | SUM(reps_done × weight_used) par exécution de séance |
| Progression | Ligne | Répétitions (axe gauche) + poids max (axe droit) au fil du temps pour un exercice donné |
Le volume ne compte que les exercices avec poids (poids > 0). Le graphique de progression ne liste que les exercices ayant au moins un journal d'exécution.
Date de création de la base
Affichée en haut sous la forme « Vous utilisez TrainUs depuis le [date] », lue depuis settings.db_created_at. Formatée via dateFormatter.js et réactive aux changements de langue.
Fichiers clés : src/views/StatsView.vue, src/composables/useStats.js, src/utils/dateFormatter.js
3.9 Import / Export (JSON)
Composables : useJsonExport, useJsonImport
Composants : ImportDialog, ExportFilterDialog, ModalDialog
Utilitaires : src/utils/jsonEnvelope.js
Format de l'enveloppe JSON
{
"metadata": {
"appName": "trainus",
"appVersion": "1.0.0",
"schemaVersion": 1,
"exportDate": "2026-03-30T12:00:00.000Z",
"exportedBy": { "name": "User Name", "uuid": "uuid-string" }
},
"data": {
"exercises": [...],
"combos": [...],
"sessions": [...],
"collections": [...],
"executionLogs": [...],
"settings": [...]
}
}
Seules les clés présentes dans data sont exportées/importées. Les exports d'une seule entité (par exemple, un seul exercice exporté) n'incluent que le tableau de cette entité.
Points d'entrée d'export
| Point d'entrée | Portée |
|---|---|
| Vue liste Exercice / Combo / Séance / Collection | Tous les éléments de ce type d'entité, ou seulement ceux filtrés si une recherche est active |
| Vue détail Exercice / Combo / Séance / Collection | Un seul élément |
| Réglages → Exporter des données | N'importe quelle combinaison de types d'entité (boîte de filtre) |
La boîte de filtre affiche des cases à cocher par type d'entité. L'export complet depuis Réglages produit trainus-full-YYYY-MM-DD.json.
Les exports depuis une vue liste transmettent la requête de recherche active (le cas échéant) à exportAllExercises/Combos/Sessions/Collections(query) dans useJsonExport.js, qui utilisent search()/searchWithExercises()/searchWithItems()/searchWithSessions() au lieu de getAll()/getAllWithX() quand une requête est présente.
Les exports de combo/séance/collection (unitaire, liste, ou boîte de filtre) ne contiennent jamais que la ligne de l'entité demandée — collectComboDependencies/collectSessionDependencies/collectCollectionDependencies dans useJsonExport.js parcourent les références FK imbriquées (les items d'une séance, les exercices d'un combo, les séances d'une collection) pour récupérer les entités dépendantes complètes et les ajouter comme tableaux exercises/combos/sessions au niveau racine de l'enveloppe. exportByFilter enchaîne cette logique collections → séances → combos → exercices afin qu'une sélection partielle des cases à cocher produise quand même un fichier important de façon autonome, sans référence manquante sur un appareil qui n'a pas déjà ces dépendances.
Points d'entrée d'import
| Point d'entrée | Notes |
|---|---|
| Vue liste Exercice / Combo / Séance / Collection | Import par entité, depuis la barre d'action |
| Réglages → Importer des données | Enveloppe multi-entités prise en charge |
Tous les points d'entrée d'import passent par le même flux généralisé analyzeAll / applyAllImport, y compris les fichiers d'exercice à entité unique (un flux historique séparé existait autrefois, mais produisait en fait une forme d'analyse identique, et a donc été supprimé).
Stratégie de gestion des conflits
| Scénario | Comportement |
|---|---|
Même utilisateur (created_by = user_uuid local), pas de correspondance d'ID/titre |
Insertion avec l'ID d'origine |
| Même utilisateur, même ID existant | Invite : Remplacer / Ignorer (avec le lot Remplacer tout / Ignorer tout) |
| Même utilisateur, ID différent mais même titre qu'une ligne existante | Même invite Remplacer/Ignorer, résolue contre l'ID de la ligne existante ; Ignorer redirige les références du même lot vers elle |
| Utilisateur différent, UUID inconnu | Demande un suffixe → enregistré dans la table suffix |
Utilisateur différent, même ID + même created_by |
Mise à jour en place (pas de nouvel ID) |
Utilisateur différent, même ID + created_by différent |
Génère un nouvel UUID |
| Utilisateur différent, titre suffixé en conflit avec une ligne existante | Même invite Remplacer/Ignorer (pas d'étape de renommage séparée) — presque toujours une réimportation d'un élément déjà importé du même utilisateur externe |
Format de titre pour les imports externes : "Titre original [SUFFIXE]".
Remappage des références entre entités
Quand l'ID d'une entité change pendant l'import (nouvel UUID généré), toutes les entités qui y font référence sont mises à jour :
combo_exercise.exercise_idsession_item.item_id(pour les exercices et les combos)collection_session.session_idexecution_log.session_idexecution_log_item.exercise_id
Restrictions sur l'import des réglages
Les réglages sont importés avec des exclusions, pour préserver l'identité locale :
| Clé | Importée ? |
|---|---|
user_uuid |
Non — toujours préservée |
app_version |
Non — toujours préservée |
db_created_at |
Non — toujours préservée |
last_backup_at |
Non — toujours préservée |
user_name, language, theme, backup_reminder_days |
Oui |
Import des journaux d'exécution
Les journaux d'exécution ne sont importés que si la séance référencée existe localement ou a été importée dans le même lot.
Flux de la boîte de dialogue d'import
stateDiagram-v2
[*] --> Loading: Fichier sélectionné
Loading --> SuffixPrompt: Utilisateur externe détecté
Loading --> Preview: Même utilisateur
SuffixPrompt --> Preview: Suffixe saisi
Preview --> Collisions: Conflits trouvés
Preview --> Applying: Aucun conflit
Collisions --> Applying: Tous résolus
Applying --> Report: Terminé
Report --> [*]: Fermé
Gestion des suffixes
La page Réglages liste tous les suffixes enregistrés (depuis la table suffix). Renommer un suffixe se propage à toutes les colonnes titre/libellé des entités via le REPLACE() SQL :
UPDATE exercise SET title = REPLACE(title, '[OLD]', '[NEW]') WHERE title LIKE '%[OLD]%'
Appliqué aux tables exercise, combo, session et collection.
Fichiers clés : src/composables/useJsonExport.js, src/composables/useJsonImport.js, src/utils/jsonEnvelope.js, src/components/ImportDialog.vue, src/components/ExportFilterDialog.vue, src/db/repositories/suffixRepository.js
3.10 Sauvegarde
Composable : useBackupStatus (refs singleton au niveau module)
Composant : BackupReminder (téléporté dans #actionbar-actions)
Utilitaire : src/utils/backupFilename.js
Format du nom de fichier
trainus-{version}-backup-YYYYMMDD-HHhMMmSSs.sqlite3
Heure locale, avec des zéros de remplissage. Les lettres h, m, s remplacent les deux-points pour la compatibilité avec le système de fichiers Windows.
Clés de réglages
| Clé | Défaut | Écrite par |
|---|---|---|
db_created_at |
Réglée au premier lancement | database._initDefaults() |
last_backup_at |
Absente | Succès de téléchargement, succès de restauration |
backup_reminder_days |
"7" |
Champ numérique des Réglages |
Détection d'ancienneté
effectiveDate = last_backup_at OU db_created_at (repli)
isStale = (effectiveDate absente) OU (now_utc − effectiveDate) > backup_reminder_days × 86400s
db_created_at comme repli donne aux nouveaux utilisateurs une période de grâce égale à l'intervalle de rappel, avant que l'avertissement n'apparaisse.
Interface du rappel
BackupReminder.vue est monté une seule fois dans App.vue (dans la branche « base prête ») et se téléporte dans #actionbar-actions. Il affiche un bouton de style avertissement avec l'icône bi-exclamation-triangle-fill. Cliquer dessus navigue vers #/settings et fait défiler jusqu'à l'ancre #settings-backup-section. Le rappel ne peut pas être masqué.
useBackupStatus utilise des refs au niveau module, afin que le même état soit partagé entre toutes les instances de composant (pattern singleton).
Formatage des dates
src/utils/dateFormatter.js fournit deux fonctions pures, sensibles à la locale, utilisées partout dans l'application :
| Fonction | Anglais | Français |
|---|---|---|
formatDate(date, lang?) |
MM/DD/YYYY |
DD/MM/YYYY |
formatDateTime(date, lang?) |
toLocaleString() |
DD/MM/YYYY, HH:MM:SS |
Le paramètre optionnel lang permet aux propriétés calculées (computed) de Vue de passer la ref réactive de langue, pour que l'affichage des dates se mette à jour immédiatement lors d'un changement de langue.
Fichiers clés : src/composables/useBackupStatus.js, src/components/BackupReminder.vue, src/utils/backupFilename.js, src/utils/dateFormatter.js
3.11 PWA
Plugin : vite-plugin-pwa + Workbox
Service worker
| Configuration | Valeur | Justification |
|---|---|---|
registerType |
autoUpdate |
Active silencieusement le nouveau SW à la navigation suivante |
maximumFileSizeToCacheInBytes |
10 Mo | Couvre sqlite3.wasm (~8 Mo) |
manifest |
false |
Conserve le public/manifest.json écrit à la main |
Workbox gère la précache, basée sur le hash du contenu, de toute la coquille de l'application au moment de l'installation, ce qui permet un support hors ligne complet. Le binaire WASM est inclus dans le manifeste de précache grâce à la limite de 10 Mo.
Manifeste de l'application
public/manifest.json déclare name, short_name, display: standalone, theme_color, background_color, et deux icônes (192×192 et 512×512) avec les usages maskable et any.
Invite d'installation
useInstallPrompt.js capture l'événement beforeinstallprompt au chargement du module (ref singleton canInstall). InstallPrompt.vue téléporte un bouton d'icône de téléchargement dans la barre d'action quand canInstall est vrai. L'invite peut être ignorée pour la session (pas besoin de persistance).
Accessibilité
Ajouts ARIA appliqués dans toute l'application :
aria-labelsur tous les boutons icône-seule de la barre d'action, sur toutes les vuesrole="progressbar"+aria-valuenow/min/maxsur la barre de progression d'exécutionrole="alert"+aria-describedbysur les messages d'erreur de formulairerequired/aria-requiredsur les champs requisrole="dialog"+aria-modal+aria-labelledby+ focus au montage + touche Échap surModalDialog
Fichiers clés : vite.config.js, public/manifest.json, src/composables/useInstallPrompt.js, src/components/InstallPrompt.vue
3.12 Invite de nom de fichier à la sauvegarde
Chaque fichier enregistré sur disque (exports JSON, export HTML de l'accueil, sauvegarde SQLite — y compris la sauvegarde forcée avant migration) demande un nom de fichier, plutôt que de se télécharger silencieusement sous un nom généré automatiquement.
Composable : useSaveFilePrompt (singleton au niveau module, même pattern que useBackupStatus). promptFilename(defaultFilename) découpe le nom par défaut en radical + extension au niveau du dernier point, le stocke dans la ref singleton pendingSave, et renvoie une Promise<string|null> résolue une fois la boîte de dialogue fermée (null en cas d'annulation).
Composant : SaveFileDialog.vue, monté une seule fois à la racine de App.vue, pour être disponible quelle que soit la vue (ou l'écran de migration bloquant) qui déclenche une sauvegarde. Affiche un champ radical modifiable (pré-rempli, texte sélectionné) à côté d'une extension fixe en lecture seule. Entrée valide ; un radical vide retombe sur la valeur par défaut.
Les trois fonctions de téléchargement de bas niveau (downloadJson dans jsonEnvelope.js, downloadBackup dans useBackupDownload.js, la fonction locale downloadHtml dans useHtmlExport.js) appellent promptFilename() avant de construire le blob/lien, et ne font rien si l'utilisateur annule — aucun autre point d'appel n'a changé. Pour les sauvegardes en particulier, markBackedUp() (et, sur l'écran de migration, l'activation du bouton Mettre à jour) ne s'exécute que si la sauvegarde n'a pas été annulée.
Fichiers clés : src/composables/useSaveFilePrompt.js, src/components/SaveFileDialog.vue, src/utils/jsonEnvelope.js, src/composables/useBackupDownload.js, src/composables/useHtmlExport.js, src/App.vue
4. Routage
Toutes les routes utilisent le mode hash (createWebHashHistory). Le chemin racine / redirige vers /home.
| Route | Vue | Notes |
|---|---|---|
#/home |
HomeView |
Aperçu de l'activité |
#/exercises |
ExerciseListView |
Liste + recherche |
#/exercises/new |
ExerciseFormView |
Mode création |
#/exercises/:id |
ExerciseDetailView |
Lecture seule |
#/exercises/:id/edit |
ExerciseFormView |
Mode édition |
#/combos |
ComboListView |
Liste + recherche |
#/combos/new |
ComboFormView |
Mode création |
#/combos/:id |
ComboDetailView |
Lecture seule |
#/combos/:id/edit |
ComboFormView |
Mode édition |
#/sessions |
SessionListView |
Liste + recherche |
#/sessions/new |
SessionFormView |
Mode création |
#/sessions/:id |
SessionDetailView |
Lecture seule |
#/sessions/:id/edit |
SessionFormView |
Mode édition |
#/sessions/:id/execute |
SessionExecuteView |
Mode exécution |
#/collections |
CollectionsView |
Liste + recherche |
#/collections/new |
CollectionFormView |
Mode création |
#/collections/:id |
CollectionDetailView |
Lecture seule |
#/collections/:id/edit |
CollectionFormView |
Mode édition |
#/stats |
StatsView |
Statistiques + graphiques |
#/settings |
SettingsView |
Réglages utilisateur |
navKeyMap dans App.vue fait correspondre les sous-routes (par exemple exercise-detail, exercise-edit) à leur clé de navigation de premier niveau (exercises), pour que l'élément du menu reste mis en évidence lorsque l'utilisateur navigue vers une vue détail/édition.
5. Internationalisation
Bibliothèques : i18next + i18next-vue
Fichiers de locale : src/i18n/locales/en.json (anglais, par défaut), src/i18n/locales/fr.json (français), src/i18n/locales/da.json (danois)
Initialisation : src/i18n/index.js enregistre les trois locales et fixe l'anglais comme langue par défaut. Le composable useTranslation() de i18next-vue est utilisé dans tous les composants — aucun appel direct à i18next dans les templates.
Démarrage : src/main.js appelle i18next.changeLanguage(stored_language) avant le premier rendu, pour que l'interface s'ouvre toujours dans la langue persistée de l'utilisateur.
Changement de langue : le sélecteur de langue des Réglages appelle i18next.changeLanguage() et enregistre la nouvelle valeur dans settings.language. Tous les appels réactifs t() se mettent à jour immédiatement.
Formatage des dates : dateFormatter.js lit i18next.language comme repli, mais accepte un paramètre lang explicite pour permettre un affichage réactif des dates dans les propriétés calculées Vue.
Voir [Guides pratiques → Ajouter une nouvelle langue]({{< relref "how-to-guides.fr.md" >}}#ajouter-une-nouvelle-langue) pour ajouter le support d'une langue supplémentaire.
6. Versionnement
Version de l'application
Issue de package.json, injectée par Vite sous la forme __APP_VERSION__ (une constante globale de type chaîne). Disponible au build (nom du cache du SW) et à l'exécution (métadonnées d'export, table settings, noms de fichiers de sauvegarde). Elle est purement informative — elle ne joue aucun rôle dans l'emplacement des données ni dans les décisions de migration.
Nommage du fichier de base de données
La base de données OPFS vit à un emplacement stable, indépendant de la version : dossier .trainus, fichier trainus.db. OPFS est scopé par origine, donc les données survivent automatiquement aux mises à jour de l'application ; les changements de schéma sont gérés par le système de migration au démarrage (§2 Système de migration), pas par un déplacement des données.
Note historique : avant l'étape 16, le dossier s'appelait
.trainus-{appVersion}, abandonnant les données à chaque release. Les anciens dossiers versionnés des machines de développement sont simplement laissés à l'abandon (à nettoyer via les outils de développement du navigateur si besoin).
Version de schéma
SCHEMA_VERSION dans src/db/schema.js est un entier stocké dans la table schema_version. Il pilote à la fois la vérification de migration au démarrage et la vérification de compatibilité de Restaurer la base de données.
Règle : SCHEMA_VERSION n'est incrémenté que lorsqu'un changement de schéma est déployé auprès des utilisateurs. Les changements en place avant la première publication (ajouter des colonnes directement dans SCHEMA_SQL) ne nécessitent pas d'augmentation.
Registre des releases
src/db/releases.js exporte un tableau RELEASES — la source de vérité unique pour l'historique du schéma. Chaque entrée déclare { schemaVersion, appVersion, dbBreak, migrationScript }, où migrationScript est le SQL qui amène une base existante de schemaVersion - 1 à schemaVersion (ou null pour une migration no-op quand dbBreak est faux). Chaque augmentation de SCHEMA_VERSION doit s'accompagner d'exactement une entrée RELEASES.
Deux consommateurs partagent ce registre :
getMigrationScripts(from, to)— scripts ordonnés pour la migration au démarrage.checkCompatibility(imported, current)— flux de Restaurer la base de données.
7. Commandes de build
npm install
npm run dev # Serveur de développement Vite sur http://localhost:5173
npm run build # Build de production dans dist/
npm run preview # Prévisualiser la build de production en local
npm run format # Formate tous les fichiers avec Prettier
npm run format:check # Vérification CI (code de sortie non nul en cas de différence)
Les en-têtes COOP/COEP sont injectés automatiquement par vite.config.js pour le serveur de développement. Voir [Guides pratiques → Déployer TrainUs]({{< relref "how-to-guides.fr.md" >}}#déployer-trainus) pour le déploiement en production.
Voir tests/README.md pour la configuration complète des tests et les instructions d'exécution.
8. Annexes
Annexe A — Largeur de contenu limitée sur les vues liste/détail
Sur les grands écrans de bureau (par exemple 1920×1080), les pages de contenu n'occupent pas toute la largeur disponible. Les cartes s'arrêtent à environ 800px.
| Vue | Limite |
|---|---|
| Vues liste (exercices, combos, séances, collections) | 800 px |
| Vues détail | 800 px |
| Vues formulaire | 600 px |
| Vue réglages | 600 px |
La limite est implémentée par vue (pas globalement) afin que chaque écran puisse opter pour une largeur de lecture différente.
Pour ajouter une mise en page multi-colonnes sur grand écran
- Augmentez la limite des vues liste à
1200px(ou supprimez-la) et remplacez la pile verticale par une grille CSS avecgrid-template-columns: repeat(auto-fill, minmax(320px, 1fr)). - Conservez les vues détail et réglages à leurs limites actuelles.
- N'enveloppez que les cartes dans un
<div class="grid-wrapper">interne, pour éviter de redessiner la barre d'action et les messages d'état vide. - Appliquez le même pattern à toutes les vues liste, par cohérence.