---
issueRef: "#163"
version: 12
updatedBy: {"kind":"user"}
updatedAt: 1785315994265
---
## Cadrage UX — agent UX (2026-07-28)

Statut : **impact UX cadré, mais le cœur du ticket reste technique** (Architect/DevBackend/QA — cf. tentative précédente KO faute de preuve device/toolchain, commit bc533d6c). Cette note ne prétend pas résoudre le problème de collecte en arrière-plan ; elle fixe ce que l'utilisateur doit voir (ou ne jamais voir) une fois la collecte réparée, et les critères d'acceptation QA côté surface.

### Principe directeur
La collecte de FC en écran éteint est un problème de **continuité de donnée**, pas un problème de surface. Aucune nouvelle UI montre ou téléphone n'est nécessaire pour ce ticket. Conformément à [[gametime-online-layer-philosophy]] et aux garde-fous déjà actés en #155 ("silence par défaut", "aucun message bloquant"), la correction doit être **invisible** pour l'utilisateur : il ne doit rien avoir à faire, rien à valider, rien à lire de nouveau.

### Comportement attendu
- La FC continue d'être **échantillonnée et enregistrée** pendant toute la séance, y compris quand l'écran de la montre est éteint/assombri (mode ambient) — l'écran éteint est un état d'affichage, pas un état de séance en pause.
- À l'écran rallumé (montre ou téléphone), la valeur affichée doit reprendre normalement, sans artefact visuel (pas de valeur figée depuis avant l'extinction affichée comme si elle était "live", pas de saut brutal non plausible).
- Aucun changement de placement/libellé : la FC reste affichée exactement comme cadré par #161 (icône cœur, écran principal montre) et #155 (barre téléphone) — ce ticket ne touche aucune de ces décisions.

### Garde-fous spécifiques à ce ticket
- **Ne jamais afficher de FC en direct correspondant à des données non capturées** : mieux vaut un trou silencieux dans les statistiques d'après-séance (cf. règle #155 "en cas de donnée partielle : afficher seulement ce qui existe") qu'une valeur interpolée ou dupliquée artificiellement pendant l'écran éteint — l'intégrité des agrégats min/moyenne/max par étape/série/exercice (#106/#144) en dépend directement. Une valeur fabriquée pendant le trou fausserait ces agrégats plus qu'elle ne les compléterait.
- **Aucune sollicitation utilisateur nouvelle** : si la résolution technique nécessite un service de fond (foreground service, exemption de mise en veille), l'autorisation doit rester dans le flux de permissions déjà débloqué par les tickets précédents (f71a1551/58272e35) — pas de nouvelle popup dédiée à ce ticket, pas d'explication technique exposée à l'utilisateur ("service en arrière-plan requis", batterie, etc.).
- Si la contrainte plateforme rend la collecte écran éteint réellement impossible sur certains appareils : dégrader **silencieusement** (la portion de séance concernée manque à l'historique, sans message), jamais bloquer la séance ni avertir l'utilisateur en cours de séance — cohérent avec la philosophie transverse "aucun message bloquant" déjà actée sur tout le chantier stats montre (#106/#144/#145/#157/#155).

### Ce qui reste hors du périmètre UX
- Le choix technique (listener passif vs foreground service vs API plateforme spécifique) appartient à Architect/DevBackend.
- La preuve de fonctionnement sur device réel / toolchain (bloquant de la tentative précédente) reste un sujet QA/Architect, pas un sujet de conception de surface.

---

## Cadrage Architect (2026-07-28)

Statut : **l'architecture cible existe déjà dans le code, ce n'est pas un chantier de conception nouvelle**. Vérifié en lecture directe (pas de supposition) :

- `WatchHeartRateCollector.kt` utilise Health Services `MeasureClient.registerMeasureCallback(DataType.HEART_RATE_BPM, ...)` — API passive recommandée par Google pour la FC en arrière-plan, pas un `SensorEventListener` classique fragile à l'ambient mode.
- `WatchHeartRateForegroundService.kt` démarre un vrai foreground service typé `FOREGROUND_SERVICE_TYPE_HEALTH` (API 34+) / type 0 sinon, avec notification ongoing — exactement le pattern Wear OS attendu pour survivre à l'écran éteint et au Doze.
- `AndroidManifest.xml` déclare déjà `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_HEALTH`, `BODY_SENSORS`, **`BODY_SENSORS_BACKGROUND`**, `health.READ_HEART_RATE` — le triplet permission+foreground service+MeasureClient est le combo correct pour la collecte écran éteint.
- `WatchBridgePlugin.updateHeartRateCollection()` pilote `start()/pause()` uniquement sur changement de `phase` (running vs pas running), pas sur un signal d'écran — donc une fois démarré, rien ne l'arrête sur extinction d'écran par construction.
- `MainActivity.getAmbientCallback()` est un callback vide (aucun override `onEnterAmbient`/`onExitAmbient`), sans wake lock — mais ce n'est pas nécessairement un défaut : le pipeline HR ne dépend pas de l'Activity ni de l'ambient callback, il dépend du foreground service + du singleton `WatchBridgePlugin`/`WatchHeartRateCollector` attaché à l'`applicationContext`.

**Conclusion** : sur le papier, l'architecture respecte déjà le pattern Wear OS standard pour la collecte FC hors écran. Le commit bc533d6c ("KO - preuve device/toolchain insuffisante") indique que le blocage précédent n'était probablement **pas un défaut de conception** mais une **impossibilité de valider sur device réel** (les émulateurs Wear OS ne simulent pas fidèlement l'ambient mode/Doze ni le comportement Health Services par OEM).

### Risques identifiés (à couvrir avant de reclore le ticket)
1. **Comportement par OEM** : Health Services et la gestion de Doze/App Standby varient entre fabricants Wear OS (Samsung Galaxy Watch, Pixel Watch, TicWatch...). Un test sur un seul device ne suffit pas à conclure. Il faut au moins 2 modèles distincts.
2. **`onRegistrationFailed` est seulement loggé** (`Log.w`), jamais remonté au bridge ni au téléphone. Si l'enregistrement `MeasureClient` échoue silencieusement sur un device donné (capability non supportée), le symptôme observé sera identique à "FC qui ne s'actualise plus écran éteint" — indiscernable du bug réel sans logs device. Recommandation QA : instrumenter une capture logcat pendant le test écran éteint, pas seulement observer le résultat UI.
3. Aucune preuve dans le code que le process/l'engine Flutter reste vivant assez longtemps en arrière-plan pour que `WatchBridgePlugin` (qui vit dans le process app, pas dans le Service) continue de transmettre les échantillons au téléphone via `Wearable.getMessageClient` — le foreground service maintient le process vivant, ce qui devrait suffire, mais c'est un point à vérifier explicitement sur device (voir si des samples sont bien reçus côté téléphone pendant la fenêtre écran éteint, pas seulement collectés côté montre).

### Plan exécutable
- **Pas de nouveau contrat, pas de nouveau port, pas de nouvelle classe** : ce ticket ne rouvre pas #156.
- Lot unique, scope **watch bridge natif + QA device** (pas de Frontend Dart/Flutter a priori) :
  1. Instrumentation temporaire (logs) sur `onDataReceived`/`onRegistrationFailed`/`onAvailabilityChanged` si absente en debug build, pour obtenir une preuve device exploitable.
  2. Test protocolé QA : séance réelle, écran éteint manuellement (bouton physique) pendant >2 min, sur au moins 2 modèles de montre compatibles, vérifier réception continue des samples côté téléphone (pas seulement côté montre).
  3. Si le test confirme une perte réelle (pas juste un artefact d'émulateur) : revenir vers Architect avec les logs device pour trancher un correctif ciblé (probable candidat : `WAKE_LOCK` partiel côté foreground service si l'OEM suspend le CPU malgré le foreground service typé health — actuellement absent du manifest).
  4. Si le test confirme que ça fonctionne déjà : reclasser le ticket en clôture QA, la "régression" perçue par l'utilisateur était probablement liée à l'ancien état du code avant f71a1551/58272e35, pas à l'état actuel.
- **Frontend (téléphone/montre Flutter)** : aucun changement requis. L'affichage FC (icône cœur, barre téléphone) consomme déjà les samples tels qu'ils arrivent ; il n'y a rien à modifier côté présentation pour ce ticket.
