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

7.3 KiB

issueRef, version, updatedBy, updatedAt
issueRef version updatedBy updatedAt
#135 6
kind
user
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::sendPrimaryActionpauseSession/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 primaryAction n'est pas exactement pause/resume au moment du tap (fenêtre de transition d'état) ou si connectionLost/commandPending est vrai suite à une commande précédente mal purgée. Vérifier la robustesse de la purge de commandPending après timeout/ack et l'exactitude de la projection primaryAction en continu.
  • « parfois lent » : commandTimeout est 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 _PendingDot dé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) appelle emitCurrentProjection() toutes les 2s.
  • WatchCompanionProjectionUseCases.emitCurrentProjection (lib/application/use_cases.dart:3334-3342) fait _revision += 1 inconditionnellement, même sans changement métier réel.
  • pauseSession/resumeSession ne sont pas dans _allowsStaleRevision (use_cases.dart:3467-3470 — seuls incrementScore/decrementScore en sont exemptés). Toute commande pause/resume envoyée avec un expectedRevision capté juste avant un tick de refresh (jusqu'à toutes les 2s, en continu, tant qu'une séance est active) risque WatchCommandAck.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 (waitingThreshold 500ms / commandTimeout 2s) — 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) exige primaryAction exactement 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 _revision dans emitCurrentProjection() que sur changement réel de projection (hors revision/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 _ManualScoreContent ligne 579-582), actif quand state.commandPending est 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 si commandPending provient 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 _PendingDot sur le bouton : sandbox via watch_app/test/presentation/watch_session_screen_test.dart — simuler commandPending=true aprè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).