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>
7.4 KiB
issueRef, version, updatedBy, updatedAt
| issueRef | version | updatedBy | updatedAt | ||
|---|---|---|---|---|---|
| #134 | 6 |
|
1785141267072 |
Cadrage UX (agent UX, 2026-07-27)
Nature : bug technique en premier lieu, avec un gap UX secondaire à corriger dans la même passe.
Localisation : watch_app/lib/presentation/watch_session_screen.dart::_handleSecondaryAction — action WatchSecondaryAction.skipCurrentSet (« Passer la série »), confirmée dans _ConfirmSheet, envoyée via WatchSessionViewModel.sendSecondaryAction.
Ce qui se passe actuellement : après confirmation, la commande est envoyée en unawaited puis l'écran revient immédiatement sur la vue Séance (_showSession()), sans attendre le résultat. Que la commande soit acceptée, rejetée (revision périmée, phase non applicable) ou acquittée en no-op, l'utilisateur voit exactement le même retour à l'affichage de la série. C'est cohérent avec le symptôme rapporté : « ça me renvoie sur l'affichage de la série sans rien faire ».
Diagnostic technique à vérifier par Architect/DevBackend/DevFrontend :
- pourquoi la commande est rejetée ou sans effet côté téléphone (mismatch
expectedRevision, phase courante qui n'accepte passkipCurrentSet, message perdu côté Data Layer) ; - si c'est un problème de routing/state machine, corriger la cause racine en priorité.
Décision UX à poser (indépendamment du bug) : le flux ne doit plus être aveugle au résultat. Ne pas naviguer en silence dans tous les cas :
- Garder la navigation immédiate vers l'écran Séance (cohérent avec le pattern existant, pas de nouvel écran d'attente qui bloquerait l'utilisateur).
- Mais si la commande est rejetée/no-op (
ackreçu ensuite avec statut rejeté), afficher un signal bref et non bloquant sur l'écran Séance — réutiliser le pattern déjà en place pour la perte de connexion (bandeau discret en haut, icône + texte court, disparaît de lui-même) plutôt qu'un dialogue. Libellé proposé : « Série non modifiée » (pas de jargon type "commande rejetée"). - Ne pas introduire de mention "hors ligne"/"mode local" (cohérent avec gametime-online-layer-philosophy déjà appliqué au reste de la montre).
Résumé : corriger la cause technique du rejet/perte de commande est prioritaire ; ajouter le signal d'échec est un complément UX nécessaire pour que l'absence d'effet ne soit plus jamais silencieuse, même si un futur bug similaire survient ailleurs.
Audit technique (agent Architect, 2026-07-27)
Root cause confirmée dans le code, partagée avec #135 : le refresh périodique côté téléphone bump la revision de projection sans condition, même quand rien n'a changé.
WatchWearDataLayerAdapter._ensureProjectionRefreshLoop(lib/infrastructure/watch_bridge/wear_data_layer_adapter.dart:151-155) appelleemitCurrentProjection()toutes les 2s (_projectionRefreshInterval), sans condition.WatchCompanionProjectionUseCases.emitCurrentProjection(lib/application/use_cases.dart:3334-3342) fait_revision += 1à chaque appel, qu'il y ait eu un changement métier ou non.- La montre envoie
expectedRevision: value.projection.revision(watch_app/lib/application/watch_session_view_model.dart:179), figé au moment du dernier projection reçue. WatchCompanionCommandHandler._allowsStaleRevision(lib/application/use_cases.dart:3467-3470) n'exempte queincrementScore/decrementScoredu contrôle de revision.skipCurrentSet(et tous les autres secondary actions) reste soumis àcommand.expectedRevision != projection.revision→WatchCommandAck.rejectedStaleRevision(use_cases.dart:3397-3399).- Le flux
skipCurrentSetimpose un_ConfirmSheet(tap "Passer la série ?" → confirmation), donc un délai utilisateur quasi garanti > 2s : la fenêtre de validité de la revision expire régulièrement avant l'envoi effectif de la commande. C'est structurellement, pas occasionnellement, défaillant. - Second bug indépendant, confirmé côté UI : même quand l'ack rejeté revient correctement (
WatchSessionViewModel._handleAcknettoie biencommandPendinget déclencherequestResync()), rien ne l'affiche :_handleSecondaryAction(watch_app/lib/presentation/watch_session_screen.dart:107-110) faitunawaited(sendSecondaryAction(action)); _showSession();sans jamais lirestate.lastAck. Aucun widget de l'écran Séance ne consultelastAckaujourd'hui — le bandeau "Série non modifiée" demandé par UX n'existe pas encore.
Contrat technique — DevBackend :
- Corriger
emitCurrentProjection()pour ne bumper_revisionque si la projection a réellement changé (comparaison hors champs volatilsrevision/projectedAtEpochMs, en traitant les champs de timer interpolables —accumulatedMs,referenceEpochMs,startedAtEpochMs,runState— comme stables tant que le timer tourne sans transition). Si rien n'a changé, republier/rafraîchir quand même (pour garder le Data Layer vivant) mais réutiliser la même revision. - Ne pas élargir
_allowsStaleRevisionàskipCurrentSetcomme raccourci :_isApplicable(use_cases.dart:3434-3465) garantit déjà que l'action est cohérente avec la phase courante ; le vrai bug est le bump inutile de revision, pas la vérification elle-même.
Contrat technique — DevFrontend (watch_app) :
WatchSessionViewModel: dans_handleAck, quand_isRejected(ack.status)est vrai pour une commande secondaire (pas primaire), exposer un état transitoire consommable par l'écran (ex. un champrejectedSecondaryNoticehorodaté dansWatchSessionUiState, à côté delastAckdéjà existant), auto-effacé après ~2.5s via unTimerinterne (pattern déjà utilisé pour_waitingTimer/_commandTimeoutTimer).watch_session_screen.dart: ajouter un bandeau réutilisant le pattern visuel deconnectionLost(icône + texte court,_SessionMainViewautour de la ligne 299-323) affichant « Série non modifiée » quand ce nouvel état transitoire est actif. Ne pas bloquer la navigation (comportement immédiat vers_showSession()conservé, conforme à la décision UX).- Point ouvert pour UX : le libellé « Série non modifiée » n'a été spécifié que pour
skipCurrentSet; le même mécanisme s'appliquera naturellement aux autres actions secondaires rejetées (skipCurrentStep,skipCurrentPassage,finishCurrentSet,skipCurrentRest). Confirmer si un libellé générique convient ou si chaque action a besoin de son propre texte.
Contrat bridge : aucune extension nécessaire. WatchCommandAckEvent/WatchCommandAck (package watch_bridge_contract) portent déjà commandId + statut suffisant pour ce fix ; c'est un fix côté projection (téléphone) + wiring d'état/UI (montre).
Testabilité :
- Fix revision (backend) : 100% sandbox, unit test sur
WatchCompanionProjectionUseCases.emitCurrentProjection()— appel répété sans changement de session ⇒ revision stable ; mutation de l'état session ⇒ revision bump. Partagé avec #135, un seul test à écrire couvre les deux tickets. - Bandeau montre : sandbox via
watch_app/test/presentation/watch_session_screen_test.dart(existe déjà, référenceskipCurrentSet) — injecter un ackrejectedStaleRevisionsimulé, vérifier l'apparition du bandeau puis sa disparition après le délai. - Vérification terrain (on-device) recommandée avant clôture : confirmer que le taux de rejet réel chute une fois le fix de revision posé, le round-trip Wear Data Layer réel n'étant pas reproductible en sandbox.