Files
GameTime/.ideai/tickets/82/carnet.md
Blomios 10fba8d2ec chore(tickets): clôture effective du ticket #82
Le ticket #82 était resté au statut open malgré le travail terminé (édition manuelle précédente insuffisante) ; clôturé proprement via l'API IdeA.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:39:50 +02:00

820 lines
34 KiB
Markdown

---
issueRef: "#82"
version: 5
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1784705972459
---
# UX — Statistiques de progression simples
## Intention
La surface doit donner une raison de revenir sans transformer GameTime en dashboard analytique. Elle reste une lecture courte de l'historique existant : combien l'utilisateur s'est entraîné, s'il garde un rythme, et si un exercice progresse dans le temps.
Principe de conception : **une vue Progression, trois blocs, aucun classement global complexe**.
## Surface et navigation
### Nouvelle entrée depuis l'accueil
Ajouter une entrée dans la liste d'accueil, après `Historique` :
```text
Progression
Volume, régularité et évolution par exercice
```
Icône recommandée : `Icons.show_chart` ou équivalent Flutter Material.
Raison : les stats doivent être découvrables au même niveau que l'historique, sans densifier la liste historique ni introduire de bottom nav. L'écran reste autonome et lisible.
### Lien depuis l'historique
Dans `Historique`, ajouter une action secondaire en AppBar ou en haut de liste :
```text
Progression
```
Ce lien ouvre le même écran. L'historique reste la liste chronologique des séances ; `Progression` est sa lecture synthétique.
## Écran `Progression`
AppBar :
```text
Progression
```
Sous l'AppBar, un sélecteur de période simple :
```text
4 semaines | 3 mois | Tout
```
Valeur par défaut : `4 semaines`.
Règle : la période filtre tous les blocs de l'écran. `Tout` permet de ne pas perdre les utilisateurs qui s'entraînent peu.
Structure verticale :
```text
Progression
[4 semaines] [3 mois] [Tout]
VOLUME
[ Séances ] [ Temps total ]
RÉGULARITÉ
Rythme récent
...
PAR EXERCICE
[Choisir un exercice]
[Choisir une mesure si nécessaire]
Graphique simple
Résumé de tendance
```
Style Court Blazer : fond existant, cartes plates rayon 6 px, bordure discrète, liseré supérieur crimson uniquement sur les blocs clés (`Volume`, `Par exercice`). Chiffres principaux en Anton couleur or, labels en Archivo. Ne pas utiliser de couleurs d'alerte pour l'absence d'activité.
## Bloc 1 — Volume
Titre de section :
```text
Volume
```
Deux tuiles compactes :
```text
Séances
8
```
```text
Temps total
5 h 40
```
Sous les tuiles, une ligne de contexte facultative si pertinente :
```text
Depuis le 24 juin
```
ou en période `Tout` :
```text
Depuis ta première séance
```
MVP : ne pas afficher de comparaison en pourcentage avec la période précédente. C'est tentant mais trop analytique et fragile avec peu de données.
## Bloc 2 — Régularité non culpabilisante
Titre de section :
```text
Régularité
```
Libellé principal :
```text
Rythme récent
```
Métrique MVP recommandée : **semaines actives sur la période**, pas streak quotidien.
Affichage :
```text
3 / 4 semaines actives
```
Texte secondaire :
```text
Une semaine active contient au moins une séance terminée.
```
Pourquoi : le basket et l'entraînement ne sont pas forcément quotidiens. Une logique de streak journalier culpabilise vite ; les semaines actives valorisent la reprise et le rythme réel.
Si la période est `3 mois` :
```text
8 / 12 semaines actives
```
Si la période est `Tout`, afficher une formulation sans ratio trop long :
```text
12 semaines actives au total
```
Ne pas afficher `0 jour de série`, `streak perdu`, `tu as raté...` ou du rouge.
## Bloc 3 — Progression par exercice
Titre de section :
```text
Par exercice
```
### Choix de l'exercice
Contrôle : menu/selecteur compact.
Placeholder :
```text
Choisir un exercice
```
Liste : exercices présents dans l'historique filtré, y compris exercices archivés avec badge :
```text
Exercice archivé
```
Option de tri MVP : derniers exercices joués en premier. Pas besoin de recherche si la liste reste raisonnable ; ajouter recherche seulement si le composant existe déjà ailleurs et se réutilise simplement.
### Choix de la mesure
Si l'exercice n'a qu'une mesure exploitable dans l'historique, ne pas afficher de second contrôle.
Si plusieurs mesures existent, afficher un segmented control :
```text
Score | Répétitions | Temps
```
Libellés selon le type exact :
- Score libre numérique : `Score`
- Score chrono : `Temps réalisé`
- Répétitions : `Répétitions`
- Temps de série : `Temps`
Si le score a un label utilisateur, l'afficher dans le résumé, pas forcément dans l'onglet :
```text
Réussites · paniers
```
### Graphique MVP
Graphique simple en ligne ou points reliés, sans multi-séries.
Un point = une séance où cet exercice a un résultat exploitable pour la mesure choisie.
Agrégation par séance :
- Score libre numérique : meilleur score de la séance pour cet exercice, avec unité utilisateur.
- Score chrono : meilleur temps réalisé de la séance pour cet exercice, format `mm:ss.d`, avec aide courte `Plus bas = mieux`.
- Répétitions : total des répétitions réalisées sur l'exercice pendant la séance.
- Temps : total du temps réalisé/saisi sur l'exercice pendant la séance.
Sous le graphique, résumé court :
```text
Meilleur : 14 paniers
Dernier : 12 paniers
```
Pour score chrono :
```text
Meilleur : 00:42.8
Dernier : 00:45.1
```
Pour répétitions :
```text
Total récent : 180 répétitions
Dernière séance : 45 répétitions
```
Éviter les promesses de tendance sophistiquée (`+12%`, `en hausse`) au MVP, sauf si Architect confirme une agrégation fiable plus tard.
### Interaction avec un point
Tap sur un point : afficher un petit détail non modal ou bottom sheet légère :
```text
12 juillet
Séance : Prépa match
Meilleur score : 14 paniers
```
Action secondaire :
```text
Voir la séance
```
qui ouvre le détail historique existant.
## États vides et limites
### Aucun historique
Écran `Progression` :
```text
Aucune progression à afficher
Termine une séance pour voir ton volume, ton rythme et tes résultats évoluer ici.
[Voir les séances]
```
Pas de carte vide multiple. Une seule surface d'état vide suffit.
### Une seule séance terminée
Afficher `Volume` normalement.
Dans `Régularité` :
```text
1 semaine active
```
Dans `Par exercice` :
```text
Encore un point de départ
Termine cet exercice dans une autre séance pour voir son évolution.
```
Si un exercice est sélectionnable, afficher le point unique mais ne pas tracer de tendance.
### Historique existant mais aucun résultat exploitable
Exemple : seulement des séries passées, scores texte non numériques, ou mesures absentes.
```text
Pas encore de mesure exploitable
Les graphiques apparaissent quand un exercice contient des répétitions, du temps ou un score numérique enregistré.
```
Conserver `Volume` et `Régularité` si des séances terminées existent.
### Score libre non numérique
Ne pas tenter de le grapher.
Message dans `Par exercice` si seule cette mesure existe :
```text
Score non graphique
Ce score est enregistré comme texte. Tu peux le retrouver dans le détail de l'historique.
```
Si d'autres mesures existent, masquer cette mesure du segmented control MVP ou la désactiver avec aide courte.
### Peu de données sur la période
Si la période sélectionnée n'a pas assez de points mais `Tout` en a :
```text
Peu de données sur cette période
Passe sur "Tout" pour voir plus d'historique.
```
Ne pas changer automatiquement la période.
## Libellés à utiliser
- `Progression`
- `Volume`
- `Séances`
- `Temps total`
- `Régularité`
- `Rythme récent`
- `semaines actives`
- `Par exercice`
- `Choisir un exercice`
- `Temps réalisé` pour score chrono
- `Plus bas = mieux` pour score chrono
- `Voir la séance`
## Libellés à éviter
- `Dashboard`
- `Analytics`
- `Performance score`
- `Objectif raté`
- `Streak perdu`
- `0 jour consécutif`
- `Échec de progression`
## Exigences de données pour Architect
L'écran exige une agrégation locale à partir de l'historique existant :
- séances terminées filtrables par date ;
- durée totale par séance ;
- nombre de séances terminées par période ;
- semaines actives par période, avec règle `au moins une séance terminée dans la semaine` ;
- liste des exercices présents dans l'historique, y compris snapshots d'exercices archivés ;
- résultats par exercice, séance, série et mesure ;
- identification du type de score : libre numérique, libre texte, chrono ;
- unité/label utilisateur du score libre ;
- agrégats par séance pour un exercice : meilleur score numérique, meilleur score chrono, total répétitions, total temps.
Point à cadrer : définir précisément quelles séries comptent dans les agrégats (`terminée` oui, `passée/skipped` non ; résultat absent non) et comment normaliser les dates de semaine côté local/timezone.
# Architect — Cadrage architecture #82
## Décision de frontière
La logique de progression appartient à `application`, exposée par use case et DTO, avec un port de lecture implémenté par Drift. La présentation ne doit pas recalculer les agrégats à partir de `WorkoutHistoryUseCases.listActive()` : elle choisit une période, un exercice, une mesure, puis affiche les DTO déjà normalisés.
Le domaine conserve les invariants existants (`WorkoutHistory`, `WorkoutHistorySetResult`, `WorkoutHistoryStepResult`, `SetResultStatus`, `ScoreInputMode`). Aucun nouveau concept métier persistant n'est nécessaire pour le MVP : les statistiques sont une projection de lecture locale sur l'historique snapshoté.
Le port #81 `ExercisePerformanceReferenceRepository` reste dédié à l'aide pendant l'exécution (`dernière performance` / `record`). Il ne doit pas devenir un port de dashboard. #82 crée donc un port séparé, étroit, orienté progression.
## Contrats application à créer
Dans `lib/application/ports.dart` :
```dart
enum ProgressionPeriod { fourWeeks, threeMonths, all }
enum ProgressionMeasure { manualScore, stopwatchScore, reps, time }
final class ProgressionDateRange {
const ProgressionDateRange({required this.startedAt, required this.endedAt});
final DateTime? startedAt; // null pour Tout
final DateTime endedAt; // clock.now(), inclusif côté use case
}
abstract interface class ProgressionStatsRepository {
Future<ProgressionGlobalStatsData> readGlobalStats(ProgressionDateRange range);
Future<List<ProgressionExerciseOptionData>> listExerciseOptions(ProgressionDateRange range);
Future<List<ProgressionMeasureOptionData>> listMeasureOptions({
required ProgressionDateRange range,
required String exerciseKey,
});
Future<ProgressionExerciseSeriesData> readExerciseSeries({
required ProgressionDateRange range,
required String exerciseKey,
required ProgressionMeasure measure,
});
}
```
`exerciseKey` est la clé stable de regroupement exposée par le repository : utiliser `sourceExerciseIdSnapshot` quand présent, sinon `exerciseSnapshotId`. Le DTO doit aussi exposer cette clé ; l'UI ne reconstruit pas la clé.
Dans `lib/application/use_cases.dart` :
```dart
final class ProgressionStatsUseCase {
const ProgressionStatsUseCase({required this.repository, required this.clock});
Future<ProgressionOverview> getOverview(ProgressionPeriod period);
Future<ProgressionExerciseSeries> getExerciseSeries({
required ProgressionPeriod period,
required String exerciseKey,
required ProgressionMeasure measure,
});
}
```
Responsabilités du use case : résoudre la période à partir de `clock.now()`, calculer `weeksInPeriod` pour `4 semaines` et `3 mois`, porter la règle `Tout`, et retourner des DTO de présentation prêts à afficher. Le calcul de début de semaine se fait en temps local de l'app : lundi 00:00 local. Les timestamps stockés restent UTC/Drift ; l'adapter convertit en local pour les buckets de semaines si le calcul n'est pas fait directement par SQLite.
## DTO à exposer à la présentation
DTO globaux :
```dart
final class ProgressionOverview {
const ProgressionOverview({
required this.period,
required this.rangeLabelKind,
required this.completedSessionCount,
required this.totalActiveMs,
required this.activeWeekCount,
required this.totalWeekCount,
required this.hasAnyCompletedHistory,
required this.exerciseOptions,
});
final ProgressionPeriod period;
final ProgressionRangeLabelKind rangeLabelKind; // sinceDate | sinceFirstSession | none
final int completedSessionCount;
final int totalActiveMs;
final int activeWeekCount;
final int? totalWeekCount; // null pour Tout
final bool hasAnyCompletedHistory;
final List<ProgressionExerciseOption> exerciseOptions;
}
final class ProgressionExerciseOption {
const ProgressionExerciseOption({
required this.exerciseKey,
required this.nameSnapshot,
required this.isArchived,
required this.lastPerformedAt,
required this.measures,
});
final String exerciseKey;
final String nameSnapshot;
final bool isArchived;
final DateTime lastPerformedAt;
final List<ProgressionMeasureOption> measures;
}
```
DTO mesures et graphique :
```dart
final class ProgressionMeasureOption {
const ProgressionMeasureOption({
required this.measure,
required this.label,
this.scoreLabel,
this.scoreUnit,
required this.lowerIsBetter,
});
final ProgressionMeasure measure;
final String label; // Score, Temps réalisé, Répétitions, Temps
final String? scoreLabel;
final String? scoreUnit;
final bool lowerIsBetter; // true seulement pour stopwatchScore
}
final class ProgressionExerciseSeries {
const ProgressionExerciseSeries({
required this.exerciseKey,
required this.exerciseNameSnapshot,
required this.measure,
required this.points,
required this.summary,
required this.state,
});
final String exerciseKey;
final String exerciseNameSnapshot;
final ProgressionMeasureOption measure;
final List<ProgressionPoint> points;
final ProgressionSeriesSummary summary;
final ProgressionSeriesState state;
}
final class ProgressionPoint {
const ProgressionPoint({
required this.workoutHistoryId,
required this.workoutNameSnapshot,
required this.startedAt,
required this.value,
required this.rawValue,
});
final String workoutHistoryId;
final String workoutNameSnapshot;
final DateTime startedAt;
final double value; // valeur numérique pour l'axe Y
final Object rawValue; // double pour score, int ms/reps/time selon mesure
}
```
`ProgressionSeriesState` doit couvrir au minimum : `ready`, `singlePoint`, `emptyForPeriod`, `emptyButHasAllTimeData`, `noGraphableMeasure`, `textScoreOnly`. Le MVP ne graphe pas les scores texte. À ce stade le modèle existant stocke `actualScore` en `double?`, donc un score libre enregistré ici est déjà numérique ; l'état `textScoreOnly` sert à rester compatible avec l'UX si des scores texte existent via snapshot JSON ou évolution future, mais DevBackend ne doit pas parser `historySnapshotJson` pour fabriquer une série.
`ProgressionSeriesSummary` porte seulement les valeurs fiables demandées par UX : `best`, `last`, `recentTotal`, `lastSessionTotal` selon la mesure. Pas de pourcentage, pas de tendance calculée.
## Règles d'agrégation
Séances incluses : `workout_history.deleted_at IS NULL`, `completed = 1`, période sur `started_at`. `ended_at` ne pilote pas le filtre MVP.
Volume : `COUNT(*)` des séances incluses, `SUM(total_active_ms)`.
Régularité : une semaine active = au moins une séance incluse dont `started_at` tombe dans la semaine locale. Pour `4 semaines`, `totalWeekCount = 4`; pour `3 mois`, `totalWeekCount = nombre de semaines locales intersectant la période` (12 ou 13 selon dates réelles, le libellé UX peut rester approximatif si Front le souhaite, mais le ratio doit venir du DTO). Pour `Tout`, `totalWeekCount = null` et `activeWeekCount` est le total distinct.
Exercices listés : résultats d'historique inclus, `status = completed`, `deleted_at IS NULL`, avec au moins une mesure exploitable (`actual_score`, `actual_score_time_ms`, `actual_reps`, `actual_time_ms`). Inclure `workout_history_set_results` et `workout_history_step_results` pour ne pas perdre les exercices à étapes. Trier par `MAX(history.started_at) DESC`.
Archivage : si `sourceExerciseIdSnapshot` pointe vers un exercice actif non archivé, `isArchived = false`; si l'exercice source est archivé, soft-deleted, absent, ou si seule la clé snapshot existe, `isArchived = true`. Le nom affiché vient toujours du snapshot historique le plus récent, pas de l'exercice courant.
Mesures disponibles :
- `manualScore` si `score_input_mode_snapshot = manual` et `actual_score IS NOT NULL`.
- `stopwatchScore` si `score_input_mode_snapshot = stopwatch` et `actual_score_time_ms IS NOT NULL`.
- `reps` si `actual_reps IS NOT NULL`.
- `time` si `actual_time_ms IS NOT NULL`.
Agrégation par séance, pour un exercice et une mesure :
- `manualScore` : meilleur `MAX(actual_score)` sur la séance.
- `stopwatchScore` : meilleur `MIN(actual_score_time_ms)` sur la séance, `lowerIsBetter = true`.
- `reps` : `SUM(actual_reps)` sur la séance.
- `time` : `SUM(actual_time_ms)` sur la séance.
Les résultats `skipped` ne comptent jamais. Les valeurs absentes ne comptent pas. Les séries et step-results peuvent coexister ; l'adapter doit les unir en projection homogène avant agrégation, pas additionner deux fois une même ligne.
## Impact repository et Drift
Créer `DriftProgressionStatsRepository` dans `lib/infrastructure/local/drift_repositories.dart` et le câbler dans `AppBootstrap` via `ProgressionStatsUseCase` + `AppDependencies`.
Les requêtes Drift recommandées peuvent être en `customSelect`, comme #81, car elles agrègent sur deux tables de résultats et des buckets temporels. Elles doivent retourner des projections primitives, mappées ensuite vers les DTO application.
Pas de nouvelle table métier. Migration uniquement si ajout d'index. Index recommandés si les tests de requêtes montrent un coût significatif, et acceptables dès B1 car les stats liront l'historique souvent :
```sql
CREATE INDEX IF NOT EXISTS idx_workout_history_completed_started
ON workout_history (completed, started_at)
WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_history_set_progression_exercise
ON workout_history_set_results (source_exercise_id_snapshot, exercise_snapshot_id, workout_history_id, status)
WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_history_step_progression_exercise
ON workout_history_step_results (source_exercise_id_snapshot, exercise_snapshot_id, workout_history_id, status)
WHERE deleted_at IS NULL;
```
Ces index imposent un bump de `schemaVersion` Drift et une migration idempotente. Ils ne changent pas les entités synchronisées et n'impactent pas le serveur.
## Découpage de lots
### B1 — Contrats application et agrégats globaux
- Ajouter enums/DTO `Progression*` dans `application`.
- Ajouter `ProgressionStatsRepository` et `ProgressionStatsUseCase`.
- Implémenter `readGlobalStats` dans `DriftProgressionStatsRepository`.
- Câbler dans `AppDependencies` / `AppBootstrap`.
- Tests application : résolution période, semaines actives, cas `Tout`, historique vide.
- Tests infra : volume et semaines actives sur plusieurs séances, soft delete ignoré, séances non terminées ignorées.
Livrable testable seul : le frontend peut déjà afficher `Volume`, `Régularité`, état vide global.
### B2 — Exercices et mesures disponibles
- Implémenter `listExerciseOptions` et `listMeasureOptions`.
- Unifier set-results et step-results en projection de lecture.
- Gérer `isArchived`, tri par dernière séance, snapshot name.
- Tests infra : exercice archivé, exercice source absent, score chrono vs score manuel, résultats skipped/absents exclus.
Livrable testable seul : le frontend peut afficher le selecteur d'exercice et le contrôle de mesure sans graphique.
### B3 — Série temporelle par exercice
- Implémenter `readExerciseSeries` avec agrégation par séance.
- Calculer `best`, `last`, `recentTotal`, `lastSessionTotal` selon mesure.
- Gérer `singlePoint`, `emptyForPeriod`, `emptyButHasAllTimeData`, `noGraphableMeasure`.
- Tests application/infra : max score manuel, min chrono, somme reps, somme temps, ordre chronologique, drill-down `workoutHistoryId`.
Livrable testable seul : DTO complet pour graphique et bottom sheet `Voir la séance`.
### F1 — Navigation et écran Progression global
- Ajouter entrée Accueil après `Historique`.
- Ajouter lien depuis `Historique` vers le même écran.
- Créer `ProgressionScreen` avec période, `Volume`, `Régularité`, états vides globaux.
- Tests widget : entrée accueil, navigation depuis historique, période par défaut `4 semaines`, empty state.
### F2 — Bloc Par exercice
- Connecter les options d'exercice/mesure.
- Afficher exercice archivé, états `pas de mesure exploitable`, `score non graphique`, `peu de données`.
- Afficher graphique simple mono-série et détail de point avec action `Voir la séance`.
- Tests widget : sélection exercice, mesure unique sans second contrôle, score chrono avec `Plus bas = mieux`, point unique sans tendance.
## Points hors MVP
Pas de progression par programme dans #82 MVP : le titre commercial mentionne programme, mais la surface UX retenue ne l'expose pas. Ne pas créer de contrat programme préventif. Si demandé plus tard, ajouter un port/DTO séparé basé sur `sourceWorkoutTemplateId` ou `programSnapshotId`, après cadrage dédié.
Pas de comparaison période précédente, pas de tendance pourcentage, pas de streak quotidien, pas de parsing de `historySnapshotJson` pour récupérer des mesures non déjà matérialisées en colonnes.
# DevBackend — Implémentation B1/B2/B3
## Réalisé
- B1 : ajout des contrats application `ProgressionPeriod`, `ProgressionMeasure`, `ProgressionStatsRepository`, DTO `ProgressionOverview` / options / série / points / résumé, et `ProgressionStatsUseCase`.
- B1 : implémentation Drift `readGlobalStats` avec filtre `deleted_at IS NULL`, `completed = 1`, période sur `started_at`, volume total et semaines actives calculées en semaine locale lundi 00:00.
- B1 : câblage `ProgressionStatsUseCase` dans `AppDependencies` / `AppBootstrap`.
- B1 : bump Drift `schemaVersion` à 18 et ajout idempotent des index recommandés.
- B2 : implémentation `listExerciseOptions` et `listMeasureOptions`, avec projection homogène `set-results` + `step-results`, clé stable `sourceExerciseIdSnapshot ?? exerciseSnapshotId`, tri par dernière séance, nom snapshot le plus récent et `isArchived`.
- B3 : implémentation `readExerciseSeries` avec agrégation par séance : `MAX(actual_score)`, `MIN(actual_score_time_ms)`, `SUM(actual_reps)`, `SUM(actual_time_ms)`, ordre chronologique et `workoutHistoryId` pour drill-down.
- B3 : calcul application des états `ready`, `singlePoint`, `emptyForPeriod`, `emptyButHasAllTimeData`, `noGraphableMeasure` et des résumés `best/last` ou `recentTotal/lastSessionTotal`.
## Fichiers touchés
- `lib/application/ports.dart`
- `lib/application/use_cases.dart`
- `lib/application/app_bootstrap.dart`
- `lib/infrastructure/local/app_database.dart`
- `lib/infrastructure/local/drift_repositories.dart`
- `test/application/use_cases_test.dart`
- `test/infrastructure/drift_repositories_test.dart`
- `test/presentation/home_screen_test.dart` (fake `AppDependencies` mis à jour)
## Vérifications backend
- `HOME=/tmp /opt/flutter/bin/cache/dart-sdk/bin/dart analyze lib/application/ports.dart lib/application/use_cases.dart lib/application/app_bootstrap.dart lib/infrastructure/local/app_database.dart lib/infrastructure/local/drift_repositories.dart test/application/use_cases_test.dart test/infrastructure/drift_repositories_test.dart test/presentation/home_screen_test.dart` : OK, aucune issue.
- `HOME=/tmp /opt/flutter/bin/flutter test test/application/use_cases_test.dart test/infrastructure/drift_repositories_test.dart` : non exécutable dans ce sandbox, le wrapper Flutter tente d'écrire dans `/opt/flutter/bin/cache` en lecture seule.
- `HOME=/tmp /opt/flutter/bin/cache/dart-sdk/bin/dart test test/application/use_cases_test.dart test/infrastructure/drift_repositories_test.dart` : non exécutable dans ce sandbox, les build hooks tentent un accès réseau à `pub.dev` (`flutter_launcher_icons`).
## Écarts / limites
- Aucun parsing de `historySnapshotJson`, conformément au hors-MVP.
- Aucun contrat programme ajouté.
- Le nom d'exercice pour les `step-results` est récupéré depuis le set-result parent quand présent ; à défaut, la projection retombe sur `step_name_snapshot`, car la table `workout_history_step_results` ne porte pas de `exercise_name_snapshot`.
## Correctif régressions (commit a8baa31, DevBackend)
Suite au rapport de test réel de Main (2 échecs directement liés à #82) :
- décodage des timestamps entiers issus de `customSelect` en secondes Unix, ce qui corrige le bucket de semaines actives (était `1970-01-19`).
- tri déterministe des options d'exercice : actifs avant archivés, puis dernière séance, nom, clé.
## Vérification Main après correctif (commit a8baa31)
`flutter test --no-pub test/application/use_cases_test.dart test/infrastructure/drift_repositories_test.dart test/presentation/home_screen_test.dart` exécuté en environnement fonctionnel (hors sandbox Dev) : 102 tests, 1 seul échec — `exercise repository round-trips exercise option combinations stopwatch score with stopwatch scored steps` (FK media, dette préexistante déjà documentée sur #80/#81, sans rapport avec #82).
Les deux régressions #82 précédemment identifiées (semaines actives mal bucketées, tri des options d'exercice) sont corrigées et confirmées vertes.
**Lot backend B1/B2/B3 : GO.**
Étape suivante : DevFrontend enchaîne F1 (navigation + écran Progression global : Volume/Régularité/état vide) puis F2 (bloc Par exercice avec graphique).
# DevFrontend — Implémentation F1/F2
## Réalisé
- F1 : ajout de l'entrée `Progression` sur l'accueil après `Historique`, avec l'icône `show_chart` et le sous-titre validé.
- F1 : ajout d'une action secondaire `Progression` dans l'AppBar de l'écran `Historique`, ouvrant la même surface.
- F1 : création de `ProgressionScreen` avec sélecteur `4 semaines` / `3 mois` / `Tout`, défaut `4 semaines`, blocs `Volume` et `Régularité`, et état vide global.
- F2 : ajout du bloc `Par exercice` consommant les options/mesures du `ProgressionStatsUseCase`, avec badge `Exercice archivé`, segmented control masqué quand une seule mesure est disponible, graphique mono-série, résumé et états dédiés.
- F2 : tap sur un point du graphique ouvrant un bottom sheet avec détail et action `Voir la séance` vers le détail d'historique existant.
## Fichiers touchés
- `lib/presentation/progression_screen.dart`
- `lib/presentation/home_screen.dart`
- `lib/presentation/history_screen.dart`
- `lib/presentation/presentation.dart`
- `test/presentation/progression_screen_test.dart`
- `test/presentation/home_screen_test.dart`
- `test/presentation/history_screen_test.dart`
## Vérifications DevFrontend
- `HOME=/tmp /opt/flutter/bin/cache/dart-sdk/bin/dart analyze lib/presentation/progression_screen.dart lib/presentation/home_screen.dart lib/presentation/history_screen.dart lib/presentation/presentation.dart test/presentation/progression_screen_test.dart test/presentation/home_screen_test.dart test/presentation/history_screen_test.dart` : OK, aucune issue.
- `HOME=/tmp /opt/flutter/bin/cache/dart-sdk/bin/dart analyze` : OK au sens exit code 0 ; 26 infos préexistantes hors périmètre touché restent signalées.
- `HOME=/tmp /opt/flutter/bin/flutter test --no-pub test/presentation/progression_screen_test.dart test/presentation/home_screen_test.dart test/presentation/history_screen_test.dart` : non exécutable dans ce sandbox, le wrapper Flutter tente d'écrire dans `/opt/flutter/bin/cache` en lecture seule.
- `HOME=/tmp /opt/flutter/bin/cache/dart-sdk/bin/dart test test/presentation/progression_screen_test.dart test/presentation/home_screen_test.dart test/presentation/history_screen_test.dart` : non exécutable dans ce sandbox, les build hooks tentent un accès réseau à `pub.dev` (`flutter_launcher_icons`).
## Écarts / limites
- L'outil `idea_ticket_update_carnet` demandé n'était pas exposé dans cette session ; le résumé a été ajouté directement au fichier carnet du ticket.
- La ligne de contexte `Depuis le ...` pour les périodes bornées n'est pas affichée, car le DTO frontend disponible n'expose pas la date de début formatable ; la ligne étant facultative côté UX, l'écran évite une approximation.
## Correctif tests widget DevFrontend
Suite au retour Main sur `test/presentation/progression_screen_test.dart` :
- le cas mesure unique affiche désormais le libellé de mesure (`Temps réalisé`, `Répétitions`, etc.) sans afficher de segmented control.
- le test de tap sur point rend explicitement le point visible avant interaction, conformément au comportement scrollable de l'écran.
Vérifications locales sandbox :
- `HOME=/tmp /opt/flutter/bin/cache/dart-sdk/bin/dart analyze lib/presentation/progression_screen.dart test/presentation/progression_screen_test.dart` : OK, aucune issue.
- `HOME=/tmp /opt/flutter/bin/flutter test --no-pub test/presentation/progression_screen_test.dart test/presentation/home_screen_test.dart test/presentation/history_screen_test.dart` : toujours non exécutable dans ce sandbox, le wrapper Flutter tente d'écrire dans `/opt/flutter/bin/cache` en lecture seule.
- Commit non réalisé dans ce sandbox : `.git/index.lock` ne peut pas être créé car `.git` est en lecture seule.
# QA — Validation fonctionnelle #82
## Périmètre relu
- Cadrage complet UX / Architect / DevBackend / DevFrontend lu via `idea_ticket_read("#82", includeCarnet=true)`.
- Fichiers relus : `lib/presentation/progression_screen.dart`, `lib/presentation/home_screen.dart`, `lib/presentation/history_screen.dart`, `test/presentation/progression_screen_test.dart`, `test/application/use_cases_test.dart`, `test/infrastructure/drift_repositories_test.dart`.
## Cohérence UX / frontend
- Accueil : entrée `Progression` présente après `Historique`, icône `Icons.show_chart`, sous-titre `Volume, régularité et évolution par exercice`.
- Historique : action secondaire `Progression` en AppBar vers la même surface.
- Écran : AppBar `Progression`, sélecteur `4 semaines` / `3 mois` / `Tout`, défaut `4 semaines`, blocs `Volume`, `Régularité`, `Par exercice`.
- Libellés attendus présents : `Séances`, `Temps total`, `Rythme récent`, `semaines actives`, `Choisir un exercice`, `Temps réalisé`, `Plus bas = mieux`, `Voir la séance`, `Exercice archivé`.
- Libellés interdits non trouvés dans les fichiers relus : `Dashboard`, `Analytics`, `Performance score`, `Objectif raté`, `Streak perdu`, `0 jour consécutif`, `Échec de progression`.
- États UX implémentés : vide global, absence de mesure exploitable, point unique, peu de données sur la période avec incitation à passer sur `Tout`, score non graphique, mesure chrono avec aide `Plus bas = mieux`.
- Écart relevé non bloquant : la ligne de contexte `Depuis le ...` pour période bornée n'est pas affichée ; elle est facultative côté UX et l'écart était déjà documenté par DevFrontend.
## Couverture de test relue
- `test/presentation/progression_screen_test.dart` couvre : période par défaut + état vide global, affichage `Volume` / `Régularité`, exercice archivé, point unique, mesure chrono `Temps réalisé` + `Plus bas = mieux`, tap sur point + bottom sheet + `Voir la séance`.
- `test/presentation/home_screen_test.dart` couvre l'entrée `Progression` après `Historique`.
- `test/presentation/history_screen_test.dart` couvre le lien secondaire `Progression`.
- `test/application/use_cases_test.dart` couvre : résolution période 4 semaines, `Tout`, compteur de semaines actives, options exercice + mesures, états de série `ready`, `emptyButHasAllTimeData`, `noGraphableMeasure`, résumés totals.
- `test/infrastructure/drift_repositories_test.dart` couvre : semaines actives et exclusion séances incomplètes / supprimées, tri actifs avant archivés, mesures score manuel / chrono / reps, exclusion skipped / valeurs absentes, signal mesure non graphique, agrégations par séance `MAX(actual_score)`, `MIN(actual_score_time_ms)`, `SUM(actual_reps)`, `SUM(actual_time_ms)`, ordre chronologique et `workoutHistoryId`, données all-time hors période.
Limite de couverture non bloquante : pas de test widget dédié à `textScoreOnly`, mais l'état est mappé dans `ProgressionScreen` et le modèle existant ne matérialise pas encore les scores texte graphiques selon le cadrage Architect.
## Commandes exécutées par QA
Commande :
```sh
HOME=/tmp /opt/flutter/bin/cache/dart-sdk/bin/dart analyze
```
Sortie réelle pertinente :
```text
Analyzing GameTime...
26 issues found.
```
Verdict commande : exit code 0 ; les 26 issues sont des infos existantes hors périmètre #82.
Commande :
```sh
HOME=/tmp /opt/flutter/bin/cache/dart-sdk/bin/dart analyze lib/presentation/progression_screen.dart lib/presentation/home_screen.dart lib/presentation/history_screen.dart test/presentation/progression_screen_test.dart test/application/use_cases_test.dart test/infrastructure/drift_repositories_test.dart
```
Sortie réelle pertinente :
```text
Analyzing progression_screen.dart, home_screen.dart, history_screen.dart, progression_screen_test.dart, use_cases_test.dart, drift_repositories_test.dart...
No issues found!
```
Verdict commande : VERT.
Commande :
```sh
HOME=/tmp /opt/flutter/bin/flutter test --no-pub test/presentation/progression_screen_test.dart test/application/use_cases_test.dart test/infrastructure/drift_repositories_test.dart
```
Sortie réelle :
```text
/opt/flutter/bin/internal/update_engine_version.sh: line 71: /opt/flutter/bin/cache/engine.stamp.tmp.24: Read-only file system
/opt/flutter/bin/internal/update_engine_version.sh: line 78: /opt/flutter/bin/cache/engine.realm: Read-only file system
```
Verdict commande : non exécutée, blocage environnement Flutter cache read-only avant lancement des tests.
Commande :
```sh
HOME=/tmp /opt/flutter/bin/cache/dart-sdk/bin/dart test test/presentation/progression_screen_test.dart test/application/use_cases_test.dart test/infrastructure/drift_repositories_test.dart
```
Sortie réelle :
```text
Running build hooks...Running build hooks...Got socket error trying to find package flutter_launcher_icons at https://pub.dev.
```
Verdict commande : non exécutée jusqu'au vert, blocage réseau/build hooks.
## Verdict QA
**GO fonctionnel et cohérence #82**, sous réserve explicite que cette session QA n'a pas pu relancer `flutter test` à cause du sandbox. Aucun écart bloquant trouvé à la lecture du cadrage, de l'implémentation frontend et de la couverture de tests. Les preuves exécutées localement sont : `dart analyze` complet exit 0 et `dart analyze` ciblé vert sans issue.