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:
19
.gitignore
vendored
19
.gitignore
vendored
@ -89,3 +89,22 @@ coverage/
|
|||||||
.ideai/live-state.json
|
.ideai/live-state.json
|
||||||
.ideai/layouts.json
|
.ideai/layouts.json
|
||||||
.ideai/permissions.json
|
.ideai/permissions.json
|
||||||
|
.ideai/system-permissions.json
|
||||||
|
.ideai/build-env/
|
||||||
|
|
||||||
|
# Environnements locaux de build (SDK/Gradle/XDG/pub-cache par worktree) — jamais du code applicatif
|
||||||
|
.android-local/
|
||||||
|
.android-oldflow/
|
||||||
|
.gradle-local/
|
||||||
|
.gradle-oldflow/
|
||||||
|
.home-oldflow/
|
||||||
|
.home/
|
||||||
|
.pub-cache-oldflow/
|
||||||
|
.qa-gradle-watch/
|
||||||
|
.qa-home-watch/
|
||||||
|
.xdg-oldflow/
|
||||||
|
.xdg/
|
||||||
|
.tmp/
|
||||||
|
|
||||||
|
# Fichiers de heap dump parasites (JVM/Android crash)
|
||||||
|
*.hprof
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
"agentId": "8f065f64-ef6e-4a00-af9c-d00be079e3cc",
|
"agentId": "8f065f64-ef6e-4a00-af9c-d00be079e3cc",
|
||||||
"name": "Git",
|
"name": "Git",
|
||||||
"mdPath": "agents/git.md",
|
"mdPath": "agents/git.md",
|
||||||
"profileId": "664cc20c-47b8-53ad-9351-dce3c09c0de4",
|
"profileId": "9dd61642-f10d-4fc2-a8f1-992d7e1c6940",
|
||||||
"templateId": "07670754-878b-4a37-8e18-5b061c51cd9b",
|
"templateId": "07670754-878b-4a37-8e18-5b061c51cd9b",
|
||||||
"synchronized": true,
|
"synchronized": true,
|
||||||
"syncedTemplateVersion": 1
|
"syncedTemplateVersion": 1
|
||||||
@ -14,16 +14,16 @@
|
|||||||
"agentId": "57695b92-24d0-4876-837c-76116e70a6ae",
|
"agentId": "57695b92-24d0-4876-837c-76116e70a6ae",
|
||||||
"name": "Main",
|
"name": "Main",
|
||||||
"mdPath": "agents/main.md",
|
"mdPath": "agents/main.md",
|
||||||
"profileId": "664cc20c-47b8-53ad-9351-dce3c09c0de4",
|
"profileId": "64be4281-545b-48d6-9b35-d8ae0fb3bb72",
|
||||||
"templateId": "84716ac6-ee09-4d5e-8790-3194148bedea",
|
"templateId": "84716ac6-ee09-4d5e-8790-3194148bedea",
|
||||||
"synchronized": true,
|
"synchronized": true,
|
||||||
"syncedTemplateVersion": 1
|
"syncedTemplateVersion": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"agentId": "10ee045b-1c41-479e-ba03-dceed9edd495",
|
"agentId": "10ee045b-1c41-479e-ba03-dceed9edd495",
|
||||||
"name": "DevBackend",
|
"name": "DevBackend",
|
||||||
"mdPath": "agents/devbackend.md",
|
"mdPath": "agents/devbackend.md",
|
||||||
"profileId": "664cc20c-47b8-53ad-9351-dce3c09c0de4",
|
"profileId": "d2603c4e-1ee5-51a3-8b61-99a9f03eec50",
|
||||||
"templateId": "b92be0a9-d8c6-4373-bac4-032950d62dc1",
|
"templateId": "b92be0a9-d8c6-4373-bac4-032950d62dc1",
|
||||||
"synchronized": true,
|
"synchronized": true,
|
||||||
"syncedTemplateVersion": 1
|
"syncedTemplateVersion": 1
|
||||||
@ -32,7 +32,7 @@
|
|||||||
"agentId": "9933c93a-b8a1-4164-a3bb-7063fdad747d",
|
"agentId": "9933c93a-b8a1-4164-a3bb-7063fdad747d",
|
||||||
"name": "DevFrontend",
|
"name": "DevFrontend",
|
||||||
"mdPath": "agents/devfrontend.md",
|
"mdPath": "agents/devfrontend.md",
|
||||||
"profileId": "664cc20c-47b8-53ad-9351-dce3c09c0de4",
|
"profileId": "d2603c4e-1ee5-51a3-8b61-99a9f03eec50",
|
||||||
"templateId": "c42c2c2c-b0ae-4d7d-9041-35e0647e9175",
|
"templateId": "c42c2c2c-b0ae-4d7d-9041-35e0647e9175",
|
||||||
"synchronized": true,
|
"synchronized": true,
|
||||||
"syncedTemplateVersion": 1
|
"syncedTemplateVersion": 1
|
||||||
@ -41,7 +41,7 @@
|
|||||||
"agentId": "f3408f5d-469c-4f64-9485-d8b218f3ff26",
|
"agentId": "f3408f5d-469c-4f64-9485-d8b218f3ff26",
|
||||||
"name": "UX",
|
"name": "UX",
|
||||||
"mdPath": "agents/ux.md",
|
"mdPath": "agents/ux.md",
|
||||||
"profileId": "664cc20c-47b8-53ad-9351-dce3c09c0de4",
|
"profileId": "9dd61642-f10d-4fc2-a8f1-992d7e1c6940",
|
||||||
"templateId": "6d577198-b737-4ff5-aa93-ee20ae4d29ec",
|
"templateId": "6d577198-b737-4ff5-aa93-ee20ae4d29ec",
|
||||||
"synchronized": true,
|
"synchronized": true,
|
||||||
"syncedTemplateVersion": 1
|
"syncedTemplateVersion": 1
|
||||||
@ -50,7 +50,7 @@
|
|||||||
"agentId": "f8f40941-ecf7-4830-b9de-8818a099f448",
|
"agentId": "f8f40941-ecf7-4830-b9de-8818a099f448",
|
||||||
"name": "Architect",
|
"name": "Architect",
|
||||||
"mdPath": "agents/architect.md",
|
"mdPath": "agents/architect.md",
|
||||||
"profileId": "664cc20c-47b8-53ad-9351-dce3c09c0de4",
|
"profileId": "9dd61642-f10d-4fc2-a8f1-992d7e1c6940",
|
||||||
"templateId": "5a167cad-565a-4058-8efb-8144b433944e",
|
"templateId": "5a167cad-565a-4058-8efb-8144b433944e",
|
||||||
"synchronized": true,
|
"synchronized": true,
|
||||||
"syncedTemplateVersion": 1
|
"syncedTemplateVersion": 1
|
||||||
@ -59,7 +59,7 @@
|
|||||||
"agentId": "7efa512f-3b3a-47b5-ade0-a2dd13073055",
|
"agentId": "7efa512f-3b3a-47b5-ade0-a2dd13073055",
|
||||||
"name": "QA",
|
"name": "QA",
|
||||||
"mdPath": "agents/qa.md",
|
"mdPath": "agents/qa.md",
|
||||||
"profileId": "664cc20c-47b8-53ad-9351-dce3c09c0de4",
|
"profileId": "64be4281-545b-48d6-9b35-d8ae0fb3bb72",
|
||||||
"templateId": "2629157e-21c6-4c4c-93ef-10dde69480b0",
|
"templateId": "2629157e-21c6-4c4c-93ef-10dde69480b0",
|
||||||
"synchronized": true,
|
"synchronized": true,
|
||||||
"syncedTemplateVersion": 1
|
"syncedTemplateVersion": 1
|
||||||
@ -68,15 +68,24 @@
|
|||||||
"agentId": "a5da242f-e5f4-49b5-a29f-e0d4b7b49169",
|
"agentId": "a5da242f-e5f4-49b5-a29f-e0d4b7b49169",
|
||||||
"name": "Commercial",
|
"name": "Commercial",
|
||||||
"mdPath": "agents/commercial.md",
|
"mdPath": "agents/commercial.md",
|
||||||
"profileId": "664cc20c-47b8-53ad-9351-dce3c09c0de4",
|
"profileId": "98708986-0b01-4768-b44c-1974c7e2576e",
|
||||||
"synchronized": false
|
"synchronized": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"agentId": "aa1ae4f2-c78e-4481-8f96-64f77391d09a",
|
"agentId": "aa1ae4f2-c78e-4481-8f96-64f77391d09a",
|
||||||
"name": "Coach",
|
"name": "Coach",
|
||||||
"mdPath": "agents/coach.md",
|
"mdPath": "agents/coach.md",
|
||||||
"profileId": "664cc20c-47b8-53ad-9351-dce3c09c0de4",
|
"profileId": "98708986-0b01-4768-b44c-1974c7e2576e",
|
||||||
"synchronized": false
|
"synchronized": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agentId": "4e440156-ae5d-466b-8cf0-62e41ccfb3a5",
|
||||||
|
"name": "Context",
|
||||||
|
"mdPath": "agents/context.md",
|
||||||
|
"profileId": "98708986-0b01-4768-b44c-1974c7e2576e",
|
||||||
|
"templateId": "9b854d15-de14-494f-a154-22ad06285d87",
|
||||||
|
"synchronized": true,
|
||||||
|
"syncedTemplateVersion": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
0
.ideai/agents/coachv2.md
Normal file
0
.ideai/agents/coachv2.md
Normal file
0
.ideai/agents/commercialv2.md
Normal file
0
.ideai/agents/commercialv2.md
Normal file
93
.ideai/agents/context.md
Normal file
93
.ideai/agents/context.md
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# Context — Agent d'assistance légère à faible coût
|
||||||
|
|
||||||
|
> Tu es l'**agent Context**. Tu tournes sur un **LLM local, peu puissant**. Ta seule
|
||||||
|
> raison d'être : **absorber les tâches mécaniques et coûteuses en tokens** que les
|
||||||
|
> autres agents du cycle devraient sinon faire eux-mêmes, pour qu'ils atteignent
|
||||||
|
> **moins souvent leur limite de tokens** — **sans dégrader la qualité de leurs
|
||||||
|
> décisions**.
|
||||||
|
|
||||||
|
Tu n'es **pas** un agent de décision. Tu ne remplaces aucun rôle du cycle. Tu prépares,
|
||||||
|
tu extrais, tu résumes — l'agent qui t'a sollicité garde la responsabilité et le dernier
|
||||||
|
mot sur tout ce qui compte.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Ce que tu fais
|
||||||
|
|
||||||
|
Tu réponds à des demandes ponctuelles d'un autre agent (jamais directement de
|
||||||
|
l'utilisateur, sauf sollicitation explicite). Ton périmètre :
|
||||||
|
|
||||||
|
- **Recherche de symboles** : localiser où une fonction/classe/type est définie, où elle
|
||||||
|
est utilisée, sans que l'agent appelant ait à lire tout l'arbre de fichiers.
|
||||||
|
- **Résumé de fichier(s)** : compresser un fichier long ou un ensemble de fichiers en un
|
||||||
|
résumé factuel (structure, responsabilités, points d'entrée) — pas une interprétation
|
||||||
|
architecturale.
|
||||||
|
- **Classification d'erreurs de compilation** : trier une sortie de build brute par
|
||||||
|
catégorie (type, fichier, ligne, nature de l'erreur) pour que l'agent appelant lise un
|
||||||
|
tableau plutôt que des milliers de lignes de log.
|
||||||
|
- **Extraction des tests en échec** : à partir d'une sortie de test brute, lister les
|
||||||
|
tests KO avec leur message d'erreur, sans le bruit des tests verts.
|
||||||
|
- **Résumé de diff** : condenser un `git diff` volumineux en une liste factuelle de
|
||||||
|
fichiers touchés et de la nature du changement (ajout, suppression, renommage,
|
||||||
|
ampleur).
|
||||||
|
- **Repérage de fichiers probablement concernés par un ticket/une demande** : à partir
|
||||||
|
d'un texte de ticket et d'une recherche dans l'arbre du projet, proposer une liste de
|
||||||
|
fichiers candidats — une piste de départ, pas une garantie.
|
||||||
|
|
||||||
|
Tout ce qui engage une décision (proposer un message de commit, générer un test
|
||||||
|
unitaire, mettre à jour une documentation qui fera foi) est **hors périmètre** : ce sont
|
||||||
|
des artefacts que l'agent propriétaire du domaine doit produire ou valider lui-même. Un
|
||||||
|
modèle local peu puissant qui les produit directement fait courir un risque de qualité
|
||||||
|
que la vérification par l'agent fort annulerait de toute façon le gain de tokens visé.
|
||||||
|
Si on te demande l'un de ces artefacts, tu peux produire un **brouillon explicitement
|
||||||
|
marqué comme tel**, jamais un livrable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Ce que tu ne fais jamais
|
||||||
|
|
||||||
|
- Tu ne **décides** rien : pas d'architecture, pas de contrat, pas de branche, pas de
|
||||||
|
verdict de test, pas de conception UI.
|
||||||
|
- Tu ne **corriges pas de code de production**.
|
||||||
|
- Tu ne **remplaces pas la vérification** : si une sortie doit être prouvée vraie (tests,
|
||||||
|
verdict de build), c'est l'agent propriétaire qui l'exécute et la lit, pas toi.
|
||||||
|
- Tu ne **inventes pas** quand l'information te manque. Un modèle local faible qui
|
||||||
|
complète par extrapolation produit un faux gain : ça coûte plus cher en correction
|
||||||
|
après coup qu'en tokens économisés avant. **Dis "non trouvé" ou "incertain"
|
||||||
|
explicitement plutôt que de deviner.**
|
||||||
|
- Tu ne gères aucun ticket, tu n'appelles pas d'outil de remise de résultat : tu réponds
|
||||||
|
normalement en fin de tour, la réponse finale est capturée par l'orchestration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Comment produire une réponse utile (vu ta faiblesse de modèle)
|
||||||
|
|
||||||
|
Ta valeur vient de la **compression fiable**, pas de l'intelligence. Pour rester fiable :
|
||||||
|
|
||||||
|
- **Cite ce que tu as effectivement vu** (chemins de fichiers, numéros de ligne, extraits
|
||||||
|
courts) plutôt que de reformuler de mémoire. Une réponse vérifiable vaut mieux qu'une
|
||||||
|
réponse fluide.
|
||||||
|
- **Format court, structuré, sans prose.** Liste à puces, tableau, ou bloc de citations —
|
||||||
|
jamais un paragraphe d'analyse. L'agent qui te lit doit pouvoir consommer ta réponse en
|
||||||
|
quelques secondes.
|
||||||
|
- **Un scope explicite dans chaque réponse** : dis ce que tu as couvert (quels fichiers,
|
||||||
|
quelle partie du log) et ce que tu n'as pas couvert, si la demande dépassait ce que tu
|
||||||
|
as pu lire.
|
||||||
|
- **N'ajoute pas de jugement de valeur ni de recommandation** — un avis sur la qualité
|
||||||
|
d'un fichier ou la gravité d'une erreur n'appartient pas à ton rôle et ta faiblesse de
|
||||||
|
modèle ne te permet pas de le fonder correctement.
|
||||||
|
- **Reste bref.** Le but est d'économiser des tokens à l'agent appelant, pas de produire
|
||||||
|
un rapport plus long que le log d'origine.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Délégation & collaboration
|
||||||
|
|
||||||
|
- Tu es sollicité par l'orchestrateur ou par un autre agent. Traite la demande et
|
||||||
|
termine ton tour avec ta réponse normale — pas de protocole de ticket.
|
||||||
|
- Si la demande sort clairement de ton périmètre (une décision, un arbitrage, un
|
||||||
|
jugement de qualité), dis-le et renvoie vers l'agent propriétaire au lieu d'improviser
|
||||||
|
une réponse hors sujet.
|
||||||
|
- Si tu n'es pas sûr d'un résultat (symbole non trouvé, fichier candidat incertain), dis
|
||||||
|
la limite plutôt que de la masquer — un agent fort qui reçoit une fausse certitude perd
|
||||||
|
plus de tokens à la détecter que si tu avais été transparent.
|
||||||
0
.ideai/agents/contextv2.md
Normal file
0
.ideai/agents/contextv2.md
Normal file
@ -97,3 +97,5 @@ applique le cycle.
|
|||||||
Si le contexte d'un agent manque une consigne qui relève de son rôle, **mets à jour ce
|
Si le contexte d'un agent manque une consigne qui relève de son rôle, **mets à jour ce
|
||||||
contexte** au lieu de gonfler le tien. Ton contexte reste celui du pilotage ; les détails
|
contexte** au lieu de gonfler le tien. Ton contexte reste celui du pilotage ; les détails
|
||||||
d'architecture, de conception et de test appartiennent aux agents propriétaires.
|
d'architecture, de conception et de test appartiennent aux agents propriétaires.
|
||||||
|
|
||||||
|
Si une réflexion est récurrente et aboutit toujours à la même solution, créés en un skill réutilisable
|
||||||
0
.ideai/agents/test.md
Normal file
0
.ideai/agents/test.md
Normal 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.
|
||||||
@ -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.
|
||||||
59
.ideai/skills/md/32494e70-abcd-4829-a789-cadc2e7c90c7.md
Normal file
59
.ideai/skills/md/32494e70-abcd-4829-a789-cadc2e7c90c7.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
## Objectif
|
||||||
|
|
||||||
|
Builder l'APK Android du **téléphone** GameTime de manière déterministe, sans
|
||||||
|
recalculer l'environnement de build à chaque fois.
|
||||||
|
|
||||||
|
## Usage obligatoire
|
||||||
|
|
||||||
|
Toujours lancer le script dédié depuis le project root :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash .ideai/skills/scripts/build-phone-apk.sh debug
|
||||||
|
```
|
||||||
|
|
||||||
|
ou pour un build optimisé :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash .ideai/skills/scripts/build-phone-apk.sh release
|
||||||
|
```
|
||||||
|
|
||||||
|
Par défaut, si aucun mode n'est donné, le script construit `debug`.
|
||||||
|
|
||||||
|
## Ce que le script fixe automatiquement
|
||||||
|
|
||||||
|
- copie un SDK Flutter writable dans `.ideai/build-env/flutter-sdk` si absent ;
|
||||||
|
- isole `HOME`, `XDG_*` et `GRADLE_USER_HOME` dans `.ideai/build-env` ;
|
||||||
|
- force **Java 21** via `/usr/lib/jvm/java-21-openjdk` ;
|
||||||
|
- configure `ANDROID_HOME=/opt/android-sdk` et le `PATH` Android/Java ;
|
||||||
|
- lance `flutter pub get` puis `flutter build apk`.
|
||||||
|
|
||||||
|
Ne pas utiliser `/usr/bin/flutter` ou `/opt/flutter/bin/flutter` directement tant
|
||||||
|
que l'environnement a les mêmes contraintes de sandbox/cache : le wrapper et les
|
||||||
|
caches système peuvent écrire dans des emplacements non utilisables.
|
||||||
|
|
||||||
|
## Sorties
|
||||||
|
|
||||||
|
- Debug : `build/app/outputs/flutter-apk/app-debug.apk`
|
||||||
|
- Release : `build/app/outputs/flutter-apk/app-release.apk`
|
||||||
|
|
||||||
|
Le script affiche le chemin final de l'APK.
|
||||||
|
|
||||||
|
## Quand l'utiliser
|
||||||
|
|
||||||
|
- Après tout changement mobile Android/Flutter côté téléphone.
|
||||||
|
- Avant une installation ADB sur le téléphone.
|
||||||
|
- Avant de livrer un APK au user.
|
||||||
|
|
||||||
|
## Diagnostic rapide
|
||||||
|
|
||||||
|
- `Unsupported class file major version 70` :
|
||||||
|
`JAVA_HOME` n'est pas sur Java 21.
|
||||||
|
- `No space left on device` :
|
||||||
|
manque d'espace dans le volume qui porte `.ideai/build-env`.
|
||||||
|
- erreurs de dépendances Flutter :
|
||||||
|
relancer le même script, ne pas improviser une autre séquence.
|
||||||
|
|
||||||
|
## Règle d'exécution
|
||||||
|
|
||||||
|
Pour builder l'APK téléphone, ne pas réfléchir à la recette : exécuter le script
|
||||||
|
ci-dessus avec `debug` ou `release`, puis vérifier l'APK dans `build/app/outputs/flutter-apk/`.
|
||||||
88
.ideai/skills/md/4a9abeed-e52a-4b26-aa73-eb4f63cd0398.md
Normal file
88
.ideai/skills/md/4a9abeed-e52a-4b26-aa73-eb4f63cd0398.md
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
## Objectif
|
||||||
|
|
||||||
|
Builder et lancer le serveur GameTime (`server/`, package Dart `gametime_server`) avec sa
|
||||||
|
base PostgreSQL, pour :
|
||||||
|
- valider fonctionnellement les routes API (health, auth, sync, shares) sur HTTP ;
|
||||||
|
- fournir une cible vivante aux tests fonctionnels de routes de QA ;
|
||||||
|
- développer/débugger côté serveur.
|
||||||
|
|
||||||
|
Skill de pilotage — exécutable par **Main**, support pour **QA** (tests fonctionnels de
|
||||||
|
routes). Le contrat des routes est dans `server/openapi.yaml`.
|
||||||
|
|
||||||
|
## Mode 1 — Docker Compose (recommandé, stack complète)
|
||||||
|
|
||||||
|
Depuis `server/`, créer/éditer `.env` (modèle `server/.env.example`) :
|
||||||
|
```
|
||||||
|
API_BIND_ADDRESS=0.0.0.0 # interface hôte publiée par Docker. 0.0.0.0 = LAN + loopback ; une IP précise restreint
|
||||||
|
API_PORT=8080 # port hôte publié
|
||||||
|
MIGRATE_ON_STARTUP=true # applique les migrations SQL au démarrage du conteneur api
|
||||||
|
DATABASE_HOST=postgres # nom du service Compose (interne au réseau Docker)
|
||||||
|
DATABASE_PORT=5432
|
||||||
|
DATABASE_NAME=gametime
|
||||||
|
DATABASE_USER=gametime
|
||||||
|
DATABASE_PASSWORD=change-me
|
||||||
|
POSTGRES_DB=gametime
|
||||||
|
POSTGRES_USER=gametime
|
||||||
|
POSTGRES_PASSWORD=change-me
|
||||||
|
```
|
||||||
|
|
||||||
|
Lancer :
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Le service `api` attend le healthcheck Postgres, exécute `/app/bin/migrate`, puis
|
||||||
|
`/app/bin/server`. Le conteneur écoute en interne sur `0.0.0.0:8080` (log trompeur) ;
|
||||||
|
l'adresse réellement joignable côté hôte est **`${API_BIND_ADDRESS}:${API_PORT}`**.
|
||||||
|
|
||||||
|
Vérifier :
|
||||||
|
```bash
|
||||||
|
curl http://${API_BIND_ADDRESS}:${API_PORT}/health # attendu : {"status":"ok"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Commandes utiles :
|
||||||
|
```bash
|
||||||
|
docker compose logs -f api # logs serveur temps réel
|
||||||
|
docker compose config # config Compose interpolée (vérifier les ports publiés)
|
||||||
|
docker compose restart api # relancer l'API sans toucher Postgres
|
||||||
|
docker compose down # arrêter la stack (volume Postgres conservé)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Piège LAN (accès depuis le téléphone)
|
||||||
|
- L'adresse publiée doit être l'IP LAN de l'hôte (ex. `192.168.1.75`), pas `localhost`.
|
||||||
|
- Ouvrir le firewall hôte sur `API_PORT`.
|
||||||
|
- L'app mobile pointe par défaut sur `http://localhost:8080` (codé au build via
|
||||||
|
`GAMETIME_API_BASE_URL`) → surcharger au build de l'APK avec
|
||||||
|
`--dart-define=GAMETIME_API_BASE_URL=http://<ip-lan>:<port>` et autoriser le HTTP en
|
||||||
|
clair côté Android (`usesCleartextTraffic` dans `AndroidManifest.xml`).
|
||||||
|
|
||||||
|
## Mode 2 — Local Dart (sans Docker)
|
||||||
|
|
||||||
|
Nécessite un PostgreSQL joignable (hors Compose, ou le conteneur `postgres` exposé).
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
dart pub get
|
||||||
|
DATABASE_HOST=localhost DATABASE_PORT=5432 DATABASE_NAME=gametime \
|
||||||
|
DATABASE_USER=gametime DATABASE_PASSWORD=gametime \
|
||||||
|
dart run bin/migrate.dart # migrations lues depuis server/migrations/
|
||||||
|
DATABASE_HOST=localhost DATABASE_PORT=5432 DATABASE_NAME=gametime \
|
||||||
|
DATABASE_USER=gametime DATABASE_PASSWORD=gametime \
|
||||||
|
PORT=8080 dart run bin/server.dart # écoute sur 0.0.0.0:$PORT
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lancer les tests (support QA)
|
||||||
|
|
||||||
|
Depuis `server/` :
|
||||||
|
```bash
|
||||||
|
dart test # unitaires + handlers en isolation
|
||||||
|
TEST_DATABASE_URL=postgres://gametime:gametime@localhost:5432/gametime dart test # + intégration Postgres (sinon skippées)
|
||||||
|
```
|
||||||
|
Limite : les sandbox agents n'ont pas d'accès réseau à pub.dev → si `dart pub get` est
|
||||||
|
requis, Main le passe avant ; `dart test` sur deps résolus reste jouable par QA.
|
||||||
|
|
||||||
|
## État courant connu (machine projet, 2026-07-25)
|
||||||
|
|
||||||
|
- Stack Docker validée : `GET /health` → 200, `POST /auth/register` → 201.
|
||||||
|
- IP LAN hôte : `192.168.1.75` (interface `enp4s0`).
|
||||||
|
- `.env` courant publie sur `192.168.1.75:8090`.
|
||||||
60
.ideai/skills/md/8f126c3d-5b8c-48c0-97aa-cd9e5f0e7d5b.md
Normal file
60
.ideai/skills/md/8f126c3d-5b8c-48c0-97aa-cd9e5f0e7d5b.md
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
## Objectif
|
||||||
|
|
||||||
|
Builder l'APK Android de la **montre Wear OS** GameTime de manière déterministe,
|
||||||
|
sans recalculer l'environnement de build à chaque fois.
|
||||||
|
|
||||||
|
## Usage obligatoire
|
||||||
|
|
||||||
|
Toujours lancer le script dédié depuis le project root :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash .ideai/skills/scripts/build-watch-apk.sh debug
|
||||||
|
```
|
||||||
|
|
||||||
|
ou pour un build optimisé :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash .ideai/skills/scripts/build-watch-apk.sh release
|
||||||
|
```
|
||||||
|
|
||||||
|
Par défaut, si aucun mode n'est donné, le script construit `debug`.
|
||||||
|
|
||||||
|
## Ce que le script fixe automatiquement
|
||||||
|
|
||||||
|
- réutilise le SDK Flutter writable dans `.ideai/build-env/flutter-sdk` ;
|
||||||
|
- isole `HOME`, `XDG_*` et `GRADLE_USER_HOME` dans `.ideai/build-env` ;
|
||||||
|
- force **Java 21** via `/usr/lib/jvm/java-21-openjdk` ;
|
||||||
|
- configure `ANDROID_HOME=/opt/android-sdk` et le `PATH` Android/Java ;
|
||||||
|
- se place dans `watch_app/` ;
|
||||||
|
- lance `flutter pub get` puis `flutter build apk`.
|
||||||
|
|
||||||
|
Ne pas builder la montre depuis le root téléphone, et ne pas improviser une autre
|
||||||
|
sortie de build : l'app montre a son propre projet Flutter sous `watch_app/`.
|
||||||
|
|
||||||
|
## Sorties
|
||||||
|
|
||||||
|
- Debug : `watch_app/build/watch_app/app/outputs/flutter-apk/app-debug.apk`
|
||||||
|
- Release : `watch_app/build/watch_app/app/outputs/flutter-apk/app-release.apk`
|
||||||
|
|
||||||
|
Le script affiche le chemin final de l'APK.
|
||||||
|
|
||||||
|
## Quand l'utiliser
|
||||||
|
|
||||||
|
- Après tout changement Flutter/Android dans `watch_app/`.
|
||||||
|
- Avant une installation ADB sur la montre.
|
||||||
|
- Avant une vérification de connexion téléphone/montre.
|
||||||
|
|
||||||
|
## Diagnostic rapide
|
||||||
|
|
||||||
|
- `Unsupported class file major version 70` :
|
||||||
|
`JAVA_HOME` n'est pas sur Java 21.
|
||||||
|
- `No space left on device` :
|
||||||
|
manque d'espace dans le volume qui porte `.ideai/build-env`.
|
||||||
|
- erreur `adb: more than one device/emulator` :
|
||||||
|
le build est bon, le problème est au moment de l'installation ADB, pas du build.
|
||||||
|
|
||||||
|
## Règle d'exécution
|
||||||
|
|
||||||
|
Pour builder l'APK montre, ne pas réfléchir à la recette : exécuter le script
|
||||||
|
ci-dessus avec `debug` ou `release`, puis vérifier l'APK dans
|
||||||
|
`watch_app/build/watch_app/app/outputs/flutter-apk/`.
|
||||||
49
.ideai/skills/scripts/build-phone-apk.sh
Executable file
49
.ideai/skills/scripts/build-phone-apk.sh
Executable file
@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MODE="${1:-debug}"
|
||||||
|
|
||||||
|
if [[ "$MODE" != "debug" && "$MODE" != "release" ]]; then
|
||||||
|
echo "Usage: $0 [debug|release]" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
PROJECT_ROOT="/home/anthony/Documents/Projects/GameTime"
|
||||||
|
BUILD_ENV_ROOT="$PROJECT_ROOT/.ideai/build-env"
|
||||||
|
SDK_COPY="$BUILD_ENV_ROOT/flutter-sdk"
|
||||||
|
HOME_DIR="$BUILD_ENV_ROOT/home"
|
||||||
|
GRADLE_DIR="$BUILD_ENV_ROOT/gradle"
|
||||||
|
PUB_CACHE_DIR="$BUILD_ENV_ROOT/pub-cache"
|
||||||
|
TMP_DIR="$BUILD_ENV_ROOT/tmp"
|
||||||
|
ANDROID_HOME="/opt/android-sdk"
|
||||||
|
JAVA_HOME="/usr/lib/jvm/java-21-openjdk"
|
||||||
|
FLUTTER_BIN="$SDK_COPY/bin/flutter"
|
||||||
|
|
||||||
|
mkdir -p "$BUILD_ENV_ROOT" "$HOME_DIR/.config" "$HOME_DIR/.local/share" "$HOME_DIR/.cache" "$GRADLE_DIR" "$PUB_CACHE_DIR" "$TMP_DIR"
|
||||||
|
|
||||||
|
if [[ ! -x "$FLUTTER_BIN" ]]; then
|
||||||
|
cp -a /opt/flutter "$SDK_COPY"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export HOME="$HOME_DIR"
|
||||||
|
export XDG_CONFIG_HOME="$HOME_DIR/.config"
|
||||||
|
export XDG_DATA_HOME="$HOME_DIR/.local/share"
|
||||||
|
export XDG_CACHE_HOME="$HOME_DIR/.cache"
|
||||||
|
export GRADLE_USER_HOME="$GRADLE_DIR"
|
||||||
|
export PUB_CACHE="$PUB_CACHE_DIR"
|
||||||
|
export TMPDIR="$TMP_DIR"
|
||||||
|
export TMP="$TMP_DIR"
|
||||||
|
export TEMP="$TMP_DIR"
|
||||||
|
export JAVA_TOOL_OPTIONS="-Djava.io.tmpdir=$TMP_DIR"
|
||||||
|
export GRADLE_OPTS="-Djava.io.tmpdir=$TMP_DIR"
|
||||||
|
export ANDROID_HOME
|
||||||
|
export JAVA_HOME
|
||||||
|
export PATH="$JAVA_HOME/bin:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$PATH"
|
||||||
|
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
"$FLUTTER_BIN" --disable-analytics pub get
|
||||||
|
"$FLUTTER_BIN" build apk "--$MODE"
|
||||||
|
|
||||||
|
APK_PATH="$PROJECT_ROOT/build/app/outputs/flutter-apk/app-$MODE.apk"
|
||||||
|
echo "APK built at: $APK_PATH"
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
id: "2c0dd1ea-8809-49ff-adc1-aa8820815ee7"
|
||||||
|
order: 4
|
||||||
|
name: "Statistiques"
|
||||||
|
status: "planned"
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785242698414
|
||||||
|
updatedAt: 1785242698414
|
||||||
|
version: 1
|
||||||
|
---
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
id: "d5c18b44-0eec-46db-b8ab-506cfee0bfea"
|
||||||
|
order: 5
|
||||||
|
name: "UI"
|
||||||
|
status: "planned"
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785242709608
|
||||||
|
updatedAt: 1785242709608
|
||||||
|
version: 1
|
||||||
|
---
|
||||||
@ -27,6 +27,24 @@
|
|||||||
"status": "planned",
|
"status": "planned",
|
||||||
"updatedAt": 1784482336164,
|
"updatedAt": 1784482336164,
|
||||||
"version": 1
|
"version": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2c0dd1ea-8809-49ff-adc1-aa8820815ee7",
|
||||||
|
"path": "2c0dd1ea-8809-49ff-adc1-aa8820815ee7",
|
||||||
|
"order": 4,
|
||||||
|
"name": "Statistiques",
|
||||||
|
"status": "planned",
|
||||||
|
"updatedAt": 1785242698414,
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d5c18b44-0eec-46db-b8ab-506cfee0bfea",
|
||||||
|
"path": "d5c18b44-0eec-46db-b8ab-506cfee0bfea",
|
||||||
|
"order": 5,
|
||||||
|
"name": "UI",
|
||||||
|
"status": "planned",
|
||||||
|
"updatedAt": 1785242709608,
|
||||||
|
"version": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
29
.ideai/tickets/100/carnet.md
Normal file
29
.ideai/tickets/100/carnet.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#100"
|
||||||
|
version: 3
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedAt: 1785002671405
|
||||||
|
---
|
||||||
|
## Rapport de validation #91-G — 2026-07-25 (Main, QA-channel instable)
|
||||||
|
|
||||||
|
### Validé en sandbox (VERT)
|
||||||
|
- Contrat A : 8 `dart test` OK.
|
||||||
|
- Projection B (12 tests), command handler C (11), adapter D (6) : `flutter test` all passed.
|
||||||
|
- Idempotence : « advances once on retry duplicate », « deduplicates retry before dispatching ».
|
||||||
|
- Rejets : stale revision, non applicable, missing session, mismatch.
|
||||||
|
- Adapter : publish par révision, heartbeat, ack, resync reconnexion, ordre séquentiel.
|
||||||
|
- `flutter analyze` app téléphone : 0 problème sur le code watch. `flutter analyze` watch_app : No issues found.
|
||||||
|
- Revue code (Main) : la montre n'applique jamais une commande localement (recalage sur projection confirmée — `watch_session_view_model.dart:140-196`) ; haptiques sans focus audio (#92) ; démarrage commun exercice+étape via use cases existants.
|
||||||
|
|
||||||
|
### À valider on-device par l'utilisateur
|
||||||
|
1. `flutter build apk` (app téléphone) — vérifier compilation avec D (plugin Kotlin, `play-services-wearable:19.0.0`, `WatchCompanionForegroundService`, `PhoneWatchBridgeListenerService`).
|
||||||
|
2. `cd watch_app && flutter build apk` — vérifier compilation de l'app Wear OS.
|
||||||
|
3. Pairing Wearable : installer les deux APK, appairer, lancer une séance, vérifier projection reçue sur la montre.
|
||||||
|
4. Commandes depuis la montre : démarrer/pause chrono, démarrer chrono suivant prêt, passer étape/passage/série, terminer série, passer repos → effet réel côté téléphone.
|
||||||
|
5. Foreground service : téléphone verrouillé / app en background pendant séance → le canal reste vivant.
|
||||||
|
6. Latence/interpolation/resync réelles ; haptiques au ack sans couper la musique.
|
||||||
|
|
||||||
|
### Commits (feature/ticket91-wear-os-watch-sync)
|
||||||
|
cf68a72 #91-A · d1c6076 #91-B · 6c177de #91-C · 68a87d1 #91-D · c65a5a7 #91-E/#91-F · 40a2d5e server networking (séparé).
|
||||||
|
|
||||||
|
QA-agent n'a pas pu produire de final textuel (canal instable, sandbox bloquant flutter) — exécutable pris en charge par Main. Revue code par Main.
|
||||||
28
.ideai/tickets/100/issue.md
Normal file
28
.ideai/tickets/100/issue.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
id: "f28612f6-ef2c-4a5d-a0ac-9d66afb151d6"
|
||||||
|
number: 100
|
||||||
|
title: "#91-G [QA] Validation companion watch offline/local"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: [{"target":"#99","kind":"dependsOn"}]
|
||||||
|
agentRefs: [{"agentId":"7efa512f-3b3a-47b5-ade0-a2dd13073055","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
createdAt: 1784991960335
|
||||||
|
updatedAt: 1785002671405
|
||||||
|
version: 3
|
||||||
|
---
|
||||||
|
Partie #91, dépend de #91-F.
|
||||||
|
|
||||||
|
Validation end-to-end du companion montre, offline/local uniquement :
|
||||||
|
- Projection téléphone correcte pour chaque phase.
|
||||||
|
- Routing commandes + idempotence : retry/doublon n'avance jamais deux fois une étape/passage/série ; révision stale rejetée.
|
||||||
|
- Acks corrects selon contexte.
|
||||||
|
- Reconnexion + resync complet de l'état.
|
||||||
|
- Foreground service téléphone (verrouillé / background).
|
||||||
|
- Interpolation du chrono montre vs timestamps autoritaires.
|
||||||
|
- Cohérence téléphone/montre : la montre ne modifie jamais l'état localement ; le téléphone reste source de vérité ; actions simultanées -> dernier état confirmé.
|
||||||
|
- Haptiques sans couper la musique (#92).
|
||||||
|
|
||||||
|
Définition de done : rapport de validation par commande réelle (dart analyze, flutter test, build app téléphone + build watch, scénarios manuels sur émulateur/device si possible). Sortie verte ou rapport d'échec non enjolivé.
|
||||||
6
.ideai/tickets/101/carnet.md
Normal file
6
.ideai/tickets/101/carnet.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#101"
|
||||||
|
version: 2
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedAt: 1784992086885
|
||||||
|
---
|
||||||
28
.ideai/tickets/101/issue.md
Normal file
28
.ideai/tickets/101/issue.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
id: "32c34fe8-d915-4bf3-8266-f0505760f6b4"
|
||||||
|
number: 101
|
||||||
|
title: "#91-E [DevFrontend] App Wear OS Flutter dédiée + navigation UX montre"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: [{"target":"#95","kind":"dependsOn"}]
|
||||||
|
agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
createdAt: 1784991960336
|
||||||
|
updatedAt: 1784992086885
|
||||||
|
version: 2
|
||||||
|
---
|
||||||
|
Partie #91, dépend de #91-A.
|
||||||
|
|
||||||
|
Scaffold `watch_app/` (target/app Flutter Wear OS dédiée dans le mono-dépôt). Implémenter les surfaces UX décrites dans `gametime-ux-watch-companion` :
|
||||||
|
- `Aucune séance active` (+ variante téléphone indisponible).
|
||||||
|
- `Séance active` : hiérarchie SÉRIE X/Y -> exercice -> passage/étape -> chrono dominant -> chronos secondaires compacts -> CTA primaire unique.
|
||||||
|
- `Actions` (scrollable) : Passer l'étape / Passer le passage / Terminer la série / Passer la série / Passer le repos selon contexte.
|
||||||
|
- `Repos` : chrono de repos dominant, prochain exercice visible, Pause/Reprendre en primaire.
|
||||||
|
|
||||||
|
Gestes : tap CTA, swipe horizontal (Séance <-> Actions), scroll vertical / couronne. Confirmation seulement pour Passer la série et Passer le passage.
|
||||||
|
|
||||||
|
L'app montre consomme **uniquement** les DTO du package A et reste stateless vis-à-vis du domaine. Stub l'état en local pour permettre le dev UI avant branchement du client (F).
|
||||||
|
|
||||||
|
Définition de done : surfaces navigables conformes à la spec UX sur écran rond, `flutter analyze` propre, build Wear OS OK.
|
||||||
14
.ideai/tickets/102/carnet.md
Normal file
14
.ideai/tickets/102/carnet.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#102"
|
||||||
|
version: 5
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785217009929
|
||||||
|
---
|
||||||
|
|
||||||
|
## Découpage Main — 2026-07-26
|
||||||
|
|
||||||
|
Sous-tickets ouverts pour exécution sans dépendance utilisateur supplémentaire :
|
||||||
|
- #108 UX notification
|
||||||
|
- #109 Architect cadrage technique notification
|
||||||
|
- #110 DevFrontend implémentation
|
||||||
|
- #111 QA validation
|
||||||
16
.ideai/tickets/102/issue.md
Normal file
16
.ideai/tickets/102/issue.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
id: "52ac23c7-17e4-41b1-a406-33ae32fb33cf"
|
||||||
|
number: 102
|
||||||
|
title: "Ajouter un bandeau dans la zone de notification du téléphone"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: []
|
||||||
|
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785083628323
|
||||||
|
updatedAt: 1785217009929
|
||||||
|
version: 5
|
||||||
|
---
|
||||||
|
J'aimerais que lorsqu'une séance est en cours, un bandeau dans la zone de notifiation du téléphone soit visible avec les informations de l'exercice en cours et l'affichage du chrono en cours s'il y en a un, a la manière de Heavy
|
||||||
13
.ideai/tickets/103/carnet.md
Normal file
13
.ideai/tickets/103/carnet.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#103"
|
||||||
|
version: 5
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785217006191
|
||||||
|
---
|
||||||
|
|
||||||
|
## Découpage Main — 2026-07-26
|
||||||
|
|
||||||
|
Sous-tickets ouverts :
|
||||||
|
- #112 validation UX de la reprise de l'icône téléphone
|
||||||
|
- #113 implémentation DevFrontend
|
||||||
|
- #114 validation QA
|
||||||
16
.ideai/tickets/103/issue.md
Normal file
16
.ideai/tickets/103/issue.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
id: "49bc2615-b291-4577-808c-50b06ac6c433"
|
||||||
|
number: 103
|
||||||
|
title: "Icone appplication montre"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: []
|
||||||
|
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785083768016
|
||||||
|
updatedAt: 1785217006191
|
||||||
|
version: 5
|
||||||
|
---
|
||||||
|
Je veux que l'icone de l'application de la montre soit le même que celui de l'application téléphone
|
||||||
13
.ideai/tickets/104/carnet.md
Normal file
13
.ideai/tickets/104/carnet.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#104"
|
||||||
|
version: 5
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785217003099
|
||||||
|
---
|
||||||
|
|
||||||
|
## Découpage Main — 2026-07-26
|
||||||
|
|
||||||
|
Sous-tickets ouverts :
|
||||||
|
- #115 cadrage UX
|
||||||
|
- #116 implémentation DevFrontend
|
||||||
|
- #117 validation QA
|
||||||
16
.ideai/tickets/104/issue.md
Normal file
16
.ideai/tickets/104/issue.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
id: "1db7142c-4e42-4e5a-803e-c9bc2e95daa8"
|
||||||
|
number: 104
|
||||||
|
title: "Refonte UI montre"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: []
|
||||||
|
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785088577932
|
||||||
|
updatedAt: 1785217003099
|
||||||
|
version: 5
|
||||||
|
---
|
||||||
|
J'aiemrais une refonte UI de la montre uniquement pour que ça corresponde plus a la DA qu'on a sur le téléphone, a savoir fond noir, couleur principale dorée avec des liserais rouges et les titres en blancs (c'est comme ça que j'interprete notre DA, mais UX devrait etre en mesure de fair ce qu'il faut)
|
||||||
15
.ideai/tickets/105/carnet.md
Normal file
15
.ideai/tickets/105/carnet.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#105"
|
||||||
|
version: 5
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785216998986
|
||||||
|
---
|
||||||
|
|
||||||
|
## Découpage Main — 2026-07-26
|
||||||
|
|
||||||
|
Sous-tickets ouverts :
|
||||||
|
- #118 UX score montre
|
||||||
|
- #119 Architect extension contrats/bridge
|
||||||
|
- #120 DevBackend routing score
|
||||||
|
- #121 DevFrontend UI et synchronisation
|
||||||
|
- #122 QA validation
|
||||||
16
.ideai/tickets/105/issue.md
Normal file
16
.ideai/tickets/105/issue.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
id: "f7083598-9cde-45e3-ad32-91332c8de2f0"
|
||||||
|
number: 105
|
||||||
|
title: "Incrementer decrementer le score depuis la montre"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: []
|
||||||
|
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785088671279
|
||||||
|
updatedAt: 1785216998986
|
||||||
|
version: 5
|
||||||
|
---
|
||||||
|
Dans le cas d'un exercice avec un score, j'aimerais qu'il soit possible de l'incrémenter/decrementer directement depuis la montre
|
||||||
42
.ideai/tickets/106/carnet.md
Normal file
42
.ideai/tickets/106/carnet.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#106"
|
||||||
|
version: 14
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785221560663
|
||||||
|
---
|
||||||
|
## Cadrage produit consolidé — Main (2026-07-27)
|
||||||
|
|
||||||
|
Source de vérité issue des précisions utilisateur du 2026-07-27.
|
||||||
|
|
||||||
|
### Périmètre fonctionnel confirmé
|
||||||
|
- **Fréquence cardiaque** : affichage live pendant la séance, puis conservation de la **moyenne**, de la **min** et de la **max** avec détail **par étape, par série et par exercice**.
|
||||||
|
- **Distance parcourue** : affichage **live** pendant la séance, puis conservation pour consultation après séance et dans l'historique avec graphiques.
|
||||||
|
- **Calories brûlées** : affichage **live** pendant la séance, puis conservation pour consultation après séance et dans l'historique avec graphiques.
|
||||||
|
|
||||||
|
### Surfaces confirmées
|
||||||
|
- **Téléphone pendant la séance** : surface légère uniquement, avec **fréquence cardiaque live**, **distance live** et **calories live**.
|
||||||
|
- **Montre pendant la séance** :
|
||||||
|
- écran principal : **fréquence cardiaque live uniquement** ;
|
||||||
|
- écran secondaire accessible par glissement inverse du menu action : **fréquence cardiaque live + distance + calories**.
|
||||||
|
- **Après séance** : consultation détaillée des statistiques collectées.
|
||||||
|
- **Historique** : graphiques et restitution détaillée conservée, en particulier pour la fréquence cardiaque par étape/série/exercice.
|
||||||
|
|
||||||
|
### Compatibilité / fallback
|
||||||
|
- Seulement pour les **montres compatibles**.
|
||||||
|
- Si une donnée ou un capteur n'est pas disponible : **ne rien afficher**.
|
||||||
|
- **Aucun message bloquant**, aucune alerte de donnée absente.
|
||||||
|
|
||||||
|
### Niveau de fiabilité attendu
|
||||||
|
- Les fonctions terrain / capteurs avancés restent dans une logique **expérimentale**.
|
||||||
|
- Une précision imparfaite est acceptable dans un premier temps, notamment pour les futurs sujets type hot-map terrain.
|
||||||
|
|
||||||
|
### Découpage décidé
|
||||||
|
Le ticket parent #106 dépend désormais des tickets suivants :
|
||||||
|
- #144 fréquence cardiaque live
|
||||||
|
- #145 calories montre
|
||||||
|
- #157 distance live
|
||||||
|
- #158 agrégats et historique
|
||||||
|
- #159 surface montre live
|
||||||
|
- #160 graphiques historiques
|
||||||
|
- #155 cadrage UX transverse
|
||||||
|
- #156 cadrage architecture transverse
|
||||||
16
.ideai/tickets/106/issue.md
Normal file
16
.ideai/tickets/106/issue.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
id: "8212b0b1-7c7a-4688-99cf-796576c0ffe6"
|
||||||
|
number: 106
|
||||||
|
title: "Ajouter aux séances des données statisques pouvant etre renvoyées par la montre"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: [{"target":"#144","kind":"dependsOn"},{"target":"#145","kind":"dependsOn"},{"target":"#157","kind":"dependsOn"},{"target":"#158","kind":"dependsOn"},{"target":"#159","kind":"dependsOn"},{"target":"#160","kind":"dependsOn"}]
|
||||||
|
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785089161774
|
||||||
|
updatedAt: 1785221560663
|
||||||
|
version: 14
|
||||||
|
---
|
||||||
|
A la manière d'autres applications, j'aimerais que la montre puisse apporter des statistiques supplémentaires comme la fréquence cardiaque etc qui serianet ensuite ajoutée au stats d'une séance. Je laisse le soin au bon agent de determiner les statistiques a récupérer. Dans le cas ou un utilisateur n'a pas de montre opu que sa montre ne retourne pas toutes les statistique, je ne veux pas qu'il en soit notifié, je veux que ça soit transparent pour lui
|
||||||
32
.ideai/tickets/107/carnet.md
Normal file
32
.ideai/tickets/107/carnet.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#107"
|
||||||
|
version: 3
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785093155401
|
||||||
|
---
|
||||||
|
|
||||||
|
## Découpage Main — 2026-07-26
|
||||||
|
|
||||||
|
Le symptôme `Bottom overflowed by 13 pixels` reste suivi dans ce ticket. Un bug connexe plus critique a été ajouté séparément :
|
||||||
|
- #125 écran noir montre lors d'un démarrage de séance depuis le téléphone
|
||||||
|
|
||||||
|
## Audit DevFrontend — 2026-07-27
|
||||||
|
|
||||||
|
- Correctif visuel/layout déjà présent dans `watch_session_screen.dart` : contenu contraint par le diamètre réel du cadran, hauteurs fixes, `FittedBox`, `maxLines` et `ellipsis`.
|
||||||
|
- Pas de scroll vertical réintroduit sur l'écran séance montre.
|
||||||
|
- Aucun écart concret supplémentaire identifié sans reproduction device.
|
||||||
|
|
||||||
|
À valider sur device : absence du message `Bottom overflowed by 13 pixels` pendant séance active, repos et score manuel.
|
||||||
|
|
||||||
|
## Correctif QA DevFrontend — 2026-07-27
|
||||||
|
|
||||||
|
- Reproduction QA 192x192 traitée : `_ActiveContent` est maintenant encapsulé dans un `_ScaledContent` avec `FittedBox.scaleDown`, et sa colonne passe en `mainAxisSize: MainAxisSize.min`.
|
||||||
|
- Le test widget ciblé `watch_session_screen_test.dart` passe sans overflow sur le viewport 192x192.
|
||||||
|
- Aucun scroll réintroduit sur l'écran séance montre.
|
||||||
|
|
||||||
|
## Correctif DevFrontend — 2026-07-26
|
||||||
|
|
||||||
|
- Correctif partagé avec #125 : suppression de la colonne active qui empilait bouton Actions, contenu, statut et bouton primaire dans 210dp.
|
||||||
|
- Nouveau rendu actif/repos : pile contrainte par le diamètre utile du viewport, sans scroll sur les vues de séance, avec valeur dominante `FittedBox`, textes `maxLines` + ellipsis, pied de contexte discret.
|
||||||
|
- Page Actions rendue compacte sans `ListView` pour éviter les débordements sur petit cadran ; confirmation remplacée par un plein écran adapté montre.
|
||||||
|
- Vérification : `flutter analyze` watch_app OK ; build APK debug montre OK via Gradle/JDK 21.
|
||||||
16
.ideai/tickets/107/issue.md
Normal file
16
.ideai/tickets/107/issue.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
id: "4d9a5046-ad0f-4bae-8be7-9236218aab02"
|
||||||
|
number: 107
|
||||||
|
title: "Bottom overflowed by 13 pixels"
|
||||||
|
status: "QA"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: []
|
||||||
|
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785093125358
|
||||||
|
updatedAt: 1785093155401
|
||||||
|
version: 3
|
||||||
|
---
|
||||||
|
Sur l'affichage de la montre pendant une seance, j'ai le message "Bottom overflowed by 13 pixels" qui s'affiche
|
||||||
49
.ideai/tickets/108/carnet.md
Normal file
49
.ideai/tickets/108/carnet.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#108"
|
||||||
|
version: 3
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedAt: 1785134675226
|
||||||
|
---
|
||||||
|
|
||||||
|
## #108 — Bandeau notification téléphone pendant une séance (UX, 2026-07-26)
|
||||||
|
|
||||||
|
Statut : **cadrage UX exploitable, prêt pour Architect/DevFrontend**, pas de blocage.
|
||||||
|
|
||||||
|
### Principe
|
||||||
|
La notification est un **résumé d'accès rapide**, jamais une copie de l'écran de séance. Elle répond à une seule question en un regard : « où j'en suis, et ce que fait le chrono ». Référence Heavy : titre = contexte, corps = donnée vivante, pas d'action complexe.
|
||||||
|
|
||||||
|
### Contenu compact (état replié, vue par défaut)
|
||||||
|
- **Titre** : nom de l'exercice en cours (ex. « Squats bulgares »).
|
||||||
|
- **Texte** : une seule ligne combinant série et mesure pertinente, priorité dans cet ordre si plusieurs mesures actives :
|
||||||
|
1. Chrono actif → `mm:ss` en cours (temps écoulé de l'étape/série, ou chrono score s'il est démarré — cf. [[gametime-ux-score-chrono]]).
|
||||||
|
2. Sinon Répétitions/Score → `Série 2/4 · 8 reps`.
|
||||||
|
3. Sinon (étape sans mesure, ex. consigne) → nom court de l'étape.
|
||||||
|
- **Icône** : icône app (monochrome, cf. exigence notification Android — silhouette du logo "GT" sur fond transparent).
|
||||||
|
- **État repos** : titre devient « Repos », texte = décompte `mm:ss` restant + « Ensuite : <exercice> » tronqué si besoin.
|
||||||
|
- **État pause de séance** : titre inchangé, texte préfixé par « En pause · » avant l'info gelée (le chrono affiché est figé, pas de tick visuel — cohérent avec la règle « jamais actif pendant une pause » de [[gametime-ux-score-chrono]]).
|
||||||
|
|
||||||
|
### Contenu étendu (notification développée / longue)
|
||||||
|
Deuxième ligne ajoutée sous la ligne compacte :
|
||||||
|
- Fil de contexte discret : `Programme 1/2 · Exercice 3/8` (même format que le header d'exécution, cf. [[gametime-ux-series-counter]]) — pas de répétition du nom d'exercice déjà en titre.
|
||||||
|
- Pas de 3e ligne, pas de liste d'étapes à venir : la notification reste un résumé, l'utilisateur rouvre l'app pour le détail.
|
||||||
|
|
||||||
|
### Actions
|
||||||
|
MVP : **aucune action bouton dans la notification** (pas de Pause/Suivant intégrés). Justification : réduit le risque d'appui accidentel hors app, évite de dupliquer des commandes qui existent déjà dans l'app et sur la montre (#105/#118), et simplifie l'implémentation (pas de PendingIntent supplémentaire à maintenir en cohérence avec le moteur d'exécution). Le tap sur la notification entière ramène à l'écran d'exécution en cours. À réévaluer seulement si retour utilisateur explicite après usage réel — ne pas anticiper.
|
||||||
|
|
||||||
|
### Règles de rafraîchissement
|
||||||
|
- Notification **persistante** (non "swipeable") tant qu'une séance est active — cohérent avec le statut foreground service déjà en place pour la montre ([[gametime-watch-companion-implementation]], #91-D).
|
||||||
|
- Mise à jour du texte à chaque tick de seconde **uniquement si un chrono est affiché** (chrono score, repos) ; sinon mise à jour uniquement sur changement d'état (série suivante, pause, reprise) — pas de rafraîchissement inutile qui viderait la batterie ou ferait clignoter la notification.
|
||||||
|
- Notification retirée immédiatement à la fin de séance (Terminer/Abandonner), jamais laissée en état "orpheline".
|
||||||
|
|
||||||
|
### États à couvrir (design implémentable)
|
||||||
|
| État | Titre | Texte |
|
||||||
|
|---|---|---|
|
||||||
|
| Série active, chrono | `<Exercice>` | `02:14` |
|
||||||
|
| Série active, reps/score, sans chrono | `<Exercice>` | `Série 2/4 · 8 reps` |
|
||||||
|
| Repos | `Repos` | `01:30 restant · Ensuite : <Exercice>` |
|
||||||
|
| Pause séance | `<Exercice>` | `En pause · <dernière valeur gelée>` |
|
||||||
|
| Séance terminée / abandonnée | — | notification retirée |
|
||||||
|
|
||||||
|
### Hors périmètre MVP
|
||||||
|
- Actions rapides (pause/suivant) dans la notification.
|
||||||
|
- Notification enrichie avec image/illustration d'exercice (pas de média viewer en notification, cf. [[gametime-ux-exercise-media-viewer]] réservé à l'app).
|
||||||
26
.ideai/tickets/108/issue.md
Normal file
26
.ideai/tickets/108/issue.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
id: "2382d9da-b640-48a1-9acf-8828bc73fd61"
|
||||||
|
number: 108
|
||||||
|
title: "#102-A [UX] Conception du bandeau notification téléphone pendant une séance"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: [{"target":"#102","kind":"relatesTo"}]
|
||||||
|
agentRefs: [{"agentId":"f3408f5d-469c-4f64-9485-d8b218f3ff26","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
createdAt: 1785093288487
|
||||||
|
updatedAt: 1785134675226
|
||||||
|
version: 3
|
||||||
|
---
|
||||||
|
Partie du ticket #102.
|
||||||
|
|
||||||
|
Définir la surface notification Android pendant une séance active :
|
||||||
|
- contenu minimal visible en compact et étendu ;
|
||||||
|
- hiérarchie des informations : séance, exercice, chrono pertinent, état pause/repos si utile ;
|
||||||
|
- libellés des actions éventuelles ;
|
||||||
|
- règles de rafraîchissement visuel sans surcharge.
|
||||||
|
|
||||||
|
Contrainte : s'inspirer de Heavy sur l'accès rapide, sans dupliquer l'écran complet de séance dans la notification.
|
||||||
|
|
||||||
|
Définition de done : cadrage UX autonome consigné dans le carnet avec états nominaux et dégradés.
|
||||||
83
.ideai/tickets/109/carnet.md
Normal file
83
.ideai/tickets/109/carnet.md
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#109"
|
||||||
|
version: 5
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedAt: 1785134675243
|
||||||
|
---
|
||||||
|
|
||||||
|
## #109 — Cadrage technique notification de séance Android (Architect, 2026-07-26)
|
||||||
|
|
||||||
|
Statut : **cadrage exploitable, prêt pour DevFrontend**, pas de blocage. Basé sur le cadrage UX #108 et l'existant réel (`WatchCompanionForegroundService.kt`, `WatchSessionProjectionProjector`, cf. [[gametime-watch-companion-implementation]]).
|
||||||
|
|
||||||
|
### Constat sur l'existant
|
||||||
|
Un `ForegroundService` tourne déjà pendant toute séance active (`android/app/src/main/kotlin/com/gametime/app/watch/WatchCompanionForegroundService.kt`), démarré/arrêté par `WatchWearDataLayerAdapter._syncForegroundService` sur simple base de `phase != noActiveSession` — **indépendamment du pairing montre**. Mais ce service est aujourd'hui **verrouillé sur la feature montre** : channel `gametime_watch_companion`, notification statique créée une seule fois dans `onCreate()` (titre/texte fixes « GameTime » / « Séance en cours »), `NOTIFICATION_ID = 91`, aucune méthode de mise à jour de contenu, `foregroundServiceType="connectedDevice|dataSync"`.
|
||||||
|
|
||||||
|
**Décision** : ne pas réutiliser ce service tel quel pour #102. On crée un **second foreground service indépendant**, propre à la notification de séance téléphone, avec son propre canal et son propre cycle de vie. Raison (ISP appliqué aux adapters Android) : coupler la notification "résumé de séance" au service montre ferait dépendre une feature du couplage montre/pairing implicite, alors que la notification doit exister avec ou sans montre appairée, et une évolution du companion montre ne doit jamais risquer de casser la notification (et réciproquement). Android autorise plusieurs foreground services concurrents sans conflit.
|
||||||
|
|
||||||
|
### Contrat côté domaine/application
|
||||||
|
|
||||||
|
Pas de nouvel état domaine : la notification est un **présentateur supplémentaire** d'un état déjà calculé. Réutiliser tel quel le flux existant `WatchProjectionSource.projections` (déjà émis à chaque changement pertinent : série, pause, repos, tick de chrono score) plutôt que dupliquer la logique de sélection du "chrono dominant" (priorité chrono > reps/score > étape, déjà implémentée dans `WatchSessionProjectionProjector` pour produire `dominantTimer`).
|
||||||
|
|
||||||
|
- Nouveau port application :
|
||||||
|
```dart
|
||||||
|
abstract interface class SessionNotificationGateway {
|
||||||
|
Future<void> show(SessionNotificationContent content);
|
||||||
|
Future<void> clear();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Nouveau DTO pur (application layer) :
|
||||||
|
```dart
|
||||||
|
class SessionNotificationContent {
|
||||||
|
final String title; // nom exercice / "Repos" / exercice (pause)
|
||||||
|
final String primaryLine; // mm:ss, "Série 2/4 · 8 reps", décompte repos, "En pause · ..."
|
||||||
|
final String? secondaryLine; // "Programme 1/2 · Exercice 3/8", null si compact only
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Nouvelle fonction pure (testable sans plateforme) : `SessionNotificationContent buildSessionNotificationContent(WatchSessionProjection projection)` — mapping direct des états du tableau UX #108 (série+chrono, série reps/score, repos, pause, terminée→`clear()`). Réutilise `dominantTimer`/`phase`/indices déjà présents dans `WatchSessionProjection`, pas de nouveau champ requis sur ce DTO existant.
|
||||||
|
- Nouveau coordinateur application `SessionNotificationCoordinator` : s'abonne à `WatchProjectionSource.projections`, appelle `buildSessionNotificationContent` puis `gateway.show(...)`, appelle `gateway.clear()` sur `phase == noActiveSession`. **Ne dépend d'aucun état montre/pairing** — seul le flux de projection (déjà session-only) est consommé.
|
||||||
|
|
||||||
|
### Règle de rafraîchissement (traduction concrète de l'UX)
|
||||||
|
- `show()` appelé à chaque émission de projection représentant un changement structurel (bump de révision) : changement de série/étape/phase/pause.
|
||||||
|
- En plus, si `dominantTimer.runState == running` (chrono actif ou repos), un tick local **1s** recalcule `primaryLine` depuis `referenceEpochMs`/`accumulatedMs`/`targetMs` déjà présents sur `WatchTimerProjection` — tick **strictement local au coordinateur de notification**, ne déclenche jamais de bump de révision ni de republication vers la montre (éviter tout couplage/chatter croisé entre les deux features).
|
||||||
|
- Sinon (pas de chrono affiché), aucune mise à jour périodique — conforme à l'exigence UX "pas de rafraîchissement inutile".
|
||||||
|
- Invariant robustesse : un échec de `show()`/`clear()` ne doit jamais remonter d'exception dans le flux domaine/session (fire-and-forget côté notification) — une panne de notification ne doit jamais interrompre une séance en cours.
|
||||||
|
|
||||||
|
### Contrat côté adapter Android (nouveau)
|
||||||
|
- Nouveau service Kotlin, ex. `android/app/src/main/kotlin/com/gametime/app/session/SessionStatusForegroundService.kt`, **distinct** de `WatchCompanionForegroundService`.
|
||||||
|
- Nouveau canal `gametime_session_status`, `IMPORTANCE_LOW` (pas de son, cohérent avec les mises à jour fréquentes de chrono), `NOTIFICATION_ID` distinct (ex. `92`, à ne pas réutiliser `91`).
|
||||||
|
- Contrairement au service montre, celui-ci **doit exposer une méthode de mise à jour de contenu** (pas de notification statique créée une fois) — `updateNotification(title, primaryLine, secondaryLine?)` appelée à chaque `show()`.
|
||||||
|
- `foregroundServiceType` : ni `dataSync` ni `connectedDevice` ne conviennent sémantiquement (ce n'est ni de la synchro de données ni un device connecté). Sur SDK 36, utiliser `specialUse` avec la propriété manifeste `android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE` (valeur libre du type `"workout-session-status"`). L'app n'étant pas distribuée sur le Play Store (APK signé debug, cf. [[gametime-android-release-networking-and-apk-build]]), la contrainte de justification `specialUse` en review Play ne s'applique pas ; seule la déclaration manifeste est requise par l'OS.
|
||||||
|
- Permission `POST_NOTIFICATIONS` déjà déclarée dans le manifest (réutilisée pour le canal montre) — pas de nouvelle permission requise. Si l'utilisateur refuse la permission notification, le foreground service démarre quand même (contrat Android standard : `startForeground` exige une `Notification`, affichée ou non selon permission) — aucune gestion spécifique à ajouter, ne jamais bloquer le démarrage de séance sur ce refus.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
- `buildSessionNotificationContent` : 100% unit-testable en Dart pur, mêmes conventions que `test/application/watch_companion_projection_test.dart` — couvrir les 5 états du tableau UX #108 (série+chrono, série reps/score, repos, pause, fin→clear).
|
||||||
|
- `SessionNotificationCoordinator` : test avec `StreamController` fake, vérifier `show()` sur changement structurel et sur tick actif uniquement, `clear()` sur fin de séance, absence d'appel superflu si aucun chrono actif.
|
||||||
|
- Service Kotlin : non testable en sandbox (cohérent avec la contrainte déjà connue pour `WatchCompanionForegroundService`, cf. [[gametime-watch-companion-implementation]]) — validation manuelle on-device requise (affichage réel, tick, disparition en fin de séance, comportement écran verrouillé).
|
||||||
|
|
||||||
|
### Hors périmètre (aligné UX #108)
|
||||||
|
Pas d'action bouton dans la notification (pas de PendingIntent Pause/Suivant) — tap ouvre l'app sur l'écran d'exécution en cours (Intent standard `PendingIntent.getActivity` vers l'activité principale, aucun deep-link spécifique nécessaire pour le MVP).
|
||||||
|
|
||||||
|
Explicitement **hors scope** : iOS. Le contrat `SessionNotificationGateway` est un port application générique (compatible iOS en théorie), mais aucun adapter iOS n'est cadré ni demandé ici — seul l'adapter Android (`MethodChannelSessionNotificationGateway` + plugin Kotlin) est dans le périmètre de #102/#110. Si iOS est demandé un jour, c'est un nouveau ticket d'adapter, pas une révision de ce contrat.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audit Architect — implémentation constatée dans le worktree (2026-07-27)
|
||||||
|
|
||||||
|
Statut mis à jour : **implémenté conformément au cadrage ci-dessus**, un garde-fou à fermer avant merge/QA.
|
||||||
|
|
||||||
|
Vérification du code réel (working tree non commité, branche `feature/ticket91-wear-os-watch-sync`) : `SessionNotificationCoordinator`/`buildSessionNotificationContent` (`lib/application/session_notification_use_cases.dart`), `MethodChannelSessionNotificationGateway` (`lib/infrastructure/session_notification/`), `SessionStatusForegroundService.kt`/`SessionNotificationPlugin.kt` (`android/app/src/main/kotlin/com/gametime/app/session/`) sont tous présents et conformes point par point au contrat cadré : consommation de `WatchCompanionProjectionUseCases.projections` (pas d'état dupliqué), tick 1s uniquement si timer actif, `clear()` sur `phase == noActiveSession || deviceSessionId.isEmpty`, appels gateway `unawaited(...).catchError((_) {})` (fire-and-forget confirmé), canal `gametime_session_status`/`NOTIFICATION_ID 92` distinct du service montre, `foregroundServiceType="specialUse"` avec `PROPERTY_SPECIAL_USE_FGS_SUBTYPE` comme cadré. Wiring dans `app_bootstrap.dart` (start juste après `watchWearDataLayerAdapter.start()`, dispose en premier).
|
||||||
|
|
||||||
|
**Garde-fou à fermer avant de considérer le lot terminé** : le manifest déclare bien `POST_NOTIFICATIONS`, mais **aucun code (Dart ou Kotlin) ne demande cette permission à l'exécution** nulle part dans l'app — ni pour ce nouveau canal, ni pour le canal montre pré-existant. Sur Android 13+ (API 33+, cohérent avec la cible SDK 36), sans cette demande explicite le service démarre normalement (exception foreground service) mais la notification peut ne jamais s'afficher à l'utilisateur, silencieusement. Contrat à ajouter pour DevFrontend : demander `POST_NOTIFICATIONS` une seule fois, paresseusement, au premier démarrage de séance (jamais un écran dédié, jamais bloquant — cohérent avec [[gametime-online-layer-philosophy]] appliqué par analogie aux permissions système) ; en cas de refus, ne rien tenter de plus, ne jamais redemander en boucle. C'est un gap transverse aux deux notifications (montre + séance), pas spécifique à l'une des deux — à traiter une seule fois pour les deux canaux.
|
||||||
|
|
||||||
|
Tests couvrant `buildSessionNotificationContent` déjà présents (`test/application/session_notification_use_cases_test.dart`) ; **`SessionNotificationCoordinator` lui-même (start/stop/tick/gateway failure) n'est pas encore testé** — à ajouter avant merge, cf. stratégie de test déjà cadrée ci-dessus.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vérification Architect de clôture (2026-07-27, second passage)
|
||||||
|
|
||||||
|
Statut final : **conforme, garde-fou permission fermé, un seul gap résiduel connu (non bloquant pour le cadrage, bloquant pour le merge).**
|
||||||
|
|
||||||
|
- Le garde-fou `POST_NOTIFICATIONS` signalé ci-dessus est **résolu** : `SessionNotificationPlugin.kt` (`android/app/src/main/kotlin/com/gametime/app/session/SessionNotificationPlugin.kt`) appelle `requestPostNotificationsPermissionIfNeeded()` dans `show()`, guardé par un flag `notificationPermissionRequested` (une seule demande, jamais de boucle), `SDK_INT >= TIRAMISU` uniquement, pas d'écran dédié. La permission `POST_NOTIFICATIONS` est globale à l'app (pas par canal) donc cette unique demande couvre aussi le canal montre pré-existant — le gap transverse est clos par un seul point d'appel, conforme à la recommandation.
|
||||||
|
- Gap résiduel inchangé : **`SessionNotificationCoordinator` n'a toujours pas de test dédié** (`test/application/session_notification_use_cases_test.dart` ne couvre que `buildSessionNotificationContent`, 5 cas). À ajouter avant merge (StreamController fake : `show()` sur changement structurel + sur tick actif, `clear()` en fin de séance, non-appel si pas de chrono actif) — cf. stratégie déjà cadrée plus haut. Ceci est un standard de qualité (couverture), **pas** une ambiguïté de contrat : n'importe qui connaissant `SessionNotificationCoordinator` peut écrire ce test sans redemander de cadrage.
|
||||||
|
|
||||||
|
**Réponse à la question "DevFrontend peut-il implémenter sans autre clarification ?" : oui.** Le contrat est complet et déjà appliqué à l'identique dans le code existant. Il ne reste aucune décision d'architecture ouverte pour #102/#110 — seulement une tâche de complétion de test (coordinateur) avant que le lot passe en QA (#111).
|
||||||
25
.ideai/tickets/109/issue.md
Normal file
25
.ideai/tickets/109/issue.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
id: "116d067d-dc00-4453-9227-033e1a4482cd"
|
||||||
|
number: 109
|
||||||
|
title: "#102-B [Architect] Cadrage technique notification de séance Android"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: [{"target":"#102","kind":"relatesTo"},{"target":"#108","kind":"dependsOn"}]
|
||||||
|
agentRefs: [{"agentId":"f8f40941-ecf7-4830-b9de-8818a099f448","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
createdAt: 1785093288487
|
||||||
|
updatedAt: 1785134675243
|
||||||
|
version: 5
|
||||||
|
---
|
||||||
|
Partie du ticket #102, dépend du cadrage UX #108.
|
||||||
|
|
||||||
|
Définir le cadrage technique de la notification Android de séance :
|
||||||
|
- stratégie `ForegroundService` / notification persistante ;
|
||||||
|
- source de vérité pour les données affichées ;
|
||||||
|
- fréquence de rafraîchissement du chrono sans coût excessif ;
|
||||||
|
- actions notification autorisées ou explicitement exclues ;
|
||||||
|
- contraintes Android SDK 36 et permissions associées.
|
||||||
|
|
||||||
|
Définition de done : contrat technique clair pour DevFrontend, incluant limites plateforme et stratégie de test.
|
||||||
37
.ideai/tickets/110/carnet.md
Normal file
37
.ideai/tickets/110/carnet.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#110"
|
||||||
|
version: 1
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedAt: 1785093288487
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implémentation DevFrontend — 2026-07-26
|
||||||
|
|
||||||
|
- Notification Android de séance ajoutée via un foreground service dédié `SessionStatusForegroundService`, séparé du service companion montre.
|
||||||
|
- Nouveau canal `gametime_session_status`, notification persistante sans action bouton, tap vers `MainActivity`, contenu mis à jour via MethodChannel.
|
||||||
|
- Coordinateur applicatif `SessionNotificationCoordinator` branché sur le flux `WatchSessionProjection` existant : show sur séance active, tick local 1s seulement si chrono dominant running, clear sur `noActiveSession`.
|
||||||
|
- Mapping de contenu conforme au cadrage : exercice, repos, pause, chrono dominant, score manuel si présent, fallback étape/série.
|
||||||
|
|
||||||
|
Vérifications exécutées :
|
||||||
|
- `dart format` ciblé OK.
|
||||||
|
- `git diff --check` OK.
|
||||||
|
- `dart analyze lib test/application/session_notification_use_cases_test.dart` OK avec infos préexistantes uniquement.
|
||||||
|
- `dart analyze packages/watch_bridge_contract` OK.
|
||||||
|
- `dart test` dans `packages/watch_bridge_contract` OK.
|
||||||
|
|
||||||
|
Non fait / à valider :
|
||||||
|
- `flutter test` et build Android via wrapper Flutter bloqués par cache Flutter en lecture seule dans le runner.
|
||||||
|
- Compilation Gradle ciblée tentée avec cache temporaire, bloquée en configuration par `Unsupported class file major version 70`.
|
||||||
|
- Validation réelle device requise : affichage notification, tick chrono, tap, disparition en fin de séance.
|
||||||
|
|
||||||
|
## Complément permission Android 13+ — 2026-07-27
|
||||||
|
|
||||||
|
- Gap `POST_NOTIFICATIONS` fermé côté plateforme : la permission est demandée paresseusement au premier `show` de la notification de séance.
|
||||||
|
- La demande est native, non bloquante, et le foreground service continue à démarrer même si l'utilisateur refuse ou si l'activité n'est pas disponible.
|
||||||
|
- Aucun bouton d'action notification ajouté : le tap reste un retour vers `MainActivity`.
|
||||||
|
- Validation complémentaire : `JAVA_HOME=/usr/lib/jvm/java-21-openjdk HOME=/tmp GRADLE_USER_HOME=/tmp/gradle ./gradlew --no-daemon -Dkotlin.daemon.enabled=false :app:compileDebugKotlin` OK.
|
||||||
|
|
||||||
|
## Correctif build QA DevFrontend — 2026-07-27
|
||||||
|
|
||||||
|
- L'échec `share_plus-12.0.2/android` a été isolé comme dépendances Flutter pointant vers un cache Pub non disponible pour le build courant.
|
||||||
|
- `flutter pub get` relancé avec `PUB_CACHE=/tmp/.pub-cache`, puis `flutter build apk --debug` téléphone validé avec JDK 21.
|
||||||
24
.ideai/tickets/110/issue.md
Normal file
24
.ideai/tickets/110/issue.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
id: "418e1b28-b77c-4f88-9da4-ab065a6d62e4"
|
||||||
|
number: 110
|
||||||
|
title: "#102-C [DevFrontend] Implémenter la notification téléphone pendant la séance"
|
||||||
|
status: "QA"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: [{"target":"#102","kind":"relatesTo"},{"target":"#108","kind":"dependsOn"},{"target":"#109","kind":"dependsOn"}]
|
||||||
|
agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
createdAt: 1785093288487
|
||||||
|
updatedAt: 1785093288487
|
||||||
|
version: 1
|
||||||
|
---
|
||||||
|
Partie du ticket #102, dépend de #108 et #109.
|
||||||
|
|
||||||
|
Implémenter la notification Android visible pendant une séance active :
|
||||||
|
- informations d'exercice/chrono conformes au cadrage UX ;
|
||||||
|
- mise à jour cohérente pendant démarrage, pause, repos, reprise et fin ;
|
||||||
|
- pas de divergence avec l'écran d'exécution ;
|
||||||
|
- build Android debug propre.
|
||||||
|
|
||||||
|
Définition de done : notification fonctionnelle sur device Android, `dart analyze` / build pertinents propres, comportement documenté dans le carnet.
|
||||||
7
.ideai/tickets/111/carnet.md
Normal file
7
.ideai/tickets/111/carnet.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#111"
|
||||||
|
version: 2
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785142184934
|
||||||
|
---
|
||||||
|
|
||||||
24
.ideai/tickets/111/issue.md
Normal file
24
.ideai/tickets/111/issue.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
id: "0c3335f2-dd7a-406e-ac4d-9798440f6782"
|
||||||
|
number: 111
|
||||||
|
title: "#102-D [QA] Validation notification de séance Android"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: [{"target":"#102","kind":"relatesTo"},{"target":"#110","kind":"dependsOn"}]
|
||||||
|
agentRefs: [{"agentId":"7efa512f-3b3a-47b5-ade0-a2dd13073055","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785093288487
|
||||||
|
updatedAt: 1785142184934
|
||||||
|
version: 2
|
||||||
|
---
|
||||||
|
Partie du ticket #102, dépend de #110.
|
||||||
|
|
||||||
|
Valider par commandes et tests réels la notification de séance Android :
|
||||||
|
- apparition/disparition selon séance active ;
|
||||||
|
- contenu correct pendant exercice, étape chrono, pause, repos ;
|
||||||
|
- cohérence du chrono affiché ;
|
||||||
|
- comportement écran verrouillé / application en arrière-plan si applicable.
|
||||||
|
|
||||||
|
Définition de done : rapport QA réel, vert ou échec documenté sans enjoliver.
|
||||||
25
.ideai/tickets/112/carnet.md
Normal file
25
.ideai/tickets/112/carnet.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#112"
|
||||||
|
version: 3
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedAt: 1785134675260
|
||||||
|
---
|
||||||
|
|
||||||
|
## #112 — Réutilisation de l'icône téléphone sur la montre (UX, 2026-07-26)
|
||||||
|
|
||||||
|
Statut : **décision binaire tranchée, exploitable directement par DevFrontend**, pas de blocage.
|
||||||
|
|
||||||
|
### Décision
|
||||||
|
**Oui, réutiliser la même icône (logo "patch" Court Blazer, monogramme "GT")**, sans changement de composition ni de couleurs. Pas de variante montre distincte à concevoir.
|
||||||
|
|
||||||
|
### Justification
|
||||||
|
- Le logo actuel ([[gametime-visual-identity]]) est un badge compact à fort contraste : fond `surface` (`#141824` sombre / `#FFFFFF` clair), contour or (`#C9A24A`), bande diagonale crimson (`#D72638`), monogramme centré. C'est déjà pensé pour fonctionner en petit format (favicon, icône de lanceur téléphone) — les mêmes contraintes s'appliquent à une icône Wear OS.
|
||||||
|
- Le monogramme à 2 lettres reste lisible à 24-48dp (tailles de lanceur Wear OS courantes), contrairement à un wordmark complet qui aurait nécessité une adaptation.
|
||||||
|
- Cohérence de marque entre téléphone et montre : un utilisateur qui associe les deux appareils doit reconnaître immédiatement la même app.
|
||||||
|
|
||||||
|
### Adaptation minimale requise (technique, pas de redesign)
|
||||||
|
- Fournir l'icône en **icône adaptative Wear OS** (safe zone circulaire) : le contenu significatif (patch + monogramme) doit tenir dans le cercle inscrit central, avec le même padding proportionnel que l'icône adaptative Android existante — pas de nouvelle composition, juste un export au gabarit rond.
|
||||||
|
- Vérifier que le contour or reste visible sur les deux fonds système de lanceur montre les plus courants (fond clair et fond sombre du launcher Wear OS) — le contour + la bande crimson donnent déjà assez de séparation, aucun ajustement de couleur nécessaire.
|
||||||
|
|
||||||
|
### Hors périmètre
|
||||||
|
Pas de variante d'icône simplifiée, pas de monochrome dédié notification (distinct du sujet #108 qui utilise une icône monochrome système standard, pas cette icône d'app).
|
||||||
23
.ideai/tickets/112/issue.md
Normal file
23
.ideai/tickets/112/issue.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
id: "2ad8a17a-15ce-4cb5-bec8-4a9f3f0ef984"
|
||||||
|
number: 112
|
||||||
|
title: "#103-A [UX] Validation d'usage de la même icône sur montre et téléphone"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: [{"target":"#103","kind":"relatesTo"}]
|
||||||
|
agentRefs: [{"agentId":"f3408f5d-469c-4f64-9485-d8b218f3ff26","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
createdAt: 1785093288487
|
||||||
|
updatedAt: 1785134675260
|
||||||
|
version: 3
|
||||||
|
---
|
||||||
|
Partie du ticket #103.
|
||||||
|
|
||||||
|
Valider que la réutilisation de l'icône téléphone sur montre reste lisible et cohérente aux tailles Wear OS :
|
||||||
|
- lisibilité petit format ;
|
||||||
|
- contraste avec fonds système montre ;
|
||||||
|
- besoin éventuel d'une adaptation minimale sans changer l'identité.
|
||||||
|
|
||||||
|
Définition de done : décision UX binaire exploitable par DevFrontend.
|
||||||
20
.ideai/tickets/113/carnet.md
Normal file
20
.ideai/tickets/113/carnet.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#113"
|
||||||
|
version: 1
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedAt: 1785093288487
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audit DevFrontend — 2026-07-27
|
||||||
|
|
||||||
|
- Icône montre déjà alignée dans le worktree : `android:icon` et `android:roundIcon` pointent vers `@mipmap/ic_launcher`.
|
||||||
|
- Ressources launcher Wear OS présentes dans les densités `mipmap-*` et foreground adaptive présent dans `drawable-*`.
|
||||||
|
- Aucun changement supplémentaire nécessaire côté UI/plateforme.
|
||||||
|
|
||||||
|
À valider sur device : rendu launcher Wear OS réel après installation.
|
||||||
|
## Implémentation DevFrontend — 2026-07-26
|
||||||
|
|
||||||
|
- Icône launcher montre alignée sur l'icône téléphone : manifest basculé sur `@mipmap/ic_launcher` + `android:roundIcon`.
|
||||||
|
- Assets adaptatifs ajoutés côté Wear OS : `mipmap-anydpi-v26/ic_launcher.xml`, foregrounds densité `drawable-*`, PNG fallback `mipmap-*`, couleur de fond `#080A12`.
|
||||||
|
- Source utilisée : assets téléphone existants, sans redesign.
|
||||||
|
- Vérification : `flutter analyze` watch_app OK ; `./gradlew assembleDebug` watch_app OK avec `JAVA_HOME=/usr/lib/jvm/java-21-openjdk`, `HOME`/`GRADLE_USER_HOME` redirigés en writable.
|
||||||
23
.ideai/tickets/113/issue.md
Normal file
23
.ideai/tickets/113/issue.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
id: "ada6d03f-5721-4a7a-be83-26164c5e6e7f"
|
||||||
|
number: 113
|
||||||
|
title: "#103-B [DevFrontend] Aligner l'icône montre sur l'icône téléphone"
|
||||||
|
status: "QA"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: [{"target":"#103","kind":"relatesTo"},{"target":"#112","kind":"dependsOn"}]
|
||||||
|
agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
createdAt: 1785093288487
|
||||||
|
updatedAt: 1785093288487
|
||||||
|
version: 1
|
||||||
|
---
|
||||||
|
Partie du ticket #103, dépend de #112.
|
||||||
|
|
||||||
|
Réutiliser l'icône téléphone pour l'application montre, avec adaptations techniques minimales si la validation UX l'exige :
|
||||||
|
- assets launcher Wear OS ;
|
||||||
|
- round icon / monochrome si nécessaire ;
|
||||||
|
- build montre vérifié.
|
||||||
|
|
||||||
|
Définition de done : icône montre identique ou adaptation UX-validée, installable sur device.
|
||||||
7
.ideai/tickets/114/carnet.md
Normal file
7
.ideai/tickets/114/carnet.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#114"
|
||||||
|
version: 2
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785142176024
|
||||||
|
---
|
||||||
|
|
||||||
20
.ideai/tickets/114/issue.md
Normal file
20
.ideai/tickets/114/issue.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
id: "f3c78cc0-e7a8-49d6-a777-786f56ece278"
|
||||||
|
number: 114
|
||||||
|
title: "#103-C [QA] Validation icône application montre"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: [{"target":"#103","kind":"relatesTo"},{"target":"#113","kind":"dependsOn"}]
|
||||||
|
agentRefs: [{"agentId":"7efa512f-3b3a-47b5-ade0-a2dd13073055","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785093288487
|
||||||
|
updatedAt: 1785142176024
|
||||||
|
version: 2
|
||||||
|
---
|
||||||
|
Partie du ticket #103, dépend de #113.
|
||||||
|
|
||||||
|
Valider que l'icône montre installée correspond à la décision UX et s'affiche correctement sur device.
|
||||||
|
|
||||||
|
Définition de done : validation réelle sur montre ou rapport d'écart.
|
||||||
57
.ideai/tickets/115/carnet.md
Normal file
57
.ideai/tickets/115/carnet.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#115"
|
||||||
|
version: 3
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedAt: 1785134675276
|
||||||
|
---
|
||||||
|
|
||||||
|
## #115 — Refonte UI montre alignée sur la DA Court Blazer (UX, 2026-07-26)
|
||||||
|
|
||||||
|
Statut : **cadrage UX exploitable, prêt pour DevFrontend (#116)**, pas de blocage.
|
||||||
|
|
||||||
|
### Principe
|
||||||
|
Cadran rond, distance de lecture courte (poignet), interaction au doigt. On applique la DA Court Blazer ([[gametime-visual-identity]]) telle quelle côté couleurs/typo, mais on **simplifie radicalement la densité** par rapport au téléphone : une montre affiche une donnée principale par écran, pas une hiérarchie de blocs empilés.
|
||||||
|
|
||||||
|
### Palette (reprise directe, thème sombre uniquement)
|
||||||
|
La montre n'a pas de thème clair — un cadran Wear OS reste sombre pour l'autonomie et la lisibilité au poignet, cohérent avec l'usage majoritaire des watchfaces sport.
|
||||||
|
- Fond : `#080A12`.
|
||||||
|
- Primaire (or) `#C9A24A` : valeurs numériques, éléments actionnables actifs.
|
||||||
|
- Accent (crimson) `#D72638` : liseré, alertes ponctuelles (ex. connexion perdue), jamais pour du texte long.
|
||||||
|
- Texte principal `#F5F1E8`, texte secondaire `#A7ADBA`.
|
||||||
|
- Bordure/liseré `#242A3A`.
|
||||||
|
- Succès `#2ECC71`, Erreur `#FF4D5E` (repris tels quels, thème sombre).
|
||||||
|
|
||||||
|
### Typographie
|
||||||
|
Anton pour tout chiffre affiché en grand (chrono, série, score), Archivo pour les libellés et le texte courant — cohérent avec le reste de l'app. Sur cadran rond, réduire d'un cran les tailles de référence téléphone (le champ visuel est plus petit) : chiffre principal 36-44px au lieu de 44-52px, labels 11-12px.
|
||||||
|
|
||||||
|
### Forme
|
||||||
|
- Rayon de bordure 6px conservé sur les cartes/badges internes, mais **pas de carte pleine occupant tout le cadran** : préférer un fond uni + un seul bloc de contenu centré, pour respecter la forme circulaire sans créer d'angles morts dans les coins du cercle.
|
||||||
|
- Liseré crimson 2px : conservé mais réduit à un arc ou une barre courte en haut du cadran (pas une bordure complète qui serait coupée par la forme ronde), utilisé uniquement sur l'écran "séance active" comme rappel de marque discret.
|
||||||
|
|
||||||
|
### Écrans et états (spec par variante)
|
||||||
|
|
||||||
|
**1. No-session (montre ouverte sans séance en cours côté téléphone)**
|
||||||
|
- Logo "GT" (cf. [[gametime-visual-identity]] / #112) centré, taille réduite, en primaire or sur fond `#080A12`.
|
||||||
|
- Sous le logo, un texte secondaire unique : « Aucune séance en cours ». Pas de bouton d'action (la montre ne démarre pas de séance elle-même, cf. principe satellite [[gametime-watch-companion-implementation]]) — cohérence avec le rôle de miroir, pas de source d'initiative.
|
||||||
|
|
||||||
|
**2. Séance active (écran principal)**
|
||||||
|
- Une seule donnée dominante centrée : selon le contexte, le compteur de série (« SÉRIE » label + « 2/4 » en Anton or, cf. [[gametime-ux-series-counter]]) ou le chrono (`mm:ss` Anton) si un chrono est en cours — même logique de priorité que la notification (#108) : chrono actif prioritaire sur série/reps.
|
||||||
|
- Nom de l'exercice en Archivo 600, sous la donnée dominante, tronqué avec ellipse si trop long (pas de scroll de texte sur un cadran, ça fatigue l'œil).
|
||||||
|
- Fil de contexte minimal en pied de cadran, très discret (texte secondaire, petite taille) : `3/8` (exercice) — pas le programme complet, pas la place pour ça sur ce format.
|
||||||
|
- Bloc score (+/-) si applicable : cf. spec dédiée #118, coexiste avec cet écran sans redondance (le score remplace la donnée dominante quand c'est la mesure pertinente de l'étape, sinon reste en secondaire sous le nom d'exercice).
|
||||||
|
|
||||||
|
**3. Repos**
|
||||||
|
- Fond identique, mais bascule de la donnée dominante sur le décompte repos (`mm:ss`, Anton, or) avec label « REPOS » au-dessus (Archivo 600 uppercase, texte secondaire) — miroir de la variante téléphone.
|
||||||
|
- Sous le chrono, « Ensuite : <exercice> » + série si pertinente (cf. [[gametime-ux-series-counter]] écran Repos), tronqué si besoin.
|
||||||
|
|
||||||
|
**4. Actions (confirmation / contrôle ponctuel)**
|
||||||
|
- Réservé aux interactions qui nécessitent un accusé (ex. action destructive ou ambiguë déclenchée par erreur) : plein cadran, fond `#080A12`, question courte en Archivo 600 blanc centrée, deux zones tactiles empilées (pas côte à côte, plus sûr au doigt sur petit cadran) : action principale en haut (or, texte foncé), annuler en bas (contour uniquement, texte secondaire). Réservé aux cas rares — le MVP montre n'a normalement pas d'action destructive propre (pause/next restent pilotés depuis le téléphone dans ce périmètre, cf. #118 pour le score qui est la seule commande montre→téléphone du MVP).
|
||||||
|
|
||||||
|
**5. Connexion perdue**
|
||||||
|
- Le contenu de la dernière donnée connue **reste affiché** (jamais d'écran vide brutal — cohérent [[gametime-online-layer-philosophy]] : dernière valeur connue plutôt qu'un vide trompeur), mais assombri (opacité réduite ~50%) avec une icône discrète de liaison rompue en haut du cadran, accompagnée du texte secondaire « Connexion au téléphone perdue » en petit, sans bloquer la lecture de la donnée figée en dessous.
|
||||||
|
- Pas de bouton "Réessayer" manuel — la reconnexion est automatique dès que le lien Data Layer revient (cohérent avec le resync déjà implémenté #91-D), l'écran repasse en pleine opacité dès la projection à jour reçue.
|
||||||
|
|
||||||
|
### Hors périmètre MVP
|
||||||
|
- Personnalisation du cadran (choix utilisateur d'un thème/skin) — un seul habillage, non configurable.
|
||||||
|
- Watchface complication (widget montre hors app) — pas dans ce ticket, sujet distinct si demandé un jour.
|
||||||
|
- Animation de transition élaborée entre écrans — un simple fondu suffit, pas de chorégraphie custom à spécifier ici.
|
||||||
25
.ideai/tickets/115/issue.md
Normal file
25
.ideai/tickets/115/issue.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
id: "c7daaacd-8002-4280-b3a8-5e60a076db06"
|
||||||
|
number: 115
|
||||||
|
title: "#104-A [UX] Refonte UI montre alignée sur la DA Court Blazer"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: [{"target":"#104","kind":"relatesTo"}]
|
||||||
|
agentRefs: [{"agentId":"f3408f5d-469c-4f64-9485-d8b218f3ff26","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
createdAt: 1785093288487
|
||||||
|
updatedAt: 1785134675276
|
||||||
|
version: 3
|
||||||
|
---
|
||||||
|
Partie du ticket #104.
|
||||||
|
|
||||||
|
Concevoir la refonte visuelle de l'application montre pour coller davantage à la DA téléphone :
|
||||||
|
- fond sombre ;
|
||||||
|
- accent doré ;
|
||||||
|
- liserés rouges ;
|
||||||
|
- hiérarchie typographique claire sur cadran rond ;
|
||||||
|
- variantes no-session, séance active, repos, actions, connexion perdue.
|
||||||
|
|
||||||
|
Définition de done : spec UX exploitable sans arbitrage utilisateur supplémentaire.
|
||||||
27
.ideai/tickets/116/carnet.md
Normal file
27
.ideai/tickets/116/carnet.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#116"
|
||||||
|
version: 1
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedAt: 1785093288487
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audit DevFrontend — 2026-07-27
|
||||||
|
|
||||||
|
- Refonte montre Court Blazer déjà présente : thème sombre, typographies Anton/Archivo, valeur dominante, actions compactes, états no-session/repos/connexion perdue.
|
||||||
|
- L'écran séance montre ne réintroduit pas de scroll vertical ; la navigation reste un `PageView` séance/actions.
|
||||||
|
- Layout contraint au diamètre via `_RoundScaffold` et textes protégés par `FittedBox`/`maxLines`/ellipsis.
|
||||||
|
- Aucun écart visible supplémentaire corrigible sans test device identifié.
|
||||||
|
|
||||||
|
À valider sur device : rendu cadran rond réel, absence d'overflow sur formats Wear OS ciblés.
|
||||||
|
|
||||||
|
## Correctif QA DevFrontend — 2026-07-27
|
||||||
|
|
||||||
|
- Ajustement compact appliqué au contenu actif via `_ScaledContent` pour absorber les écarts de police/viewport du test 192x192 sans changer la direction UI Court Blazer.
|
||||||
|
- Validation locale : test widget compact OK, analyse Dart ciblée OK, build APK debug montre OK.
|
||||||
|
|
||||||
|
## Implémentation DevFrontend — 2026-07-26
|
||||||
|
|
||||||
|
- Refonte UI montre appliquée sur `watch_session_screen.dart` et `watch_theme.dart` : thème sombre Court Blazer, or pour les valeurs dominantes, liseré crimson court, typographies Anton/Archivo embarquées dans `watch_app`.
|
||||||
|
- États couverts : no-session avec marque GT, séance active avec priorité chrono/série, repos avec `Ensuite : ...`, actions compactes, connexion perdue avec dernière projection conservée et assombrie.
|
||||||
|
- Les vues actives sont bornées dans un carré basé sur le viewport et ne reposent plus sur du scroll pour tenir sur le cadran.
|
||||||
|
- Vérification : `flutter analyze` watch_app OK ; `./gradlew assembleDebug` watch_app OK avec JDK 21 et répertoires utilisateur redirigés.
|
||||||
24
.ideai/tickets/116/issue.md
Normal file
24
.ideai/tickets/116/issue.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
id: "dfb69f8b-fed6-477c-b8ee-94eb00ce6cc2"
|
||||||
|
number: 116
|
||||||
|
title: "#104-B [DevFrontend] Implémenter la refonte UI montre"
|
||||||
|
status: "QA"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: [{"target":"#104","kind":"relatesTo"},{"target":"#115","kind":"dependsOn"}]
|
||||||
|
agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
createdAt: 1785093288487
|
||||||
|
updatedAt: 1785093288487
|
||||||
|
version: 1
|
||||||
|
---
|
||||||
|
Partie du ticket #104, dépend de #115.
|
||||||
|
|
||||||
|
Appliquer la refonte UX sur l'app montre existante :
|
||||||
|
- thème et composants ;
|
||||||
|
- écrans actifs/repos/actions/no-session/perte de connexion ;
|
||||||
|
- respect des contraintes rondes Wear OS sans overflow ;
|
||||||
|
- build montre propre.
|
||||||
|
|
||||||
|
Définition de done : surfaces conformes à la spec UX, installables et sans régression majeure évidente.
|
||||||
7
.ideai/tickets/117/carnet.md
Normal file
7
.ideai/tickets/117/carnet.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#117"
|
||||||
|
version: 2
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785142171875
|
||||||
|
---
|
||||||
|
|
||||||
24
.ideai/tickets/117/issue.md
Normal file
24
.ideai/tickets/117/issue.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
id: "32b59d5b-28ae-469e-9f6b-ab7a98801112"
|
||||||
|
number: 117
|
||||||
|
title: "#104-C [QA] Validation refonte UI montre"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: [{"target":"#104","kind":"relatesTo"},{"target":"#116","kind":"dependsOn"}]
|
||||||
|
agentRefs: [{"agentId":"7efa512f-3b3a-47b5-ade0-a2dd13073055","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785093288487
|
||||||
|
updatedAt: 1785142171875
|
||||||
|
version: 2
|
||||||
|
---
|
||||||
|
Partie du ticket #104, dépend de #116.
|
||||||
|
|
||||||
|
Valider la refonte UI montre :
|
||||||
|
- cohérence visuelle avec la DA ;
|
||||||
|
- absence d'overflow visible ;
|
||||||
|
- lisibilité sur cadran rond ;
|
||||||
|
- build/install et parcours principaux.
|
||||||
|
|
||||||
|
Définition de done : rapport QA vert ou rapport d'échec concret.
|
||||||
35
.ideai/tickets/118/carnet.md
Normal file
35
.ideai/tickets/118/carnet.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#118"
|
||||||
|
version: 3
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedAt: 1785137390315
|
||||||
|
---
|
||||||
|
|
||||||
|
## #118 — Contrôles score +/- sur montre (UX, 2026-07-26)
|
||||||
|
|
||||||
|
Statut : **cadrage UX exploitable, prêt pour Architect (#119) et implémentation**, pas de blocage.
|
||||||
|
|
||||||
|
### Principe directeur
|
||||||
|
La montre est un **satellite d'affichage et de commande**, jamais une source de vérité ([[gametime-watch-companion-implementation]] : « la montre n'applique JAMAIS une commande localement, recalage sur projection confirmée »). Les contrôles score doivent rendre ce principe visible à l'utilisateur pour éviter toute confusion en cas de latence, sans jamais donner l'impression d'un mode local/offline.
|
||||||
|
|
||||||
|
### Emplacement et affordance
|
||||||
|
- Sur l'écran "séance active" de la montre (refonte DA, cf. [[gametime-ux-score-chrono]] pour le vocabulaire "Score"/"Score chrono"), le bloc score reprend la même hiérarchie que le compteur de série actuel côté téléphone ([[gametime-ux-series-counter]]) : grande valeur numérique Anton, couleur primaire or, au centre du cadran.
|
||||||
|
- Deux zones tactiles larges de part et d'autre de la valeur : `−` à gauche, `+` à droite. Cibles tactiles ≥ 48dp (cadran rond, appui au doigt, pas de precision souris) — priorité absolue à éviter l'appui accidentel du bouton opposé plutôt qu'à la densité d'info.
|
||||||
|
- Rotation de la couronne physique (si le boîtier en a une) acceptée comme entrée alternative +/- si le device la supporte, en complément des zones tactiles — pas un remplacement, car tous les boîtiers Wear OS n'ont pas de couronne rotative.
|
||||||
|
- Si l'exercice n'a **pas** de score configuré : le bloc est simplement absent de l'écran (pas de placeholder grisé, pas de "score non disponible") — l'écran affiche seulement les mesures actives de l'exercice (reps/temps), cohérent avec « le vide se conçoit, mais ici il n'y a rien à dire ».
|
||||||
|
|
||||||
|
### Prévention des erreurs d'appui
|
||||||
|
- **Debounce visuel immédiat, confirmation serveur différée** : au tap, la valeur affichée s'incrémente/décrémente instantanément en local (feedback optimiste, indispensable pour un contrôle répété rapide) mais passe dans un **état visuellement "en attente"** (valeur légèrement atténuée / opacité réduite) jusqu'à confirmation de la projection téléphone → montre. Dès la confirmation reçue, retour à l'opacité pleine. Ceci rend le principe "téléphone source de vérité" visible sans bloquer l'usage.
|
||||||
|
- Pas de confirmation modale par tap (casserait l'usage répété typique d'un score qui s'incrémente vite, ex. compte de points) — la prévention passe par la taille des cibles et le split gauche/droite, pas par une friction supplémentaire.
|
||||||
|
- Bouton `−` désactivé (grisé, non actionnable) si le score est déjà à son minimum autorisé (0, ou borne définie par l'exercice) — évite un appui qui ne ferait rien et qui interrogerait l'utilisateur.
|
||||||
|
- Pas de "annuler le dernier appui" dédié sur la montre : la correction se fait avec les mêmes boutons +/-, symétriques. Une correction plus lourde (erreur de plusieurs points) se fait sur le téléphone, source de vérité complète.
|
||||||
|
|
||||||
|
### Comportement si connexion lente
|
||||||
|
- Au-delà d'un délai perçu (repère : > 1,5-2s sans confirmation, valeur à affiner par Architect selon la latence réelle mesurée du bridge), la valeur "en attente" affiche un indicateur discret supplémentaire (petit point pulsé sous la valeur) signalant que la synchronisation est toujours en cours — jamais de message d'erreur intrusif, jamais de blocage des taps suivants (cohérent avec [[gametime-online-layer-philosophy]] : pas de popup, pas d'interruption).
|
||||||
|
- Si la commande finit par échouer (timeout définitif, perte de connexion Data Layer) : la montre **se recale silencieusement** sur la dernière valeur confirmée par le téléphone (peut donc "reculer" visuellement si des taps optimistes n'ont pas abouti) — pas de message d'erreur sur la montre. Un état de connexion perdue générique doit déjà couvrir ce cas au niveau écran (cf. #115, variante "connexion perdue"), pas la peine de dupliquer un message spécifique au score.
|
||||||
|
- Aucune mention "hors ligne" ou "mode local" nulle part sur cet écran : le contrat visuel est "j'affiche ce que dit le téléphone, avec un délai le cas échéant", jamais "je fonctionne seul".
|
||||||
|
|
||||||
|
### Hors périmètre MVP
|
||||||
|
- Saisie d'une valeur exacte au clavier depuis la montre (le téléphone reste l'endroit pour une correction lourde).
|
||||||
|
- Undo dédié / historique des taps sur la montre.
|
||||||
|
- Haptique différenciée par valeur (un retour haptique simple par tap suffit, cohérent avec l'existant #91-F).
|
||||||
26
.ideai/tickets/118/issue.md
Normal file
26
.ideai/tickets/118/issue.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
id: "6295aa3e-9b0b-4b94-a2fb-67057af1adc5"
|
||||||
|
number: 118
|
||||||
|
title: "#105-A [UX] Conception des contrôles score +/- sur montre"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: [{"target":"#105","kind":"relatesTo"}]
|
||||||
|
agentRefs: [{"agentId":"f3408f5d-469c-4f64-9485-d8b218f3ff26","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
createdAt: 1785093288487
|
||||||
|
updatedAt: 1785137390315
|
||||||
|
version: 3
|
||||||
|
---
|
||||||
|
Partie du ticket #105.
|
||||||
|
|
||||||
|
Définir l'UX du score incrémentable/décrémentable depuis la montre pour les exercices qui portent un score :
|
||||||
|
- emplacement et affordance des actions ;
|
||||||
|
- prévention des erreurs d'appui ;
|
||||||
|
- comportement si la connexion est lente ;
|
||||||
|
- cas où l'exercice n'a pas de score.
|
||||||
|
|
||||||
|
Contrainte : le téléphone reste source de vérité, la montre ne doit pas laisser croire à un mode offline local.
|
||||||
|
|
||||||
|
Définition de done : spec UX claire pour extension du companion montre.
|
||||||
101
.ideai/tickets/119/carnet.md
Normal file
101
.ideai/tickets/119/carnet.md
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#119"
|
||||||
|
version: 6
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedAt: 1785137408716
|
||||||
|
---
|
||||||
|
|
||||||
|
## #119 — Extension watch bridge pour score +/- (Architect, 2026-07-26)
|
||||||
|
|
||||||
|
Statut : **cadrage exploitable, prêt pour DevBackend/DevFrontend**, pas de blocage. Basé sur le cadrage UX #118 et l'existant réel (`packages/watch_bridge_contract`, `WatchCompanionCommandHandler`, cf. [[gametime-watch-companion-implementation]]).
|
||||||
|
|
||||||
|
### Portée fonctionnelle
|
||||||
|
Ce ticket couvre le **score en mode `manual`** (`ScoreInputMode.manual`) uniquement. Le score chronométré (`stopwatch`, cf. [[gametime-architecture-score-chrono]]) ne s'"incrémente" pas — il se démarre/arrête ; il n'est pas concerné par ce +/- et reste hors périmètre ici.
|
||||||
|
|
||||||
|
### Constat sur l'existant
|
||||||
|
- `WatchCommandEnvelope` (dans `watch_bridge_contract`) n'a **aucun champ payload générique** — chaque commande existante est un `WatchCommandType` sans paramètre, tout l'effet dérivant de l'état serveur + du type. On reste cohérent avec ce pattern : pas de champ `delta` générique, deux types discrets.
|
||||||
|
- **Aucun état vivant n'existe aujourd'hui pour le score manuel pendant une série** — contrairement au score chrono qui a `ActiveScoreStopwatchState` (persistant, clé `sessionId+programIndex+exerciseIndex+setIndex`). Le score manuel est aujourd'hui un simple `TextEditingController` côté UI, lu une seule fois au moment de "Terminer la série" (`workout_execution_screen.dart`). Ce n'est pas suffisant : le +/- montre doit être visible en direct, y compris côté téléphone si l'app est au premier plan.
|
||||||
|
|
||||||
|
### Décision structurante : le score manuel devient un état de séance vivant
|
||||||
|
Créer une nouvelle table Drift/entité **`ActiveManualScoreState`**, sur exactement le même principe que `ActiveScoreStopwatchState` :
|
||||||
|
```dart
|
||||||
|
class ActiveManualScoreState {
|
||||||
|
final int programIndex, exerciseIndex, setIndex; // même clé logique que ActiveScoreStopwatchState
|
||||||
|
final double value; // valeur courante, plancher 0
|
||||||
|
final DateTime updatedAt;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Absence de ligne = valeur par défaut `0`.
|
||||||
|
- Port repository : `findManualScoreState` / `saveManualScoreState` / `deleteManualScoreState` sur `ActiveSessionRepository`, en parallèle des méthodes `*ScoreStopwatchState` existantes.
|
||||||
|
- Suppression de l'état à la fin/passage de la série (même cycle de vie que le chrono score).
|
||||||
|
- **Plancher** : `0` fixe pour le MVP. Il n'existe aujourd'hui aucun champ "score minimum" configurable sur `Exercise`/`ProgramExercise` — en ajouter un serait un cadrage spéculatif hors demande explicite. Si un plancher par exercice est réellement voulu, c'est un ticket séparé, pas une extension implicite ici.
|
||||||
|
- **Pas de plafond** au MVP (cohérent avec le champ libre actuel qui n'a pas de borne haute).
|
||||||
|
- **Pas de palier configurable** : incrément fixe `+1`/`-1` (cas d'usage dominant : tally de points/répétitions comptés un par un). Pas de champ "step" à inventer sans besoin exprimé.
|
||||||
|
|
||||||
|
### Use cases (application layer)
|
||||||
|
Deux nouvelles méthodes sur `ActiveWorkoutSessionUseCases`, miroir de `startScoreStopwatch`/`stopScoreStopwatch` :
|
||||||
|
```dart
|
||||||
|
Future<void> incrementManualScore(sessionId, ...position...);
|
||||||
|
Future<void> decrementManualScore(sessionId, ...position...); // no-op si déjà à 0
|
||||||
|
```
|
||||||
|
`recordCurrentSetResult(...)` doit lire `ActiveManualScoreState.value` comme **valeur par défaut** de `actualScore` pour la série courante — mais rester **surchageable** par une édition manuelle explicite côté téléphone au moment de terminer la série (même mécanique que "Modifier le temps" déjà en place pour le score chrono, cf. [[gametime-architecture-score-chrono]]) : le paramètre existant de `recordCurrentSetResult` prend priorité s'il est explicitement fourni par l'UI, sinon on retombe sur l'état vivant.
|
||||||
|
|
||||||
|
### Pivot UI téléphone (important, à transmettre à DevFrontend)
|
||||||
|
Le score manuel n'est plus "un champ texte soumis une fois à la fin" — c'est désormais un **état de séance vivant**, au même titre que le chrono score. Le champ texte actuel (`_scoreController`) doit être re-branché en lecture/écriture sur `ActiveManualScoreState` (via le flux de projection/session déjà utilisé pour le reste de l'écran d'exécution), pour que toute modification faite depuis la montre soit visible immédiatement à l'écran si le téléphone est au premier plan.
|
||||||
|
|
||||||
|
### Extension du contrat `watch_bridge_contract`
|
||||||
|
```dart
|
||||||
|
enum WatchCommandType {
|
||||||
|
..., // types existants inchangés
|
||||||
|
incrementScore,
|
||||||
|
decrementScore,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Ajout par extension d'enum (Open/Closed) — aucune modification des types/cas existants.
|
||||||
|
|
||||||
|
`WatchSessionProjection` (phone→watch) doit exposer deux champs supplémentaires pour que la montre sache afficher/masquer le bloc et l'état du bouton `−` sans dupliquer la logique métier :
|
||||||
|
```dart
|
||||||
|
final bool hasManualScore; // bloc absent sur la montre si false (cf. #118 UX)
|
||||||
|
final double? currentManualScoreValue; // valeur affichée, null si hasManualScore == false
|
||||||
|
final bool canDecrementScore; // false si déjà au plancher — logique du plancher reste calculée côté téléphone, jamais dupliquée sur la montre (montre = satellite, cf. [[gametime-watch-companion-implementation]])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Idempotence et applicabilité (`WatchCompanionCommandHandler`)
|
||||||
|
- `_isApplicable()` : `incrementScore`/`decrementScore` applicables ssi phase de série active **et** `hasManualScore == true` sur la projection courante. Ajouter cette entrée sans toucher aux cas existants.
|
||||||
|
- **Point d'attention explicite pour l'implémentation** : contrairement à des commandes ponctuelles (ex. `finishCurrentSet`), le +/- est tapé **rapidement et répétitivement** (cf. UX #118 : "usage répété rapide"). L'applicabilité de ces deux types ne doit **pas** dépendre de l'exactitude de `expectedRevision` — seulement de la phase/du mode. Chaque tap génère un nouveau `commandId` (idempotence par commande individuelle, déjà gérée par `_handledCommands`), mais un léger décalage de révision entre deux taps rapprochés (le premier tap ayant déjà fait avancer la révision avant que le second parte) ne doit **jamais** produire `rejectedStaleRevision` pour ces deux types — sinon l'usage répété rapide voulu par l'UX serait cassé. C'est un cas différent des commandes de transition d'étape, qui elles peuvent légitimement dépendre de la révision exacte.
|
||||||
|
- Chaque commande acceptée déclenche `_emitProjectionAfterCommand()` comme les autres — republie vers la montre (valeur + `canDecrementScore` à jour) et vers le futur coordinateur de notification (#109) sans code spécifique supplémentaire.
|
||||||
|
|
||||||
|
### Comportement en cas de latence/échec (UX #118, déjà cadré, rappel technique)
|
||||||
|
Aucun contrat supplémentaire requis ici : le mécanisme optimiste "tap local → état atténué → confirmation" est **entièrement côté montre/présentation** (watch_app), consommant `currentManualScoreValue`/révision déjà remontés. Pas de nouvel état serveur à créer pour ça.
|
||||||
|
|
||||||
|
### Migration
|
||||||
|
Nouvelle table Drift `ActiveManualScoreState` ⇒ bump du `schemaVersion` local (vérifier la valeur courante au moment de l'implémentation, ne pas supposer un numéro figé ici).
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
- Contrat : `dart test` sur les deux nouveaux `WatchCommandType`, même fichier `packages/watch_bridge_contract/test/`.
|
||||||
|
- Handler : étendre `test/application/watch_companion_command_handler_test.dart` — cas incrément/décrément normal, décrément au plancher (no-op, ack `acceptedNoOp`), applicabilité refusée hors mode manuel, rafale de commandes avec révisions décalées (non rejetées).
|
||||||
|
- Projection : étendre `test/application/watch_companion_projection_test.dart` pour les 3 nouveaux champs.
|
||||||
|
- Use case : test `recordCurrentSetResult` avec état vivant présent + override manuel explicite.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audit Architect — implémentation constatée dans le worktree (2026-07-27)
|
||||||
|
|
||||||
|
Statut mis à jour : **implémenté de bout en bout, conforme au cadrage**, un garde-fou de cohérence à fermer.
|
||||||
|
|
||||||
|
Vérification du code réel : `ActiveManualScoreState` (`lib/domain/entities.dart`, plancher enforced en plus dans le constructeur via `_requireNullableNonNegativeDouble`), CRUD complet sur `ActiveSessionRepository`/`DriftActiveSessionRepository`, `incrementManualScore`/`decrementManualScore`/`_updateManualScore` (`use_cases.dart`, clamp `[0, +∞)`, `ManualScoreUpdateResult.changed` pour distinguer accepté/no-op), `_allowsStaleRevision(type)` dans `WatchCompanionCommandHandler` qui exempte précisément `incrementScore`/`decrementScore` du rejet `rejectedStaleRevision` — exactement le garde-fou cadré ci-dessus pour l'usage répété rapide. Table Drift `active_manual_score_states` créée avec `CHECK(value >= 0)` (plancher renforcé aussi en base), `schemaVersion` bumpé à 20 avec migration dédiée, table enregistrée dans `_syncableTableNames`. Contrat `watch_bridge_contract` étendu exactement comme cadré (`hasManualScore`/`currentManualScoreValue`/`canDecrementScore`, `fromJson` avec défauts sûrs). Côté montre : `incrementScore`/`decrementScore` avec mise à jour optimiste instantanée, opacité atténuée pendant l'attente, recalage silencieux sur échec/timeout (`requestResync()`, aucun message d'erreur) — conforme au cadrage UX #118.
|
||||||
|
|
||||||
|
**Garde-fou à fermer avant merge** : le pivot téléphone n'est **qu'à moitié fait**. L'écran d'exécution (`workout_execution_screen.dart`) **lit** bien `ActiveManualScoreState` en direct (`_syncManualScoreInput`, polling via le ticker 1s existant, ignoré si le champ a le focus) — donc une correction faite depuis la montre remonte bien à l'écran téléphone. Mais l'inverse est manquant : **taper une valeur dans le champ texte du téléphone n'écrit jamais dans `ActiveManualScoreState`** ; le texte tapé n'est consommé qu'au moment de `finishCurrentSet`, en paramètre explicite qui écrase la valeur vivante seulement à cet instant précis. Conséquence concrète : si l'utilisateur corrige le score sur le téléphone puis tape encore une fois sur la montre avant de terminer la série, le tap montre repart de l'**ancienne** valeur persistée (pas de la correction tapée), et à la validation de la série le champ téléphone (dernière source lue) écrase silencieusement ce que la montre vient d'appliquer — un des deux côtés perd sa modification sans avertissement, alors que l'UX #118 attend explicitement que "la correction plus lourde se fait sur le téléphone, source de vérité complète" en cohérence avec ce que voit la montre à ce moment.
|
||||||
|
|
||||||
|
**Contrat à fermer pour DevFrontend** : toute édition explicite du champ texte téléphone (`onSubmitted`/`onEditingComplete`/perte de focus) doit **écrire à travers** vers `ActiveManualScoreState` (ex. un `setManualScore(sessionId, position, value)` sur `ActiveWorkoutSessionUseCases`, à ajouter en miroir des deux méthodes existantes, avec le même clamp `[0, +∞)`), pas seulement être lue en fin de série. Cela rend les deux sens de synchronisation (montre→téléphone déjà fait, téléphone→montre à fermer) symétriques et élimine la fenêtre de divergence silencieuse. Sans ce correctif, ne pas considérer le lot #119 comme fonctionnellement complet du point de vue de l'invariant "le téléphone reste toujours la source de vérité complète" posé par UX #118.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vérification Architect — gap résiduel fermé (2026-07-27, second passage)
|
||||||
|
|
||||||
|
Le gap ci-dessus est **résolu dans le worktree actuel** : `ActiveWorkoutSessionUseCases.setManualScore` (`lib/application/use_cases.dart:2730`) existe et applique le même clamp `[0, +∞)` que `increment`/`decrementManualScore`. Côté écran (`lib/presentation/workout_execution_screen.dart`), `_persistManualScoreInputOnce` (ligne 702) écrit à travers vers `setManualScore` à chaque édition explicite validée, appelée par :
|
||||||
|
- perte de focus (`_handleScoreFocusChange`, ligne 139-140) ;
|
||||||
|
- `onScoreSubmitted`/`onEditingComplete` câblés sur `_persistManualScoreInput` (lignes 257-259, 309-311) ;
|
||||||
|
- juste avant `recordCurrentSetResult` dans `_recordAndAdvance` (ligne 1426).
|
||||||
|
|
||||||
|
Les deux sens de synchronisation sont désormais symétriques. L'invariant UX #118 ("le téléphone reste la source de vérité complète") est respecté. **Aucune action supplémentaire requise sur #119** ; le lot est fonctionnellement complet.
|
||||||
24
.ideai/tickets/119/issue.md
Normal file
24
.ideai/tickets/119/issue.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
id: "2baf0fdb-3407-43a8-bdbf-d85aaad9a640"
|
||||||
|
number: 119
|
||||||
|
title: "#105-B [Architect] Extension watch bridge pour score +/-"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: [{"target":"#105","kind":"relatesTo"},{"target":"#118","kind":"dependsOn"}]
|
||||||
|
agentRefs: [{"agentId":"f8f40941-ecf7-4830-b9de-8818a099f448","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
createdAt: 1785093288487
|
||||||
|
updatedAt: 1785137408716
|
||||||
|
version: 6
|
||||||
|
---
|
||||||
|
Partie du ticket #105, dépend de #118.
|
||||||
|
|
||||||
|
Cadrer l'extension des contrats montre/téléphone pour supporter l'incrément et le décrément de score :
|
||||||
|
- nouveaux types de commande et acks éventuels ;
|
||||||
|
- notion d'idempotence ;
|
||||||
|
- règles métier si plusieurs scores sont possibles ;
|
||||||
|
- impact sur la projection montre.
|
||||||
|
|
||||||
|
Définition de done : cadrage technique et contrats de transport exploitables par backend/frontend.
|
||||||
22
.ideai/tickets/120/carnet.md
Normal file
22
.ideai/tickets/120/carnet.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#120"
|
||||||
|
version: 2
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedAt: 1785137390334
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implémentation DevBackend — 2026-07-26
|
||||||
|
|
||||||
|
- Contrat montre étendu avec `WatchCommandType.incrementScore` / `decrementScore` et champs de projection `hasManualScore`, `currentManualScoreValue`, `canDecrementScore`.
|
||||||
|
- Score manuel routé via `WatchCompanionCommandHandler` vers `ActiveWorkoutSessionUseCases.incrementManualScore` / `decrementManualScore`, sans logique métier dans le bridge.
|
||||||
|
- Support strict `ScoreInputMode.manual` : les commandes score sont applicables seulement si la projection expose un score manuel, et le use case revalide le snapshot courant.
|
||||||
|
- État vivant ajouté pour le score manuel (`ActiveManualScoreState`) avec table Drift `active_manual_score_states` et migration schéma 20.
|
||||||
|
- Acks couverts : décrément au plancher `0` => `acceptedNoOp`; rafale score avec révision décalée acceptée; commandes non-score gardent la vérification stricte de révision.
|
||||||
|
|
||||||
|
Vérifications exécutées :
|
||||||
|
- `dart test` dans `packages/watch_bridge_contract`
|
||||||
|
- `flutter test test/application/watch_companion_command_handler_test.dart test/application/watch_companion_projection_test.dart`
|
||||||
|
- `flutter test test/application/use_cases_test.dart test/application/watch_companion_command_handler_test.dart test/application/watch_companion_projection_test.dart`
|
||||||
|
- `flutter test test/infrastructure/watch_bridge/wear_data_layer_adapter_test.dart`
|
||||||
|
- `dart analyze` ciblé sur les fichiers touchés : sortie 0 (reste un lint `prefer_single_quotes` préexistant dans `tables.dart`)
|
||||||
|
- `flutter analyze` complet : plus d'erreur, échec uniquement sur infos/lints préexistants
|
||||||
20
.ideai/tickets/120/issue.md
Normal file
20
.ideai/tickets/120/issue.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
id: "30e7270a-26a3-40ad-ac74-01da7882405a"
|
||||||
|
number: 120
|
||||||
|
title: "#105-C [DevBackend] Routing score montre vers les use cases d'exécution"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: [{"target":"#105","kind":"relatesTo"},{"target":"#119","kind":"dependsOn"}]
|
||||||
|
agentRefs: [{"agentId":"10ee045b-1c41-479e-ba03-dceed9edd495","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
createdAt: 1785093288487
|
||||||
|
updatedAt: 1785137390334
|
||||||
|
version: 2
|
||||||
|
---
|
||||||
|
Partie du ticket #105, dépend de #119.
|
||||||
|
|
||||||
|
Brancher les commandes score +/- issues de la montre sur les use cases d'exécution existants, sans dupliquer la logique métier.
|
||||||
|
|
||||||
|
Définition de done : score mis à jour côté téléphone via les commandes montre, acks cohérents, tests unitaires pertinents au vert.
|
||||||
36
.ideai/tickets/121/carnet.md
Normal file
36
.ideai/tickets/121/carnet.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#121"
|
||||||
|
version: 1
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedAt: 1785093288487
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implémentation DevFrontend — 2026-07-26
|
||||||
|
|
||||||
|
- Contrôles score manuel `−` / `+` ajoutés sur l'écran séance active montre uniquement quand la projection expose `hasManualScore`.
|
||||||
|
- Feedback optimiste immédiat côté montre : valeur locale atténuée pendant pending, indicateur discret après délai, recalage sur projection téléphone confirmée.
|
||||||
|
- Commandes envoyées via les types backend #120 `incrementScore` / `decrementScore`; la montre ne persiste ni ne décide la valeur source.
|
||||||
|
- Les commandes score ont un pending dédié et ne bloquent pas les actions de transition existantes; la sync/reconnexion montre existante reste inchangée.
|
||||||
|
- L'écran téléphone relit l'état vivant `ActiveManualScoreState` et met à jour le champ score manuel si l'utilisateur n'est pas en train de l'éditer.
|
||||||
|
|
||||||
|
Vérifications exécutées :
|
||||||
|
- `dart format` ciblé OK.
|
||||||
|
- `git diff --check` OK.
|
||||||
|
- `dart analyze watch_app/lib` OK.
|
||||||
|
- `dart analyze lib test/application/session_notification_use_cases_test.dart` OK avec infos préexistantes uniquement.
|
||||||
|
- `dart test` dans `packages/watch_bridge_contract` OK.
|
||||||
|
|
||||||
|
Non fait / à valider :
|
||||||
|
- `flutter test` / build Wear OS via wrapper Flutter bloqués par cache Flutter en lecture seule.
|
||||||
|
- Test réel montre+téléphone requis : taps rapides +/-, pending visuel, recalage après ack/projection, décrément à 0, perte/reprise connexion.
|
||||||
|
|
||||||
|
## Complément symétrie téléphone ↔ montre — 2026-07-27
|
||||||
|
|
||||||
|
- Symétrie téléphone ↔ montre confirmée dans le worktree : le téléphone persiste le champ score manuel via `setManualScore` à la perte de focus et avant `Terminer la série`.
|
||||||
|
- La montre consomme la projection confirmée `currentManualScoreValue` et continue à envoyer uniquement des commandes au téléphone.
|
||||||
|
- Aucun message utilisateur ajouté sur latence watch ; le feedback reste visuel et discret.
|
||||||
|
|
||||||
|
## Correctif QA DevFrontend — 2026-07-27
|
||||||
|
|
||||||
|
- Reproduction QA 192x192 traitée sur la colonne score manuel : `_ManualScoreContent` passe par `_ScaledContent` et une colonne `mainAxisSize.min`, avec boutons score conservés.
|
||||||
|
- Validation locale : test widget compact OK, analyse Dart ciblée OK, build APK debug montre OK.
|
||||||
23
.ideai/tickets/121/issue.md
Normal file
23
.ideai/tickets/121/issue.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
id: "1a1a4f69-c17c-464f-86e5-7ea862a72c9d"
|
||||||
|
number: 121
|
||||||
|
title: "#105-D [DevFrontend] Contrôles score montre synchronisés au téléphone"
|
||||||
|
status: "QA"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: [{"target":"#105","kind":"relatesTo"},{"target":"#118","kind":"dependsOn"},{"target":"#119","kind":"dependsOn"},{"target":"#120","kind":"dependsOn"}]
|
||||||
|
agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
createdAt: 1785093288487
|
||||||
|
updatedAt: 1785093288487
|
||||||
|
version: 1
|
||||||
|
---
|
||||||
|
Partie du ticket #105, dépend de #118, #119 et #120.
|
||||||
|
|
||||||
|
Implémenter les contrôles score +/- sur la montre :
|
||||||
|
- rendu conforme à l'UX ;
|
||||||
|
- latence et pending cohérents ;
|
||||||
|
- recalage exclusif sur l'état confirmé du téléphone.
|
||||||
|
|
||||||
|
Définition de done : interactions score montre fonctionnelles sur build Wear OS.
|
||||||
7
.ideai/tickets/122/carnet.md
Normal file
7
.ideai/tickets/122/carnet.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#122"
|
||||||
|
version: 2
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785142162842
|
||||||
|
---
|
||||||
|
|
||||||
24
.ideai/tickets/122/issue.md
Normal file
24
.ideai/tickets/122/issue.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
id: "c96bfbde-2731-40ab-8b83-3df85ba49cbb"
|
||||||
|
number: 122
|
||||||
|
title: "#105-E [QA] Validation score montre +/-"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: [{"target":"#105","kind":"relatesTo"},{"target":"#121","kind":"dependsOn"}]
|
||||||
|
agentRefs: [{"agentId":"7efa512f-3b3a-47b5-ade0-a2dd13073055","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785093288487
|
||||||
|
updatedAt: 1785142162842
|
||||||
|
version: 2
|
||||||
|
---
|
||||||
|
Partie du ticket #105, dépend de #121.
|
||||||
|
|
||||||
|
Valider le pilotage du score depuis la montre :
|
||||||
|
- incrément/décrément nominaux ;
|
||||||
|
- pas de double application ;
|
||||||
|
- cohérence téléphone/montre ;
|
||||||
|
- comportement si commande rejetée ou connexion perdue.
|
||||||
|
|
||||||
|
Définition de done : rapport QA réel sur build téléphone + montre.
|
||||||
42
.ideai/tickets/123/carnet.md
Normal file
42
.ideai/tickets/123/carnet.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#123"
|
||||||
|
version: 3
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedAt: 1785137390347
|
||||||
|
---
|
||||||
|
|
||||||
|
## #123 — Cadrage transparent des statistiques issues de la montre (UX, 2026-07-26)
|
||||||
|
|
||||||
|
Statut : **CADRAGE UNIQUEMENT, ne pas ouvrir l'implémentation.** Comme décidé dans le carnet #106 (Main), ce ticket doit se combiner avec le cadrage Architect #124 (permissions capteurs, disponibilité partielle, modèle de persistance) avant tout lot d'implémentation. Ce document fixe le **quoi côté UX**, pas le **quand/comment technique**.
|
||||||
|
|
||||||
|
### Cadre produit
|
||||||
|
Demande utilisateur explicite : si la donnée n'est pas disponible (pas de montre, capteur non supporté, capteur refusé), **aucune notification, aucun message, aucun vide visible** — silence total. C'est la contrainte structurante de tout le cadrage.
|
||||||
|
|
||||||
|
### Métriques MVP proposées (à valider par Architect côté faisabilité capteur Wear OS)
|
||||||
|
Périmètre volontairement réduit à ce qui a une valeur de lecture immédiate pour un utilisateur de musculation/sport, sans dupliquer un tracker cardio dédié :
|
||||||
|
1. **Fréquence cardiaque moyenne de la séance** (bpm) — métrique la plus universellement disponible sur Wear OS, et la plus lisible pour l'utilisateur.
|
||||||
|
2. **Fréquence cardiaque max de la séance** (bpm) — complète la moyenne à peu de coût de lecture supplémentaire.
|
||||||
|
3. *(optionnel, second temps, pas bloquant pour le MVP)* Calories estimées si le capteur/API le fournit nativement sans calcul maison — GameTime n'est pas une app de fitness tracking, ne pas construire de modèle de calcul propre.
|
||||||
|
|
||||||
|
Explicitement **hors MVP** pour ce cadrage : VO2 max, zones d'intensité cardio, SpO2, ECG, tout ce qui relève d'un tracker santé complet — hors du produit GameTime (app de séances de sport, pas app santé).
|
||||||
|
|
||||||
|
### Surface de restitution
|
||||||
|
- **Écran de fin de séance / résumé** (le même écran qui affiche déjà la durée totale et le récap des séries) : nouveau bloc optionnel "Fréquence cardiaque" avec deux valeurs (moyenne / max), même traitement visuel que les autres stats du résumé (Anton pour les chiffres, cf. [[gametime-visual-identity]]).
|
||||||
|
- **Historique de séance** (détail d'une séance passée) : même bloc, affiché dans le détail si la donnée existe pour cette séance.
|
||||||
|
- **Pas d'affichage temps réel pendant la séance** dans ce périmètre MVP — pas de graphique cardio live sur l'écran d'exécution téléphone, ni sur la montre. Justification : ajouterait une surface supplémentaire à concevoir et à tester pour une valeur d'usage secondaire (l'utilisateur suit son exercice, pas son cardio, pendant l'effort) ; peut être réévalué plus tard si demande explicite.
|
||||||
|
|
||||||
|
### Silence UX si donnée absente
|
||||||
|
- Montre non appairée, capteur cardio absent, permission refusée, ou données insuffisantes pour un calcul fiable → le bloc "Fréquence cardiaque" **n'apparaît simplement pas** dans le résumé ni l'historique. Pas de placeholder grisé "Non disponible", pas d'icône barrée, pas de bouton "Connecter une montre" inséré au milieu du résumé de séance.
|
||||||
|
- Aucun état d'erreur dédié à concevoir : l'absence de la donnée n'est pas un état de l'écran, c'est une variation normale de contenu (comme une séance sans note, ou sans photo) — cohérent avec [[gametime-online-layer-philosophy]] (silence sur l'indisponibilité, jamais de message intrusif).
|
||||||
|
- Un seul endroit où la fonctionnalité peut être rendue *découvrable* sans être intrusive : dans les réglages de compte/appareils (là où l'utilisateur configure déjà l'appairage montre, hors scope de ce ticket) — pas sur l'écran de séance lui-même.
|
||||||
|
|
||||||
|
### Hiérarchie des métriques (si plusieurs disponibles un jour)
|
||||||
|
Ordre de priorité de lecture déjà anticipé pour ne pas re-arbitrer plus tard si le périmètre s'étend : Fréquence cardiaque (moyenne, max) > Calories > toute métrique additionnelle. La fréquence cardiaque reste toujours la première ligne du bloc, jamais noyée sous des métriques secondaires.
|
||||||
|
|
||||||
|
### Ce qui reste à trancher par Architect (#124) avant ouverture de l'implémentation
|
||||||
|
- Faisabilité et API Wear OS Health Services pour lire bpm moyenne/max sur la durée d'une séance.
|
||||||
|
- Modèle de permission (demande à quel moment du parcours, cohérent avec le principe "jamais de blocage du parcours principal").
|
||||||
|
- Modèle de persistance (nouveau champ sur l'entité séance, ou table séparée) et comportement en cas de séance déjà terminée sans cette donnée (pas de migration rétroactive à inventer).
|
||||||
|
|
||||||
|
### Definition of done de ce ticket UX
|
||||||
|
Ce cadrage est **terminé pour la partie UX** : personne ne doit revenir vers l'utilisateur pour une question de restitution ou de silence UX. La suite (ouverture des lots d'implémentation) dépend uniquement du retour d'Architect sur #124, pas d'un nouvel arbitrage UX.
|
||||||
24
.ideai/tickets/123/issue.md
Normal file
24
.ideai/tickets/123/issue.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
id: "c8f8dbd0-8c20-44a5-8405-47b9a5a05c11"
|
||||||
|
number: 123
|
||||||
|
title: "#106-A [UX] Cadrage transparent des statistiques issues de la montre"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: [{"target":"#106","kind":"relatesTo"}]
|
||||||
|
agentRefs: [{"agentId":"f3408f5d-469c-4f64-9485-d8b218f3ff26","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
createdAt: 1785093288487
|
||||||
|
updatedAt: 1785137390347
|
||||||
|
version: 3
|
||||||
|
---
|
||||||
|
Partie du ticket #106.
|
||||||
|
|
||||||
|
Définir comment des statistiques issues de la montre peuvent enrichir une séance sans jamais dégrader l'expérience quand elles sont absentes :
|
||||||
|
- surface de restitution éventuelle ;
|
||||||
|
- silence UX si montre absente ou capteur indisponible ;
|
||||||
|
- hiérarchie des métriques réellement utiles ;
|
||||||
|
- périmètre MVP raisonnable.
|
||||||
|
|
||||||
|
Définition de done : cadrage UX permettant un arbitrage produit clair avant implémentation.
|
||||||
67
.ideai/tickets/124/carnet.md
Normal file
67
.ideai/tickets/124/carnet.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#124"
|
||||||
|
version: 3
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedAt: 1785137390360
|
||||||
|
---
|
||||||
|
|
||||||
|
## #124 — Architecture collecte capteurs montre → séance (Architect, 2026-07-26)
|
||||||
|
|
||||||
|
Statut : **cadrage technique, ouvre la voie à l'implémentation** mais reste combiné à #123 comme décidé par Main sur #106 — aucun lot d'implémentation ne doit s'ouvrir sans relecture croisée de ce cadrage et du cadrage UX #123 (déjà "cadrage terminé côté UX", en attente de ce document).
|
||||||
|
|
||||||
|
### Constat sur l'existant (fait, pas une supposition)
|
||||||
|
Recherche exhaustive sur `android/`, `watch_app/android/` et tous les Gradle : **aucune infrastructure capteur/santé n'existe aujourd'hui**. Ni `BODY_SENSORS`, ni dépendance Health Services/Health Connect, ni Horologist. La seule dépendance Wear présente (`com.google.android.gms:play-services-wearable:19.0.0`) est l'API Wearable Data Layer classique (MessageClient/DataClient), utilisée uniquement pour le bridge commandes/projection existant — **sans rapport avec les capteurs**. C'est donc une **capacité entièrement nouvelle**, pas une extension d'existant.
|
||||||
|
|
||||||
|
### Faisabilité et choix d'API
|
||||||
|
- API recommandée : **Wear OS Health Services** (`androidx.health:health-services-client`), l'API moderne recommandée par Google pour la fréquence cardiaque sur Wear OS 3+ (l'ancienne lecture directe `Sensor.TYPE_HEART_RATE` est déconseillée/restreinte).
|
||||||
|
- **Choix structurant : `MeasureClient` (mesure passive), pas `ExerciseClient`.** `ExerciseClient` déclare un "Exercise" au niveau système (peut entrer en conflit avec d'autres apps fitness, affiche sa propre UI/notification système d'exercice en cours). GameTime a déjà son propre concept de séance ; on ne veut pas d'un second concept de "session" concurrent au niveau OS. `MeasureClient.registerMeasureCallback(DataType.HEART_RATE_BPM, ...)` suffit pour une mesure passive sans ce couplage.
|
||||||
|
- Dépendance à ajouter : `watch_app/android/app/build.gradle.kts` uniquement (la fréquence cardiaque se mesure sur la montre, jamais sur le téléphone).
|
||||||
|
- Permission `android.permission.BODY_SENSORS` (dangereuse, runtime) dans `watch_app/android/app/src/main/AndroidManifest.xml` ; évaluer `BODY_SENSORS_BACKGROUND` si la mesure doit continuer écran éteint au poignet (probable en usage réel de séance).
|
||||||
|
|
||||||
|
### Modèle de permission (répond à la question ouverte du carnet UX #123)
|
||||||
|
Demande **une seule fois, paresseuse**, déclenchée par le premier passage en "séance active" côté montre après déploiement de la feature — jamais un écran d'onboarding dédié (évite une surface supplémentaire non demandée). Refus ou ignorance → la montre ne mesure jamais, le téléphone ne reçoit jamais de résumé, l'UI reste silencieuse — exactement le contrat UX déjà posé par #123 (silence total, jamais de blocage du parcours principal).
|
||||||
|
|
||||||
|
### Décision structurante : agrégation locale montre, résumé unique en fin de séance
|
||||||
|
Pas de flux temps réel synchronisé en continu (inutile : UX #123 exclut explicitement tout affichage live). La montre :
|
||||||
|
1. Accumule localement (min/max/somme/count — arithmétique triviale, pas un "modèle de calcul maison" au sens où l'UX l'exclut pour les calories) pendant toute la séance, **pause-aware** : suspend l'agrégation quand la séance est en pause ou que le repos court sans effort (aligné sur le comportement déjà existant des autres chronos pause-aware, cf. [[gametime-session-execution-timer-refactor]]).
|
||||||
|
2. Envoie **un seul message résumé** en fin de séance (terminer/abandonner), pas un flux continu — élimine la complexité de synchronisation/idempotence en continu, cohérent avec le fait que rien ne consomme la donnée en direct.
|
||||||
|
3. Le canal Data Layer (`MessageClient`) gère nativement la fiabilité/retry si la montre est temporairement injoignable au moment de l'envoi ; si le message n'arrive jamais (montre débranchée, désinstallée...), la séance reste simplement sans ce bloc — silencieux pour toujours, aucune donnée en attente à représenter, aucune migration rétroactive à inventer (conforme à la demande explicite d'UX #123).
|
||||||
|
|
||||||
|
### Nouveau contrat (séparé du contrat commandes existant)
|
||||||
|
Ne pas réutiliser `WatchCommandEnvelope` (sémantique commande utilisateur + ack) pour de la télémétrie sans accusé de réception. Nouveau type dans `packages/watch_bridge_contract` :
|
||||||
|
```dart
|
||||||
|
class WatchSensorSummary {
|
||||||
|
final int schemaVersion;
|
||||||
|
final String sessionId;
|
||||||
|
final int sampleCount;
|
||||||
|
final double? averageHeartRateBpm;
|
||||||
|
final int? maxHeartRateBpm;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- `sampleCount` permet au téléphone de décider "donnée insuffisante" (UX #123 : "données insuffisantes pour un calcul fiable" → bloc absent). Seuil recommandé : `sampleCount < 3` ⇒ traiter comme absent (pas de moyenne calculée sur un bruit d'une ou deux mesures).
|
||||||
|
- Pas d'ack requis (fire-and-forget, perte tolérée par design).
|
||||||
|
|
||||||
|
### Persistance téléphone
|
||||||
|
- Champs nullables **au niveau racine de l'entité d'historique de séance** (celle qui porte déjà la durée totale), pas par série/exercice — UX #123 demande une fréquence cardiaque **de la séance**, une seule paire moyenne/max par séance, pas un état vivant par set :
|
||||||
|
```
|
||||||
|
averageHeartRateBpm: double?
|
||||||
|
maxHeartRateBpm: int?
|
||||||
|
```
|
||||||
|
(nom exact de l'entité à confirmer par DevBackend selon le modèle d'historique réel — non exploré en détail dans ce cadrage.)
|
||||||
|
- **Écriture non bloquante (point important)** : ne jamais faire attendre la finalisation de "Terminer la séance" sur l'arrivée du résumé capteur. La séance se termine et s'enregistre immédiatement, `averageHeartRateBpm`/`maxHeartRateBpm` restent `null`. Si le résumé arrive ensuite (avant ou après la fin de séance, l'ordre n'est pas garanti), un use case dédié `updateWorkoutHistoryHeartRateSummary(historySessionId, ...)` applique un **patch idempotent** a posteriori si les champs sont encore `null` sur l'enregistrement d'historique correspondant, silencieusement si l'enregistrement n'existe plus/a été supprimé.
|
||||||
|
- Justification : cohérent par analogie avec [[gametime-online-layer-philosophy]] — ne jamais bloquer une action locale critique (ici, terminer une séance) en attendant une confirmation externe, même si ce n'est pas strictement un flux réseau serveur.
|
||||||
|
|
||||||
|
### Calories (hors MVP, second temps déjà indiqué par UX)
|
||||||
|
Même pipeline (`MeasureClient`, `DataType.CALORIES_TOTAL` si disponible nativement) — champ additif `estimatedCalories: double?` sur le même résumé, à ajouter seulement si demandé explicitement plus tard. Ne pas l'inclure au lot 1.
|
||||||
|
|
||||||
|
### Découpage en lots proposé (permet d'ouvrir l'implémentation sans ambiguïté)
|
||||||
|
1. **Watch (natif)** : dépendance Gradle + permission + `HeartRateSampler` (`MeasureClient`) + accumulateur local pause-aware + envoi résumé fin de séance.
|
||||||
|
2. **Contrat** : `WatchSensorSummary` dans `watch_bridge_contract` + tests `dart test`.
|
||||||
|
3. **Backend/domaine téléphone** : champs nullables sur l'entité d'historique + migration + `updateWorkoutHistoryHeartRateSummary` + écoute du message natif (extension du service d'écoute bridge déjà existant) + logique de patch tardif idempotent.
|
||||||
|
4. **Frontend téléphone** : bloc "Fréquence cardiaque" optionnel sur écran de fin de séance + détail historique (absent si `null`, aucun placeholder — conforme #123).
|
||||||
|
5. **QA** : permission refusée, montre sans capteur cardio, déconnexion mi-séance, arrivée tardive du résumé après clôture, seuil `sampleCount` insuffisant.
|
||||||
|
|
||||||
|
Chaque lot est testable isolément : accumulateur watch (calcul pur), DTO contrat (dart test existant), use case patch (test avec repo fake), rendu UI (widget test null vs présent).
|
||||||
|
|
||||||
|
### Ce qui reste explicitement hors MVP (repris d'UX #123, confirmé faisable/non-faisable ici)
|
||||||
|
VO2 max, zones d'intensité, SpO2, ECG — Health Services les expose en partie mais hors périmètre produit GameTime, pas seulement hors scope technique. Pas d'affichage temps réel (choix d'architecture confirmé ci-dessus, pas seulement une préférence UX : la mesure continue en flux aurait needlessly recompliqué idempotence/sync pour zéro valeur d'usage dans ce MVP).
|
||||||
25
.ideai/tickets/124/issue.md
Normal file
25
.ideai/tickets/124/issue.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
id: "9dd46f87-2ff7-45e5-ac65-31807d563707"
|
||||||
|
number: 124
|
||||||
|
title: "#106-B [Architect] Architecture collecte capteurs montre -> séance"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: [{"target":"#106","kind":"relatesTo"},{"target":"#123","kind":"dependsOn"}]
|
||||||
|
agentRefs: [{"agentId":"f8f40941-ecf7-4830-b9de-8818a099f448","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
createdAt: 1785093288487
|
||||||
|
updatedAt: 1785137390360
|
||||||
|
version: 3
|
||||||
|
---
|
||||||
|
Partie du ticket #106, dépend de #123.
|
||||||
|
|
||||||
|
Cadrer l'architecture de récupération de données capteurs montre vers une séance :
|
||||||
|
- métriques MVP recommandées ;
|
||||||
|
- permissions et contraintes Android Wear Health Services / capteurs ;
|
||||||
|
- stockage optionnel côté séance/historique ;
|
||||||
|
- comportement quand la donnée est absente, intermittente ou partielle ;
|
||||||
|
- impact sur contrats téléphone/montre.
|
||||||
|
|
||||||
|
Définition de done : cadrage technique permettant ensuite d'ouvrir les tickets d'implémentation sans ambiguïté.
|
||||||
39
.ideai/tickets/125/carnet.md
Normal file
39
.ideai/tickets/125/carnet.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#125"
|
||||||
|
version: 2
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
||||||
|
updatedAt: 1785127016437
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audit DevFrontend — 2026-07-27
|
||||||
|
|
||||||
|
- Correctif déjà présent dans le worktree : émissions `EventChannel` postées sur le main thread côté téléphone et montre.
|
||||||
|
- Requête de projection Wear OS avec authority wildcard déjà présente.
|
||||||
|
- Rafraîchissement projection téléphone périodique déjà présent pour réaligner la montre quand une séance démarre depuis le téléphone.
|
||||||
|
- Aucun changement UI supplémentaire nécessaire sans nouvelle reproduction.
|
||||||
|
|
||||||
|
À valider sur device : démarrage d'une séance depuis le téléphone, affichage montre non noir, puis réception des transitions séance/repos/actions.
|
||||||
|
|
||||||
|
## Correctif QA DevFrontend — 2026-07-27
|
||||||
|
|
||||||
|
- Le test widget 192x192 confirme maintenant le passage `no-session` vers contrôles score manuel sans écran noir ni overflow.
|
||||||
|
- Correction associée : interpolation chrono basée sur `referenceEpochMs`, ce qui stabilise l'affichage quand la projection de test ou sync porte une référence temporelle distincte de `startedAtEpochMs`.
|
||||||
|
- Build APK debug montre validé après réalignement du build dir Gradle sur le chemin attendu par Flutter.
|
||||||
|
|
||||||
|
## Correctif DevFrontend — 2026-07-26
|
||||||
|
|
||||||
|
- Cause frontend traitée avec #107 : la vue active pouvait remplacer la projection par un écran de connexion séparé et empiler trop de contrôles dans la zone ronde, rendant le rendu fragile au moment du passage hors `noActiveSession`.
|
||||||
|
- Correctif : la projection de séance reste toujours rendue ; en perte de connexion elle est seulement assombrie avec indicateur `Connexion au téléphone perdue`.
|
||||||
|
- Le layout actif est maintenant contraint et non scrollable, avec valeur dominante `FittedBox` et textes tronqués, ce qui évite une rupture de rendu lors du démarrage depuis le téléphone.
|
||||||
|
- Vérification : `flutter analyze` watch_app OK ; build APK debug montre OK via Gradle/JDK 21.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audit Architect — revue du correctif (2026-07-27)
|
||||||
|
|
||||||
|
Statut : **correctif confirmé comme root-cause fix, pas un patch cosmétique**. Vérification faite sur le diff réel de `watch_app/lib/presentation/watch_session_screen.dart`.
|
||||||
|
|
||||||
|
- Cause racine confirmée : `_RoundScaffold` utilisait `SafeArea(minimum: ...) + Center + ConstrainedBox(maxWidth/maxHeight: 210)` sur un cadran rond — pattern classique de crash `RenderFlex overflow`/assertion de contraintes infinies au moment où le contenu bascule de l'état "aucune séance" (compact) à l'état "séance active" (plus riche : chrono, actions, bientôt score). Le correctif remplace ça par un `LayoutBuilder` qui dérive `contentSize` de `constraints.biggest.shortestSide`, ce qui élimine la classe d'erreur, pas seulement un symptôme.
|
||||||
|
- **Aucun deuxième risque latent identifié** malgré l'ajout simultané des champs de score (`hasManualScore`/`currentManualScoreValue`/`canDecrementScore`, cf. #119) sur la même `WatchSessionProjection` que cet écran consomme : les trois champs ont des défauts sûrs en constructeur et en `fromJson`, tous les accès dans l'écran sont null-safe, et aucune nouvelle valeur d'enum (`WatchSessionPhase`/`WatchTimerKind`) n'a été ajoutée — donc aucun `switch` existant n'est rendu non-exhaustif par ce chantier parallèle.
|
||||||
|
- **Garde-fou adjacent déjà fermé, à noter pour ne pas le rouvrir par erreur** : le diff inclut aussi un fix côté téléphone dans `WatchBridgePlugin.kt` (`android/app/src/main/kotlin/com/gametime/app/watch/`) postant les appels `EventChannel.EventSink.success(...)` sur `Handler(Looper.getMainLooper())` — appeler ces méthodes hors thread principal aurait pu être une seconde cause plausible d'instabilité autour du démarrage de séance ; déjà corrigé dans ce même diff, à ne pas régresser lors d'un futur refactor du plugin.
|
||||||
|
- **Definition of done** : cause + correctif tiennent la route architecturalement. Il reste, comme indiqué par DevFrontend, le build/installation on-device pour la validation finale (non re-vérifiable en sandbox, cohérent avec la contrainte connue, cf. [[gametime-watch-companion-implementation]]) — aucun blocage architecture supplémentaire de mon côté.
|
||||||
20
.ideai/tickets/125/issue.md
Normal file
20
.ideai/tickets/125/issue.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
id: "89f80757-ff68-41f8-91f1-11e3dc63f5a9"
|
||||||
|
number: 125
|
||||||
|
title: "[Bug] Écran noir montre quand la séance démarre depuis le téléphone"
|
||||||
|
status: "QA"
|
||||||
|
priority: "high"
|
||||||
|
sprint: null
|
||||||
|
links: [{"target":"#91","kind":"relatesTo"},{"target":"#107","kind":"relatesTo"}]
|
||||||
|
agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
||||||
|
createdAt: 1785093288487
|
||||||
|
updatedAt: 1785127016437
|
||||||
|
version: 2
|
||||||
|
---
|
||||||
|
Bug observé après les corrections du companion montre : quand une séance est lancée depuis le téléphone, la montre passe sur un écran noir au lieu d'afficher l'état de séance.
|
||||||
|
|
||||||
|
Piste déjà identifiée : rupture de layout/runtime au moment de rendre la vue active Wear OS quand la projection n'est plus `noActiveSession`.
|
||||||
|
|
||||||
|
Définition de done : cause identifiée, correctif implémenté, build montre installé et comportement retesté.
|
||||||
44
.ideai/tickets/126/carnet.md
Normal file
44
.ideai/tickets/126/carnet.md
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#126"
|
||||||
|
version: 6
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785216995347
|
||||||
|
---
|
||||||
|
|
||||||
|
## #126 — Cadrage technique Architect (2026-07-27)
|
||||||
|
|
||||||
|
**Root cause identifiée dans le code réel** — bug de calcul, pas une question de conception.
|
||||||
|
|
||||||
|
`_stepTimerProjection` (`lib/application/use_cases.dart:4032-4055`) construit la projection du "chrono étape" envoyée à la montre avec :
|
||||||
|
```dart
|
||||||
|
referenceEpochMs: _epochMs(now),
|
||||||
|
accumulatedMs: state?.accumulatedMs ?? 0, // BUG : valeur brute persistée, pas l'écoulé réel
|
||||||
|
startedAtEpochMs: state?.status == runningTimer ? _epochMs(state!.startedAt!) : null,
|
||||||
|
```
|
||||||
|
Comparer avec `_restTimerProjection` (ligne 4018-4030), qui fait ça correctement :
|
||||||
|
```dart
|
||||||
|
referenceEpochMs: _epochMs(now),
|
||||||
|
accumulatedMs: rest.elapsedMillisecondsAt(now), // écoulé réel jusqu'à `now`
|
||||||
|
startedAtEpochMs: paused ? null : _epochMs(now), // "reparti" depuis `now`, cohérent avec accumulatedMs déjà à jour
|
||||||
|
```
|
||||||
|
`ActiveExerciseStepProgressState` possède déjà `elapsedMillisecondsAt(now)` (`lib/domain/entities.dart:1329-1334`, `accumulatedMs + now.difference(startedAt).inMilliseconds`) — exactement la méthode que `_restTimerProjection` utilise et que `_stepTimerProjection` **n'appelle pas**.
|
||||||
|
|
||||||
|
**Pourquoi ça boucle visuellement** : `WatchWearDataLayerAdapter._ensureProjectionRefreshLoop` (`lib/infrastructure/watch_bridge/wear_data_layer_adapter.dart:139-143`) republie la projection complète toutes les **2 secondes** (`projectionRefreshInterval`, keepalive/résilience), indépendamment de tout changement d'état. À chaque republication, le chrono étape reçoit un nouveau `referenceEpochMs = now` mais un `accumulatedMs` resté à sa valeur brute persistée (proche de 0 tant que l'étape n'a pas de checkpoint) — la montre recalcule alors un écoulé quasi nul et le countdown "resaute" près de sa valeur cible (`targetMs`), avant de redécompter localement pendant ~2s jusqu'à la prochaine republication. D'où le motif observé "1:00 → 59s → 1:00" en boucle, calé sur le cycle de 2s.
|
||||||
|
|
||||||
|
### Correctif (contrat clair pour DevFrontend/DevBackend)
|
||||||
|
Aligner `_stepTimerProjection` sur le pattern déjà utilisé par `_restTimerProjection` :
|
||||||
|
```dart
|
||||||
|
accumulatedMs: state?.elapsedMillisecondsAt(now) ?? 0,
|
||||||
|
startedAtEpochMs: state?.status == ActiveExerciseStepProgressStatus.runningTimer
|
||||||
|
? _epochMs(now)
|
||||||
|
: null,
|
||||||
|
```
|
||||||
|
Aucun changement de contrat (`WatchTimerProjection` inchangé), aucun impact sur la montre (l'interprétation `_interpolatedElapsedMs` côté watch_app est déjà correcte — c'est uniquement la donnée source côté téléphone qui est fausse).
|
||||||
|
|
||||||
|
### Périmètre de vérification
|
||||||
|
Vérifier s'il existe un bug symétrique ailleurs — `_setTimerProjection` (ligne 4079) et `_scoreStopwatchTimerProjection` (ligne 4057) utilisent déjà `state.accumulatedMs` brut, **mais** ce n'est pas un bug pour eux : ce sont des chronos `displayMode: elapsed` (pas `countdown`) dont `startedAtEpochMs` pointe vers le vrai `state.startedAt` d'origine (pas `now`) — cohérent, l'écoulé est recalculé correctement côté montre à partir du vrai point de départ. Seul `_stepTimerProjection` mélange les deux styles (accumulatedMs brut + referenceEpochMs à `now`), ce qui est l'incohérence à corriger.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
Test unitaire ciblé sur `_stepTimerProjection`/`WatchSessionProjectionProjector` : un chrono étape `runningTimer` avec `startedAt` antérieur à `now` doit produire un `accumulatedMs` qui reflète l'écoulé réel (pas 0), republié à l'identique après un second appel à `now+2s` sans avoir régressé. Peut s'ajouter à `test/application/watch_companion_projection_test.dart` (fichier déjà existant pour ce projecteur, cf. [[gametime-watch-companion-implementation]]).
|
||||||
|
|
||||||
|
**Prêt pour implémentation directe, aucun arbitrage supplémentaire nécessaire.**
|
||||||
16
.ideai/tickets/126/issue.md
Normal file
16
.ideai/tickets/126/issue.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
id: "20210988-6146-4772-802f-265ded405d59"
|
||||||
|
number: 126
|
||||||
|
title: "[Bug] Chrono de la montre qui ne descend pas"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: []
|
||||||
|
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785132624040
|
||||||
|
updatedAt: 1785216995347
|
||||||
|
version: 6
|
||||||
|
---
|
||||||
|
Le chrono de la montre passe de 1min à 59 secondes puis repasse à une minute et boucle comme ça
|
||||||
28
.ideai/tickets/127/carnet.md
Normal file
28
.ideai/tickets/127/carnet.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#127"
|
||||||
|
version: 9
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785216992076
|
||||||
|
---
|
||||||
|
|
||||||
|
## #127 — Avis UX bref (2026-07-27)
|
||||||
|
|
||||||
|
Pas une question de conception : le comportement attendu ne fait aucun doute (l'utilisateur doit retrouver l'écran d'exécution exact, pas le launcher, après ré-allumage par mouvement de poignet). C'est un bug de cycle de vie Wear OS (probable absence de gestion Ambient Mode / activité tuée au lieu d'assombrie). Recommandation : implémenter le mode Ambient (écran assombri, app conservée) plutôt que laisser le système détruire l'activité ; au réveil, restituer l'état exact sans flash de chargement. Prêt pour implémentation directe (Architect/DevFrontend), aucun arbitrage UX supplémentaire nécessaire.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cadrage technique Architect (2026-07-27)
|
||||||
|
|
||||||
|
**Root cause confirmée dans le code réel** : `watch_app/android/app/src/main/kotlin/com/gametime/watch/MainActivity.kt` est une `FlutterActivity` nue, sans aucune gestion Ambient (`AmbientModeSupport`/`AmbientLifecycleObserver` absents), et le manifest (`watch_app/android/app/src/main/AndroidManifest.xml`) ne déclare aucune capability ambient. Sans ambient mode, Wear OS **détruit l'activité** à l'extinction de l'écran plutôt que de la conserver assombrie ; au réveil (mouvement de poignet), le système relance le launcher/watch face au lieu de restaurer l'app — exactement le symptôme rapporté.
|
||||||
|
|
||||||
|
### Contrat technique
|
||||||
|
- Dépendance : `androidx.wear:wear-ambient` dans `watch_app/android/app/build.gradle.kts` (actuellement seule dépendance wear présente : `play-services-wearable:19.0.0`, sans rapport avec l'ambient).
|
||||||
|
- `MainActivity` doit implémenter `AmbientModeSupport.AmbientCallbackProvider` et appeler `AmbientModeSupport.attach(this)` dans `onCreate` — cela maintient l'activité en vie (assombrie) à l'extinction de l'écran, au lieu de la laisser être détruite par le système.
|
||||||
|
- **Aucune reconstruction d'état nécessaire au réveil** : l'architecture existante s'y prête déjà — `WatchWearDataLayerAdapter._nativeChannel.connectionEvents` déclenche `emitCurrentProjection()` sur `isReachable`/`requestsResync`, et le `_ensureProjectionRefreshLoop` republie toutes les 2s (cf. audit #126 dans ce même lot d'audit). Tant que l'activité Flutter reste résidente pendant l'ambient (au lieu d'être tuée), l'état affiché au réveil est déjà à jour sans code de restauration supplémentaire à écrire.
|
||||||
|
- Ne pas suivre la voie `ExerciseClient`/mode "exercise" système (déjà écarté par le cadrage #124 pour la collecte capteurs, même raison ici : GameTime ne doit pas déclarer un second concept de session au niveau OS).
|
||||||
|
- Vigilance batterie : en ambient, limiter les rebuilds au strict nécessaire (le `Timer.periodic(1s)` déjà présent dans `watch_session_screen.dart` pour l'interpolation locale des chronos peut continuer sans changement — c'est un `setState` léger, pas un accès réseau/capteur).
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
Non testable en sandbox (cycle de vie Android natif, cohérent avec la contrainte déjà connue pour les autres services natifs watch, cf. [[gametime-watch-companion-implementation]]) — validation manuelle on-device requise : écran éteint puis rallumé par mouvement de poignet pendant une séance active, vérifier que l'app reste au premier plan sur l'écran d'exécution exact (pas de flash de chargement, pas de retour au launcher).
|
||||||
|
|
||||||
|
**Prêt pour implémentation directe, aucun arbitrage supplémentaire nécessaire.**
|
||||||
16
.ideai/tickets/127/issue.md
Normal file
16
.ideai/tickets/127/issue.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
id: "9f298d88-aa3f-4bdc-834c-4469f37df70c"
|
||||||
|
number: 127
|
||||||
|
title: "[Bug] Application de la montre qui s'enlève quand l'écran s'éteint"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: []
|
||||||
|
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785132689726
|
||||||
|
updatedAt: 1785216992076
|
||||||
|
version: 9
|
||||||
|
---
|
||||||
|
Lorsque l'écran s'eteint, si je bouge le poignet pour rallumer l'écran, je me retrouve sur l'écran principal de ma montre au lieu d'être sur l'application
|
||||||
34
.ideai/tickets/128/carnet.md
Normal file
34
.ideai/tickets/128/carnet.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#128"
|
||||||
|
version: 7
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785216987980
|
||||||
|
---
|
||||||
|
|
||||||
|
## #128 — Bouton pause/play chrono sur la montre (UX, 2026-07-27)
|
||||||
|
|
||||||
|
Statut : **cadrage UX exploitable, prêt pour DevFrontend/Architect**, pas de blocage.
|
||||||
|
|
||||||
|
### Lecture du besoin
|
||||||
|
« Remettre » indique un contrôle déjà présent avant la refonte #115/#91. La montre n'a pas de pause spécifique à un chrono isolé côté téléphone (seul le chrono score a Démarrer/Arrêter/Reprendre, cf. [[gametime-ux-score-chrono]]) : le seul concept de pause transverse à tous les chronos est la **pause de séance** existante (« jamais actif pendant une pause »). Ce bouton pilote donc la pause/reprise de séance, pas un chrono isolé.
|
||||||
|
|
||||||
|
### Décision
|
||||||
|
Icône seule (pas de texte, conforme à la demande) :
|
||||||
|
- **⏸ (pause)** quand la séance tourne et qu'un chrono actif est affiché à l'écran.
|
||||||
|
- **▶ (play)** quand la séance est en pause.
|
||||||
|
- Tap = bascule immédiate (optimiste), même logique de feedback que le score montre en cours ([[gametime-ux-watch-companion-round2]] #118) : icône bascule tout de suite, réconciliation silencieuse si le téléphone rejette.
|
||||||
|
|
||||||
|
### Affichage conditionnel
|
||||||
|
Visible uniquement quand un chrono actif (mm:ss) est la donnée dominante à l'écran (chrono d'exercice, décompte repos) — pas sur les écrans reps/score sans chrono, conformément à l'énoncé « quand un chrono est en cours ».
|
||||||
|
|
||||||
|
### Placement
|
||||||
|
Petit bouton icône circulaire centré directement sous le chiffre mm:ss, au-dessus du nom d'exercice — ne casse pas le principe une-donnée-dominante (le bouton est un contrôle, pas une 2e donnée). Cible tactile ≥ 48dp (recommandation Wear OS). Style : icône primaire or, fond surface `#141824`, anneau liseré crimson au tap (feedback pressé), cohérent DA [[gametime-visual-identity]].
|
||||||
|
|
||||||
|
### Effet visuel pause
|
||||||
|
Réutilise l'état déjà spécifié dans #115 (chrono gelé, teinte assombrie) — pas de nouvel état à inventer.
|
||||||
|
|
||||||
|
### Risque à signaler (non bloquant pour l'UX, mais dépendance technique)
|
||||||
|
Ceci introduit une **nouvelle commande montre→téléphone** (pause/reprise), en plus du score (#118) déjà en cours. Contrainte de scope MVP antérieure (« seul le score est montre→téléphone ») explicitement levée par cette demande utilisateur — à signaler à Architect pour extension du contrat bridge (`watch_bridge_contract`, cf. #91-A/#91-C) plutôt qu'un simple ajout UI.
|
||||||
|
|
||||||
|
### Hors périmètre
|
||||||
|
Pas de bouton Suivant/Passer sur ce ticket — uniquement pause/reprise.
|
||||||
16
.ideai/tickets/128/issue.md
Normal file
16
.ideai/tickets/128/issue.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
id: "2f8f74d2-33db-459a-899b-cd5dee219cdc"
|
||||||
|
number: 128
|
||||||
|
title: "[UI] remettre un bouton pause/play de chrono sur la montre"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: []
|
||||||
|
agentRefs: [{"agentId":"f3408f5d-469c-4f64-9485-d8b218f3ff26","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785132771335
|
||||||
|
updatedAt: 1785216987980
|
||||||
|
version: 7
|
||||||
|
---
|
||||||
|
J'aimerais remettre un bouton pause/play sur l'écran de la montre quand un chrono est en cours. Il faut que ce bouton soit un icone pause/play, pas un texte pour garder une interface assez legère
|
||||||
31
.ideai/tickets/129/carnet.md
Normal file
31
.ideai/tickets/129/carnet.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#129"
|
||||||
|
version: 6
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785217016893
|
||||||
|
---
|
||||||
|
|
||||||
|
## #129 — Afficher l'objectif de score sur la montre (UX, 2026-07-27)
|
||||||
|
|
||||||
|
Statut : **cadrage UX exploitable, prêt pour DevFrontend**, dépend de l'écran score montre (#118, en cours) pour l'emplacement final — pas bloquant, s'ajoute dessus.
|
||||||
|
|
||||||
|
### Décision
|
||||||
|
Sur l'écran score montre, si un objectif existe pour l'étape/série (cible score libre ou objectif de chrono, cf. [[gametime-ux-score-chrono]]), l'afficher en texte secondaire discret **au-dessus** de la valeur de score dominante :
|
||||||
|
|
||||||
|
```
|
||||||
|
Objectif : 8
|
||||||
|
SCORE
|
||||||
|
5
|
||||||
|
[-] [+]
|
||||||
|
```
|
||||||
|
|
||||||
|
- Libellé et format : **réutiliser exactement** le libellé déjà utilisé côté téléphone pour ce champ (« Objectif » pour score chrono, « Cible » pour score libre selon le mode configuré) — ne pas inventer un nouveau mot, cohérence de vocabulaire cross-device.
|
||||||
|
- Style : Archivo, texte secondaire `#A7ADBA`, petite taille (11-12px), au-dessus du chiffre Anton or du score.
|
||||||
|
- **Silence total si aucun objectif défini** — pas de placeholder « Pas d'objectif » (cohérent [[gametime-online-layer-philosophy]] et la règle déjà appliquée aux stats capteur montre).
|
||||||
|
- Purement additif : n'ajoute aucune interaction, ne modifie pas le flux +/- existant.
|
||||||
|
|
||||||
|
### Risque
|
||||||
|
Aucun sur le fond. Séquencement : à implémenter en même temps ou juste après #118 (l'écran d'accueil du score) pour éviter de spécifier un emplacement qui bougerait.
|
||||||
|
|
||||||
|
### Hors périmètre
|
||||||
|
Pas d'édition de l'objectif depuis la montre (lecture seule, l'objectif se configure côté téléphone/programme).
|
||||||
16
.ideai/tickets/129/issue.md
Normal file
16
.ideai/tickets/129/issue.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
id: "11b988aa-b7dc-4dbf-a1fa-a68aef02cfad"
|
||||||
|
number: 129
|
||||||
|
title: "[UI] Afficher l'objectif de score sur la montre"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: []
|
||||||
|
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785132920623
|
||||||
|
updatedAt: 1785217016893
|
||||||
|
version: 6
|
||||||
|
---
|
||||||
|
Lorsqu'il y a un objectif de score a atteindre, en plus de pouvoir ajouter son score sur la montre, j'aimerais que l'utilisateur puisse voir l'objectif a atteindre
|
||||||
49
.ideai/tickets/130/carnet.md
Normal file
49
.ideai/tickets/130/carnet.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#130"
|
||||||
|
version: 6
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785243026193
|
||||||
|
---
|
||||||
|
|
||||||
|
## #130 — Cadrage technique Architect (2026-07-27)
|
||||||
|
|
||||||
|
**Root cause identifiée dans le code réel** — bug critique de non-idempotence, confirmé et reproductible par construction (pas besoin du détail score 20/25 pour l'expliquer, mais cohérent avec le scénario rapporté).
|
||||||
|
|
||||||
|
Erreur SQLite 2067 = violation de contrainte `UNIQUE`. La table `active_set_results` (`lib/infrastructure/local/tables.dart:473-513`) porte :
|
||||||
|
```sql
|
||||||
|
UNIQUE (active_workout_session_id, program_index, exercise_index, set_index)
|
||||||
|
```
|
||||||
|
|
||||||
|
`ActiveWorkoutSessionUseCases.recordCurrentSetResult` (`lib/application/use_cases.dart:2451-2511`), appelée par le bouton "Terminer la série" (`workout_execution_screen.dart:1459`, via `_recordAndAdvance`), construit **toujours** un `ActiveSetResult` avec une **metadata/id neuve** (`_newMetadata(ids, originDeviceId, now)`, ligne 2485) puis appelle `sessionRepository.saveSetResult` → `insertOnConflictUpdate` (`drift_repositories.dart:1130-1140`). Drift résout ce conflit sur la **clé primaire** (`id`), pas sur la contrainte `UNIQUE` métier — donc si une ligne existe déjà pour `(session, program, exercise, set)` avec un autre `id`, `insertOnConflictUpdate` ne trouve pas de conflit sur `id`, tente un `INSERT`, et percute l'`UNIQUE` → 2067.
|
||||||
|
|
||||||
|
**Comparer avec le code correct existant** : `upsertSetResultAtPosition` (ligne 2513-2588, utilisée pour l'édition a posteriori d'une série passée) fait ça bien — elle **cherche d'abord** la ligne existante via `listSetResults` (ligne 2546-2555) et réutilise son `metadata`/`id` (`existing.metadata.touch(now)`) si trouvée, sinon en crée une neuve. C'est le pattern à répliquer dans `recordCurrentSetResult`.
|
||||||
|
|
||||||
|
**Deux points d'entrée indépendants écrivent sur la même clé sans coordination**, ce qui rend la collision non hypothétique :
|
||||||
|
1. Le bouton téléphone (`_recordAndAdvance` → `recordCurrentSetResult`).
|
||||||
|
2. Le handler de commande montre `_finishCurrentSet` (`use_cases.dart:3546`, déclenché par `WatchCommandType.finishCurrentSet`/`skipCurrentSet`, ligne 3472-3476) qui appelle **aussi** `recordCurrentSetResult` (ligne 3574) — un chemin d'écriture séparé pour la même position logique.
|
||||||
|
|
||||||
|
Toute situation où une ligne existe déjà pour cette position au moment de l'appel (retry après un premier appel resté en vol, double-tap sans garde métier au niveau du use case, ou commande montre + tap téléphone proches dans le temps) déclenche l'erreur — pas seulement le cas particulier score/reps décrit dans le ticket, mais c'est cohérent avec un scénario où l'utilisateur a interagi via les deux surfaces (téléphone + montre) pour la même série.
|
||||||
|
|
||||||
|
### Correctif (contrat clair pour DevBackend)
|
||||||
|
Faire de `recordCurrentSetResult` un **véritable upsert sur la clé logique**, à l'identique du pattern déjà en place dans `upsertSetResultAtPosition` :
|
||||||
|
```dart
|
||||||
|
final existingResults = await sessionRepository.listSetResults(sessionId);
|
||||||
|
final existing = existingResults.firstWhereOrNull((r) =>
|
||||||
|
r.programIndex == programIndex &&
|
||||||
|
r.exerciseIndex == exerciseIndex &&
|
||||||
|
r.setIndex == setIndex);
|
||||||
|
final result = ActiveSetResult(
|
||||||
|
metadata: existing == null
|
||||||
|
? _newMetadata(ids, originDeviceId, now)
|
||||||
|
: existing.metadata.touch(now),
|
||||||
|
// ... reste inchangé
|
||||||
|
);
|
||||||
|
```
|
||||||
|
Ce correctif rend `recordCurrentSetResult` idempotent et sûr vis-à-vis des deux points d'entrée (téléphone/montre) et de tout retry, sans changer sa signature publique ni le contrat vu par l'UI/la montre.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
- Use case : appeler `recordCurrentSetResult` deux fois de suite pour la même position (simulant un retry ou une double soumission téléphone/montre) → doit réussir les deux fois sans exception, avec la seconde valeur qui gagne (dernière écriture) et un seul enregistrement en base.
|
||||||
|
- Cas croisé : `recordCurrentSetResult` puis vérifier `listSetResults` ne retourne qu'une ligne pour cette position.
|
||||||
|
- Régression : vérifier que `upsertSetResultAtPosition` reste inchangée et continue de passer ses tests existants.
|
||||||
|
|
||||||
|
**Prêt pour implémentation directe, aucun arbitrage supplémentaire nécessaire. Priorité critique confirmée** (bloque la fin de série dans un cas déjà survenu en usage réel).
|
||||||
16
.ideai/tickets/130/issue.md
Normal file
16
.ideai/tickets/130/issue.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
id: "b13bf705-ee2e-4c88-b8f4-894468987db8"
|
||||||
|
number: 130
|
||||||
|
title: "[Bug] Erreur sur le bouton terminer la série"
|
||||||
|
status: "closed"
|
||||||
|
priority: "critical"
|
||||||
|
sprint: null
|
||||||
|
links: []
|
||||||
|
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785133011266
|
||||||
|
updatedAt: 1785243026193
|
||||||
|
version: 6
|
||||||
|
---
|
||||||
|
En voulant temriner une série via le bouton "terminer la série" j'ai une erreur. Je suis en série 1/3, avec un nombre de répétitions à entrer. Mon score n'est pas à nul, il n'y a pas de chrono. J'ai un score de 20 avec un objectif de 25 répétition dans la série. L'erreur dit quelque chose comme: Impossible de terminer la serie SqlLiteException (2067). Visiblement c'est en essayant d'insérer une valeur dans un table active_set_result, les valeurs sont toutes à ?
|
||||||
31
.ideai/tickets/131/carnet.md
Normal file
31
.ideai/tickets/131/carnet.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#131"
|
||||||
|
version: 5
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785216979125
|
||||||
|
---
|
||||||
|
|
||||||
|
## #131 — Cadrage technique Architect (2026-07-27)
|
||||||
|
|
||||||
|
**Root cause identifiée dans le code réel** — combinaison de contenu dense jamais rendue scrollable, dans une branche de layout spécifique.
|
||||||
|
|
||||||
|
L'écran d'exécution (`lib/presentation/workout_execution_screen.dart`) a **deux branches de layout** selon que l'exercice a des étapes (`hasStepSequence`, ligne 192-323) :
|
||||||
|
- Branche **sans étapes** (ligne 283-318) : le contenu (`SetMeasureInput`) est déjà enveloppé dans un `ListView` scrollable (ligne 300-317) — pas de risque d'overflow, quelle que soit la densité.
|
||||||
|
- Branche **avec étapes** (ligne 227-282, celle du scénario rapporté : exercice à 2 étapes) : empile un `_StepSequencePanel` (`Expanded`, non scrollable) puis, si activés, un bloc score de série `_StepSetResultSummary` (`SizedBox(height: 66)`) et/ou `_ScoreStopwatchInput` (`SizedBox(height: 96)`) — **aucun scroll nulle part dans cette branche**.
|
||||||
|
|
||||||
|
À l'intérieur, `_CurrentStepPane` (ligne 2321-2449) empile pour l'étape courante, dans un `Column` non scrollable : titre d'étape, nom, corps de l'étape (`Flexible(fit: FlexFit.tight)` — chrono ou reps), **puis** si `step.hasScore` (score au niveau de l'étape, ligne 2395-2409) un `_StepScoreInput` supplémentaire (`Flexible(fit: FlexFit.loose)`), puis la rangée de boutons "Passer l'étape". `Flexible.loose` ne force pas le contenu à rétrécir sous sa taille intrinsèque minimale (champ de score + boutons +/- + libellés) — il autorise seulement le widget à être plus petit que l'espace alloué, pas à comprimer son propre contenu.
|
||||||
|
|
||||||
|
**Le scénario rapporté combine trois blocs verticaux simultanément** : chrono d'étape (10s) + score d'étape ("panier", `step.hasScore`) + score de série ("panier" aussi, `_exercise.manualScoreEnabled` → déclenche le `SizedBox(height: 66)` supplémentaire en dehors de `_CurrentStepPane`). C'est une combinaison de configuration peu courante (score à la fois au niveau étape ET au niveau série, avec un chrono d'étape court) qui n'avait probablement jamais été testée sur petit écran — la somme des hauteurs minimales requises dépasse la hauteur réellement disponible, et comme rien n'est scrollable dans cette branche, Flutter rapporte l'overflow exact ("Bottom Overflowed by 82 pixels") au lieu de dégrader proprement.
|
||||||
|
|
||||||
|
### Correctif (contrat clair pour DevFrontend)
|
||||||
|
Rendre la branche `hasStepSequence` dégradable par scroll, symétriquement à ce que fait déjà la branche sans étapes :
|
||||||
|
- Option minimale et cohérente avec l'existant : envelopper le contenu de `_CurrentStepPane` (ligne 2356, le `Column` qui empile texte/corps d'étape/score d'étape/boutons) dans un `SingleChildScrollView` lorsque le contenu dépasse l'espace disponible — remplacer les `Flexible` internes qui n'ont plus de sens dans un contexte scrollable par leur contenu intrinsèque directement (un `ScrollView` donne une hauteur non bornée à son enfant, donc `Flexible`/`Expanded` y sont invalides et doivent être retirés à cet endroit précis).
|
||||||
|
- Alternative équivalente au niveau du parent : envelopper le `Column` de la branche `hasStepSequence` (ligne 228-281, qui empile `_StepSequencePanel` + blocs score de série) dans un `SingleChildScrollView`, en remplaçant l'`Expanded` du `_StepSequencePanel` (ligne 231) par une hauteur qui s'adapte à son contenu.
|
||||||
|
- Ne pas retirer le mode "tient sur un écran sans scroll" pour le cas courant (chrono seul, ou score seul) : le `SingleChildScrollView` ne change rien visuellement quand tout le contenu tient déjà — c'est un filet de sécurité pour les combinaisons denses, pas un changement de comportement pour le cas nominal.
|
||||||
|
- Ne pas retirer d'information ni de bouton pour "faire tenir" le contenu (pas de compromis produit ici) — c'est un problème de layout à corriger, pas un périmètre fonctionnel à réduire.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
- Widget test avec la configuration exacte du ticket (3 séries, 20 passages, 2 étapes, étape courante = chrono 10s + score d'étape "panier", score de série "panier") sur une hauteur d'écran réduite (ex. contrainte de test type Wear/petit téléphone) : aucune exception `RenderFlex overflowed`, le contenu doit rester scrollable jusqu'au bouton "Passer l'étape".
|
||||||
|
- Test de non-régression sur le cas nominal (chrono seul, pas de score d'étape ni de série) : layout identique à avant, pas de scroll visible si le contenu tient.
|
||||||
|
|
||||||
|
**Prêt pour implémentation directe, aucun arbitrage supplémentaire nécessaire. Priorité critique confirmée** (bloque l'affichage complet de l'écran d'exécution pour cette combinaison de configuration).
|
||||||
16
.ideai/tickets/131/issue.md
Normal file
16
.ideai/tickets/131/issue.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
id: "9a32f9c4-9821-4903-90e4-59850dd0a21b"
|
||||||
|
number: 131
|
||||||
|
title: "[UI] téléphone erreur Bottom Overflowed by 82 pixel"
|
||||||
|
status: "closed"
|
||||||
|
priority: "critical"
|
||||||
|
sprint: null
|
||||||
|
links: []
|
||||||
|
agentRefs: []
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785133345684
|
||||||
|
updatedAt: 1785216979125
|
||||||
|
version: 5
|
||||||
|
---
|
||||||
|
Quand j'essaie d'afficher un exercice qui comprend: 3séries, 20 passages, 2 étapes, que l'étape en cours est composée d'un crhono d'objectif de 10s avec un score d'étape en nombre de panier et que la serie a elle meme un score de panier, j'ai une erreur "Bottom Overflowed by 82 pixel" qui s'affiche sur l'application téléphone
|
||||||
30
.ideai/tickets/132/carnet.md
Normal file
30
.ideai/tickets/132/carnet.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#132"
|
||||||
|
version: 8
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785141275322
|
||||||
|
---
|
||||||
|
|
||||||
|
## #132 — Avis UX bref (2026-07-27)
|
||||||
|
|
||||||
|
Décision simple : rendre le geste symétrique. Si swipe gauche (séance → actions) existe déjà comme pager horizontal, activer le swipe droit (actions → séance) sur ce même pager plutôt que d'exiger le tap. Garder le bouton existant en complément (pas de régression pour qui ne découvre pas le geste) — pas un remplacement, une addition. Aucun nouveau libellé ni état à concevoir. Prêt pour implémentation directe.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cadrage technique Architect (2026-07-27)
|
||||||
|
|
||||||
|
**Root cause probable identifiée** — conflit avec le geste système Wear OS, pas un bug applicatif Flutter classique.
|
||||||
|
|
||||||
|
`watch_session_screen.dart` utilise déjà un `PageView` standard (`_pageController`, ligne 18-24, `PageView` ligne 52) avec deux pages : session (index 0) et actions (index 1). Un `PageView` Flutter standard est **nativement bidirectionnel** — rien dans `_ActionsView` (`lib/presentation/watch_session_screen.dart:638` et suivants) ne bloque le swipe : ce sont des `OutlinedButton` en pleine largeur dans un `Column`/`Stack`, aucun `GestureDetector`/scrollable horizontal imbriqué qui capturerait le drag avant le `PageView`.
|
||||||
|
|
||||||
|
Donc l'asymétrie observée (swipe gauche marche, swipe droit ne marche pas) ne vient probablement pas du code Flutter, mais du **geste système Wear OS "swipe to dismiss"** : par défaut, Android Wear réserve la zone de bord gauche de l'écran pour un swipe-vers-la-droite (glisser le doigt de gauche à droite en partant du bord) interprété comme "retour/dismiss" au niveau de la fenêtre native, **avant** que Flutter ne reçoive le geste. Aucune configuration n'a été trouvée dans `watch_app/android/app/src/main/res/values*/styles.xml` (`windowSwipeToDismiss` absent, donc comportement par défaut du thème actif). C'est cohérent avec le symptôme précis : seul le swipe *partant du bord gauche vers la droite* est absorbé par le système, pas le swipe gauche (séance→actions) qui part du centre/droite de l'écran vers la gauche.
|
||||||
|
|
||||||
|
### Contrat technique (à valider on-device par DevFrontend, deux pistes par ordre de préférence)
|
||||||
|
1. **Désactiver le swipe-to-dismiss système sur cet écran** pour laisser le geste au `PageView` applicatif : `android:windowSwipeToDismiss="false"` sur le thème de `MainActivity` (`styles.xml`), ou gestion explicite via `OnBackInvokedCallback`/`BackHandler` si l'app cible l'API de predictive-back. Cohérent avec le fait que GameTime n'a pas besoin du dismiss système ici : le bouton retour physique/couronne reste la sortie d'app standard, le swipe droit devient un geste de navigation interne au pager.
|
||||||
|
2. Si la contrainte OS empêche de désactiver complètement le dismiss (à vérifier on-device), alterative : ne pas ancrer `_ActionsView` sur le bord gauche de l'écran (ex. léger padding gauche pour que le point de départ du swipe utilisateur naturel soit hors de la zone système réservée) — solution de repli, moins propre, seulement si l'option 1 s'avère impossible.
|
||||||
|
- Garder le bouton `IconButton`/`chevron_left` existant (ligne 673-681) en complément, conformément à l'avis UX ci-dessus — c'est déjà le cas, aucun changement requis sur ce point.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
Non testable en sandbox (comportement de geste système natif Wear OS, cohérent avec les autres limites déjà connues, cf. [[gametime-watch-companion-implementation]]) — validation manuelle on-device requise : swipe droit depuis l'écran actions doit revenir sur l'écran session, sans déclencher de dismiss/sortie d'app.
|
||||||
|
|
||||||
|
**Prêt pour implémentation directe. Un point à vérifier on-device par DevFrontend avant de considérer le correctif complet** : confirmer que la piste 1 (`windowSwipeToDismiss=false`) ne réintroduit pas de régression sur la sortie d'app via bouton retour physique — pas un arbitrage produit, une vérification technique de mise en œuvre.
|
||||||
16
.ideai/tickets/132/issue.md
Normal file
16
.ideai/tickets/132/issue.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
id: "ba05555e-820f-4b70-863c-4a7789a1bac1"
|
||||||
|
number: 132
|
||||||
|
title: "[Bug] Switch actions vers séance en cours sur la montre"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: []
|
||||||
|
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785133538696
|
||||||
|
updatedAt: 1785141275322
|
||||||
|
version: 8
|
||||||
|
---
|
||||||
|
Sur la montre, je peux switch de l'affichage de ma séance en cours vers les action en glissant vers la gauche, par contre pour retourner sur ma séance en cours je ne peux pas glisser vers la droite, je suis obligé de cliquer sur le bouton. J'aimerais que ça soit possible
|
||||||
33
.ideai/tickets/133/carnet.md
Normal file
33
.ideai/tickets/133/carnet.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#133"
|
||||||
|
version: 8
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785216975714
|
||||||
|
---
|
||||||
|
|
||||||
|
## #133 — Retirer le titre redondant « Étape 1/2 » (UX, 2026-07-27)
|
||||||
|
|
||||||
|
Statut : **cadrage UX exploitable, prêt pour DevFrontend**, pas de blocage. Priorité critique respectée : correction ciblée, pas de refonte du module séquence.
|
||||||
|
|
||||||
|
### Analyse
|
||||||
|
Le module séquence d'étapes ([[gametime-ux-exercise-steps]]) affiche déjà la position courante via les chips `[1][2][3][4]` (chip courante = fond primaire or) juste au-dessus du label texte `ÉTAPE X / Y`. L'utilisateur a raison : dans le cas courant (chips entièrement visibles, ≤6 étapes), le texte est une pure redondance visuelle qui prend de la place sans ajouter d'information.
|
||||||
|
|
||||||
|
### Décision
|
||||||
|
Retirer la ligne de titre `ÉTAPE X / Y` du module séquence en exécution. Structure simplifiée :
|
||||||
|
|
||||||
|
```
|
||||||
|
SÉQUENCE
|
||||||
|
Passage 1 / 10
|
||||||
|
[1] [2] [3] [4]
|
||||||
|
|
||||||
|
Dribble main droite
|
||||||
|
Objectif : 10 s
|
||||||
|
```
|
||||||
|
|
||||||
|
Les chips seules portent la position (courante = or) et le total (nombre de chips). Le nom de l'étape suit directement, sans label intermédiaire.
|
||||||
|
|
||||||
|
### Point d'attention (à ne pas perdre en route)
|
||||||
|
Le cas **> 6 étapes avec défilement horizontal** des chips est le seul cas où l'info peut se perdre : si l'utilisateur a scrollé et que la chip courante sort du cadre visible, plus aucun repère de position n'est visible sans le label. Ne pas supprimer purement et simplement dans ce cas précis : garder un repère compact minimal uniquement quand la rangée est scrollable, par exemple un petit indicateur `3/8` discret en coin (texte secondaire, pas un titre pleine largeur) — pas la peine de bloquer le ticket là-dessus, DevFrontend peut trancher le détail visuel de ce sous-cas dans l'implémentation tant que le principe « position toujours visible si scroll » est respecté.
|
||||||
|
|
||||||
|
### Hors périmètre
|
||||||
|
Ne touche pas au header `Programme 1/2 · Exercice 3/8` (#34, déjà tranché et distinct), ni au bloc `SÉRIE X/Y`.
|
||||||
16
.ideai/tickets/133/issue.md
Normal file
16
.ideai/tickets/133/issue.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
id: "a0cd0398-f0cd-44ac-80f5-db62b9bd60b1"
|
||||||
|
number: 133
|
||||||
|
title: "[UI] retirer le titre étape 1/2"
|
||||||
|
status: "closed"
|
||||||
|
priority: "critical"
|
||||||
|
sprint: null
|
||||||
|
links: []
|
||||||
|
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785133690575
|
||||||
|
updatedAt: 1785216975714
|
||||||
|
version: 8
|
||||||
|
---
|
||||||
|
Dans une séance en cours, je pense qu'on devrait retirer le titre qui dit "Etape 1/2" par exemple. Cette information est déjà disponible avec les carré qui affiche a quelle étape on est. Ca fait une information redondante qui pend sde la place pour rien
|
||||||
53
.ideai/tickets/134/carnet.md
Normal file
53
.ideai/tickets/134/carnet.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#134"
|
||||||
|
version: 6
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785141267072
|
||||||
|
---
|
||||||
|
## Cadrage UX (agent UX, 2026-07-27)
|
||||||
|
|
||||||
|
**Nature** : bug technique en premier lieu, avec un gap UX secondaire à corriger dans la même passe.
|
||||||
|
|
||||||
|
**Localisation** : `watch_app/lib/presentation/watch_session_screen.dart::_handleSecondaryAction` — action `WatchSecondaryAction.skipCurrentSet` (« Passer la série »), confirmée dans `_ConfirmSheet`, envoyée via `WatchSessionViewModel.sendSecondaryAction`.
|
||||||
|
|
||||||
|
**Ce qui se passe actuellement** : après confirmation, la commande est envoyée en `unawaited` puis l'écran revient immédiatement sur la vue Séance (`_showSession()`), **sans attendre le résultat**. Que la commande soit acceptée, rejetée (revision périmée, phase non applicable) ou acquittée en no-op, l'utilisateur voit exactement le même retour à l'affichage de la série. C'est cohérent avec le symptôme rapporté : « ça me renvoie sur l'affichage de la série sans rien faire ».
|
||||||
|
|
||||||
|
**Diagnostic technique à vérifier par Architect/DevBackend/DevFrontend** :
|
||||||
|
- pourquoi la commande est rejetée ou sans effet côté téléphone (mismatch `expectedRevision`, phase courante qui n'accepte pas `skipCurrentSet`, message perdu côté Data Layer) ;
|
||||||
|
- si c'est un problème de routing/state machine, corriger la cause racine en priorité.
|
||||||
|
|
||||||
|
**Décision UX à poser (indépendamment du bug)** : le flux ne doit plus être aveugle au résultat. Ne pas naviguer en silence dans tous les cas :
|
||||||
|
- Garder la navigation immédiate vers l'écran Séance (cohérent avec le pattern existant, pas de nouvel écran d'attente qui bloquerait l'utilisateur).
|
||||||
|
- Mais si la commande est rejetée/no-op (`ack` reçu ensuite avec statut rejeté), afficher un signal bref et non bloquant sur l'écran Séance — réutiliser le pattern déjà en place pour la perte de connexion (bandeau discret en haut, icône + texte court, disparaît de lui-même) plutôt qu'un dialogue. Libellé proposé : « Série non modifiée » (pas de jargon type "commande rejetée").
|
||||||
|
- Ne pas introduire de mention "hors ligne"/"mode local" (cohérent avec [[gametime-online-layer-philosophy]] déjà appliqué au reste de la montre).
|
||||||
|
|
||||||
|
**Résumé** : corriger la cause technique du rejet/perte de commande est prioritaire ; ajouter le signal d'échec est un complément UX nécessaire pour que l'absence d'effet ne soit plus jamais silencieuse, même si un futur bug similaire survient ailleurs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audit technique (agent Architect, 2026-07-27)
|
||||||
|
|
||||||
|
**Root cause confirmée dans le code, partagée avec #135** : le refresh périodique côté téléphone bump la revision de projection **sans condition**, même quand rien n'a changé.
|
||||||
|
|
||||||
|
- `WatchWearDataLayerAdapter._ensureProjectionRefreshLoop` (`lib/infrastructure/watch_bridge/wear_data_layer_adapter.dart:151-155`) appelle `emitCurrentProjection()` toutes les 2s (`_projectionRefreshInterval`), sans condition.
|
||||||
|
- `WatchCompanionProjectionUseCases.emitCurrentProjection` (`lib/application/use_cases.dart:3334-3342`) fait `_revision += 1` à **chaque** appel, qu'il y ait eu un changement métier ou non.
|
||||||
|
- La montre envoie `expectedRevision: value.projection.revision` (`watch_app/lib/application/watch_session_view_model.dart:179`), figé au moment du dernier projection reçue.
|
||||||
|
- `WatchCompanionCommandHandler._allowsStaleRevision` (`lib/application/use_cases.dart:3467-3470`) n'exempte que `incrementScore`/`decrementScore` du contrôle de revision. `skipCurrentSet` (et tous les autres secondary actions) reste soumis à `command.expectedRevision != projection.revision` → `WatchCommandAck.rejectedStaleRevision` (`use_cases.dart:3397-3399`).
|
||||||
|
- Le flux `skipCurrentSet` impose un `_ConfirmSheet` (tap "Passer la série ?" → confirmation), donc un délai utilisateur quasi garanti > 2s : la fenêtre de validité de la revision expire régulièrement avant l'envoi effectif de la commande. C'est structurellement, pas occasionnellement, défaillant.
|
||||||
|
- Second bug indépendant, confirmé côté UI : même quand l'ack rejeté revient correctement (`WatchSessionViewModel._handleAck` nettoie bien `commandPending` et déclenche `requestResync()`), rien ne l'affiche : `_handleSecondaryAction` (`watch_app/lib/presentation/watch_session_screen.dart:107-110`) fait `unawaited(sendSecondaryAction(action)); _showSession();` sans jamais lire `state.lastAck`. Aucun widget de l'écran Séance ne consulte `lastAck` aujourd'hui — le bandeau "Série non modifiée" demandé par UX n'existe pas encore.
|
||||||
|
|
||||||
|
**Contrat technique — DevBackend** :
|
||||||
|
1. Corriger `emitCurrentProjection()` pour ne bumper `_revision` que si la projection a réellement changé (comparaison hors champs volatils `revision`/`projectedAtEpochMs`, en traitant les champs de timer interpolables — `accumulatedMs`, `referenceEpochMs`, `startedAtEpochMs`, `runState` — comme stables tant que le timer tourne sans transition). Si rien n'a changé, republier/rafraîchir quand même (pour garder le Data Layer vivant) mais **réutiliser la même revision**.
|
||||||
|
2. Ne pas élargir `_allowsStaleRevision` à `skipCurrentSet` comme raccourci : `_isApplicable` (`use_cases.dart:3434-3465`) garantit déjà que l'action est cohérente avec la phase courante ; le vrai bug est le bump inutile de revision, pas la vérification elle-même.
|
||||||
|
|
||||||
|
**Contrat technique — DevFrontend (watch_app)** :
|
||||||
|
1. `WatchSessionViewModel` : dans `_handleAck`, quand `_isRejected(ack.status)` est vrai pour une commande **secondaire** (pas primaire), exposer un état transitoire consommable par l'écran (ex. un champ `rejectedSecondaryNotice` horodaté dans `WatchSessionUiState`, à côté de `lastAck` déjà existant), auto-effacé après ~2.5s via un `Timer` interne (pattern déjà utilisé pour `_waitingTimer`/`_commandTimeoutTimer`).
|
||||||
|
2. `watch_session_screen.dart` : ajouter un bandeau réutilisant le pattern visuel de `connectionLost` (icône + texte court, `_SessionMainView` autour de la ligne 299-323) affichant « Série non modifiée » quand ce nouvel état transitoire est actif. Ne pas bloquer la navigation (comportement immédiat vers `_showSession()` conservé, conforme à la décision UX).
|
||||||
|
3. Point ouvert pour UX : le libellé « Série non modifiée » n'a été spécifié que pour `skipCurrentSet` ; le même mécanisme s'appliquera naturellement aux autres actions secondaires rejetées (`skipCurrentStep`, `skipCurrentPassage`, `finishCurrentSet`, `skipCurrentRest`). Confirmer si un libellé générique convient ou si chaque action a besoin de son propre texte.
|
||||||
|
|
||||||
|
**Contrat bridge** : aucune extension nécessaire. `WatchCommandAckEvent`/`WatchCommandAck` (package `watch_bridge_contract`) portent déjà `commandId` + statut suffisant pour ce fix ; c'est un fix côté projection (téléphone) + wiring d'état/UI (montre).
|
||||||
|
|
||||||
|
**Testabilité** :
|
||||||
|
- Fix revision (backend) : 100% sandbox, unit test sur `WatchCompanionProjectionUseCases.emitCurrentProjection()` — appel répété sans changement de session ⇒ revision stable ; mutation de l'état session ⇒ revision bump. Partagé avec #135, un seul test à écrire couvre les deux tickets.
|
||||||
|
- Bandeau montre : sandbox via `watch_app/test/presentation/watch_session_screen_test.dart` (existe déjà, référence `skipCurrentSet`) — injecter un ack `rejectedStaleRevision` simulé, vérifier l'apparition du bandeau puis sa disparition après le délai.
|
||||||
|
- Vérification terrain (on-device) recommandée avant clôture : confirmer que le taux de rejet réel chute une fois le fix de revision posé, le round-trip Wear Data Layer réel n'étant pas reproductible en sandbox.
|
||||||
16
.ideai/tickets/134/issue.md
Normal file
16
.ideai/tickets/134/issue.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
id: "3c2f5bc5-395e-49f9-b075-384d99673ad6"
|
||||||
|
number: 134
|
||||||
|
title: "[Bug] le bouton de la montre \"passer la serie\" ne fonctionne pas"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: []
|
||||||
|
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785138754732
|
||||||
|
updatedAt: 1785141267072
|
||||||
|
version: 6
|
||||||
|
---
|
||||||
|
Le bouton temrienr la serie sur la montre ne produit rien, il me renvoie sur l'affichage de la série sans rien faire
|
||||||
52
.ideai/tickets/135/carnet.md
Normal file
52
.ideai/tickets/135/carnet.md
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#135"
|
||||||
|
version: 6
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785151136170
|
||||||
|
---
|
||||||
|
## Cadrage UX (agent UX, 2026-07-27)
|
||||||
|
|
||||||
|
**Nature** : bug technique probable côté transport (latence/perte Wear Data Layer), avec un gap UX qui aggrave la perception du problème — à corriger aussi.
|
||||||
|
|
||||||
|
**Localisation** : `watch_session_view_model.dart::sendPrimaryAction` → `pauseSession`/`resumeSession` ; bouton `_TimerToggleButton` dans `watch_session_screen.dart`, activé seulement si `canToggleTimer` (`!connectionLost && !commandPending && primaryAction ∈ {pauseSession, resumeSession}`).
|
||||||
|
|
||||||
|
**Diagnostic technique à vérifier par Architect/DevBackend** :
|
||||||
|
- « parfois inopérant » : le bouton est désactivé si `primaryAction` n'est pas exactement pause/resume au moment du tap (fenêtre de transition d'état) ou si `connectionLost`/`commandPending` est vrai suite à une commande précédente mal purgée. Vérifier la robustesse de la purge de `commandPending` après timeout/ack et l'exactitude de la projection `primaryAction` en continu.
|
||||||
|
- « parfois lent » : `commandTimeout` est fixé à 2s, `waitingThreshold` à 500ms — un aller-retour Data Layer peut légitimement approcher ces bornes. Vérifier s'il y a des retries/pertes réseau anormales plutôt que juste la latence normale du canal.
|
||||||
|
|
||||||
|
**Décision UX à poser (indépendamment du bug)** : pas d'optimisme sur pause/play — c'est un choix assumé (la montre n'est jamais source de vérité, cf. [[gametime-watch-companion-implementation]]), donc le geste ne doit jamais changer l'icône avant confirmation. Mais le seul signal actuel pendant l'attente est un texte en bas d'écran (« Envoi... » / « En attente du téléphone »), facile à manquer sur un cadran de montre — c'est ce qui fait percevoir la lenteur comme un blocage plutôt qu'un traitement en cours.
|
||||||
|
- Ajouter un signal directement sur le bouton pendant `commandPending` : réutiliser le pattern `_PendingDot` déjà utilisé pour le score (petit point/anneau discret superposé à l'icône pause/play), pas un nouveau composant.
|
||||||
|
- Le bouton reste désactivé pendant l'attente (déjà correct, évite les doubles envois) — mais son état désactivé doit être visuellement identique qu'il soit désactivé pour attente de commande ou pour connexion perdue, ce qui est déjà le cas via `disabledColor`.
|
||||||
|
- Pas de message d'erreur nouveau si le timeout expire : le comportement existant (repasse en `connectionLost`, la surface entière s'assombrit et affiche « Connexion au téléphone perdue ») est suffisant et cohérent — ne pas dupliquer un message par bouton.
|
||||||
|
|
||||||
|
**Résumé** : corriger la robustesse de la commande pause/resume (état + transport) est prioritaire ; ajouter un indicateur "en cours" sur le bouton lui-même est le seul ajustement UX nécessaire pour que la latence normale ne soit plus confondue avec une panne.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audit technique (agent Architect, 2026-07-27)
|
||||||
|
|
||||||
|
**Root cause confirmée dans le code, partagée avec #134** : même mécanisme de bump de revision inconditionnel côté téléphone.
|
||||||
|
|
||||||
|
- `WatchWearDataLayerAdapter._ensureProjectionRefreshLoop` (`lib/infrastructure/watch_bridge/wear_data_layer_adapter.dart:151-155`) appelle `emitCurrentProjection()` toutes les 2s.
|
||||||
|
- `WatchCompanionProjectionUseCases.emitCurrentProjection` (`lib/application/use_cases.dart:3334-3342`) fait `_revision += 1` **inconditionnellement**, même sans changement métier réel.
|
||||||
|
- `pauseSession`/`resumeSession` ne sont **pas** dans `_allowsStaleRevision` (`use_cases.dart:3467-3470` — seuls `incrementScore`/`decrementScore` en sont exemptés). Toute commande pause/resume envoyée avec un `expectedRevision` capté juste avant un tick de refresh (jusqu'à toutes les 2s, en continu, tant qu'une séance est active) risque `WatchCommandAck.rejectedStaleRevision` (`use_cases.dart:3397-3399`) même si rien n'a réellement changé côté téléphone entre-temps.
|
||||||
|
- Ça explique directement les deux symptômes rapportés :
|
||||||
|
- **« parfois inopérant »** : rejet silencieux par staleness de revision, sans lien avec un vrai désaccord d'état pause/resume — c'est un faux positif de la vérification de concurrence.
|
||||||
|
- **« parfois lent »** : latence normale du round-trip Wear Data Layer (`waitingThreshold` 500ms / `commandTimeout` 2s) — comportement transport attendu, pas un bug ; c'est bien un pur besoin d'indicateur UI comme cadré par UX.
|
||||||
|
- Point vérifié et **non buggé** : `canToggleTimer` (`watch_app/lib/presentation/watch_session_screen.dart:248-253`) exige `primaryAction` exactement pause/resume et `!commandPending` — c'est le comportement voulu (pas d'optimisme), le bouton se désactive/réactive correctement au fil des projections ; aucune correction nécessaire ici.
|
||||||
|
|
||||||
|
**Contrat technique — DevBackend** :
|
||||||
|
- Même fix que #134 : ne bumper `_revision` dans `emitCurrentProjection()` que sur changement réel de projection (hors `revision`/`projectedAtEpochMs`, timers interpolables traités comme stables tant qu'ils tournent sans transition). Un seul correctif, bénéficie aux deux tickets.
|
||||||
|
- Ne pas ajouter `pauseSession`/`resumeSession` à `_allowsStaleRevision` : contrairement aux actions terminales de #134, accepter une revision périmée sur pause/resume pourrait appliquer le mauvais toggle si le téléphone a un état réellement différent (ex. auto-pause côté téléphone pour une autre raison) — la vérification de revision est la bonne garde-fou ici, conforme à la décision UX "pas d'optimisme". Le vrai fix est d'arrêter le bump inutile, pas d'affaiblir le contrôle.
|
||||||
|
|
||||||
|
**Contrat technique — DevFrontend (watch_app)** :
|
||||||
|
- Implémenter l'indicateur déjà cadré par UX : ajouter un prop `pending: bool` à `_TimerToggleButton` (`watch_session_screen.dart:624-649`), affichant un point/anneau discret superposé à l'icône (réutiliser `_PendingDot`, déjà utilisé dans `_ManualScoreContent` ligne 579-582), actif quand `state.commandPending` est vrai **et** que le bouton concerné est celui qui a déclenché la commande (`onTogglePause != null`, i.e. `canToggleTimer` était vrai au moment du tap) — ne pas afficher le point si `commandPending` provient d'une autre action (ex. `skipCurrentRest`) sur un bouton qui n'a pas été pressé pour pause/resume.
|
||||||
|
- Wiring aux deux call sites existants : `_ActiveContent` (ligne 484-487) et `_RestContent` (ligne 686-690).
|
||||||
|
- Ne rien changer au comportement de timeout existant (retour à `connectionLost`) — conforme à la décision UX de ne pas dupliquer de message par bouton.
|
||||||
|
|
||||||
|
**Contrat bridge** : aucune extension nécessaire — fix uniquement côté projection téléphone (partagé avec #134) + rendu local montre à partir de `commandPending`, déjà suivi dans `WatchSessionViewModel`.
|
||||||
|
|
||||||
|
**Testabilité** :
|
||||||
|
- Fix revision (backend) : sandbox, test unique partagé avec #134 sur `emitCurrentProjection()`.
|
||||||
|
- Indicateur `_PendingDot` sur le bouton : sandbox via `watch_app/test/presentation/watch_session_screen_test.dart` — simuler `commandPending=true` après tap pause/resume, vérifier l'affichage du point, puis sa disparition à l'ack.
|
||||||
|
- Vérification terrain (on-device) recommandée après le fix backend partagé : confirmer que le taux d'échec perçu ("inopérant") chute significativement une fois le bump de revision corrigé ; la latence résiduelle ("lent") restera normale et sera couverte par l'indicateur UI, à valider visuellement sur montre réelle (difficile à juger en sandbox pur).
|
||||||
16
.ideai/tickets/135/issue.md
Normal file
16
.ideai/tickets/135/issue.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
id: "9fddc019-342f-4063-a194-a5a77907a793"
|
||||||
|
number: 135
|
||||||
|
title: "[Bug] bouton pause/play de la montre qui ne fonctionne pas toujours"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: []
|
||||||
|
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785138943933
|
||||||
|
updatedAt: 1785151136170
|
||||||
|
version: 6
|
||||||
|
---
|
||||||
|
Le bouton pause/play ne fonctionne pas toujours. De temps en temps il met du temps a répondre et de temps en temps il ne fonctionne pas du tout
|
||||||
20
.ideai/tickets/136/carnet.md
Normal file
20
.ideai/tickets/136/carnet.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#136"
|
||||||
|
version: 8
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
updatedAt: 1785244375293
|
||||||
|
---
|
||||||
|
|
||||||
|
## #136 — Cadrage UX (2026-07-28)
|
||||||
|
|
||||||
|
**Comportement attendu** : tap sur `Passer le repos` → bouton passe en état pending bref (`Envoi...`, pattern déjà spécifié [[gametime-ux-watch-companion]]) → commande envoyée au téléphone → téléphone applique le skip (repos terminé, phase avance) → **c'est la projection confirmée qui fait changer l'écran montre**, jamais un tap local. Le repos réel côté téléphone doit changer ; l'écran montre ne doit refléter ce changement qu'une fois l'ack/la nouvelle projection reçus.
|
||||||
|
|
||||||
|
**Contraintes UI/feedback** :
|
||||||
|
- Ne jamais quitter l'écran `Repos` avant réception d'un ack `accepted`/`acceptedNoOp` + nouvelle projection. Si rejet (`rejectedStaleRevision`, `rejectedNotApplicable`…), rester sur `Repos` avec son état confirmé actuel, sans transition fantôme.
|
||||||
|
- Si latence perceptible, réutiliser les seuils déjà cadrés (état `Envoi...` court, puis `En attente du téléphone`) plutôt qu'un basculement d'écran optimiste.
|
||||||
|
|
||||||
|
**À ne pas faire** :
|
||||||
|
- Naviguer localement vers l'écran séance active sur simple tap, avant confirmation — c'est exactement le bug actuel (l'écran change sans que le repos ait réellement sauté côté téléphone).
|
||||||
|
- Ajouter une confirmation (`Passer le repos ?`) : cette action reste dans la liste des actions immédiates, pas dans celles à confirmer ([[gametime-ux-watch-companion]]).
|
||||||
|
|
||||||
|
**Libellé/navigation** : aucune évolution nécessaire. Le libellé `Passer le repos` et le modèle « écran piloté par la projection confirmée » sont déjà correctement spécifiés — c'est un bug d'implémentation (navigation locale avant état confirmé, ou commande mal routée côté téléphone), pas un manque de conception.
|
||||||
16
.ideai/tickets/136/issue.md
Normal file
16
.ideai/tickets/136/issue.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
id: "cd125c5f-5e37-4100-8b61-58cbaab5eac1"
|
||||||
|
number: 136
|
||||||
|
title: "[Bug] \"passer le repos\" sur la montre ne fonctionne pas"
|
||||||
|
status: "qa"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: "abc4f969-b169-45f7-988c-daeeab762201"
|
||||||
|
links: []
|
||||||
|
agentRefs: []
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
|
createdAt: 1785141207792
|
||||||
|
updatedAt: 1785244375293
|
||||||
|
version: 8
|
||||||
|
---
|
||||||
|
Le bouton "passer le repos" sur la montre ne fonctionne pas, il me renvoie sur l'écran de la séance sur la montre mais ne passe pas le repos sur le téléphone
|
||||||
6
.ideai/tickets/137/carnet.md
Normal file
6
.ideai/tickets/137/carnet.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#137"
|
||||||
|
version: 4
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785147020900
|
||||||
|
---
|
||||||
16
.ideai/tickets/137/issue.md
Normal file
16
.ideai/tickets/137/issue.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
id: "c3423267-4ba2-4158-a802-55882c981875"
|
||||||
|
number: 137
|
||||||
|
title: "[Bug] Connexion au compte impossible"
|
||||||
|
status: "closed"
|
||||||
|
priority: "high"
|
||||||
|
sprint: null
|
||||||
|
links: []
|
||||||
|
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785141546668
|
||||||
|
updatedAt: 1785147020900
|
||||||
|
version: 4
|
||||||
|
---
|
||||||
|
Je ne peux pas me connecter à mon compte, j'ai une erreur "Connexion impossible pour le moment. Réessaie plus tard". Le serveur est pourtant bien lancé. Même soucis pour la création de compte
|
||||||
19
.ideai/tickets/138/carnet.md
Normal file
19
.ideai/tickets/138/carnet.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#138"
|
||||||
|
version: 6
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1785216971861
|
||||||
|
---
|
||||||
|
## Cadrage UX (agent UX, 2026-07-27)
|
||||||
|
|
||||||
|
**Nature** : pur copy, prêt à implémenter immédiatement, aucun arbitrage supplémentaire.
|
||||||
|
|
||||||
|
**Principe** : ne pas rassurer défensivement sur "pas besoin de compte" — un compte doit être présenté positivement pour ce qu'il apporte (synchronisation, partage), jamais comme une option qu'on justifie de pouvoir ignorer. Garder l'explication utile (à quoi sert le compte), retirer uniquement la tournure "l'app fonctionne/reste utilisable sans compte".
|
||||||
|
|
||||||
|
**Chaînes concrètes à corriger** (grep `compte`, hors faux positif `exercise_library_screen.dart:1101` "compte à rebours" qui ne concerne pas les comptes utilisateur) :
|
||||||
|
- `profile_screen.dart:197` « GameTime fonctionne entièrement sans compte. Connecte-toi... » → « Connecte-toi pour synchroniser tes données et partager tes contenus. »
|
||||||
|
- `profile_screen.dart:955` « Tu peux continuer à utiliser GameTime sans compte. » → supprimer la ligne, le bouton "Créer un compte" au-dessus suffit.
|
||||||
|
- `profile_screen.dart:1105-1106` « Le compte sert à synchroniser tes données et partager tes contenus. L'app reste utilisable sans compte. » → garder la première phrase, retirer la seconde.
|
||||||
|
- `share_screen.dart:174-175` « Connecte-toi pour envoyer $targetLabel à un autre compte GameTime. Le reste de l'app reste utilisable sans compte. » → « Connecte-toi pour envoyer $targetLabel à un autre compte GameTime. »
|
||||||
|
|
||||||
|
Pas de découpage nécessaire, un seul lot copy pour DevFrontend.
|
||||||
16
.ideai/tickets/138/issue.md
Normal file
16
.ideai/tickets/138/issue.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
id: "6e1f3467-5bd3-4c79-8dce-cd8890765f4c"
|
||||||
|
number: 138
|
||||||
|
title: "[UI] Enlever les textes qui informent qu'on a pas besoin de compte"
|
||||||
|
status: "closed"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: []
|
||||||
|
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1785141676541
|
||||||
|
updatedAt: 1785216971861
|
||||||
|
version: 6
|
||||||
|
---
|
||||||
|
Il faudrait enlever les textes qui disent des choses du genre "pas besoin d'avoir un compte pour utiliser l'application". L'utilisateur n'a pas a en être informé
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user