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>
This commit is contained in:
2026-07-28 16:48:54 +02:00
parent 58272e354a
commit 917777e18b
279 changed files with 13546 additions and 674 deletions

View File

@ -0,0 +1,62 @@
---
name: gametime-android-release-networking-and-apk-build
description: memory note gametime-android-release-networking-and-apk-build
metadata:
type: project
---
# GameTime — Réseau Android release, cleartext et build APK
Capitalise le bug de création de compte sur APK release (« Création impossible pour le moment. Réessaie plus tard ») et la procédure de build/distribution APK.
## Causes du bug (toutes nécessaires, stacking)
L'erreur UI générique `Création impossible pour le moment` (`lib/presentation/profile_screen.dart`, `_registerErrorMessage`) est retournée pour `RemoteAuthFailure.network` **ou** tout échec non `emailAlreadyUsed`. Trois causes indépendantes cumulées sur le release :
1. **Permission `INTERNET` absente du manifest release.** Elle n'était déclarée que dans `android/app/src/debug/AndroidManifest.xml` et `.../profile/...`. Le release (merge main+release, sans manifest release dédié) n'en héritait pas → aucun socket réseau possible → `http.ClientException``RemoteAuthFailure.network`.
- Fix définitif : `INTERNET` déclaré dans `android/app/src/main/AndroidManifest.xml` (racine `<manifest>`), hérité par tous les build types.
2. **Trafic cleartext bloqué.** Serveur de dev/LAN en `http://` (pas de TLS ; reverse proxy HTTPS externe). Sur Android 9+ (API 28+) le cleartext est bloqué par défaut.
- Fix : `android:usesCleartextTraffic="true"` sur `<application>` dans `src/main/AndroidManifest.xml`. Acceptable tant qu'il n'y a pas de TLS bout-en-bout ; à remplacer par une `network_security_config` ciblée quand la prod passe en HTTPS.
3. **Port serveur.** Le conteneur API publie le port interne 8080 sur le port hôte **8090** (`server/.env` : `API_BIND_ADDRESS=192.168.1.75`, `API_PORT=8090` ; `server/docker-compose.yaml` `${API_BIND_ADDRESS}:${API_PORT}:8080`). Donc l'URL joignable est `http://192.168.1.75:8090`, **pas** 8080.
## Contrat d'injection d'URL (ne pas coder en dur)
`HttpApiClient.defaultBaseUrl` (`lib/infrastructure/remote/http_api_client.dart`) reste `http://localhost:8080` par défaut (fallback de dev local). L'URL réelle est injectée au build via `--dart-define=GAMETIME_API_BASE_URL=...`. Ne pas hardcoder d'IP/port serveur dans le code (validé par Architect). Le défaut `localhost:8080` est volontairement non fonctionnel sur device physique distant.
## Build APK release (env Main)
Pré-requis machine projet : Flutter 3.44.6 (`/usr/bin/flutter`), Android SDK `/opt/android-sdk`, **JDK 21 obligatoire** (`/usr/lib/jvm/java-21-openjdk`) — le JDK système par défaut (java-26) casse Gradle.
```bash
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk
export ANDROID_HOME=/opt/android-sdk
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$PATH"
cd /home/anthony/Documents/Projects/GameTime
flutter build apk --release --dart-define=GAMETIME_API_BASE_URL=http://192.168.1.75:8090
```
Sortie : `build/app/outputs/flutter-apk/app-release.apk` (+ `.sha1`). Signé avec les clés debug (`android/app/build.gradle.kts` : `signingConfig = debug`) → installable sans keystore prod.
Note : les sandboxes agents (QA, codex runner) échouent à lancer Gradle (« Could not determine a usable wildcard IP for this machine ») ; le build release se fait depuis l'environnement Main uniquement.
## Vérification du manifest mergé d'un APK
```bash
AAPT=/opt/android-sdk/build-tools/34.0.0/aapt2
$AAPT dump permissions <apk> # doit lister android.permission.INTERNET
$AAPT dump xmltree <apk> --file AndroidManifest.xml | grep -iE 'INTERNET|usesCleartextTraffic'
```
Pour un release GameTime sain : `INTERNET` présent ET `usesCleartextTraffic=true`.
## Santé serveur (pre-flight)
```bash
curl http://192.168.1.75:8090/health # attendu {"status":"ok"}
curl -X POST http://192.168.1.75:8090/auth/register \
-H 'content-type: application/json' \
-d '{"email":"probe@example.com","password":"probe12345"}' # attendu HTTP 201
```
`docker ps` : `server-api-1` expose `192.168.1.75:8090->8080/tcp`, `server-postgres-1` healthy.

View File

@ -0,0 +1,10 @@
---
name: gametime-architecture-telemetry-live-heart-rate-distance-calories
description: >
metadata:
type: project
---
- Ne pas mélanger telemetry et projection de commande.
- Ne pas réutiliser une estimation de calories comme mesure réelle.
- Ne pas compter l'ordre d'arrivée réseau pour les agrégats.
- Les données manquantes restent silencieuses.

View File

@ -0,0 +1,452 @@
---
name: gametime-architecture-watch-companion
description: memory note gametime-architecture-watch-companion
metadata:
type: project
---
# GameTime — Architecture interface montre synchronisée
Décision d'architecture pour la feature #91, basée sur `gametime-ux-watch-companion`, `gametime-architecture-initial-stack-data-model`, `gametime-session-execution-timer-refactor`, `gametime-architecture-step-chaining-override` et `gametime-architecture-exercise-steps`.
## Décision structurante
La montre est un **companion stateless côté domaine** :
- le **téléphone** reste l'unique source de vérité d'exécution ;
- la **montre** n'exécute aucune logique métier de séance, ne persiste aucun état métier et ne calcule aucun enchaînement ;
- toute action montre est une **commande adressée au téléphone** ;
- la montre ne se recale que sur l'**état confirmé** renvoyé par le téléphone.
Conséquence non négociable : le cas clé "première étape chrono + exercice chrono = démarrage commun" reste implémenté uniquement dans `ActiveWorkoutSessionUseCases.startCurrentExerciseTimers(...)`. La montre invoque ce même point métier ; elle ne recompose jamais ce démarrage elle-même.
## Structure projet retenue
Option retenue : **app Wear OS Flutter dédiée dans le mono-dépôt**, pas un module compagnon embarqué dans l'app téléphone.
Structure recommandée :
```text
/
lib/ // app téléphone existante
android/ // app téléphone existante
watch_app/ // nouvelle app Flutter Wear OS dédiée
lib/
android/
pubspec.yaml
packages/
watch_bridge_contract/ // package Dart pur partagé (DTO + enums + codecs)
```
Justification :
- l'app téléphone actuelle est un package Flutter unique déjà câblé pour Android/iOS ; y greffer une surface Wear OS dans le même target Android mélangerait trop la config mobile phone, la config watch et le bridge natif ;
- une app Wear OS dédiée isole les manifestes, permissions, icônes, navigation et cadence de release montre sans polluer l'app téléphone ;
- le mono-dépôt reste simple : un second package Flutter avec dépendance locale vers un package Dart partagé suffit ; pas besoin d'introduire un nouveau runtime, ni de dupliquer le domaine ;
- l'architecture hexagonale reste propre : le package partagé ne contient que des **contrats de transport**, jamais du métier.
Option écartée : module compagnon dans l'app téléphone.
- elle réduit légèrement le nombre de packages, mais couple trop fort les couches Android et rend plus fragile la maintenance du bridge Wearable/Data Layer et des variantes téléphone/montre.
## Canal téléphone ↔ montre retenu
Canal retenu : **Wearable Data Layer natif Android** exposé à Flutter via un adapter d'infrastructure fin.
Répartition :
- **MessageClient** pour les **commandes montre -> téléphone** et les **acks**.
- **DataClient** pour la **projection d'état téléphone -> montre** sous forme de "latest state".
- **CapabilityClient** pour la **découverte de nœud** et la reprise de connexion.
Décision d'implémentation :
- ne pas rendre le domaine/application dépendants d'un plugin tiers ;
- encapsuler le Data Layer dans un adapter Android dédié (`infrastructure/watch_bridge`) exposé à Flutter par `MethodChannel`/`EventChannel` ou `Pigeon`.
Justification :
- fonctionne **offline/local** via le lien téléphone-montre existant, sans cloud ;
- `MessageClient` est adapté aux intentions impératives basse latence ;
- `DataClient` est adapté au **dernier état compact** à rejouer après reconnexion, sans devoir rejouer un historique d'événements ;
- le couple Message/Data est plus robuste qu'un flux message-only : la montre peut toujours se réaligner sur le dernier snapshot autoritaire.
## Architecture hexagonale cible
### Côté téléphone
Ajouter une façade applicative dédiée, additive et non invasive :
`WatchCompanionUseCases`
Responsabilités :
- recevoir une `WatchCommandEnvelope` depuis l'adapter Wear ;
- sérialiser l'exécution des commandes montre ;
- router chaque commande vers les **use cases existants** (`ActiveWorkoutSessionUseCases`, `ActiveExerciseStepUseCases` et lecture repository) ;
- construire une `WatchSessionProjection` compacte à partir de l'état persistant téléphone ;
- publier cette projection à chaque mutation d'exécution pertinente.
Ports recommandés côté application :
- `WatchCommandIngress`
- `Future<WatchCommandAck> dispatch(WatchCommandEnvelope command)`
- `WatchProjectionPublisher`
- `Future<void> publish(WatchSessionProjection projection)`
- `WatchProjectionSource`
- `Future<WatchSessionProjection> currentProjection()`
Important :
- `WatchCompanionUseCases` est une **façade d'orchestration**, pas une seconde logique métier ;
- les règles d'exécution restent dans les use cases existants ;
- les adapters Wear n'appellent jamais directement Drift ni la présentation Flutter téléphone.
### Côté montre
L'app Wear OS a trois couches :
- `presentation/` : écrans UX montre, état local de connexion/pending ;
- `application/` : interprétation minimale des DTO et orchestration UI ;
- `infrastructure/` : adapter Data Layer.
La montre peut persister seulement :
- préférences UI locales ;
- dernier état reçu pour reprise visuelle courte durée si l'app montre est recréée.
Elle ne persiste jamais :
- session métier ;
- timers métier ;
- résultats ;
- historique de commandes comme source de vérité.
## Sémantique des flux
## 1. Montre -> téléphone : contrat de commande
### Enveloppe
```dart
enum WatchCommandType {
startCurrentExercise,
pauseSession,
resumeSession,
startPreparedTimedStep,
skipCurrentStep,
skipCurrentPassage,
finishCurrentSet,
skipCurrentSet,
skipCurrentRest,
}
final class WatchCommandEnvelope {
final int schemaVersion;
final String commandId;
final WatchCommandType type;
final String sessionId;
final int expectedRevision;
final int sentAtEpochMs;
}
```
Décisions :
- `commandId` : UUID généré côté montre, unique par tentative utilisateur.
- `sessionId` : session téléphone visée ; empêche l'application d'une commande à une autre séance après reconnexion.
- `expectedRevision` : révision de projection sur laquelle l'utilisateur a agi.
- pas de payload métier supplémentaire en v1 : toutes les commandes portent implicitement sur la **position courante** de la séance active.
### Mapping métier obligatoire
- `startCurrentExercise`
- appelle la même commande applicative que le téléphone pour `Démarrer l'exercice`.
- route vers `ActiveWorkoutSessionUseCases.startCurrentExerciseTimers(...)`.
- couvre explicitement le démarrage commun timer de série + première étape chrono + score chrono.
- `pauseSession`
- route vers `ActiveWorkoutSessionUseCases.pause(...)`.
- `resumeSession`
- route vers `ActiveWorkoutSessionUseCases.resume(...)`.
- `startPreparedTimedStep`
- route vers `ActiveExerciseStepUseCases.startTimer(...)`.
- réservé au cas `Chrono suivant prêt`.
- `skipCurrentStep`
- route vers `ActiveExerciseStepUseCases.skipCurrentStep(...)`.
- `skipCurrentPassage`
- route vers `ActiveExerciseStepUseCases.skipCurrentPassage(...)`.
- `finishCurrentSet`
- route vers le même enchaînement applicatif que le bouton téléphone `Terminer la série` :
- arrêt/enregistrement des chronos actifs via les use cases existants ;
- création/mise à jour du résultat de série ;
- démarrage éventuel du repos ;
- progression de curseur.
- `skipCurrentSet`
- route vers le même enchaînement applicatif que `Passer la série`, avec skip des chronos/séquence et progression.
- `skipCurrentRest`
- route vers `ActiveWorkoutSessionUseCases.skipRest(...)`.
### Ack
```dart
enum WatchCommandAckStatus {
accepted,
acceptedNoOp,
rejectedStaleRevision,
rejectedNotApplicable,
rejectedNoActiveSession,
rejectedSessionMismatch,
rejectedPhoneBusy,
}
final class WatchCommandAck {
final int schemaVersion;
final String commandId;
final WatchCommandAckStatus status;
final String sessionId;
final int revisionAtAck;
final int ackedAtEpochMs;
final String? reasonCode;
}
```
Règles :
- `accepted` : la commande a été appliquée ; une projection mise à jour doit suivre immédiatement.
- `acceptedNoOp` : la commande était déjà satisfaite ou doublonnée sans effet métier.
- `rejectedStaleRevision` : l'état téléphone a avancé depuis `expectedRevision` ; la commande n'est pas rejouée sur le nouvel état. La montre doit attendre le snapshot courant et se recaler.
- `rejectedNotApplicable` : action impossible dans l'état courant.
- `rejectedPhoneBusy` : réservé au cas exceptionnel où le téléphone n'a pas pu sérialiser immédiatement ; la montre ne rejoue pas en boucle sans nouvel état.
### Garantie d'ordre et d'idempotence
Décision :
- les commandes montre sont traitées **séquentiellement** côté téléphone, via une file mono-consommateur dans `WatchCompanionUseCases` ;
- le téléphone incrémente une `revision` entière de projection à chaque mutation visible montre ;
- une commande n'est appliquée que si `expectedRevision == currentRevision` ;
- après succès, la nouvelle projection porte `revision + 1` ;
- un duplicate/retry avec ancien `expectedRevision` est rejeté `rejectedStaleRevision` et ne peut donc pas skipper une étape supplémentaire par accident.
Conséquence :
- **pas besoin** d'une persistance métier de reçus de commandes sur la montre ;
- la combinaison `sessionId + expectedRevision + commandId` suffit pour obtenir un comportement effectivement idempotent côté UX ;
- l'ordre réel retenu est toujours celui du téléphone, jamais celui reconstruit par la montre.
## 2. Téléphone -> montre : DTO de projection d'état
### DTO racine
```dart
enum WatchSessionPhase {
noActiveSession,
ready,
running,
paused,
nextTimerReady,
restRunning,
restPaused,
betweenSetsReady,
}
enum WatchPrimaryAction {
none,
startCurrentExercise,
pauseSession,
resumeSession,
startPreparedTimedStep,
skipCurrentRest,
}
enum WatchSecondaryAction {
skipCurrentStep,
skipCurrentPassage,
finishCurrentSet,
skipCurrentSet,
skipCurrentRest,
}
final class WatchSessionProjection {
final int schemaVersion;
final String deviceSessionId;
final int revision;
final int projectedAtEpochMs;
final WatchSessionPhase phase;
final bool phoneReachable;
final int seriesIndex;
final int seriesTotal;
final String exerciseName;
final int? passageIndex;
final int? passageTotal;
final int? stepIndex;
final int? stepTotal;
final String? stepName;
final WatchTimerProjection? dominantTimer;
final List<WatchTimerProjection> secondaryTimers;
final WatchPrimaryAction primaryAction;
final List<WatchSecondaryAction> secondaryActions;
final String? nextExerciseName;
final String? statusLabel;
}
```
### DTO timer
```dart
enum WatchTimerKind {
rest,
step,
scoreStopwatch,
setTimer,
}
enum WatchTimerDisplayMode {
countdown,
elapsed,
}
enum WatchTimerRunState {
stopped,
running,
paused,
}
final class WatchTimerProjection {
final WatchTimerKind kind;
final String label;
final WatchTimerDisplayMode displayMode;
final WatchTimerRunState runState;
final int referenceEpochMs;
final int accumulatedMs;
final int? startedAtEpochMs;
final int? targetMs;
}
```
### Règles de calcul
- `seriesIndex` / `seriesTotal` sont **1-based** pour éviter toute logique de mapping montre.
- `passageIndex`, `stepIndex` et leurs totals sont omis si non applicables.
- `dominantTimer` suit strictement la priorité UX :
1. repos ;
2. étape temps ou `nextTimerReady` ;
3. score chrono ;
4. temps de série.
- `secondaryTimers` contient seulement les autres chronos utiles à l'affichage compact, ordonnés.
- `nextExerciseName` n'est renseigné que pendant `restRunning` / `restPaused`.
- `statusLabel` sert aux libellés compacts type `Chrono étape`, `Séance en pause`, `Prêt pour la série suivante`.
### Interpolation locale du chrono
Décision :
- la montre **interpole localement l'affichage du chrono** à partir de `referenceEpochMs`, `startedAtEpochMs`, `accumulatedMs` et `targetMs` ;
- le téléphone envoie une projection immédiatement à chaque transition métier et un **heartbeat de resynchronisation léger toutes les 5 secondes** tant qu'au moins un chrono est `running`.
Justification :
- réduit fortement le trafic et la batterie par rapport à un push haute fréquence ;
- exploite le modèle téléphone déjà persistant par horodatages ;
- garde la montre lisible même avec une brève latence ;
- la montre n'utilise cette interpolation que pour **l'affichage**, jamais pour décider d'un changement métier.
## États de connexion, latence et resync
Décision de seuils v1 :
- après tap sur la montre : état local `Envoi...` immédiat ;
- si pas d'ack après **500 ms** : afficher `En attente du téléphone` ;
- si pas d'ack après **2 s** : commande considérée en timeout UX ;
- si aucun ack ni projection fraîche depuis **10 s** : état `Connexion perdue`, actions désactivées ;
- si une projection reçue date de plus de **6 s** pendant une séance active, la montre la marque `dernier état reçu` mais garde encore l'écran.
Stratégie de reprise :
- à reconnexion d'un nœud téléphone, la montre demande un `resync` ;
- le téléphone republie la `WatchSessionProjection` complète courante via `DataClient` ;
- la projection complète remplace toujours l'état montre en entier, jamais patch par patch.
## Service premier plan téléphone
Décision : quand une séance est active côté téléphone (`running`, `paused` ou repos actif), le bridge montre doit vivre dans un **foreground service Android** dédié au companion.
Responsabilités du service :
- garder le process téléphone vivant pendant la séance ;
- écouter les commandes Wear Data Layer ;
- invoquer `WatchCompanionUseCases` ;
- publier les projections et heartbeats ;
- exposer une notification persistante `Séance en cours`.
Contraintes :
- le service ne porte **aucune logique métier** ; il orchestre uniquement le bridge et les use cases existants ;
- il doit redémarrer à partir de l'état persistant téléphone si Android recrée le process pendant une séance ;
- type Android recommandé : `connectedDevice`, avec complément `dataSync` seulement si requis par l'implémentation exacte du bridge ;
- arrêt du service quand la séance passe en `completed`, `abandoned` ou `savedExit` et qu'aucune synchronisation montre n'est encore en vol.
## Conflits et source de vérité
Confirmation de la règle UX :
- une action montre n'est **jamais appliquée localement** sur la montre ;
- la montre ne fait qu'afficher un pending local puis attend `ack + projection` ;
- si téléphone et montre agissent presque simultanément, l'ordre retenu est celui appliqué par le téléphone ;
- une commande fondée sur une révision périmée est rejetée `rejectedStaleRevision`, puis remplacée visuellement par l'état réel courant.
## Haptiques
Décision :
- déclenchement **côté montre**, à réception d'un `ack` ou d'une projection franchissant un jalon ;
- jamais côté téléphone pour la montre ;
- aucun son requis au MVP ;
- si un son est ajouté plus tard, il doit être configuré sans prise de focus audio.
Mapping v1 :
- `accepted` / `acceptedNoOp` pour start/pause/reprise : impulsion courte ;
- projection entrant en `nextTimerReady`, fin de repos ou fin de chrono visible : double impulsion ;
- perte de connexion après action : impulsion lourde unique optionnelle.
Invariant #92 :
- aucune API haptique/son montre ne doit prendre le focus audio ni interrompre la musique du téléphone.
## Invariants à préserver
- le domaine d'exécution reste centralisé sur le téléphone ;
- aucune logique d'enchaînement d'étapes, de repos ou de timers n'est dupliquée sur la montre ;
- toute commande montre passe par les mêmes use cases applicatifs que l'UI téléphone ;
- la projection montre reste compacte et dérivée, jamais source de vérité ;
- la reconnexion remplace intégralement l'état montre par le dernier snapshot téléphone ;
- aucune nouvelle base métier n'est introduite sur la montre.
## Sous-tickets recommandés
- #91-A `[DevBackend] Contrats watch bridge partagés + façade applicative WatchCompanionUseCases`
- créer le package `packages/watch_bridge_contract`
- définir `WatchCommandEnvelope`, `WatchCommandAck`, `WatchSessionProjection`
- créer la façade applicative téléphone et la file séquentielle
- dépendances : aucune
- #91-B `[DevBackend] Projection compacte d'exécution téléphone -> montre`
- dériver `WatchSessionProjection` depuis l'état persistant d'exécution
- gérer `revision`, priorisation du chrono dominant, actions autorisées
- dépend de `#91-A`
- #91-C `[DevBackend] Routing des commandes montre vers les use cases d'exécution existants`
- mapper toutes les commandes watch vers `ActiveWorkoutSessionUseCases` / `ActiveExerciseStepUseCases`
- appliquer contrôle `sessionId + expectedRevision`
- produire `WatchCommandAck`
- dépend de `#91-A` et `#91-B`
- #91-D `[DevBackend] Adapter Android Wear Data Layer + foreground service téléphone`
- implémenter l'adapter natif MessageClient/DataClient/CapabilityClient
- brancher le service premier plan, réception commandes, publication projections/heartbeats
- dépend de `#91-B` et `#91-C`
- #91-E `[DevFrontend] App Wear OS Flutter dédiée + navigation UX montre`
- créer `watch_app/`
- implémenter écrans `pas de séance`, `séance active`, `actions`, `repos`, `connexion perdue`
- dépend de `#91-A`
- #91-F `[DevFrontend] Client watch bridge + états pending/latence/reconnexion + haptiques`
- consommer `ack` et `WatchSessionProjection`
- gérer interpolation locale, timeouts UX, désactivation actions, resync complet
- déclencher haptiques montre
- dépend de `#91-D` et `#91-E`
- #91-G `[QA] Validation companion watch offline/local`
- vérifier ordre/idempotence, rejet de révision périmée, reconnexion, écran verrouillé/téléphone en arrière-plan, absence d'interruption audio
- dépend de `#91-F`
Ordre recommandé :
- `#91-A`
- `#91-B` et `#91-E` en parallèle
- `#91-C`
- `#91-D`
- `#91-F`
- `#91-G`

View File

@ -0,0 +1,22 @@
---
name: gametime-ux-watch-companion-round2
description: memory note gametime-ux-watch-companion-round2
metadata:
type: project
---
# GameTime — Cadrage UX watch round 2 (notification, icône, refonte UI, score, stats) — 2026-07-26
Cadrages UX consignés dans les carnets de #108, #112, #115, #118, #123 (parents #102-#106). Résumé pour référence rapide inter-agent — le détail exploitable est dans chaque carnet ticket, pas dupliqué ici.
## Décisions structurantes transverses
- **La montre reste un satellite d'affichage/commande, jamais une source de vérité** ni une source d'initiative (pas de démarrage de séance, pas de pause/next depuis la montre dans ce MVP — seul le score +/- est une commande montre→téléphone). Cf. [[gametime-watch-companion-implementation]].
- **Notification Android (#108)** : résumé d'accès rapide façon Heavy, priorité d'affichage chrono > reps/score > étape, aucune action bouton en MVP, tap = ouvrir l'app. Retirée immédiatement en fin de séance.
- **Icône montre (#112)** : réutilisation stricte du logo "GT" Court Blazer existant, simple export au gabarit rond Wear OS, aucun redesign.
- **Refonte UI montre (#115)** : DA Court Blazer en thème sombre uniquement (pas de thème clair sur montre), une donnée dominante par écran, 5 variantes spécifiées (no-session, séance active, repos, actions/confirmation, connexion perdue — dernière valeur connue assombrie, jamais d'écran vide).
- **Score +/- montre (#118)** : feedback optimiste immédiat + état visuel "en attente" jusqu'à confirmation téléphone, recalage silencieux en cas d'échec, jamais de mention "hors ligne"/"mode local".
- **Stats montre (#123)** : **cadrage seulement, implémentation non ouverte** — dépend du retour Architect #124 (faisabilité Health Services, permissions, persistance). Périmètre MVP proposé : fréquence cardiaque moyenne/max de séance uniquement, affichée dans résumé fin de séance + historique, silence total (aucun placeholder) si donnée absente.
## Cohérence de vocabulaire à respecter par les devs
- "Série X/Y" en Anton or = pattern déjà établi ([[gametime-ux-series-counter]]), réutilisé identique sur montre et notification.
- Score chrono vs score libre = mode existant ([[gametime-ux-score-chrono]]), pas de nouveau concept introduit par ces tickets.
- Philosophie silence/non-blocage réseau = [[gametime-online-layer-philosophy]], appliquée à la latence de sync montre et à l'absence de stats capteur.

View File

@ -0,0 +1,530 @@
---
name: gametime-ux-watch-companion
description: memory note gametime-ux-watch-companion
metadata:
type: project
---
# GameTime — UX interface montre synchronisée (ticket #91, UX 2026-07-25)
Conception UX d'une app compagnon Wear OS synchronisée avec l'app téléphone pour piloter une séance en cours depuis le poignet.
Références à respecter :
- `gametime-session-execution-timer-refactor`
- `gametime-ux-step-chaining-override`
- `gametime-architecture-step-chaining-override`
- `gametime-ux-score-chrono`
- `gametime-online-layer-philosophy`
## Décision produit structurante
La montre est une **surface compagnon de pilotage**, pas une deuxième app autonome de séance.
- **Téléphone = source de vérité de l'exécution**.
- **Montre = télécommande + miroir d'état**.
- Toute action lancée sur la montre est une **intention utilisateur** envoyée au téléphone, puis confirmée par le retour d'état.
- La montre ne doit jamais exposer un modèle d'exécution différent de celui du téléphone.
Justification :
- la logique de séance, des chronos, des overrides d'étapes et du repos existe déjà côté téléphone ;
- cela évite les divergences de calcul entre deux appareils ;
- cela rend la cohérence UX compréhensible : un seul état réel, visible sur deux surfaces.
## Principe de surface
Sur montre, la hiérarchie doit être radicale :
1. **où j'en suis** : série, exercice, étape/passage si nécessaire ;
2. **ce qui se passe maintenant** : chrono dominant ou état dominant ;
3. **l'action unique la plus probable** ;
4. **les actions de contournement** dans une surface secondaire.
La montre ne doit pas tenter de reproduire toute l'interface téléphone. Pas de médias, pas d'édition, pas de paramètres, pas de détails historiques.
## Surfaces montre
## 1. État sans séance active
Écran affiché si aucune séance n'est en cours côté téléphone.
Contenu :
```text
Aucune séance en cours
Lance une séance sur le téléphone.
```
CTA optionnel si le système le permet :
```text
[Actualiser]
```
Règles :
- aucun contrôle d'exécution affiché ;
- pas de faux bouton `Démarrer` ;
- si la montre n'est pas connectée au téléphone, le message devient :
```text
Téléphone indisponible
Rouvre GameTime sur le téléphone.
```
## 2. Écran principal `Séance active`
Écran par défaut dès qu'une séance en cours existe. C'est la surface centrale de la feature.
Structure recommandée sur écran rond :
```text
SÉRIE 2 / 5
Pompes tempo
Passage 1 / 3 · Étape 2 / 4
00:18
Chrono étape
Série 01:42 · Score 00:51
[Pause]
```
### Hiérarchie d'information
L'ordre visuel doit être fixe :
1. `SÉRIE X / Y` toujours en haut.
2. Nom de l'exercice sur 1 à 2 lignes max.
3. Ligne contextuelle compacte :
- `Passage A / B` seulement si l'exercice en a ;
- `Étape C / D` seulement si l'exercice en a ;
- si les deux existent, les concaténer sur une seule ligne.
4. **Chrono dominant** en très grand.
5. Libellé du chrono dominant ou de l'état.
6. Ligne compacte des autres chronos simultanés, si utile.
7. Bouton principal pleine largeur.
### Règle du chrono dominant
La montre ne doit afficher qu'un seul chrono en grand. Ordre de priorité UX :
1. `Repos` si un repos est en cours.
2. `Étape` si une étape temps est active ou en état `Chrono suivant prêt`.
3. `Score chrono` s'il est actif et qu'aucune étape temps n'est prioritaire.
4. `Temps de série` si actif.
5. Sinon, l'état dominant remplace le chrono.
Les autres chronos actifs restent en secondaire sur une seule ligne compacte, par exemple :
```text
Série 03:12 · Score 01:08
```
But : rester lisible pendant l'effort tout en respectant le modèle multi-chronos existant.
### Bouton principal contextuel
Le bas de l'écran porte **une seule action primaire** dont le libellé dépend de l'état :
- `Démarrer l'exercice`
- `Pause`
- `Reprendre`
- `Démarrer le chrono`
- `Passer le repos`
Cette action unique est le raccourci du cas d'usage le plus probable à cet instant.
## 3. Écran `Actions`
Surface secondaire accessible depuis `Séance active` par swipe horizontal ou tap sur un affordance discret `Actions`.
Cette surface contient les actions moins fréquentes, sous forme de gros boutons verticaux scrollables.
Ordre des actions :
```text
[Passer l'étape] // seulement si applicable
[Passer le passage] // seulement si applicable
[Terminer la série]
[Passer la série]
[Passer le repos] // seulement pendant le repos
```
Règles :
- n'afficher que les actions réellement applicables à l'état courant ;
- ne pas afficher d'action impossible ou déjà satisfaite ;
- pendant un chrono en cours, `Passer la série` doit demander une confirmation ;
- pendant un repos, `Terminer la série` disparaît car la série est déjà terminée.
### Confirmations montre
Deux confirmations seulement, pour éviter les erreurs grossières pendant l'effort :
1. `Passer la série ?`
`Le chrono en cours sera ignoré.`
`[Annuler] [Passer]`
2. `Passer le passage ?`
`L'étape en cours sera ignorée.`
`[Annuler] [Passer]`
`Passer l'étape` peut être immédiat : coût faible, fréquence plus élevée.
## 4. Écran `Repos`
Quand le repos est en cours, l'écran principal change de nature et devient un écran repos, pas un simple bandeau.
Structure :
```text
REPOS
Après série 2 / 5
00:37
Repos en cours
Exercice suivant
Fentes sautées
[Pause]
```
Actions secondaires sur l'écran `Actions` :
```text
[Passer le repos]
```
Si le repos est en pause :
```text
REPOS
00:37
Repos en pause
[Reprendre]
```
## États à couvrir
## 1. Séance pas encore lancée / début de série
Cas : la série existe, mais aucun chrono qui démarre au début de série n'a encore été lancé.
Affichage :
```text
SÉRIE 1 / 4
Burpees
Étape 1 / 3
00:20
Prêt à démarrer
[Démarrer l'exercice]
```
Règle obligatoire :
- si l'exercice est sous chrono **et** que la première étape est une étape `Temps`, le bouton reste **unique** :
```text
[Démarrer l'exercice]
```
Cette action démarre à la fois :
- le timer de série si `timeEnabled` ;
- le chrono de la première étape ;
- le score chrono si `scoreInputMode == stopwatch`.
La montre ne doit jamais afficher deux boutons concurrents du type `Démarrer l'exercice` et `Démarrer l'étape`.
## 2. Chrono running
Cas nominal pendant l'effort.
Affichage :
- chrono dominant animé visuellement ;
- libellé `En cours` ou libellé du chrono (`Chrono étape`, `Score chrono`, `Temps de série`) ;
- bouton principal `Pause`.
Effet du bouton :
- met la séance en pause ;
- la pause suspend tous les chronos running, y compris le repos.
## 3. Séance en pause
Affichage :
```text
SÉRIE 2 / 5
Pompes tempo
00:18
Séance en pause
[Reprendre]
```
Règles :
- l'état doit être explicite ;
- aucun chrono ne doit sembler continuer ;
- si l'utilisateur reprend depuis le téléphone, la montre revient automatiquement à l'état running.
## 4. `Chrono suivant prêt`
Cas imposé par `autoStartNextTimedStep = false` après une étape temps suivie d'une autre étape temps.
Affichage :
```text
SÉRIE 2 / 5
Pompes tempo
Passage 1 / 3 · Étape 3 / 4
00:15
Chrono suivant prêt
[Démarrer le chrono]
```
Règles :
- ne pas réutiliser `Démarrer l'exercice` ;
- ne pas auto-démarrer ;
- conserver l'état après pause/reprise ;
- l'écran doit rendre évident qu'on est déjà avancé dans la séquence, mais en attente d'un lancement manuel.
## 5. Entre les séries
Deux cas distincts :
1. repos configuré et lancé ;
2. pas de repos en cours, prochaine série prête.
Si pas de repos :
```text
SÉRIE 3 / 5
Pompes tempo
Prêt pour la série suivante
[Démarrer l'exercice]
```
L'utilisateur comprend qu'il redémarre une nouvelle série, pas la séance depuis zéro.
## 6. Repos running / repos en pause
Voir surface `Repos`.
Le repos doit être traité comme un état de premier rang, car c'est souvent le seul moment où l'utilisateur regarde la montre entre deux séries.
## 7. Pas de séance active
Voir surface `État sans séance active`.
## 8. Téléphone temporairement indisponible
Cas spécifique à la cohabitation téléphone/montre.
La montre peut afficher le dernier état connu, mais il doit être clairement marqué comme potentiellement obsolète :
```text
Connexion perdue
Dernier état reçu il y a quelques secondes
[Réessayer]
```
Règles :
- désactiver les actions de pilotage tant que la connexion n'est pas rétablie ;
- ne pas laisser croire qu'un tap local a réellement modifié la séance sans confirmation ;
- au retour de connexion, la montre remplace entièrement son affichage par l'état confirmé du téléphone.
## Mapping des contrôles
## Gestes retenus
- **Tap** sur le bouton principal : action primaire contextuelle.
- **Swipe horizontal** : bascule entre `Séance active` et `Actions`.
- **Scroll vertical / couronne** : parcours des actions si la liste dépasse.
- **Bouton système / retour système** : navigation système uniquement.
Décision UX : ne pas attribuer de commande métier obligatoire à un bouton physique matériel. Les montres Wear OS n'offrent pas toutes la même ergonomie matérielle.
## Mapping détaillé
### Sur `Séance active`
- `Démarrer l'exercice`
- déclenche tous les chronos de début de série applicables ;
- couvre explicitement le cas exercice chrono + première étape chrono.
- `Pause`
- met la séance en pause ;
- suspend série, étape, score chrono, repos.
- `Reprendre`
- reprend exactement l'état suspendu.
- `Démarrer le chrono`
- démarre l'étape temps prête après un enchaînement manuel.
- `Passer le repos`
- raccourci contextuel possible si UX test montre que c'est plus utile que `Pause` pendant le repos ;
- sinon garder `Pause` en primaire et déplacer `Passer le repos` dans `Actions`.
Décision recommandée pour v1 :
- pendant le repos, **bouton principal = `Pause`**, pour garder la cohérence avec le reste de la séance ;
- `Passer le repos` reste en action secondaire.
### Sur `Actions`
- `Passer l'étape`
- disponible seulement si une étape courante existe.
- `Passer le passage`
- disponible seulement si l'exercice a plusieurs passages/séquences et si le passage courant n'est pas déjà terminé.
- `Terminer la série`
- finalise la série et arrête/enregistre les chronos actifs selon les règles existantes.
- `Passer la série`
- ignore la série courante avec confirmation si un chrono ou une séquence est en cours.
- `Passer le repos`
- disponible seulement si repos en cours.
## Cohérence téléphone ↔ montre
## Source de vérité
Décision UX ferme :
- l'état autoritaire de séance vit sur le téléphone ;
- la montre affiche une **projection compacte** de cet état ;
- la montre n'invente jamais un état final seule.
## Modèle d'interaction
Cycle UX attendu pour une action montre :
1. l'utilisateur tape sur la montre ;
2. la montre passe brièvement le bouton en état `Envoi...` ;
3. le téléphone applique la commande ;
4. la montre reçoit l'état confirmé et remplace l'affichage.
Si l'état confirmé diffère de l'intention initiale parce que le téléphone a déjà changé entre-temps, c'est **le dernier état confirmé** qui gagne visuellement.
Exemple :
- l'utilisateur tape `Pause` sur la montre ;
- presque au même moment, il a déjà repris sur le téléphone ;
- la montre ne doit pas essayer de "corriger" localement ; elle se recale sur le dernier snapshot confirmé.
## Règle de conflit UX
En cas d'actions quasi simultanées téléphone/montre :
- le système applique un ordre réel côté source de vérité ;
- la montre n'affiche jamais un dialogue de conflit ;
- elle se contente d'afficher le résultat réel le plus récent.
Autrement dit : **pas de résolution de conflit visible par l'utilisateur**, seulement un réalignement rapide de l'UI.
## Règle de latence
La montre doit distinguer trois cas UX :
1. **latence courte**
- simple état `Envoi...` sur le bouton.
2. **latence perceptible**
- texte discret `En attente du téléphone`.
3. **absence de réponse**
- état `Connexion perdue` et actions désactivées.
Les seuils temporels exacts sont laissés à Architect, mais la surface doit prévoir ces trois états distincts.
## Signaux sonores et haptiques
## Principe
Pour respecter #92, la montre doit privilégier **l'haptique**. L'audio montre n'est pas requis pour le MVP.
Décisions UX :
- par défaut, **pas de son obligatoire côté montre** ;
- retour principal = vibration ;
- si un son est ajouté plus tard, il doit être bref, non intrusif, et ne jamais prendre le focus audio au détriment de la musique de fond.
## Patterns recommandés
- démarrage/pause/reprise confirmé : **impulsion courte** ;
- fin d'un chrono ou fin de repos : **double impulsion** ;
- état `Chrono suivant prêt` : **double impulsion** au moment où l'état est atteint, puis silence ;
- perte de connexion après action utilisateur : **impulsion lourde unique** optionnelle.
Règles :
- pas de bip de décompte 3-2-1 imposé sur la montre au MVP ;
- pas de répétition haptique continue ;
- éviter la duplication agressive téléphone + montre sur le même événement.
## Exigences de donnée pour Architect
La montre a besoin d'un view model compact, orienté exécution, pas de tout le snapshot séance brut.
Minimum UX requis :
- présence ou absence d'une séance active ;
- statut de connexion téléphone↔montre ;
- `seriesIndex`, `seriesTotal` ;
- `exerciseName` ;
- contexte séquence :
- `sequenceIndex?`, `sequenceTotal?`
- `stepIndex?`, `stepTotal?`
- `stepName?`
- type et valeur du **chrono dominant** ;
- liste compacte des autres chronos actifs visibles ;
- état global :
- `ready`
- `running`
- `paused`
- `nextTimerReady`
- `restRunning`
- `restPaused`
- `noActiveSession`
- `phoneUnavailable`
- libellé de l'action primaire autorisée ;
- liste des actions secondaires autorisées ;
- indicateur `commandPending`.
## Hypothèses techniques laissées à Architect
Points explicitement non tranchés par UX, à valider techniquement :
1. faisabilité et coût d'une app Wear OS Flutter dédiée ou module compagnon séparé ;
2. capacité à exposer côté téléphone un flux d'état d'exécution suffisamment compact et fréquent pour la montre ;
3. stratégie de mise à jour du chrono affiché sur la montre :
- push fréquent depuis le téléphone ;
- ou interpolation locale à partir d'horodatages autoritaires ;
4. transport exact téléphone↔montre et garanties d'acknowledgement pour les commandes ;
5. comportement si le téléphone est verrouillé, en arrière-plan, ou si le process principal est suspendu ;
6. possibilité de déclencher des vibrations montre sans prise de focus audio ;
7. capacité à rendre idempotentes les commandes utilisateur (`pause`, `resume`, `skipStep`, `finishSet`, etc.) ;
8. découpage des use cases côté téléphone pour exposer uniquement les commandes compatibles montre ;
9. gestion exacte des timeouts et des seuils de latence visibles dans l'UI ;
10. politique de duplication ou non des signaux entre téléphone et montre sur un même événement.
## Synthèse décisionnelle pour la suite
La montre doit rester une surface extrêmement simple :
- un écran principal `Séance active` ;
- un écran secondaire `Actions` ;
- un écran dédié `Repos` quand le repos est l'état dominant ;
- des états explicites pour pause, `Chrono suivant prêt`, absence de séance et perte de connexion ;
- une seule action primaire contextuelle à la fois ;
- téléphone autoritaire, montre suiveuse interactive.
Cette forme couvre l'objectif utilisateur réel : piloter entièrement la séance sans sortir le téléphone, tout en conservant la logique d'exécution déjà stabilisée dans GameTime.

View File

@ -0,0 +1,39 @@
---
name: gametime-watch-companion-implementation
description: memory note gametime-watch-companion-implementation
metadata:
type: project
---
# GameTime — Implémentation companion watch (#91) et contraintes sandbox
## État (2026-07-25)
Feature #91 « Interface montre synchronisée » entièrement codée, validée verte en sandbox, committée sur `feature/ticket91-wear-os-watch-sync`.
Sous-tickets : #91-A contrats (`packages/watch_bridge_contract/`), #91-B projection téléphone, #91-C routing commandes, #91-D adapter Android Wear Data Layer + foreground service, #91-E app Wear OS (`watch_app/`), #91-F client montre + latence/haptiques.
Commits : cf68a72 (#91-A), d1c6076 (#91-B), 6c177de (#91-C), 68a87d1 (#91-D), c65a5a7 (#91-E/#91-F). + 40a2d5e `feat(android): local server networking (INTERNET + cleartext)` isolé du watch (pré-existant au serveur, séparé à la demande utilisateur).
## Validation sandbox (verte, par Main)
- Contrat A : 8 `dart test`.
- Projection B (12), command handler C (11), adapter D (6) : `flutter test` all passed. Cas clés : idempotence (retry/doublon n'avance pas deux fois), rejets stale/non-applicable/missing/mismatch, resync reconnexion, heartbeat, ordre séquentiel.
- `flutter analyze` app téléphone : 0 problème watch (24 `info` pré-existants hors #91). `flutter analyze` watch_app : No issues found.
- Revue code Main : invariant source-de-vérité respecté (la montre n'applique JAMAIS une commande localement, recalage sur projection confirmée) ; haptiques sans focus audio (#92) ; démarrage commun exercice+étape via use cases existants (pas de logique montre).
## NON validé en sandbox → on-device utilisateur
- Build gradle app téléphone (`flutter build apk`) avec D (plugin Kotlin, `play-services-wearable`, services watch).
- Build watch_app (`cd watch_app && flutter build apk`).
- Pairing Wearable téléphone↔montre ; commande/projection réelles ; foreground service (verrouillé/background) ; latence/interpolation/resync réelles.
## Contraintes sandbox IdeA (durable, important pour les futures sessions)
Les **agents** (Git, DevBackend, DevFrontend, QA) ne peuvent **pas** écrire `.git` (read-only), ni lancer `flutter` (cache engine read-only), ni accéder au réseau (build hook sqlite3 → SocketException). **Main (orchestrateur) le peut** : `.git` inscriptible, Flutter 3.44.6 OK, réseau OK.
Conséquences pratiques :
- Validation exécutable (`flutter test`/`analyze`) = **Main**, pas les agents.
- Commits/branches = **Main** (agents bloqués).
- Build natif gradle + on-device = **utilisateur** (hors sandbox).
- Messagerie inter-agent instable (« no final text » / « Tool execution aborted ») : le travail atterrit souvent dans le working tree même si la réponse est perdue → vérifier le working tree directement après chaque délégation.
- Identité git repo-local : `Blomios <blomios@gmail.com>` (réutilisée de l'historique).
## Décisions de cadrage #85 / #86 (report Main)
- #85 (packs partageables) : reporté — gâté sur retour d'usage du partage ciblé (encore en QA).
- #86 (analytics basket avancées) : reporté, périmètre v1 gelé (réussite par exercice de tir + charge hebdo estimée), à reprendre après #91 si usage justifie.

View File

@ -0,0 +1,10 @@
---
name: gametime-watch-lot-148-154-cadrage
description: >
metadata:
type: project
---
- No-session = état informatif, jamais de lancement.
- `startLastWorkoutTemplate` doit être retiré du flux actif montre.
- Le score détape indépendant doit rester séparé du score de série.
- `stepName` suffit comme donnée, la forme est maintenant figée par UX.