Files
GameTime/.ideai/tickets/82/carnet.md
Blomios 3885709d5a chore(tickets): clôture du ticket #82 et ajout des tickets #92/#93
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 <noreply@anthropic.com>
2026-07-22 09:02:23 +02:00

34 KiB

issueRef, version, updatedBy, updatedAt
issueRef version updatedBy updatedAt
#82 4
kind agent_id
agent 57695b92-24d0-4876-837c-76116e70a6ae
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 :

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 :

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 :

Progression

Sous l'AppBar, un sélecteur de période simple :

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 :

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 :

Volume

Deux tuiles compactes :

Séances
8
Temps total
5 h 40

Sous les tuiles, une ligne de contexte facultative si pertinente :

Depuis le 24 juin

ou en période Tout :

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 :

Régularité

Libellé principal :

Rythme récent

Métrique MVP recommandée : semaines actives sur la période, pas streak quotidien.

Affichage :

3 / 4 semaines actives

Texte secondaire :

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 :

8 / 12 semaines actives

Si la période est Tout, afficher une formulation sans ratio trop long :

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 :

Par exercice

Choix de l'exercice

Contrôle : menu/selecteur compact.

Placeholder :

Choisir un exercice

Liste : exercices présents dans l'historique filtré, y compris exercices archivés avec badge :

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 :

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 :

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 :

Meilleur : 14 paniers
Dernier : 12 paniers

Pour score chrono :

Meilleur : 00:42.8
Dernier : 00:45.1

Pour répétitions :

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 :

12 juillet
Séance : Prépa match
Meilleur score : 14 paniers

Action secondaire :

Voir la séance

qui ouvre le détail historique existant.

États vides et limites

Aucun historique

Écran Progression :

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é :

1 semaine active

Dans Par exercice :

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.

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

HOME=/tmp /opt/flutter/bin/cache/dart-sdk/bin/dart analyze

Sortie réelle pertinente :

Analyzing GameTime...
26 issues found.

Verdict commande : exit code 0 ; les 26 issues sont des infos existantes hors périmètre #82.

Commande :

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 :

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 :

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 :

/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 :

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 :

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.