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:
@ -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.
|
||||
@ -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.
|
||||
452
.ideai/memory/gametime-architecture-watch-companion.md
Normal file
452
.ideai/memory/gametime-architecture-watch-companion.md
Normal 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`
|
||||
22
.ideai/memory/gametime-ux-watch-companion-round2.md
Normal file
22
.ideai/memory/gametime-ux-watch-companion-round2.md
Normal 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.
|
||||
530
.ideai/memory/gametime-ux-watch-companion.md
Normal file
530
.ideai/memory/gametime-ux-watch-companion.md
Normal 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.
|
||||
39
.ideai/memory/gametime-watch-companion-implementation.md
Normal file
39
.ideai/memory/gametime-watch-companion-implementation.md
Normal 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.
|
||||
10
.ideai/memory/gametime-watch-lot-148-154-cadrage.md
Normal file
10
.ideai/memory/gametime-watch-lot-148-154-cadrage.md
Normal 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.
|
||||
Reference in New Issue
Block a user