---
issueRef: "#157"
version: 8
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1785321673274
---
## Cadrage UX — agent UX (2026-07-28)

Statut : **cadrage exploitable**, aucune nouvelle surface à créer. Ce ticket branche une donnée (distance live) sur des emplacements déjà spécifiés et déjà construits par [[gametime-ux-online-client]]-adjacent (#155 cadrage transverse) et #159 (surface montre, fermé). Ne pas concevoir de nouvel écran ni de nouveau composant : la distance vient simplement remplir un slot déjà réservé, aujourd'hui silencieusement absent.

### Rappel des deux emplacements concernés (déjà cadrés par #155, à ne pas rouvrir)

**Téléphone, barre stats live compacte sous le bloc d'exécution :**
```
FC 142 bpm   ·   0,84 km   ·   186 kcal
```
- ordre fixe **FC → Distance → Calories**, la distance est toujours au milieu ;
- aujourd'hui la barre affiche seulement `FC 142 bpm` (distance et calories absentes) — c'est le comportement silencieux attendu, pas un bug ;
- dès que la distance devient disponible, elle s'insère à sa place dans l'ordre, sans changement de layout, sans réanimation d'apparition particulière au-delà du refresh discret déjà en place pour la FC.

**Montre, écran secondaire `Stats` (glissement inverse d'`Actions`, cf. #159) :**
- une ligne `Distance` vient s'ajouter aux côtés de `FC` (icône cœur, cf. #161) et `Calories`, toujours dans l'ordre FC / Distance / Calories ;
- pas d'icône dédiée à inventer pour la distance : libellé texte `Distance` en Archivo au-dessus de la valeur, même traitement typographique que `Calories` (aucune des deux n'est la donnée dominante de l'écran montre, contrairement à la FC sur l'écran principal) ;
- écran principal montre : **la distance n'y apparaît jamais**, seule la FC y a droit (règle #155/#161 déjà actée, ce ticket ne la touche pas).

### Format d'affichage
- Unité `km`, séparateur décimal virgule (locale FR), deux décimales : `0,84 km` (cohérent avec l'exemple historique `2,84 km` déjà utilisé dans le cadrage #155/carte historique).
- Pas de conversion m/km dynamique à ce stade (pas de "840 m" sous 1km) — rester simple, un seul format, cohérent partout où la distance est affichée (téléphone, montre, futur après-séance/historique #158/#160).

### Garde-fous
- **Silence total si la donnée est absente** (pas de capteur GPS/distance sur la montre, pas de fix GPS encore acquis, montre non compatible) : la ligne/le segment disparaît, jamais de `--` ni de placeholder. Le comportement actuel (barre réduite à `FC` seule) est déjà la bonne référence.
- **Perte temporaire de mesure live** : garder la dernière valeur connue plutôt que de faire disparaître puis réapparaître le segment en boucle (cf. règle #155 "perte temporaire → dernière valeur connue si la session l'utilise déjà").
- Ne pas transformer la distance en donnée dominante nulle part sur ce lot : elle reste secondaire au téléphone (barre compacte) et secondaire à la montre (écran Stats uniquement, jamais écran principal).
- Aucun message bloquant, aucune demande de permission spécifique visible à l'utilisateur au-delà de ce qui est déjà géré par les tickets permissions capteurs montre déjà livrés (cf. commits f71a1551/58272e35).

### Compatibilité avec #155/#159/#161 déjà traités
Ce ticket ne modifie aucune décision de #155 (surfaces), #159 (navigation montre par glissement inverse) ni #161 (icône cœur FC, dédoublonnage écran principal). Il alimente uniquement les emplacements distance déjà prévus et actuellement vides faute de donnée.

---

## Cadrage Architect (2026-07-28)

Statut : **surprise majeure — ce ticket n'a rien de "Frontend" restant à faire**. Vérifié en lecture directe :

- `lib/presentation/workout_execution_screen.dart` (`_LiveSensorBar`, ~L2221) affiche déjà FC + Distance + Calories en pills conditionnelles (`if (sensorState.latestHeartRateBpm case final heartRate?)`, `if (_distanceLabel(sensorState) case final distanceLabel?)`, `if (_caloriesLabel(sensorState) case final caloriesLabel?)`) — silence total déjà implémenté exactement comme cadré par UX.
- `watch_app/lib/presentation/watch_session_screen.dart` (~L1187-1196) affiche déjà `Distance`/`Calories` dans l'écran secondaire `Stats`, via `_distanceLabel`/`_caloriesLabel` sur `WatchSensorSample`.
- Le contrat `packages/watch_bridge_contract/.../watch_bridge_contract.dart` porte déjà `distanceMeters` et `caloriesKcal` sur `WatchSensorSample`/`WatchSensorSummary` (schemaVersion 4), sérialisation JSON incluse.
- Donc l'intégralité de la chaîne UI téléphone + UI montre + contrat de transport est **déjà livrée**. Le titre "[DevFrontend] Distance live montre" ne correspond plus à l'état réel du code.

### Root cause probable du "distance toujours vide" — risque architecture réel côté watch natif
`WatchHeartRateCollector.kt:106-107` enregistre `DataType.DISTANCE` et `DataType.CALORIES` via **`MeasureClient.registerMeasureCallback`**, le même client que pour la FC. Or Health Services `MeasureClient` est documenté par Google comme ne supportant en pratique de façon fiable que quelques types passifs (FC, et sur certains devices SpO2) : **la distance et les calories nécessitent normalement un `ExerciseClient`** (session d'exercice Health Services explicite, avec `ExerciseType`, capacités GPS le cas échéant), pas le mode passif "measure" utilisé ici.

Conséquence probable : sur device réel, `registerMeasureCallback(DataType.DISTANCE, ...)` échoue ou ne remonte simplement jamais de données — et comme `onRegistrationFailed` n'est que loggé (jamais remonté), ce échec est **indiscernable du fallback silencieux voulu par l'UX** ("pas de donnée = rien ne s'affiche"). C'est très probablement pour cela que le ticket reste ouvert malgré une UI déjà terminée : la donnée source ne remonte jamais côté device réel, mais aucun signal ne le révèle sans instrumentation ciblée.

### Plan exécutable
1. **Vérification (watch bridge natif, DevBackend/watch)** : appeler `measureClient.getCapabilities()` sur un device Wear OS réel compatible et confirmer si `DataType.DISTANCE`/`DataType.CALORIES` sont listés comme supportés en mode `MeasureClient`. C'est un test de quelques lignes, pas un chantier.
2. **Si non supporté (cas attendu)** : cadrer un sous-lot dédié `ExerciseClient` pour distance/calories, distinct du pipeline FC actuel — cycle de vie propre (start/pause/stop de session d'exercice aligné sur la séance GameTime), permissions supplémentaires possibles (`ACCESS_FINE_LOCATION` selon device pour la distance GPS-based). Ne pas le faire rentrer de force dans `WatchHeartRateCollector` : il mérite son propre collecteur (`WatchExerciseMetricsCollector` ou équivalent) branché sur le même pipeline de sample/summary existant (mêmes champs `WatchSensorSample.distanceMeters/caloriesKcal`, pas de nouveau contrat nécessaire).
3. **Si supporté mais silencieux pour une autre raison** (permission refusée, capability disponible mais jamais déclenchée) : corriger l'enregistrement/l'octroi de permission ciblé, toujours sans toucher au contrat ni à l'UI.
4. **Aucun travail Frontend Dart/Flutter requis** dans les deux cas — l'UI est prête et attend juste que le sample porte une valeur non nulle.
5. Reclassement recommandé : ce ticket devrait passer de `[DevFrontend]` à `[DevBackend]`/watch bridge natif dans son titre, pour éviter qu'il soit repris à tort comme un chantier Flutter.

### Risque transverse
Si le sous-lot `ExerciseClient` s'avère nécessaire, il ajoute une complexité (permission localisation, gestion double session Health Services FC+Exercise en parallèle) non anticipée par le cadrage initial #156, qui supposait implicitement une seule source de collecte. À valider avec Main si le produit veut assumer cette complexité maintenant ou accepter un dégradé silencieux permanent (distance/calories jamais peuplées) sur les devices où seul `MeasureClient` HR fonctionne.
