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

8.1 KiB

issueRef, version, updatedBy, updatedAt
issueRef version updatedBy updatedAt
#124 3
kind agent_id
agent 57695b92-24d0-4876-837c-76116e70a6ae
1785137390360

#124 — Architecture collecte capteurs montre → séance (Architect, 2026-07-26)

Statut : cadrage technique, ouvre la voie à l'implémentation mais reste combiné à #123 comme décidé par Main sur #106 — aucun lot d'implémentation ne doit s'ouvrir sans relecture croisée de ce cadrage et du cadrage UX #123 (déjà "cadrage terminé côté UX", en attente de ce document).

Constat sur l'existant (fait, pas une supposition)

Recherche exhaustive sur android/, watch_app/android/ et tous les Gradle : aucune infrastructure capteur/santé n'existe aujourd'hui. Ni BODY_SENSORS, ni dépendance Health Services/Health Connect, ni Horologist. La seule dépendance Wear présente (com.google.android.gms:play-services-wearable:19.0.0) est l'API Wearable Data Layer classique (MessageClient/DataClient), utilisée uniquement pour le bridge commandes/projection existant — sans rapport avec les capteurs. C'est donc une capacité entièrement nouvelle, pas une extension d'existant.

Faisabilité et choix d'API

  • API recommandée : Wear OS Health Services (androidx.health:health-services-client), l'API moderne recommandée par Google pour la fréquence cardiaque sur Wear OS 3+ (l'ancienne lecture directe Sensor.TYPE_HEART_RATE est déconseillée/restreinte).
  • Choix structurant : MeasureClient (mesure passive), pas ExerciseClient. ExerciseClient déclare un "Exercise" au niveau système (peut entrer en conflit avec d'autres apps fitness, affiche sa propre UI/notification système d'exercice en cours). GameTime a déjà son propre concept de séance ; on ne veut pas d'un second concept de "session" concurrent au niveau OS. MeasureClient.registerMeasureCallback(DataType.HEART_RATE_BPM, ...) suffit pour une mesure passive sans ce couplage.
  • Dépendance à ajouter : watch_app/android/app/build.gradle.kts uniquement (la fréquence cardiaque se mesure sur la montre, jamais sur le téléphone).
  • Permission android.permission.BODY_SENSORS (dangereuse, runtime) dans watch_app/android/app/src/main/AndroidManifest.xml ; évaluer BODY_SENSORS_BACKGROUND si la mesure doit continuer écran éteint au poignet (probable en usage réel de séance).

Modèle de permission (répond à la question ouverte du carnet UX #123)

Demande une seule fois, paresseuse, déclenchée par le premier passage en "séance active" côté montre après déploiement de la feature — jamais un écran d'onboarding dédié (évite une surface supplémentaire non demandée). Refus ou ignorance → la montre ne mesure jamais, le téléphone ne reçoit jamais de résumé, l'UI reste silencieuse — exactement le contrat UX déjà posé par #123 (silence total, jamais de blocage du parcours principal).

Décision structurante : agrégation locale montre, résumé unique en fin de séance

Pas de flux temps réel synchronisé en continu (inutile : UX #123 exclut explicitement tout affichage live). La montre :

  1. Accumule localement (min/max/somme/count — arithmétique triviale, pas un "modèle de calcul maison" au sens où l'UX l'exclut pour les calories) pendant toute la séance, pause-aware : suspend l'agrégation quand la séance est en pause ou que le repos court sans effort (aligné sur le comportement déjà existant des autres chronos pause-aware, cf. gametime-session-execution-timer-refactor).
  2. Envoie un seul message résumé en fin de séance (terminer/abandonner), pas un flux continu — élimine la complexité de synchronisation/idempotence en continu, cohérent avec le fait que rien ne consomme la donnée en direct.
  3. Le canal Data Layer (MessageClient) gère nativement la fiabilité/retry si la montre est temporairement injoignable au moment de l'envoi ; si le message n'arrive jamais (montre débranchée, désinstallée...), la séance reste simplement sans ce bloc — silencieux pour toujours, aucune donnée en attente à représenter, aucune migration rétroactive à inventer (conforme à la demande explicite d'UX #123).

Nouveau contrat (séparé du contrat commandes existant)

Ne pas réutiliser WatchCommandEnvelope (sémantique commande utilisateur + ack) pour de la télémétrie sans accusé de réception. Nouveau type dans packages/watch_bridge_contract :

class WatchSensorSummary {
  final int schemaVersion;
  final String sessionId;
  final int sampleCount;
  final double? averageHeartRateBpm;
  final int? maxHeartRateBpm;
}
  • sampleCount permet au téléphone de décider "donnée insuffisante" (UX #123 : "données insuffisantes pour un calcul fiable" → bloc absent). Seuil recommandé : sampleCount < 3 ⇒ traiter comme absent (pas de moyenne calculée sur un bruit d'une ou deux mesures).
  • Pas d'ack requis (fire-and-forget, perte tolérée par design).

Persistance téléphone

  • Champs nullables au niveau racine de l'entité d'historique de séance (celle qui porte déjà la durée totale), pas par série/exercice — UX #123 demande une fréquence cardiaque de la séance, une seule paire moyenne/max par séance, pas un état vivant par set :
averageHeartRateBpm: double?
maxHeartRateBpm: int?

(nom exact de l'entité à confirmer par DevBackend selon le modèle d'historique réel — non exploré en détail dans ce cadrage.)

  • Écriture non bloquante (point important) : ne jamais faire attendre la finalisation de "Terminer la séance" sur l'arrivée du résumé capteur. La séance se termine et s'enregistre immédiatement, averageHeartRateBpm/maxHeartRateBpm restent null. Si le résumé arrive ensuite (avant ou après la fin de séance, l'ordre n'est pas garanti), un use case dédié updateWorkoutHistoryHeartRateSummary(historySessionId, ...) applique un patch idempotent a posteriori si les champs sont encore null sur l'enregistrement d'historique correspondant, silencieusement si l'enregistrement n'existe plus/a été supprimé.
  • Justification : cohérent par analogie avec gametime-online-layer-philosophy — ne jamais bloquer une action locale critique (ici, terminer une séance) en attendant une confirmation externe, même si ce n'est pas strictement un flux réseau serveur.

Calories (hors MVP, second temps déjà indiqué par UX)

Même pipeline (MeasureClient, DataType.CALORIES_TOTAL si disponible nativement) — champ additif estimatedCalories: double? sur le même résumé, à ajouter seulement si demandé explicitement plus tard. Ne pas l'inclure au lot 1.

Découpage en lots proposé (permet d'ouvrir l'implémentation sans ambiguïté)

  1. Watch (natif) : dépendance Gradle + permission + HeartRateSampler (MeasureClient) + accumulateur local pause-aware + envoi résumé fin de séance.
  2. Contrat : WatchSensorSummary dans watch_bridge_contract + tests dart test.
  3. Backend/domaine téléphone : champs nullables sur l'entité d'historique + migration + updateWorkoutHistoryHeartRateSummary + écoute du message natif (extension du service d'écoute bridge déjà existant) + logique de patch tardif idempotent.
  4. Frontend téléphone : bloc "Fréquence cardiaque" optionnel sur écran de fin de séance + détail historique (absent si null, aucun placeholder — conforme #123).
  5. QA : permission refusée, montre sans capteur cardio, déconnexion mi-séance, arrivée tardive du résumé après clôture, seuil sampleCount insuffisant.

Chaque lot est testable isolément : accumulateur watch (calcul pur), DTO contrat (dart test existant), use case patch (test avec repo fake), rendu UI (widget test null vs présent).

Ce qui reste explicitement hors MVP (repris d'UX #123, confirmé faisable/non-faisable ici)

VO2 max, zones d'intensité, SpO2, ECG — Health Services les expose en partie mais hors périmètre produit GameTime, pas seulement hors scope technique. Pas d'affichage temps réel (choix d'architecture confirmé ci-dessus, pas seulement une préférence UX : la mesure continue en flux aurait needlessly recompliqué idempotence/sync pour zéro valeur d'usage dans ce MVP).