chore(ideai): synchronise etat de suivi (tickets, agents)

This commit is contained in:
2026-07-29 11:22:17 +02:00
parent 30c6259748
commit dd0c124818
32 changed files with 1415 additions and 75 deletions

View File

@ -117,3 +117,23 @@ Tu interviens **au début** du cycle, et **en arbitrage** ensuite :
- Avec **UX** : la forme conditionne les contrats. Si sa conception impose une frontière - Avec **UX** : la forme conditionne les contrats. Si sa conception impose une frontière
coûteuse, dis-le et proposez ensemble un compromis — ne redessine pas dans ton coin. coûteuse, dis-le et proposez ensemble un compromis — ne redessine pas dans ton coin.
- Avec **QA** : signale les invariants à tester et les cas limites que tu as identifiés. - Avec **QA** : signale les invariants à tester et les cas limites que tu as identifiés.
---
## 6. Revue d'impact synchro serveur
Pour **chaque feature qui touche des données métier**, tu dois déterminer explicitement si
elle a un impact sur la **synchro serveur**.
- Tu ne supposes jamais que la synchro est hors sujet : tu conclus explicitement
`impact synchro : aucun` ou `impact synchro : oui`.
- Si l'impact existe, tu précises **où** : payload push, pull/apply distant, DTO,
ressources syncables, enfants d'agrégat, migration locale, conflit/résolution,
dérivés d'historique/statistiques.
- Si une feature ajoute ou modifie une donnée sur `exercise`, `program`,
`workoutTemplate`, `workoutHistory`, partage, média, ou toute projection persistée,
tu vérifies si cette donnée doit voyager au serveur et revenir au pull.
- Si tu conclus `aucun impact synchro`, tu donnes la raison en une phrase.
- Tes lots et invariants doivent désormais mentionner les besoins de tests de synchro
quand ils existent, en particulier pour les enfants d'agrégat et les données dérivées
d'historique/statistiques.

View File

@ -96,3 +96,24 @@ avant d'écrire** — c'est un arbitrage d'Architect, pas une décision d'implé
solution, ne l'impose pas. solution, ne l'impose pas.
- Avec **QA** : signale les cas limites que tu sais fragiles. Un rapport d'échec est une - Avec **QA** : signale les cas limites que tu sais fragiles. Un rapport d'échec est une
information, pas une attaque — tu corriges la cause, pas le symptôme. information, pas une attaque — tu corriges la cause, pas le symptôme.
---
## 6. Vérification systématique de la synchro serveur
Pour **chaque feature qui touche des données persistées ou synchronisées**, tu dois vérifier
si elle impacte la **synchro serveur**.
- Tu conclus explicitement `impact synchro : aucun` ou `impact synchro : oui` dans ton
retour à Main.
- Si l'impact existe, tu vérifies au minimum : payload push, réhydratation au pull,
application des enfants d'agrégat, `change_log`/déclenchement sync, migrations locales,
compatibilité serveur et résolution de conflit.
- Si un champ est ajouté sur `exercise`, `program`, `workoutTemplate`, `workoutHistory`,
partage, média, ou sur un enfant de ces agrégats, tu ne considères pas le lot fini tant
que tu n'as pas vérifié comment ce champ voyage aller/retour.
- Une donnée dérivée d'historique/statistiques ne doit pas être ajoutée ou modifiée sans
vérifier si sa source synchronisée est suffisante pour la recalculer correctement sur un
autre appareil.
- Si tu penses qu'aucun changement sync n'est nécessaire, tu le dis avec la raison précise,
pas par omission.

View File

@ -91,3 +91,19 @@ cœur du produit.
- Avec **Architect** : tout écart de contrat remonte pour arbitrage. - Avec **Architect** : tout écart de contrat remonte pour arbitrage.
- Avec **QA** : un rapport d'échec est une information. Tu corriges la cause, pas le - Avec **QA** : un rapport d'échec est une information. Tu corriges la cause, pas le
symptôme. symptôme.
---
## 6. Signalement des impacts de synchro serveur
Même sur un lot UI, tu dois vérifier si la feature manipule une donnée qui vit dans une
**ressource synchronisée**.
- Si la surface crée, édite, affiche ou dépend d'une donnée persistée/synchronisée, tu
signales explicitement à Main si le contrat actuel suffit ou si un impact synchro doit
être traité par Architect/DevBackend.
- Tu ne supposes jamais que l'existant sync transporte déjà la donnée : si un champ est
nouveau ou si un enfant d'agrégat devient visible/éditable, tu le remontes.
- Si tu relies une UI à une donnée absente du pull distant ou du payload sync, tu le dis
avant d'implémenter plutôt que de contourner localement.
- Si tu conclus `aucun impact synchro`, tu le mentionnes explicitement avec la raison.

View File

@ -20,6 +20,7 @@ feature ou correction applicative, tu passes par les agents spécialisés :
- **DevBackend** pour le code côté serveur/domaine/données. - **DevBackend** pour le code côté serveur/domaine/données.
- **DevFrontend** pour le code d'interface. - **DevFrontend** pour le code d'interface.
- **QA** pour écrire et exécuter les tests, et produire les rapports d'échec. - **QA** pour écrire et exécuter les tests, et produire les rapports d'échec.
- **Context** pour les tâches mécaniques peu risquées et coûteuses en tokens : recherche de symboles, résumés de fichiers/logs/diffs, extraction d'échecs, repérage de fichiers candidats. Context n'arbitre rien et ne remplace jamais UX/Architect/Git/QA/dev.
**Exception limitée** : tu peux modifier toi-même les fichiers de contexte, de mémoire, **Exception limitée** : tu peux modifier toi-même les fichiers de contexte, de mémoire,
la documentation de pilotage et la configuration d'orchestration quand la demande porte la documentation de pilotage et la configuration d'orchestration quand la demande porte
@ -64,6 +65,10 @@ tomber par défaut dans les mains d'un dev. UX passe **avant** Architect quand l
conditionne les contrats, et **avec** lui quand une contrainte technique borne la conditionne les contrats, et **avec** lui quand une contrainte technique borne la
conception. UX ne bloque pas les lots sans surface utilisateur. conception. UX ne bloque pas les lots sans surface utilisateur.
**Quand solliciter Git — la règle.** Pour tout lot applicatif amené à modifier le code ou à relancer une feature existante, tu sollicites Git après UX/Architect et avant DevBackend/DevFrontend, même si le lot semble petit. Tu ne décides pas toi-même qu'un lot peut rester implicitement sur la branche courante.
**Quand solliciter Context — la règle.** Avant d'absorber toi-même une recherche volumineuse, un tri de logs ou un résumé de diff/fichiers sans arbitrage, tu envisages d'abord Context. Tu gardes les décisions et la synthèse finale, Context ne sert qu'à compresser de l'information factuelle.
--- ---
## 4. Répartition des responsabilités ## 4. Répartition des responsabilités
@ -82,6 +87,8 @@ spécialisé dans son domaine** :
l'utilisateur s'il faut brancher, committer ou merger** : sollicite Git, qui tranche. l'utilisateur s'il faut brancher, committer ou merger** : sollicite Git, qui tranche.
Aucune action sortante (push, publication, PR distante) sans validation explicite Aucune action sortante (push, publication, PR distante) sans validation explicite
de l'utilisateur. de l'utilisateur.
- **Context** compresse l'information brute mais ne prend aucune décision produit,
architecture, UI, QA ou git.
--- ---

View File

@ -93,3 +93,18 @@ un diagnostic, pas une excuse pour passer au vert.
Remonte les frontières qui rendent le test impossible. Remonte les frontières qui rendent le test impossible.
- Avec les **devs** : ton rapport vise le code, jamais la personne. Donne de quoi - Avec les **devs** : ton rapport vise le code, jamais la personne. Donne de quoi
reproduire, pas de quoi deviner. reproduire, pas de quoi deviner.
---
## 6. Vérifications de synchro serveur
Pour toute feature qui touche des **données synchronisées** ou susceptibles de l'être, tu
vérifies si un lot de **tests de synchro serveur** est nécessaire.
- Tu conclus explicitement `tests sync requis : oui/non` dans ton retour.
- Si oui, tu couvres au minimum les comportements critiques : push, pull, réhydratation
locale, enfants d'agrégat, multi-appareil si pertinent, et cohérence des historiques/
statistiques dérivées.
- Une feature data n'est pas considérée terminée si son impact synchro identifié par
Architect/DevBackend n'a pas été validé par des commandes réelles.
- Si tu juges qu'aucun test sync n'est nécessaire, tu donnes la raison précise.

View File

@ -1,6 +1,63 @@
--- ---
issueRef: "#157" issueRef: "#157"
version: 5 version: 7
updatedBy: {"kind":"user"} updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
updatedAt: 1785252668623 updatedAt: 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.

View File

@ -8,9 +8,9 @@ sprint: "d5c18b44-0eec-46db-b8ab-506cfee0bfea"
links: [{"target":"#142","kind":"blocks"},{"target":"#106","kind":"relatesTo"},{"target":"#145","kind":"dependsOn"}] links: [{"target":"#142","kind":"blocks"},{"target":"#106","kind":"relatesTo"},{"target":"#145","kind":"dependsOn"}]
agentRefs: [] agentRefs: []
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedBy: {"kind":"user"} updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
createdAt: 1785180656723 createdAt: 1785180656723
updatedAt: 1785252668623 updatedAt: 1785256180427
version: 5 version: 7
--- ---
Ajouter la distance parcourue live remontée par la montre compatible, avec affichage léger côté téléphone pendant la séance et réutilisation côté montre dans le panneau secondaire. Dégradation silencieuse si donnée indisponible. Ajouter la distance parcourue live remontée par la montre compatible, avec affichage léger côté téléphone pendant la séance et réutilisation côté montre dans le panneau secondaire. Dégradation silencieuse si donnée indisponible.

View File

@ -1,6 +1,6 @@
--- ---
issueRef: "#159" issueRef: "#159"
version: 4 version: 5
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} updatedBy: {"kind":"user"}
updatedAt: 1785248859208 updatedAt: 1785255482109
--- ---

View File

@ -2,15 +2,15 @@
id: "35022a8a-a4ef-4ffc-99d7-cd0a0e925fde" id: "35022a8a-a4ef-4ffc-99d7-cd0a0e925fde"
number: 159 number: 159
title: "[DevFrontend] Surface montre statistiques live (FC principale + panneau secondaire)" title: "[DevFrontend] Surface montre statistiques live (FC principale + panneau secondaire)"
status: "qa" status: "closed"
priority: "medium" priority: "medium"
sprint: "d5c18b44-0eec-46db-b8ab-506cfee0bfea" sprint: "d5c18b44-0eec-46db-b8ab-506cfee0bfea"
links: [{"target":"#106","kind":"relatesTo"},{"target":"#144","kind":"dependsOn"},{"target":"#145","kind":"dependsOn"}] links: [{"target":"#106","kind":"relatesTo"},{"target":"#144","kind":"dependsOn"},{"target":"#145","kind":"dependsOn"}]
agentRefs: [] agentRefs: []
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} updatedBy: {"kind":"user"}
createdAt: 1785180656758 createdAt: 1785180656758
updatedAt: 1785248859208 updatedAt: 1785255482109
version: 4 version: 5
--- ---
Faire évoluer l'UI montre pour afficher la fréquence cardiaque live sur l'écran principal de séance, puis un écran/panneau secondaire accessible par glissement inverse du menu action avec fréquence cardiaque live, distance parcourue et calories live depuis le début de la séance. Rien n'est affiché pour les données indisponibles ; aucun message bloquant. Faire évoluer l'UI montre pour afficher la fréquence cardiaque live sur l'écran principal de séance, puis un écran/panneau secondaire accessible par glissement inverse du menu action avec fréquence cardiaque live, distance parcourue et calories live depuis le début de la séance. Rien n'est affiché pour les données indisponibles ; aucun message bloquant.

View File

@ -1,6 +1,54 @@
--- ---
issueRef: "#163" issueRef: "#163"
version: 6 version: 12
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} updatedBy: {"kind":"user"}
updatedAt: 1785254327435 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.

View File

@ -2,15 +2,15 @@
id: "2c794bcc-09c1-47c7-a7ed-1e2f0e408ffd" id: "2c794bcc-09c1-47c7-a7ed-1e2f0e408ffd"
number: 163 number: 163
title: "Frequence cardiaque qui ne semble plus calculée si la montre à l'écran éteint" title: "Frequence cardiaque qui ne semble plus calculée si la montre à l'écran éteint"
status: "inProgress" status: "qa"
priority: "medium" priority: "medium"
sprint: "2c0dd1ea-8809-49ff-adc1-aa8820815ee7" sprint: "2c0dd1ea-8809-49ff-adc1-aa8820815ee7"
links: [] links: []
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}] agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
createdBy: {"kind":"user"} createdBy: {"kind":"user"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} updatedBy: {"kind":"user"}
createdAt: 1785242433447 createdAt: 1785242433447
updatedAt: 1785254327435 updatedAt: 1785315994265
version: 6 version: 12
--- ---
Quand l'écran de la montre est éteint, la fréquence cardiaque n'est aps actualisée (l'affichage est assombris sur le téléphone et la fréquance ne change pas). Pour les statistiques il fauit que la fréquence cardiaque continue d'être enregistrée Quand l'écran de la montre est éteint, la fréquence cardiaque n'est aps actualisée (l'affichage est assombris sur le téléphone et la fréquance ne change pas). Pour les statistiques il fauit que la fréquence cardiaque continue d'être enregistrée

View File

@ -1,8 +1,8 @@
--- ---
issueRef: "#164" issueRef: "#164"
version: 5 version: 8
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} updatedBy: {"kind":"user"}
updatedAt: 1785244375351 updatedAt: 1785315994240
--- ---
## #164 — Cadrage UX (2026-07-28) ## #164 — Cadrage UX (2026-07-28)
@ -20,3 +20,15 @@ updatedAt: 1785244375351
- Ne pas inventer un nouveau pattern haptique montre sans vérifier d'abord si le double-impulsion déjà spécifié pour "fin de chrono" couvre déjà ce cas. - Ne pas inventer un nouveau pattern haptique montre sans vérifier d'abord si le double-impulsion déjà spécifié pour "fin de chrono" couvre déjà ce cas.
**Libellé/navigation** : aucune évolution nécessaire, correctif de feedback sonore/haptique uniquement. **Libellé/navigation** : aucune évolution nécessaire, correctif de feedback sonore/haptique uniquement.
## Complément Main (2026-07-28)
Le besoin utilisateur est désormais plus précis que le ticket initial :
- le son de fin de chrono manque toujours ;
- la vibration montre doit être **beaucoup plus perceptible en course/sauts** ;
- il existe un doute fort sur l'absence de vibration quand l'écran montre est éteint ;
- la préférence produit est de **retirer encore de la logique métier de la montre**.
Conséquence :
- `#164` reste le ticket bug de feedback utilisateur observable ;
- le cadrage d'architecture correspondant est désormais extrait dans `#173` pour trancher proprement le pilotage téléphone -> montre des alertes chrono.

View File

@ -2,15 +2,15 @@
id: "879caaa3-2658-4ad8-bfa0-a7444cffc1b0" id: "879caaa3-2658-4ad8-bfa0-a7444cffc1b0"
number: 164 number: 164
title: "[Bug] Plus de son en fin de chrono" title: "[Bug] Plus de son en fin de chrono"
status: "inProgress" status: "qa"
priority: "medium" priority: "medium"
sprint: "abc4f969-b169-45f7-988c-daeeab762201" sprint: "abc4f969-b169-45f7-988c-daeeab762201"
links: [] links: []
agentRefs: [] agentRefs: []
createdBy: {"kind":"user"} createdBy: {"kind":"user"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} updatedBy: {"kind":"user"}
createdAt: 1785243194849 createdAt: 1785243194849
updatedAt: 1785244375351 updatedAt: 1785315994240
version: 5 version: 8
--- ---
Il n'y a plus de bips en fin de chrono. Sur les 3 derniere seconde il doit y avoir un big court et un bip plus long à 0. J'aiemrais que la montre vibre quand le chrono arrive à 0 Il n'y a plus de bips en fin de chrono. Sur les 3 derniere seconde il doit y avoir un big court et un bip plus long à 0. J'aiemrais que la montre vibre quand le chrono arrive à 0

View File

@ -1,8 +1,8 @@
--- ---
issueRef: "#166" issueRef: "#166"
version: 8 version: 12
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1785254327361 updatedAt: 1785311597894
--- ---
## #166/#167/#169 — Cadrage UX coordonné : slot "donnée dominante sans chrono" (UX, 2026-07-28) ## #166/#167/#169 — Cadrage UX coordonné : slot "donnée dominante sans chrono" (UX, 2026-07-28)
@ -65,3 +65,28 @@ Le sous-titre disparaît, l'espace libéré n'est **pas** réutilisé pour un no
- #166 et #167 modifient la **même zone d'écran** (slot donnée dominante + bouton d'action pour une étape reps-only) — les livrer séparément risquerait qu'un commit écrase la mise en page de l'autre, ou que le bouton `[✓]` soit ajouté sans que la redondance `SÉRIE X/Y` soit corrigée en même temps (ou l'inverse). - #166 et #167 modifient la **même zone d'écran** (slot donnée dominante + bouton d'action pour une étape reps-only) — les livrer séparément risquerait qu'un commit écrase la mise en page de l'autre, ou que le bouton `[✓]` soit ajouté sans que la redondance `SÉRIE X/Y` soit corrigée en même temps (ou l'inverse).
- #169 touche un état adjacent du même écran (`Séance active` sans chrono actif) et applique le même principe (pas de texte qui ne porte pas d'action ni de donnée nécessaire) — le traiter isolément ferait revivre exactement la dérive de densité déjà diagnostiquée et corrigée par #161 (accumulation de correctifs isolés qui perdent la cohérence d'ensemble). - #169 touche un état adjacent du même écran (`Séance active` sans chrono actif) et applique le même principe (pas de texte qui ne porte pas d'action ni de donnée nécessaire) — le traiter isolément ferait revivre exactement la dérive de densité déjà diagnostiquée et corrigée par #161 (accumulation de correctifs isolés qui perdent la cohérence d'ensemble).
- Recommandation : un seul lot Architect/DevFrontend pour les trois, avec cette section comme spec unique de référence. - Recommandation : un seul lot Architect/DevFrontend pour les trois, avec cette section comme spec unique de référence.
---
## Cadrage Architect (2026-07-28)
Statut : **ticket déjà implémenté dans le code actuel, conforme au cadrage UX ci-dessus**. Vérifié en lecture directe sur `watch_app/lib/presentation/watch_session_screen.dart` :
- `_ActiveContent.build()` (~L704-788) calcule `repsTarget` et bascule déjà sur `_DominantRepsLine` quand `repsTarget != null` (L753-758), au lieu de la donnée `SÉRIE`#167 déjà appliqué.
- `_DominantRepsLine` (~L790-815) affiche la valeur reps + `_CompleteStepButton` sur la même ligne, gabarit `SizedBox(height: 52)` + `Row` — conforme au template ligne unique posé par #161.
- `_CompleteStepButton` (~L817-844) : `SizedBox.square(dimension: 48)` (cible tactile 48dp respectée), icône `Icons.check`, couleur or `#C9A24A` sur fond `#141824` avec liseré crimson `#D72638` — conforme au style #128/#161, visuellement distinct de pause/play.
- Câblage commande : le bouton appelle `onCompleteStep``WatchSessionViewModel.completeCurrentStep()` (`watch_session_view_model.dart:190-191`) → `WatchCommandType.completeCurrentStep`, une commande **distincte** de `WatchCommandType.skipCurrentStep` (utilisée par `Passer l'étape` dans `Actions`) — la séparation validation/contournement exigée par le cadrage UX est bien respectée, pas de confusion de commande.
- Libellé `Répétitions` déjà affiché une seule fois au-dessus de la valeur (L751, `_SmallLabel(repsTarget == null ? dominantLabel : 'Répétitions')`) — #167 confirmé.
- Recherche du texte `Prêt pour la série suivante` dans tout le repo Dart : **aucune occurrence** — le sous-titre visé par #169 n'existe déjà plus.
- Aucun double bouton `[✓]`/`[⏸]` possible : `_ActiveContent.build()` structure les branches `repsTarget != null` / `timer == null` / chrono en `if/else if/else` mutuellement exclusifs (L753-767).
### Conclusion
**Aucun développement restant.** #166 (et vraisemblablement #167/#169 associés dans ce même carnet) sont fonctionnellement livrés dans le code présent sur la branche. Il n'y a ni contrat, ni port, ni frontière hexagonale à faire évoluer ici — pure présentation Flutter déjà en place.
### Recommandation
- Ne pas relancer de lot DevFrontend sur ce ticket : le repasser directement en validation **QA** sur device/émulateur Wear OS réel, avec ces points de contrôle précis :
1. Étape reps-only sans chrono → bouton `[✓]` visible et fonctionnel sur l'écran principal (pas besoin de swiper vers `Actions`).
2. Étape avec score chrono manuel (`_ManualScoreContent`) → bouton `[✓]` absent de ce layout dédié, pas de régression croisée.
3. Écran `Prêt pour la série suivante` → absence confirmée du sous-titre, pas de nouvel élément parasite.
4. Cible tactile et absence d'ambiguïté visuelle avec le bouton pause/play sur écran chrono.
- Si le ticket reste "open" c'est vraisemblablement un oubli de requalification après une implémentation antérieure (probablement dans le même lot que #161/#159, cf. commit bc533d6c "lot #165-169 validé QA") plutôt qu'un travail réellement en attente. À confirmer avec Main avant clôture pure, mais aucune architecture nouvelle n'est à cadrer.

View File

@ -2,7 +2,7 @@
id: "71f99d03-7ac8-435f-92ef-087e61c0f873" id: "71f99d03-7ac8-435f-92ef-087e61c0f873"
number: 166 number: 166
title: "[UI] mettre un bouton sur l'écran principal de la montre pour passer à l'étape suivante dans le cas de répétitions dans une étape" title: "[UI] mettre un bouton sur l'écran principal de la montre pour passer à l'étape suivante dans le cas de répétitions dans une étape"
status: "qa" status: "closed"
priority: "medium" priority: "medium"
sprint: "d5c18b44-0eec-46db-b8ab-506cfee0bfea" sprint: "d5c18b44-0eec-46db-b8ab-506cfee0bfea"
links: [] links: []
@ -10,7 +10,7 @@ agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}
createdBy: {"kind":"user"} createdBy: {"kind":"user"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1785250931982 createdAt: 1785250931982
updatedAt: 1785254327361 updatedAt: 1785311597894
version: 8 version: 12
--- ---
Quand on a une étape qui ne demande qu'un nombre de répétitions, il faudrait que le bouton pour calider l'étape directement sur l'écran principal de la montre. Quand on a une étape qui ne demande qu'un nombre de répétitions, il faudrait que le bouton pour calider l'étape directement sur l'écran principal de la montre.

View File

@ -1,8 +1,8 @@
--- ---
issueRef: "#169" issueRef: "#169"
version: 6 version: 7
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} updatedBy: {"kind":"user"}
updatedAt: 1785254327400 updatedAt: 1785255487215
--- ---
## #169 — Cadrage UX (2026-07-28) ## #169 — Cadrage UX (2026-07-28)

View File

@ -2,15 +2,15 @@
id: "7e5b1ba4-5f5c-46d1-b05a-6d6f5af73f23" id: "7e5b1ba4-5f5c-46d1-b05a-6d6f5af73f23"
number: 169 number: 169
title: "[UI] Enlever \"Prêt pour la série suivante\" sur la montre" title: "[UI] Enlever \"Prêt pour la série suivante\" sur la montre"
status: "qa" status: "closed"
priority: "medium" priority: "medium"
sprint: "d5c18b44-0eec-46db-b8ab-506cfee0bfea" sprint: "d5c18b44-0eec-46db-b8ab-506cfee0bfea"
links: [] links: []
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}] agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
createdBy: {"kind":"user"} createdBy: {"kind":"user"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} updatedBy: {"kind":"user"}
createdAt: 1785251491623 createdAt: 1785251491623
updatedAt: 1785254327400 updatedAt: 1785255487215
version: 6 version: 7
--- ---
Je pense que le titre "Prêt pour la série suivante" est a enlever de la montre, il prend de la place pour pas grand chose Je pense que le titre "Prêt pour la série suivante" est a enlever de la montre, il prend de la place pour pas grand chose

View File

@ -1,6 +1,39 @@
--- ---
issueRef: "#170" issueRef: "#170"
version: 4 version: 6
updatedBy: {"kind":"user"} updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1785255295122 updatedAt: 1785311597912
--- ---
## #170 — Cadrage UX (2026-07-28)
Statut : **nouveau lot, cadrage exploitable, indépendant de #166/#167/#169.**
### Pourquoi ce n'est pas un doublon de #166/#167/#169
Ces trois tickets couvrent le cas **reps-only sans score manuel** sur la montre (branche `_ActiveContent` / `_DominantRepsLine` dans `watch_session_screen.dart`, déjà livrée). #170 décrit un **autre écran** : une étape qui combine `type = reps` **et** `hasScore` en mode manuel (`ScoreInputMode.manual`) — cette étape bascule sur `projection.hasManualScore == true` et rend `_ManualScoreContent` (watch_session_screen.dart:894), un layout entièrement différent qui n'a jamais affiché le nombre de répétitions cible. Vérifié en lecture directe :
- `_watchManualScoreProjection` (lib/application/use_cases.dart:4285-4318) ne connaît que la **cible de score** (`targetValue`/`targetLabel: 'Cible'`), jamais `step.defaultTargetValue` (le nombre de répétitions). Le champ répétitions n'existe nulle part dans cette branche de données.
- `_ManualScoreContent.build()` (watch_session_screen.dart:907-993) affiche : nom d'exercice, bande étape, puis **si `target != null`** une ligne caption `"$targetLabel : $target"` (ex. `Cible : 500`), puis le label `SCORE` et la ligne +/- score. Rien n'y référence `step.type` ni les répétitions.
Donc le constat utilisateur est exact : sur une étape score-libre + répétitions, la montre affiche le score (incrémentable) mais **jamais** le nombre de répétitions à réaliser pendant ce score — contrairement au cas reps-only (#166/#167) qui, lui, a bien sa donnée dominante dédiée.
### Comportement attendu
Réutiliser le gabarit caption déjà en place (`_ManualScoreContent`, juste au-dessus du label `SCORE`), sans ajouter de nouvelle ligne ni de nouveau composant :
```text
Pompes tempo
Répétitions : 10 ← nouvelle info, même style que "Cible : 500"
SCORE
42 +
♥ 118
```
- Si l'étape a **aussi** une cible de score (`targetValue` non nul), joindre les deux informations sur la **même ligne**, séparées par `·` — même règle de mutualisation que les chronos secondaires (`secondaryTimers.join(' · ')`, déjà dans ce fichier) : `Répétitions : 10 · Cible : 500`. Ne jamais empiler deux lignes de caption : une étape à la fois ne doit montrer qu'une seule ligne d'info secondaire au-dessus du score, pour ne pas reproduire la densité déjà corrigée par #161.
- Si l'étape n'a **pas** de cible de score, la ligne devient simplement `Répétitions : 10` (remplace l'espace aujourd'hui vide quand `target == null`).
- Le score reste la donnée manipulable (boutons /+ inchangés) : les répétitions ici sont une **cible informative**, pas un compteur actionnable — pas de bouton de validation à ajouter sur cette ligne (l'étape se termine via le flux existant de fin de score/étape, non modifié par ce ticket).
- Style : reprendre exactement `Theme.of(context).textTheme.bodySmall` + couleur `#A7ADBA` + `fontSize: 11`, `maxLines: 1`, `overflow: ellipsis` — déjà le style de la ligne `Cible`, aucune nouvelle charte à définir.
### Ce qu'il faut pour Architect
Donnée manquante à faire remonter dans `_WatchManualScoreProjectionData` / `WatchSessionProjection` : le nombre de répétitions cible de l'étape courante (`step.defaultTargetValue` quand `step.type == ExerciseStepType.reps`), à côté du `targetValue`/`targetLabel` de score existant. Recommandation : soit un champ dédié (`repsTargetValue`), soit généraliser la caption en une liste de segments texte assemblés côté UI — au choix d'Architect selon ce qui est le plus cohérent avec le modèle de projection existant. Le rendu attendu (label unique, jointure `·`, une seule ligne) est fixé ci-dessus et ne doit pas varier selon l'implémentation choisie.

View File

@ -2,15 +2,15 @@
id: "90277a05-bd6a-4360-942e-dd500c2020e0" id: "90277a05-bd6a-4360-942e-dd500c2020e0"
number: 170 number: 170
title: "[UI] Afficher le nombre de répétitions à faire dans une étape avec score et répétitions" title: "[UI] Afficher le nombre de répétitions à faire dans une étape avec score et répétitions"
status: "open" status: "closed"
priority: "medium" priority: "medium"
sprint: "d5c18b44-0eec-46db-b8ab-506cfee0bfea" sprint: "d5c18b44-0eec-46db-b8ab-506cfee0bfea"
links: [] links: []
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}] agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
createdBy: {"kind":"user"} createdBy: {"kind":"user"}
updatedBy: {"kind":"user"} updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1785255090292 createdAt: 1785255090292
updatedAt: 1785255295122 updatedAt: 1785311597912
version: 4 version: 6
--- ---
Dans une étape avec score libre + répétition il faut que l'utilisateur ai d'affiché sur sa montre le nombre de répétitions à faire en plus de pouvoir incrémenter ou décrémenter le score. Actuellement, il manque l'affichage du nombre de répétitions Dans une étape avec score libre + répétition il faut que l'utilisateur ai d'affiché sur sa montre le nombre de répétitions à faire en plus de pouvoir incrémenter ou décrémenter le score. Actuellement, il manque l'affichage du nombre de répétitions

View File

@ -1,6 +1,42 @@
--- ---
issueRef: "#171" issueRef: "#171"
version: 4 version: 6
updatedBy: {"kind":"user"} updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1785255293774 updatedAt: 1785311597930
--- ---
## #171 — Cadrage UX (2026-07-28)
Statut : **nouveau lot, cadrage exploitable, sans lien avec #166/#167/#169** (ces trois tickets portent sur l'app montre `watch_app/`, celui-ci porte sur l'app téléphone, `lib/presentation/workout_execution_screen.dart` — écran d'exécution de séance, panneau étape courante).
### Constat, vérifié en lecture directe
Dans `_CurrentStepPane.build()` (workout_execution_screen.dart:2711-2898), deux mises en page coexistent pour l'étape courante :
- Cas **reps + score chrono** (`hasRepsWithStopwatchScore`, L2723-2803) : layout en `Column` de hauteur fixe, pas de scroll, boutons `Passer l'étape` / `Étape suivante` **côte à côte en bas**, toujours visibles.
- Cas **reps seules** (sans score), branche `else` (L2805-2895) : layout enveloppé dans `SingleChildScrollView` + `IntrinsicHeight`, avec :
1. nom de l'étape,
2. `Expanded``_RepsStepBody` (valeur reps + bouton `Étape suivante`, aligné en haut du `Expanded`),
3. ligne `Passer l'étape` + menu `...` (passer passage/séquence), **après** le `Expanded`.
Comme `_RepsStepBody` ne remplit pas toute la hauteur du `Expanded` (son contenu est aligné en haut via `Align(topCenter)` + `FittedBox`), la ligne `Passer l'étape` se retrouve repoussée en bas du contenu scrollable, potentiellement hors de la zone visible du panneau → scroll nécessaire pour l'atteindre. C'est exactement le symptôme décrit par le ticket, et le bouton `Étape suivante` existe déjà (dans `_RepsStepBody`) mais **pas au même endroit** que `Passer l'étape`.
### Comportement attendu
Aligner le cas reps-seules sur le gabarit déjà validé du cas reps+score chrono (L2769-2793), pour cohérence et sans scroll :
```text
Pompes tempo
10
RÉPÉTITIONS
[ Passer l'étape ] [...] [ ✓ Étape suivante ]
```
- Supprimer le `SingleChildScrollView` + `IntrinsicHeight` de la branche reps-seules : la hauteur du panneau doit se calculer normalement (nom étape + valeur reps + score input optionnel + une seule ligne de boutons), sans scroll, comme le fait déjà la branche voisine.
- Retirer le bouton `Étape suivante` de `_RepsStepBody` (workout_execution_screen.dart:3243-3269) — il ne doit plus être dupliqué à mi-écran. `_RepsStepBody` n'affiche plus que la valeur + le libellé `RÉPÉTITIONS`.
- La ligne de boutons en bas devient : `Passer l'étape` (Expanded, comme aujourd'hui) + menu `...` (inchangé, passage/séquence) + `Étape suivante` (nouveau, `FilledButton.icon` avec check, même style que la branche `hasRepsWithStopwatchScore` L2782-2791). Trois éléments sur une seule ligne — largeur téléphone suffisante, pas de contrainte watch ici.
- Garder `Passer l'étape` et `Étape suivante` comme deux commandes distinctes (contournement vs validation), même principe que sur la montre (#166) : ne pas les fusionner en un seul bouton.
- Si l'étape a aussi un score (`step.hasScore`, saisie manuelle), le `_StepScoreInput` reste affiché entre la valeur reps et la ligne de boutons, comme aujourd'hui (L2843-2854) — ce ticket ne touche pas cette sous-partie, seulement le retrait du scroll et le repositionnement du bouton de validation.
### Ce qu'il faut pour Architect / DevFrontend
Pur remaniement de présentation Flutter dans un seul fichier (`workout_execution_screen.dart`), pas de nouvelle donnée ni de nouveau contrat : il s'agit de réutiliser un layout déjà existant (celui de la branche `hasRepsWithStopwatchScore`) pour la branche reps-seules, et de déplacer un bouton existant (`onCompleteStep`) d'un widget à l'autre. Point de vigilance dev : vérifier que le panneau (`_BoundedAccentPanel`, hauteur contrainte par son parent) a bien assez de place pour ce layout à hauteur fixe sur les plus petites tailles d'écran supportées — sinon réduire les paddings/tailles de police via `FittedBox`/`Theme` déjà utilisés ailleurs dans ce fichier, sans réintroduire de scroll.

View File

@ -2,15 +2,15 @@
id: "1bb57b78-7855-4e19-92e6-6d67c9155447" id: "1bb57b78-7855-4e19-92e6-6d67c9155447"
number: 171 number: 171
title: "[UI] enlever le scrolling dans le cas d'une étape avec répétitions seulement" title: "[UI] enlever le scrolling dans le cas d'une étape avec répétitions seulement"
status: "open" status: "closed"
priority: "medium" priority: "medium"
sprint: "d5c18b44-0eec-46db-b8ab-506cfee0bfea" sprint: "d5c18b44-0eec-46db-b8ab-506cfee0bfea"
links: [] links: []
agentRefs: [{"agentId":"f3408f5d-469c-4f64-9485-d8b218f3ff26","role":"assigned"}] agentRefs: [{"agentId":"f3408f5d-469c-4f64-9485-d8b218f3ff26","role":"assigned"}]
createdBy: {"kind":"user"} createdBy: {"kind":"user"}
updatedBy: {"kind":"user"} updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1785255221480 createdAt: 1785255221480
updatedAt: 1785255293774 updatedAt: 1785311597930
version: 4 version: 6
--- ---
Actuellement dans le cas ou une étape contient un nombre de répétitions seulement, le bouton "passer l'étape" est tout en bas et necessite un scrolling. Il faudrait qu'il soit alligné avec le bouton Etape suivante je pense pour éviter le scrolling. Voir avec UX Actuellement dans le cas ou une étape contient un nombre de répétitions seulement, le bouton "passer l'étape" est tout en bas et necessite un scrolling. Il faudrait qu'il soit alligné avec le bouton Etape suivante je pense pour éviter le scrolling. Voir avec UX

View File

@ -0,0 +1,103 @@
---
issueRef: "#172"
version: 2
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1785272880017
---
## #172 — Cadrage Main (2026-07-28)
### Décision produit
Direction validée :
- la montre reste un **capteur + télécommande** ;
- le téléphone reste **source de vérité métier** ;
- on optimise le trafic et la chauffe par **batching côté montre**, pas par déplacement de logique métier.
### 1. Télémétrie stats montre
Deux modes à cadrer :
- **écran montre allumé** : cadence plus fréquente pour un live crédible ;
- **écran montre éteint / ambient** : flush agrégé toutes les ~30s, inspiré du comportement observé sur Heavy.
La montre ne calcule pas les agrégats métier finaux. Elle bufferise seulement des échantillons ou micro-agrégats transport :
- FC : min/max/moy locale de fenêtre + latest utile à l'affichage ;
- distance/calories : latest cumulée de fenêtre, jamais recalcul métier final ;
- timestamps de capture et contexte d'exécution.
Le téléphone :
- persiste ;
- fusionne ;
- construit les agrégats de séance / étape / série / exercice / historique ;
- résout les conflits et les trous.
### 2. Score manuel montre
Décision : **oui au debounce 500ms, non au "score final sans contexte"**.
Le flux cible minimal :
- la montre met à jour le score **optimistement** à chaque tap ;
- chaque tap reset un timer de 500ms ;
- au silence de 500ms, la montre envoie au téléphone une intention de type `setManualScore` contextualisée ;
- le téléphone valide, persiste, reprojette ;
- la montre se recale silencieusement sur la projection confirmée.
### 3. Garde-fous architecture
- Pas de logique métier durable sur la montre.
- Pas de calcul d'historique ni de règle d'invariants sur la montre.
- Chaque message montre -> téléphone doit embarquer le contexte minimal : `sessionId`, indices d'exécution, `baseRevision`, horodatage.
- Rejet/conflit côté téléphone : la projection confirmée gagne toujours.
- Crash montre avant flush : perte limitée à la fenêtre non flushée, acceptable pour le score seulement si la montre ne prétend jamais avoir persisté côté téléphone.
### 4. Contrat à cadrer ensuite
Ticket attendu après celui-ci :
- extension ou adaptation du contrat watch bridge pour batches télémétrie ;
- ajout d'une commande contextualisée `setManualScore` ou équivalent ;
- stratégie de flush explicite selon état écran / foreground / ambient ;
- QA device sur chauffe, batterie, cohérence de resync.
## Cadrage Architect (2026-07-28)
Statut : **cadrage exploitable, prêt pour découpage dev**.
### Architecture cible minimale
- Garder `phone = source de vérité`, `watch = capteur + télécommande`.
- Conserver `WatchCompanionCommandHandler` comme point d'entrée unique des intentions montre.
- Ajouter un flux applicatif téléphone dédié pour la télémétrie batchée : `WatchTelemetryBatchIngress` -> validation -> appel de `WorkoutTelemetryUseCases.recordTelemetrySample(...)` pour chaque sample -> `WorkoutHistoryUseCases.updateHeartRateSummary(...)` au flush final.
- La montre ne calcule ni score final ni agrégats métier ; elle ne bufferise que du transport.
### Télémétrie batchée watch -> phone
- Recommandation : nouveau DTO `WatchTelemetryBatch`, `schemaVersion = 5`, nouveau path message `/gametime/watch/telemetry-batch`.
- Le téléphone déduplique au niveau **sample** (`sampleId` stable), pas au niveau batch.
- `WatchSensorSummary` est conservé pour le résumé final de séance, pas remplacé.
### Stratégie interactive vs ambient/screenOff
- `interactive` : flush toutes les `5s` max.
- `ambient/screenOff` : flush toutes les `30s` max.
- Flush immédiat aussi sur : fin de séance, pause explicite si la collecte s'arrête, reconnexion téléphone, changement d'état écran, buffer plein.
- Cap de sécurité : couper les batches trop gros (~60 samples).
- Une petite outbox locale de télémétrie côté montre est acceptable pour survivre à un crash : c'est du transport, pas du métier.
### Score manuel debounce
- Décision Architect : **ne pas faire de `setManualScore(value)` comme contrat principal**.
- Recommandation : un batch d'intentions delta, type `applyManualScoreDeltaBatch`.
- La montre accumule les taps `+/-` pendant `500 ms`, puis envoie un seul delta net avec `sessionId`, position courante, `baseRevision`, horodatage.
- Le téléphone applique le delta seulement si la cible métier est toujours la même ; sinon rejet et resync.
### Invariants et risques
- La montre ne calcule jamais la valeur finale du score.
- Les écritures d'historique et agrégats durables restent côté téléphone.
- Une intention score batchée n'est applicable que si `sessionId + scope + position courante` correspondent encore.
- Les retries de télémétrie ne doivent jamais dupliquer les samples : `sampleId` stable obligatoire.
- En cas de crash montre :
- score pending perdu puis recalage sur projection téléphone ;
- télémétrie non flushée reprise depuis l'outbox local.
- En reconnexion :
- resync projection immédiat ;
- flush immédiat de l'outbox télémétrie ;
- aucun replay d'anciennes intentions score.
### Lots recommandés
1. `B1 [DevBackend] Contrat bridge v5 et ingress télémétrie batchée`
2. `B2 [DevBackend] Application téléphone score debounce + validation de cible`
3. `B3 [DevBackend] Déduplication, retries, flush final`
4. `F1 [DevFrontend] Buffer montre télémétrie et stratégie interactive vs ambient`
5. `F2 [DevFrontend] Score optimiste debounce 500 ms`
6. `QA1 [QA] Matrice de cohérence et robustesse`

View File

@ -0,0 +1,20 @@
---
id: "7536c1ce-3683-4fb6-989f-7a6d3365cc69"
number: 172
title: "[Architect] Cadrage batching stats montre et debounce score montre -> téléphone"
status: "closed"
priority: "high"
sprint: "2c0dd1ea-8809-49ff-adc1-aa8820815ee7"
links: [{"target":"#119","kind":"relatesTo"},{"target":"#155","kind":"relatesTo"},{"target":"#156","kind":"relatesTo"},{"target":"#158","kind":"relatesTo"},{"target":"#163","kind":"relatesTo"}]
agentRefs: []
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1785257860728
updatedAt: 1785272880017
version: 2
---
Formaliser une architecture montre/téléphone plus économe en batterie et en requêtes :
- statistiques montre collectées localement puis envoyées au téléphone par batch/flush périodique, notamment écran montre éteint ;
- téléphone propriétaire des agrégats métier, de l'historique et des décisions ;
- montre limitée aux capteurs qu'elle seule connaît et aux commandes utilisateur ;
- score manuel montre avec UI optimiste locale et envoi debounce au téléphone, sans déplacer la logique métier sur la montre.

View File

@ -0,0 +1,101 @@
---
issueRef: "#173"
version: 2
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1785272880037
---
## #173 — Cadrage Main (2026-07-28)
### Décision produit
La direction proposée est retenue :
- **le téléphone décide** qu'un chrono arrive à 3, 2, 1, 0 ;
- **la montre ne dérive pas seule** un événement métier "chrono terminé" ;
- la montre reçoit une alerte à jouer et exécute l'haptique localement, même écran éteint si possible.
### Périmètre exact
Ce ticket ne remplace pas `#164`, il le complète architecturalement.
Il faut cadrer :
- le contrat téléphone -> montre d'alerte chrono ;
- la robustesse quand l'écran montre est éteint / ambient ;
- la force/pattern haptique suffisant pour activité physique réelle ;
- l'absence de doublon agressif ou de logique divergente entre téléphone et montre.
### Recommandation cible
- téléphone : source des événements `countdownTick(3|2|1)` et `countdownFinished` ;
- montre : simple exécution d'un pattern reçu, idempotent et court ;
- support minimal hors écran : foreground/ongoing/session state suffisamment vivant pour que la montre puisse encore vibrer ;
- si une alerte est ratée faute de connectivité, la projection confirmée doit rester cohérente et ne pas faire vibrer en retard plusieurs secondes après.
### Risques à cadrer
- latence si l'on transporte chaque tick individuellement ;
- duplication si la montre déclenche aussi localement ;
- vibration faible selon OEM si on reste sur un impact standard ;
- absence de vibration écran éteint si le process n'est pas au bon niveau de foreground/ongoing.
### Attendu suite
Créer ensuite le lot d'architecture/implémentation qui tranche :
- événements instantanés vs petits batches d'alertes planifiées ;
- pattern haptique fort mais court ;
- comportement dégradé sans connexion ;
- QA sur montre réelle en mouvement.
## Cadrage Architect (2026-07-28)
Statut : **cadrage exploitable, prêt pour découpage dev**.
### Architecture cible minimale
- Le téléphone reste l'autorité unique des événements d'alerte chrono.
- La montre n'infère plus seule une fin de chrono pour décider une vibration ; elle exécute une alerte explicite reçue du téléphone.
- La projection d'état actuelle est conservée pour l'affichage, avec un **flux séparé phone -> watch d'alertes éphémères**.
- Côté téléphone, une façade `WatchAlertPublisher` suffit au point où la logique chrono sait déjà qu'un `3/2/1/0/fin` se produit.
### Répartition téléphone / montre
- Téléphone :
- détecte `3`, `2`, `1`, `0`, `fin chrono`, `fin repos` si retenu ;
- joue le son téléphone ;
- publie une alerte structurée vers la montre ;
- décide si une alerte est encore pertinente ou périmée.
- Montre :
- reçoit l'alerte ;
- déclenche l'haptique locale forte ;
- peut réveiller brièvement l'écran si faisable natif Wear ;
- ne décide jamais seule qu'un `0` est atteint.
### Contrat phone -> watch recommandé
- Nouveau DTO `WatchAlertEnvelope`, `schemaVersion = 5`, transporté via `MessageClient` sur un path type `/gametime/phone/alert`.
- Champs clés : `alertId`, `sessionId`, `revision`, `kind`, `timerKind`, position courante, `scheduledForEpochMs`, `expiresAtEpochMs`, `pattern`.
- `alertId` sert à la déduplication.
- `scheduledForEpochMs` + `expiresAtEpochMs` servent à ignorer une alerte arrivée trop tard.
### Latence / duplication / retard
- Envoi au moment de l'événement métier, pas à intervalle fixe.
- La montre ignore :
- tout `alertId` déjà joué ;
- tout `sessionId` non cohérent avec la séance projetée courante ;
- toute alerte reçue après son expiration.
- TTL recommandé :
- `3/2/1` : ~700 ms ;
- `0/fin` : ~2000 ms.
- Pas d'ACK obligatoire en v1 : une alerte manquée n'est pas rejouée après resync.
### Foreground / ambient / ongoing
- S'appuyer sur l'existant :
- `WatchOngoingActivityController` ;
- `WatchHeartRateForegroundService` pendant `running`.
- Ne pas créer un second service d'alerte.
- Si un réveil écran bref est faisable, le réserver à `timerZero|timerFinished`, jamais à `3/2/1`.
- En ambient, l'alerte doit rester haptique-first.
### Articulation avec #164
- `#164` reste le ticket observable audio/alertes chrono.
- `#173` ajoute la prolongation architecturale vers la montre.
- Le haptique montre actuel basé sur transition de projection devient au mieux un fallback transitoire ; la cible propre est le nouveau flux d'alertes explicites.
### Lots recommandés
1. `B1 [DevBackend] Contrat bridge alertes chrono + publisher téléphone`
2. `B2 [DevBackend] Déduplication / TTL / filtrage session`
3. `F1 [DevFrontend] Exécution locale alerte montre`
4. `F2 [DevFrontend] Intégration foreground/ambient`
5. `QA1 [QA] Validation alertes chrono phone/watch`

View File

@ -0,0 +1,20 @@
---
id: "7b5853b0-61c2-427e-bc07-e8c662f35ec4"
number: 173
title: "[Architect] Pilotage téléphone -> montre des alertes chrono (son + haptique forte + écran éteint)"
status: "closed"
priority: "high"
sprint: "abc4f969-b169-45f7-988c-daeeab762201"
links: [{"target":"#164","kind":"relatesTo"},{"target":"#127","kind":"relatesTo"}]
agentRefs: []
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1785257860729
updatedAt: 1785272880037
version: 2
---
Cadrer le pilotage des alertes de chrono pour respecter la règle "un maximum de logique métier côté téléphone" :
- le téléphone décide quand une alerte de chrono doit partir ;
- la montre exécute une vibration suffisamment forte pour être sentie en course/sauts ;
- l'alerte doit continuer à fonctionner écran montre éteint si la séance est toujours active ;
- le son téléphone de fin de chrono reste requis sans couper la musique.

View File

@ -0,0 +1,64 @@
---
issueRef: "#174"
version: 2
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1785311597948
---
## #174 — Cadrage Main (2026-07-28)
### Symptôme utilisateur
Parfois, au lancement d'un chrono, l'affichage ne reste pas sur GameTime et retourne à l'accueil de la montre.
### Position
Ce ticket est un **suivi plus précis** de `#127`, mais avec un déclencheur confirmé supplémentaire : le lancement/changement d'état du chrono.
### Attendu
- tant qu'une séance est active, le retour gestuel ou le réveil écran doit favoriser le retour à GameTime ;
- un changement de projection de séance ou de chrono ne doit pas éjecter l'utilisateur hors de l'app ;
- l'app montre doit rester le point de focus naturel pendant la séance, sans demander de relancer manuellement l'app.
### Axes à instruire ensuite
- ongoing activity / reprise d'activité ;
- task affinity / launch mode / intent de retour ;
- interaction entre foreground service, écran ambient et navigation Flutter ;
- différence entre "écran éteint puis réveil" et "lancement d'un chrono".
## Cadrage Architect (2026-07-28)
Statut : **cadrage exploitable, prêt pour lot frontend natif montre**.
### Causes techniques plausibles
- `MainActivity` Wear est trop passive : pas de `onResume`, pas de `onNewIntent`, pas de logique de reentry/resync.
- La reprise dépend surtout du flux Flutter `EventChannel` et de `requestLatestProjection(...)`, sans ré-ancrage explicite de l'activity.
- Les intents ongoing/notification utilisent seulement `SINGLE_TOP | CLEAR_TOP`, sans action dédiée "ouvrir la séance active".
- Si `POST_NOTIFICATIONS` manque, l'ongoing activity peut ne pas fournir de point d'ancrage système.
- Le foreground service watch ne tourne que pour `phase == running`, donc l'UI peut perdre son point d'appui natif pendant certaines transitions alors que la séance existe encore.
### Correctif minimal recommandé
- Corriger côté watch natif, sans changement de contrat métier.
- Ajouter une vraie voie de réentrée locale :
- intent explicite `ACTION_OPEN_ACTIVE_SESSION` ;
- même intent réutilisé par ongoing activity et foreground notification ;
- `onNewIntent` et `onResume` dans `MainActivity` qui appellent `requestLatestProjection(...)` + `requestCapabilityRefresh(...)`.
- Garder côté natif la dernière projection utile reçue ; si elle indique une séance active **encore fraîche ou rapidement reconfirmée**, la réouverture doit retomber sur GameTime, pas sur l'accueil système.
- Si la séance n'est plus confirmée, ne pas forcer de reopen : cela doit rester distingué de `#175`.
### Impacts
- Contrats bridge : aucun changement obligatoire.
- Natif watch :
- action/extra de reentry ;
- cache de dernière projection utile ;
- resync explicite sur resume/new intent.
- UI Flutter montre : pas de changement structurel, au plus un resync plus agressif au retour foreground.
- DevBackend : non requis pour le correctif minimal.
### Garde-fous vs #175
- `#174` : le téléphone est vivant, la séance existe, mais l'activity montre décroche ou ne se ré-ancre pas.
- `#175` : la projection doit expirer quand la source téléphone disparaît durablement.
- Donc le reopen forcé de `#174` ne doit se faire que si la projection locale est encore fraîche ou si un resync confirme rapidement la séance.
### Lots recommandés
1. `F1 [DevFrontend] Reentry natif montre`
2. `F2 [DevFrontend] Robustesse réveil/ambient`
3. `QA1 [QA] Validation montre ancrée`

View File

@ -0,0 +1,16 @@
---
id: "f2db430b-48cf-4626-beae-611ffb352043"
number: 174
title: "[Bug] La montre quitte l'application ou revient à l'accueil pendant une séance/au lancement d'un chrono"
status: "qa"
priority: "high"
sprint: "abc4f969-b169-45f7-988c-daeeab762201"
links: [{"target":"#127","kind":"relatesTo"},{"target":"#165","kind":"relatesTo"}]
agentRefs: []
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1785257860730
updatedAt: 1785311597948
version: 2
---
Corriger le cas où, pendant une séance, le lancement d'un chrono ou certains changements d'état renvoient la montre vers l'écran d'accueil système au lieu de rester sur l'application GameTime. Le comportement attendu est que la montre reste ancrée sur la séance active tant qu'une séance est en cours.

View File

@ -0,0 +1,86 @@
---
issueRef: "#175"
version: 4
updatedBy: {"kind":"user"}
updatedAt: 1785315994252
---
## #175 — Cadrage Main (2026-07-28)
### Symptôme utilisateur
Si l'utilisateur tue l'app téléphone sans quitter d'abord la séance, la montre conserve une séance active apparente et continue d'ouvrir l'ancien écran de séance.
### Gravité
Critique côté cohérence :
- la montre affirme un état actif sans source de vérité confirmée ;
- l'utilisateur peut croire que la séance continue alors que le téléphone a disparu du cycle ;
- cela contredit le principe "logique métier côté téléphone".
### Décision de cadrage
La montre ne doit pas garder indéfiniment un état "séance active" orphelin.
Comportement cible à trancher techniquement :
- soit le téléphone envoie explicitement un état terminal/deconnexion avant extinction contrôlée ;
- soit la montre invalide l'état de séance après une fenêtre d'absence de heartbeat/projection ;
- soit combinaison des deux, avec dégradation vers un état clair "Téléphone indisponible" ou retour hors séance.
### Garde-fous
- ne pas perdre une vraie séance lors d'une micro-coupure normale ;
- ne pas laisser une séance fantôme persister après fermeture forcée ;
- distinguer reconnexion brève et disparition durable du téléphone ;
- l'écran montre doit refléter un état confirmé ou explicitement dégradé, jamais un faux "tout va bien".
### Attendu suite
Ticket d'architecture/implémentation à sortir de celui-ci :
- stratégie heartbeat / TTL projection ;
- comportement UI montre si la source téléphone disparaît ;
- tests kill téléphone / reprise / relance app.
## Cadrage Architect (2026-07-28)
Statut : **cadrage exploitable, prêt pour découpage dev**.
### Diagnostic
Le bug vient du mécanisme de reprise montre : au démarrage ou à la reconnexion, la montre relit le dernier `DataItem` de projection et continue de l'afficher même si le téléphone a disparu. Le `WatchSessionViewModel` peut marquer la projection `stale` puis `connectionLost`, mais n'invalide jamais la projection elle-même.
### Décision d'architecture
- Le téléphone reste l'unique source de vérité.
- La projection montre doit être traitée comme un **cache temporaire**, jamais comme une preuve durable qu'une séance est encore active.
- Une projection phone -> watch devient **expirable**.
- Sans rafraîchissement explicite du téléphone dans une fenêtre courte, la montre doit considérer la projection invalide et revenir à un état `téléphone indisponible / aucune séance confirmée`.
### Mécanisme recommandé
- Étendre `WatchSessionProjection` avec un TTL explicite, par exemple `expiresAtEpochMs`.
- À chaque projection publiée, le téléphone renseigne `expiresAtEpochMs = now + ttl`.
- Recommandation : TTL de `12s`, cohérent avec le refresh périodique existant toutes les `2s`.
- Si l'app téléphone est killée, le heartbeat s'arrête ; la montre laisse expirer la projection.
- Si le téléphone sait explicitement qu'il n'y a plus de séance, il publie `phase = noActiveSession` immédiatement comme aujourd'hui ; le TTL couvre le cas brutal.
### Comportement montre attendu
- Avant expiration : garder l'écran courant, avec l'état `stale` existant si utile.
- Après expiration durable :
- remplacer localement la projection active par une projection synthétique `noActiveSession` ;
- vider `sensorSample`, commandes pending, score optimiste ;
- arrêter ongoing / foreground liés à la séance ;
- afficher un état type `Téléphone indisponible / rouvre GameTime sur le téléphone`, pas l'ancienne séance.
- Si le téléphone republie ensuite une projection valide, la montre se recale normalement.
### Garde-fous micro-coupures
- Ne pas invalider sur simple `connectionLost` capability.
- Invalider seulement quand aucune projection fraîche n'est reçue au-delà du TTL et qu'aucun ack/commande retour ne prouve une communication en cours.
- Fenêtres recommandées :
- refresh téléphone : `2s` inchangé ;
- seuil `stale` : `6s` actuel conservé ;
- expiration dure : `12s` à `15s`.
### Impacts
- Contrat : `WatchSessionProjection` gagne `expiresAtEpochMs`, `schemaVersion` incrémentée.
- Téléphone : renseigne ce champ à chaque publish.
- Bridge natif watch : pas de refonte structurelle ; la lecture du dernier `DataItem` peut rester, mais ce snapshot sera auto-invalidé par TTL.
- UI montre : ajouter l'invalidation locale dans `_syncFreshnessState()` ; injecter une projection synthétique `noActiveSession` après expiration.
### Lots recommandés
1. `B1 [DevBackend] Projection expirable côté téléphone`
2. `F1 [DevFrontend] Invalidation locale montre sur TTL expiré`
3. `F2 [DevFrontend] Ajustements natifs reprise/ongoing`
4. `QA1 [QA] Validation disparition source téléphone`

View File

@ -0,0 +1,16 @@
---
id: "3528d6d9-1b1a-4b77-ac36-6ab37470eba0"
number: 175
title: "[Bug] Séance fantôme sur la montre après fermeture forcée de l'application téléphone"
status: "qa"
priority: "critical"
sprint: "abc4f969-b169-45f7-988c-daeeab762201"
links: [{"target":"#100","kind":"relatesTo"},{"target":"#108","kind":"relatesTo"}]
agentRefs: []
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedBy: {"kind":"user"}
createdAt: 1785257860731
updatedAt: 1785315994252
version: 4
---
Quand l'application téléphone est fermée de force pendant qu'une séance est encore active, la montre continue de croire qu'une séance est en cours : logo de séance toujours présent sur l'écran principal et retour vers le dernier écran de séance au tap. Il faut cadrer puis corriger la stratégie de vérité/déconnexion pour qu'une montre ne garde pas une séance fantôme sans source téléphone vivante.

View File

@ -1,8 +1,8 @@
--- ---
issueRef: "#85" issueRef: "#85"
version: 4 version: 5
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1785248859243 updatedAt: 1785311597729
--- ---
## #85 — Cadrage UX V1 packs partageables (UX, 2026-07-28) ## #85 — Cadrage UX V1 packs partageables (UX, 2026-07-28)

View File

@ -2,7 +2,7 @@
id: "78ce00f0-b70c-4e21-a22a-42a78f31fa48" id: "78ce00f0-b70c-4e21-a22a-42a78f31fa48"
number: 85 number: 85
title: "Packs de séances partageables (au-delà du partage ciblé existant)" title: "Packs de séances partageables (au-delà du partage ciblé existant)"
status: "inProgress" status: "closed"
priority: "low" priority: "low"
sprint: "4237adfd-91eb-45ff-9f32-6ce1daa39822" sprint: "4237adfd-91eb-45ff-9f32-6ce1daa39822"
links: [] links: []
@ -10,8 +10,8 @@ agentRefs: []
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784650728867 createdAt: 1784650728867
updatedAt: 1785248859243 updatedAt: 1785311597729
version: 4 version: 5
--- ---
Constat Commercial : certains concurrents s'appuient sur des contenus créés par des coachs/communauté. GameTime a déjà une base de partage ciblé de programmes/séances entre comptes (#51, #66, #69, en QA) ; l'étape suivante serait des "packs" de séances plus larges (coach ou communauté), mais uniquement après arbitrage explicite de Main sur le positionnement (éviter de dériver vers un réseau social ou une marketplace). Constat Commercial : certains concurrents s'appuient sur des contenus créés par des coachs/communauté. GameTime a déjà une base de partage ciblé de programmes/séances entre comptes (#51, #66, #69, en QA) ; l'étape suivante serait des "packs" de séances plus larges (coach ou communauté), mais uniquement après arbitrage explicite de Main sur le positionnement (éviter de dériver vers un réseau social ou une marketplace).

File diff suppressed because it is too large Load Diff