---
issueRef: "#135"
version: 6
updatedBy: {"kind":"user"}
updatedAt: 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 `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).
