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.3 KiB
issueRef, version, updatedBy, updatedAt
| issueRef | version | updatedBy | updatedAt | ||
|---|---|---|---|---|---|
| #135 | 6 |
|
1785151136170 |
Cadrage UX (agent UX, 2026-07-27)
Nature : bug technique probable côté transport (latence/perte Wear Data Layer), avec un gap UX qui aggrave la perception du problème — à corriger aussi.
Localisation : watch_session_view_model.dart::sendPrimaryAction → pauseSession/resumeSession ; bouton _TimerToggleButton dans watch_session_screen.dart, activé seulement si canToggleTimer (!connectionLost && !commandPending && primaryAction ∈ {pauseSession, resumeSession}).
Diagnostic technique à vérifier par Architect/DevBackend :
- « parfois inopérant » : le bouton est désactivé si
primaryActionn'est pas exactement pause/resume au moment du tap (fenêtre de transition d'état) ou siconnectionLost/commandPendingest vrai suite à une commande précédente mal purgée. Vérifier la robustesse de la purge decommandPendingaprès timeout/ack et l'exactitude de la projectionprimaryActionen continu. - « parfois lent » :
commandTimeoutest fixé à 2s,waitingThresholdà 500ms — un aller-retour Data Layer peut légitimement approcher ces bornes. Vérifier s'il y a des retries/pertes réseau anormales plutôt que juste la latence normale du canal.
Décision UX à poser (indépendamment du bug) : pas d'optimisme sur pause/play — c'est un choix assumé (la montre n'est jamais source de vérité, cf. gametime-watch-companion-implementation), donc le geste ne doit jamais changer l'icône avant confirmation. Mais le seul signal actuel pendant l'attente est un texte en bas d'écran (« Envoi... » / « En attente du téléphone »), facile à manquer sur un cadran de montre — c'est ce qui fait percevoir la lenteur comme un blocage plutôt qu'un traitement en cours.
- Ajouter un signal directement sur le bouton pendant
commandPending: réutiliser le pattern_PendingDotdéjà utilisé pour le score (petit point/anneau discret superposé à l'icône pause/play), pas un nouveau composant. - Le bouton reste désactivé pendant l'attente (déjà correct, évite les doubles envois) — mais son état désactivé doit être visuellement identique qu'il soit désactivé pour attente de commande ou pour connexion perdue, ce qui est déjà le cas via
disabledColor. - Pas de message d'erreur nouveau si le timeout expire : le comportement existant (repasse en
connectionLost, la surface entière s'assombrit et affiche « Connexion au téléphone perdue ») est suffisant et cohérent — ne pas dupliquer un message par bouton.
Résumé : corriger la robustesse de la commande pause/resume (état + transport) est prioritaire ; ajouter un indicateur "en cours" sur le bouton lui-même est le seul ajustement UX nécessaire pour que la latence normale ne soit plus confondue avec une panne.
Audit technique (agent Architect, 2026-07-27)
Root cause confirmée dans le code, partagée avec #134 : même mécanisme de bump de revision inconditionnel côté téléphone.
WatchWearDataLayerAdapter._ensureProjectionRefreshLoop(lib/infrastructure/watch_bridge/wear_data_layer_adapter.dart:151-155) appelleemitCurrentProjection()toutes les 2s.WatchCompanionProjectionUseCases.emitCurrentProjection(lib/application/use_cases.dart:3334-3342) fait_revision += 1inconditionnellement, même sans changement métier réel.pauseSession/resumeSessionne sont pas dans_allowsStaleRevision(use_cases.dart:3467-3470— seulsincrementScore/decrementScoreen sont exemptés). Toute commande pause/resume envoyée avec unexpectedRevisioncapté juste avant un tick de refresh (jusqu'à toutes les 2s, en continu, tant qu'une séance est active) risqueWatchCommandAck.rejectedStaleRevision(use_cases.dart:3397-3399) même si rien n'a réellement changé côté téléphone entre-temps.- Ça explique directement les deux symptômes rapportés :
- « parfois inopérant » : rejet silencieux par staleness de revision, sans lien avec un vrai désaccord d'état pause/resume — c'est un faux positif de la vérification de concurrence.
- « parfois lent » : latence normale du round-trip Wear Data Layer (
waitingThreshold500ms /commandTimeout2s) — comportement transport attendu, pas un bug ; c'est bien un pur besoin d'indicateur UI comme cadré par UX.
- Point vérifié et non buggé :
canToggleTimer(watch_app/lib/presentation/watch_session_screen.dart:248-253) exigeprimaryActionexactement pause/resume et!commandPending— c'est le comportement voulu (pas d'optimisme), le bouton se désactive/réactive correctement au fil des projections ; aucune correction nécessaire ici.
Contrat technique — DevBackend :
- Même fix que #134 : ne bumper
_revisiondansemitCurrentProjection()que sur changement réel de projection (horsrevision/projectedAtEpochMs, timers interpolables traités comme stables tant qu'ils tournent sans transition). Un seul correctif, bénéficie aux deux tickets. - Ne pas ajouter
pauseSession/resumeSessionà_allowsStaleRevision: contrairement aux actions terminales de #134, accepter une revision périmée sur pause/resume pourrait appliquer le mauvais toggle si le téléphone a un état réellement différent (ex. auto-pause côté téléphone pour une autre raison) — la vérification de revision est la bonne garde-fou ici, conforme à la décision UX "pas d'optimisme". Le vrai fix est d'arrêter le bump inutile, pas d'affaiblir le contrôle.
Contrat technique — DevFrontend (watch_app) :
- Implémenter l'indicateur déjà cadré par UX : ajouter un prop
pending: boolà_TimerToggleButton(watch_session_screen.dart:624-649), affichant un point/anneau discret superposé à l'icône (réutiliser_PendingDot, déjà utilisé dans_ManualScoreContentligne 579-582), actif quandstate.commandPendingest vrai et que le bouton concerné est celui qui a déclenché la commande (onTogglePause != null, i.e.canToggleTimerétait vrai au moment du tap) — ne pas afficher le point sicommandPendingprovient d'une autre action (ex.skipCurrentRest) sur un bouton qui n'a pas été pressé pour pause/resume. - Wiring aux deux call sites existants :
_ActiveContent(ligne 484-487) et_RestContent(ligne 686-690). - Ne rien changer au comportement de timeout existant (retour à
connectionLost) — conforme à la décision UX de ne pas dupliquer de message par bouton.
Contrat bridge : aucune extension nécessaire — fix uniquement côté projection téléphone (partagé avec #134) + rendu local montre à partir de commandPending, déjà suivi dans WatchSessionViewModel.
Testabilité :
- Fix revision (backend) : sandbox, test unique partagé avec #134 sur
emitCurrentProjection(). - Indicateur
_PendingDotsur le bouton : sandbox viawatch_app/test/presentation/watch_session_screen_test.dart— simulercommandPending=trueaprès tap pause/resume, vérifier l'affichage du point, puis sa disparition à l'ack. - Vérification terrain (on-device) recommandée après le fix backend partagé : confirmer que le taux d'échec perçu ("inopérant") chute significativement une fois le bump de revision corrigé ; la latence résiduelle ("lent") restera normale et sera couverte par l'indicateur UI, à valider visuellement sur montre réelle (difficile à juger en sandbox pur).