From 3885709d5a834e048f4daaad92f2ead7b98e2b33 Mon Sep 17 00:00:00 2001 From: Blomios Date: Wed, 22 Jul 2026 09:02:23 +0200 Subject: [PATCH] =?UTF-8?q?chore(tickets):=20cl=C3=B4ture=20du=20ticket=20?= =?UTF-8?q?#82=20et=20ajout=20des=20tickets=20#92/#93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Métadonnées de suivi IdeA : ticket #82 (statistiques de progression) passé à closed après validation QA GO, ajout des nouveaux tickets #92 et #93 non encore traités. Co-Authored-By: Claude Opus 4.8 --- .ideai/tickets/82/carnet.md | 817 +++++++++++++++++++++++++++++++++++- .ideai/tickets/82/issue.md | 4 +- .ideai/tickets/92/carnet.md | 6 + .ideai/tickets/92/issue.md | 16 + .ideai/tickets/93/carnet.md | 6 + .ideai/tickets/93/issue.md | 19 + .ideai/tickets/counter.json | 2 +- .ideai/tickets/index.json | 26 +- 8 files changed, 890 insertions(+), 6 deletions(-) create mode 100644 .ideai/tickets/92/carnet.md create mode 100644 .ideai/tickets/92/issue.md create mode 100644 .ideai/tickets/93/carnet.md create mode 100644 .ideai/tickets/93/issue.md diff --git a/.ideai/tickets/82/carnet.md b/.ideai/tickets/82/carnet.md index 47ba701..b5447c6 100644 --- a/.ideai/tickets/82/carnet.md +++ b/.ideai/tickets/82/carnet.md @@ -1,6 +1,819 @@ --- issueRef: "#82" -version: 1 +version: 4 updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} -updatedAt: 1784650716408 +updatedAt: 1784702739569 --- +# 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 readGlobalStats(ProgressionDateRange range); + Future> listExerciseOptions(ProgressionDateRange range); + Future> listMeasureOptions({ + required ProgressionDateRange range, + required String exerciseKey, + }); + Future 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 getOverview(ProgressionPeriod period); + Future 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 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 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 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. diff --git a/.ideai/tickets/82/issue.md b/.ideai/tickets/82/issue.md index 6c485fe..1d8401a 100644 --- a/.ideai/tickets/82/issue.md +++ b/.ideai/tickets/82/issue.md @@ -10,8 +10,8 @@ agentRefs: [] createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} createdAt: 1784650716408 -updatedAt: 1784650716408 -version: 1 +updatedAt: 1784702739569 +version: 4 --- Constat Commercial : l'historique actuel liste les séances jouées mais ne donne pas de lecture de progression. Les concurrents (JEFIT, Fitbod) valorisent fortement les graphiques de progression et le volume d'entraînement pour donner une raison de revenir. diff --git a/.ideai/tickets/92/carnet.md b/.ideai/tickets/92/carnet.md new file mode 100644 index 0000000..968c142 --- /dev/null +++ b/.ideai/tickets/92/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#92" +version: 2 +updatedBy: {"kind":"user"} +updatedAt: 1784701920855 +--- diff --git a/.ideai/tickets/92/issue.md b/.ideai/tickets/92/issue.md new file mode 100644 index 0000000..ac9e032 --- /dev/null +++ b/.ideai/tickets/92/issue.md @@ -0,0 +1,16 @@ +--- +id: "048920d0-e0e0-4f5c-a0f9-d59b22a9c4c4" +number: 92 +title: "Ne pas couper la musique avec les chronos" +status: "open" +priority: "high" +sprint: null +links: [] +agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1784701912384 +updatedAt: 1784701920855 +version: 2 +--- +Lorsqu'un chrono de temps d'une step arrive sur les 3 dernières secondes, le son produit ne doit pas couper la musique de l'utilisateur \ No newline at end of file diff --git a/.ideai/tickets/93/carnet.md b/.ideai/tickets/93/carnet.md new file mode 100644 index 0000000..edff06f --- /dev/null +++ b/.ideai/tickets/93/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#93" +version: 2 +updatedBy: {"kind":"user"} +updatedAt: 1784703025159 +--- diff --git a/.ideai/tickets/93/issue.md b/.ideai/tickets/93/issue.md new file mode 100644 index 0000000..c3942e5 --- /dev/null +++ b/.ideai/tickets/93/issue.md @@ -0,0 +1,19 @@ +--- +id: "7e1153ec-7bbf-4eb5-86fa-7e264c946a15" +number: 93 +title: "[UI] UI séance a revoir" +status: "open" +priority: "high" +sprint: null +links: [] +agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1784702625510 +updatedAt: 1784703025159 +version: 2 +--- +L'UI d'une séance est a revoir. Toutes les informations sont sans scroll comme demandé et c'est très bien il faut garder ça. Je pense cependant qu'on peut enlever la partie "Passages réalisés", cette information est déjà dans la partie juste au dessus ou on affiche soit "Passage en cours" soit "passage 1/10" par exemple. Ça fera déjà gagner un peu de place. +Ensuite dans la première partie il y a trop de chose. On affiche notre série actuelle, notre programme, notre exercice c'est beaucoup trop. Je pense qu'afficher la série et l'exercice est suffisant. Pour peut par exemple jouter le nom du programme en cours en plus petit et crop si nécessaire a côté du nom de la séance. Pour le chrono de l'exercice, "Demarrer l'exercice" est peut être trop long. +Les références affichés pour des temps ne me semble pas de la bonne unité, j'ai plusieurs milliers. +Pour finir dans l'encadré de l'étape en cours il semble y avoir un soucis tant que je n'ai pas lancé le chrono de l'étape, le temps et le bouton affiches sont en tout petit \ No newline at end of file diff --git a/.ideai/tickets/counter.json b/.ideai/tickets/counter.json index 1e2c60e..082352b 100644 --- a/.ideai/tickets/counter.json +++ b/.ideai/tickets/counter.json @@ -1,3 +1,3 @@ { - "nextNumber": 92 + "nextNumber": 94 } \ No newline at end of file diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json index b0d4495..b8a4b81 100644 --- a/.ideai/tickets/index.json +++ b/.ideai/tickets/index.json @@ -931,7 +931,7 @@ "priority": "medium", "sprint": null, "assignedAgentIds": [], - "updatedAt": 1784650716408 + "updatedAt": 1784702739569 }, { "issueRef": "#83", @@ -1014,6 +1014,30 @@ "sprint": null, "assignedAgentIds": [], "updatedAt": 1784656427938 + }, + { + "issueRef": "#92", + "path": "92", + "title": "Ne pas couper la musique avec les chronos", + "status": "open", + "priority": "high", + "sprint": null, + "assignedAgentIds": [ + "57695b92-24d0-4876-837c-76116e70a6ae" + ], + "updatedAt": 1784701920855 + }, + { + "issueRef": "#93", + "path": "93", + "title": "[UI] UI séance a revoir", + "status": "open", + "priority": "high", + "sprint": null, + "assignedAgentIds": [ + "57695b92-24d0-4876-837c-76116e70a6ae" + ], + "updatedAt": 1784703025159 } ] } \ No newline at end of file