Files
GameTime/.ideai/tickets/94/carnet.md
Blomios 917777e18b chore(wip): consolidation intermédiaire multi-tickets (sprints Statistiques, UI, Bug resolution, Serveur-client)
Regroupe l'état de travail en cours réalisé dans un même worktree sur
plusieurs tickets/sprints (#85, #136, #145, #155-160, #162-164),
mélangeant des tickets QA et inProgress. Ne constitue pas une feature
terminée : commit de sauvegarde avant triage/split par ticket en
branches feature/* dédiées. Exclut les dossiers d'environnement de
build locaux et le heap dump parasite (.gitignore mis à jour).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 16:48:54 +02:00

11 KiB

issueRef, version, updatedBy, updatedAt
issueRef version updatedBy updatedAt
#94 8
kind agent_id
agent 57695b92-24d0-4876-837c-76116e70a6ae
1784936913063

Architect — Cadrage point 2 : regroupement des étapes sans chrono (ticket #94)

Périmètre : uniquement le point 2 de l'analyse UX ci-dessus (regroupement/auto-complétion silencieuse des étapes sans chrono). Les points 1/3/4/5 restent des ajustements UI purs, DevFrontend les traite directement sans ce cadrage.

Ce qui existe déjà et se réutilise tel quel

Code lu : lib/application/use_cases.dart (ActiveExerciseStepUseCases, ~L3087-3653) et lib/domain/entities.dart (ActiveExerciseStepProgressState, ActiveExerciseStepResult, ExerciseStep, ActiveExerciseStepProgressStatus).

  • _StepSequenceContext porte déjà steps, expectedPassages, autoStartNextTimedStep (résolu depuis #73/#76) — tout ce qu'il faut pour scanner la timeline à venir sans lecture DB supplémentaire.
  • _advanceState(context, state, now, {completedStep}) fait déjà avancer d'une étape en traversant les frontières de passage (nextStep >= steps.length → nextPassage++) et sait déjà produire sequenceComplete en fin de dernier passage. C'est exactement la mécanique de traversée qu'il faut réutiliser en boucle pour le regroupement — pas besoin de réécrire la logique de frontière de passage, juste de l'appeler plusieurs fois d'affilée avant l'action déclenchante, comme _autoAdvanceElapsedTimers le fait déjà pour les chronos qui s'enchaînent.
  • _stepResult(...) sait déjà construire un ActiveExerciseStepResult completed avec actualReps ?? step.defaultTargetValue (cf. completeCurrentStep, L3301-3303) — c'est la convention à réutiliser pour l'auto-complétion silencieuse (valeur cible enregistrée comme valeur réalisée, jamais null, cohérent avec gametime-architecture-exercise-steps qui laissait ce choix à l'implémentation).
  • ActiveExerciseStepProgressStatus.waitingManual reste le statut correct pour une étape reps individuelle ; on ne touche pas à l'enum.

Ce qui doit changer : nouvelle logique de calcul, pas de nouveau champ persisté

Pas de changement de modèle de données. ActiveExerciseStepProgressState et ActiveExerciseStepResult restent inchangés dans leur forme. La notion de « suite silencieuse » est dérivée à la volée depuis context.steps + position courante, exactement comme _isNextTimedStepReady (présentation, ~L2354-2368) dérive déjà Chrono suivant prêt sans champ dédié. Raison : la suite n'est qu'une lecture en avance sur une liste déjà chargée en mémoire (max 8 étapes, cf. gametime-architecture-exercise-steps), aucune raison de la persister.

Règle d'éligibilité au regroupement silencieux

Une étape est éligible au regroupement silencieux si et seulement si :

bool _isSilentStep(ExerciseStep step) {
  if (step.type == ExerciseStepType.time) return false;
  if (step.hasScore && step.scoreInputMode == ScoreInputMode.stopwatch) return false;
  return true; // reps sans score, ou reps avec score manuel
}

C'est la traduction directe du cas limite déjà identifié par UX : une étape reps à score chrono a son propre chrono actif et reste un point d'interaction à part entière.

Calcul de la suite courante (nouvelle fonction application, pure, sans I/O)

_SilentStepGroup _resolveSilentGroup(
  _StepSequenceContext context,
  ActiveExerciseStepProgressState state,
) {
  final steps = <ExerciseStep>[];
  var passage = state.currentPassageIndex;
  var index = state.currentStepIndex;
  while (true) {
    final step = context.steps[index];
    if (!_isSilentStep(step)) {
      return _SilentStepGroup(steps: steps, blockingStep: step, blockingPassageIndex: passage, reachesSequenceEnd: false);
    }
    steps.add(step);
    index++;
    if (index >= context.steps.length) {
      index = 0;
      passage++;
      if (passage >= context.expectedPassages) {
        return _SilentStepGroup(steps: steps, blockingStep: null, blockingPassageIndex: passage, reachesSequenceEnd: true);
      }
    }
  }
}

Appelée uniquement quand l'étape courante (_currentStep(context, state)) est elle-même silencieuse — sinon la vue reste le mode _CurrentStepPane classique existant (étape chronométrée, ou étape reps isolée). En pratique une étape reps isolée forme un groupe à 1 élément : le layout groupé et le layout actuel convergent visuellement pour ce cas trivial. DevFrontend affiche le layout groupé dès qu'il y a ≥1 étape silencieuse en tête, pas seulement ≥2, pour ne pas avoir deux présentations différentes selon la longueur de la suite.

Port/contrat exposé à la présentation

Étendre ActiveExerciseStepProgressView (pas de nouveau port séparé — c'est le même port ActiveExerciseStepUseCases/vue déjà consommé par l'écran) avec :

final class ActiveExerciseStepProgressView {
  // ... champs existants inchangés (state, steps, expectedPassages, results)
  final List<ExerciseStep> silentPendingSteps; // vide si l'étape courante n'est pas silencieuse
  final ExerciseStep? silentGroupBlockingStep; // étape chronométrée qui suit la suite, null si fin de séquence
  final bool silentGroupReachesSequenceEnd; // true si la suite va jusqu'à sequenceComplete
}

Calculée dans startOrResumeProgress/readProgress à partir de _resolveSilentGroup, en lecture seule — aucune écriture tant que l'utilisateur n'a pas déclenché l'action de clôture. silentPendingSteps est la liste à afficher (« Pompes 10 reps / Squats 15 reps / Fentes 10 reps ») ; silentGroupBlockingStep alimente la ligne « Chrono suivant : Gainage · 20 s » ; silentGroupReachesSequenceEnd sert à afficher le message de fin de série au lieu du bouton Lancer.

Nouvelle méthode d'auto-complétion silencieuse (écriture)

Un seul point d'entrée, appelé par les deux actions déclenchantes :

Future<ActiveExerciseStepProgressState> completePendingSilentSteps({
  required String sessionId,
  required int programIndex,
  required int exerciseIndex,
  required int setIndex,
}) async {
  final context = await _stepContext(...);
  var state = await _requiredStepProgressState(...);
  final now = clock.now();
  while (_isSilentStep(_currentStep(context, state))) {
    final step = _currentStep(context, state);
    final result = _stepResult(
      context: context, state: state, step: step,
      status: SetResultStatus.completed, now: now,
      actualReps: step.type == ExerciseStepType.reps ? step.defaultTargetValue : null,
    );
    await sessionRepository.saveExerciseStepResult(result);
    state = _advanceState(context, state, now, completedStep: step);
  }
  await sessionRepository.saveExerciseStepProgressState(state);
  return state;
}

Réutilise _advanceState tel quel pour la traversée multi-passages (aucune modification de cette méthode nécessaire : elle gère déjà nextPassage++ et sequenceComplete).

Intégration côté « Lancer » (chrono suivant) : startTimer(...) doit appeler completePendingSilentSteps(...) en tout premier (si l'étape courante est silencieuse) avant sa logique actuelle — après quoi l'étape courante est bien l'étape chronométrée bloquante, et le reste de startTimer fonctionne sans modification.

Intégration côté « Terminer la série » : le use case derrière _finishSet côté présentation (recordCurrentSetResult/upsertSetResultAtPosition, lib/application/use_cases.dart ~L2435-2487) doit, quand une séquence d'étapes existe pour la série (context résoluble sans exception) et que l'état n'est pas déjà sequenceComplete, appeler completePendingSilentSteps(...) avant d'enregistrer le résultat de série — silencieusement, sans passer par un dialogue supplémentaire (contrairement au dialogue existant Chrono non lancé qui gère un autre cas). Point d'attention DevBackend : ne déclencher cet appel que si l'étape courante est silencieuse ; si elle est déjà chronométrée/bloquante, ne rien faire (comportement actuel inchangé, l'utilisateur clôture une série avec une étape chrono en cours comme aujourd'hui).

Cas limites couverts par ce calcul

  • Traversée de plusieurs passages : couverte nativement par la boucle while de _resolveSilentGroup/completePendingSilentSteps, qui réutilise _advanceState — aucune règle spéciale à écrire, c'est la même mécanique que _autoAdvanceElapsedTimers applique déjà pour les chronos.
  • Étape reps à score chrono : exclue par _isSilentStep, casse le regroupement comme demandé — elle redevient un point d'arrêt classique (waitingManual), sans changement de comportement pour elle.
  • Séquence entièrement sans chrono : silentGroupReachesSequenceEnd == true dès startOrResumeProgress, dès la première étape ; completePendingSilentSteps sera appelée uniquement à Terminer la série, donc aucune écriture avant cette action (conforme à « l'utilisateur ne touche le téléphone qu'une fois, à la fin »).
  • Reprise après kill/pause : rien à faire de spécial, silentPendingSteps est recalculé à chaque startOrResumeProgress/readProgress depuis l'état persistant réel (pas de champ volatile en mémoire à perdre).

Pas de réglage configurable

Confirmé avec la recommandation UX : comportement toujours actif, pas d'override façon autoStartNextTimedStep (#73). Contrairement à #73, il n'existe ici aucune fenêtre de temps réel entre étapes silencieuses (pas de chrono qui tourne), donc aucun cas d'usage produit ne justifie une confirmation étape par étape. Ne pas ajouter de champ domaine Exercise/ProgramExercise/WorkoutTemplateExerciseOverride pour ce point. Si un besoin réel émerge après usage, rouvrir un ticket dédié plutôt que d'anticiper.

Découpage en lots

  • B1 — DevBackend : _isSilentStep, _resolveSilentGroup, extension de ActiveExerciseStepProgressView (3 nouveaux champs), completePendingSilentSteps, intégration dans startTimer et dans le use case de clôture de série (recordCurrentSetResult/équivalent derrière _finishSet). Tests ciblés : suite silencieuse simple, suite traversant 2+ passages, suite bloquée par étape à score chrono, suite qui va jusqu'à sequenceComplete, reprise après kill au milieu d'une suite silencieuse.
  • F — DevFrontend (en plus des points 1/3/4/5 déjà cadrés UI par UX) : nouveau layout du panneau séquence quand silentPendingSteps.isNotEmpty (liste « À enchaîner sans interruption » + ligne Chrono suivant : nom · cible + bouton Lancer, ou message de fin de série sans bouton dédié si silentGroupReachesSequenceEnd), réutilisant le format déjà défini par UX dans son cadrage ci-dessus. Dépend de B1.

Étape suivante

  • Git : créer la branche de travail pour #94.
  • DevBackend : lot B1 ci-dessus (point 2 uniquement).
  • DevFrontend : points 1, 2 (lot F, dépend de B1), 3, 4, 5 — cadrage UX complet disponible plus haut dans ce carnet pour 1/3/4/5, cadrage Architect ci-dessus pour le point 2.