Files
GameTime/.ideai/tickets/126/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

3.8 KiB

issueRef, version, updatedBy, updatedAt
issueRef version updatedBy updatedAt
#126 6
kind
user
1785216995347

#126 — Cadrage technique Architect (2026-07-27)

Root cause identifiée dans le code réel — bug de calcul, pas une question de conception.

_stepTimerProjection (lib/application/use_cases.dart:4032-4055) construit la projection du "chrono étape" envoyée à la montre avec :

referenceEpochMs: _epochMs(now),
accumulatedMs: state?.accumulatedMs ?? 0,   // BUG : valeur brute persistée, pas l'écoulé réel
startedAtEpochMs: state?.status == runningTimer ? _epochMs(state!.startedAt!) : null,

Comparer avec _restTimerProjection (ligne 4018-4030), qui fait ça correctement :

referenceEpochMs: _epochMs(now),
accumulatedMs: rest.elapsedMillisecondsAt(now),   // écoulé réel jusqu'à `now`
startedAtEpochMs: paused ? null : _epochMs(now),  // "reparti" depuis `now`, cohérent avec accumulatedMs déjà à jour

ActiveExerciseStepProgressState possède déjà elapsedMillisecondsAt(now) (lib/domain/entities.dart:1329-1334, accumulatedMs + now.difference(startedAt).inMilliseconds) — exactement la méthode que _restTimerProjection utilise et que _stepTimerProjection n'appelle pas.

Pourquoi ça boucle visuellement : WatchWearDataLayerAdapter._ensureProjectionRefreshLoop (lib/infrastructure/watch_bridge/wear_data_layer_adapter.dart:139-143) republie la projection complète toutes les 2 secondes (projectionRefreshInterval, keepalive/résilience), indépendamment de tout changement d'état. À chaque republication, le chrono étape reçoit un nouveau referenceEpochMs = now mais un accumulatedMs resté à sa valeur brute persistée (proche de 0 tant que l'étape n'a pas de checkpoint) — la montre recalcule alors un écoulé quasi nul et le countdown "resaute" près de sa valeur cible (targetMs), avant de redécompter localement pendant ~2s jusqu'à la prochaine republication. D'où le motif observé "1:00 → 59s → 1:00" en boucle, calé sur le cycle de 2s.

Correctif (contrat clair pour DevFrontend/DevBackend)

Aligner _stepTimerProjection sur le pattern déjà utilisé par _restTimerProjection :

accumulatedMs: state?.elapsedMillisecondsAt(now) ?? 0,
startedAtEpochMs: state?.status == ActiveExerciseStepProgressStatus.runningTimer
    ? _epochMs(now)
    : null,

Aucun changement de contrat (WatchTimerProjection inchangé), aucun impact sur la montre (l'interprétation _interpolatedElapsedMs côté watch_app est déjà correcte — c'est uniquement la donnée source côté téléphone qui est fausse).

Périmètre de vérification

Vérifier s'il existe un bug symétrique ailleurs — _setTimerProjection (ligne 4079) et _scoreStopwatchTimerProjection (ligne 4057) utilisent déjà state.accumulatedMs brut, mais ce n'est pas un bug pour eux : ce sont des chronos displayMode: elapsed (pas countdown) dont startedAtEpochMs pointe vers le vrai state.startedAt d'origine (pas now) — cohérent, l'écoulé est recalculé correctement côté montre à partir du vrai point de départ. Seul _stepTimerProjection mélange les deux styles (accumulatedMs brut + referenceEpochMs à now), ce qui est l'incohérence à corriger.

Tests

Test unitaire ciblé sur _stepTimerProjection/WatchSessionProjectionProjector : un chrono étape runningTimer avec startedAt antérieur à now doit produire un accumulatedMs qui reflète l'écoulé réel (pas 0), republié à l'identique après un second appel à now+2s sans avoir régressé. Peut s'ajouter à test/application/watch_companion_projection_test.dart (fichier déjà existant pour ce projecteur, cf. gametime-watch-companion-implementation).

Prêt pour implémentation directe, aucun arbitrage supplémentaire nécessaire.