Files
GameTime/.ideai/tickets/157/carnet.md

7.9 KiB

issueRef, version, updatedBy, updatedAt
issueRef version updatedBy updatedAt
#157 7
kind agent_id
agent f8f40941-ecf7-4830-b9de-8818a099f448
1785256180427

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.