GET /api/plan/session (coach_sportif c8d1541) rend la séance du jour au format CoachSessionPlan, déployé et vérifié en prod le 21/08 : 32 étapes / 40 min sur le 15×(1'/1') du jour, alternance work/recovery correcte, bornes issues des zones datées. Reste les couches 2 (pousser le plan par WCSession) et 3 (la vue qui appelle engine.update et joue l'haptique sur les transitions). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
202 lines
15 KiB
Markdown
202 lines
15 KiB
Markdown
# COWORK — coordination multi-Claude (coach-ios)
|
||
|
||
Ce repo est travaillé **en parallèle par 2 instances de Claude** :
|
||
|
||
- **Claude « dev » (serveur Linux, sans Xcode)** : écrit le **code** (Swift des plugins, config projet) + le backend/web (repo `coach_sportif`). **Ne peut PAS builder iOS** (pas de Xcode).
|
||
- **Claude « Cowork » (app Claude sur le Mac mini de Sylvain)** : fait les **builds Xcode, la signature, les tests sur device**. → C'est toi si tu lis ça depuis le Mac.
|
||
|
||
Les deux ne communiquent **que via git + ce fichier**. ⚠️ La mémoire de chaque Claude est **locale à sa machine** (non partagée) → **l'état partagé, c'est CE fichier** : tiens-le à jour.
|
||
|
||
## 🔑 Règles d'or (pour ne pas se bloquer)
|
||
1. **`git pull` AVANT de toucher au projet** ; **`git push` après chaque changement.**
|
||
2. ⚠️ **Ne jamais laisser Xcode ouvert pendant un `git pull`** — Xcode garde/réécrit le `pbxproj` en mémoire → fichiers Swift non compilés + conflits (ça nous a bloqués 2× le 2026-06-05). Quitter Xcode (`Cmd+Q`), pull, rouvrir.
|
||
3. Après toute modif faite **dans Xcode** (package SPM, capability, signing) → **commit + push `project.pbxproj` (+ `Package.resolved`) immédiatement**, sinon le prochain `git pull` est bloqué par les modifs locales.
|
||
4. Zone sensible = `ios/App/App.xcodeproj/project.pbxproj` : le Claude dev y ajoute les fichiers Swift (entrées manuelles), le Claude Mac y ajoute les packages SPM. → committer de suite limite les collisions.
|
||
|
||
## 🛠️ Workflow type « le dev a poussé du code natif, build-le »
|
||
1. **Quitter Xcode.**
|
||
2. `git pull`
|
||
3. Nouveau `*.swift` ? vérifier qu'il est dans le projet : `grep -c <Nom>.swift ios/App/App.xcodeproj/project.pbxproj` (doit être ≠ 0). Si Xcode ne le voit pas après ouverture → clic droit dossier **App** → *Add Files to "App"…* (décocher *Copy items*, cocher target **App**).
|
||
4. Ouvrir Xcode → **Clean Build Folder** (`Cmd+Shift+K`) → **Run** (`Cmd+R`) sur l'iPhone.
|
||
5. Erreurs → corriger, committer le fix, push.
|
||
6. Diagnostic plugins : Safari → Développement → iPhone → coach.hypnotruck.ch → console → `Object.keys(Capacitor.Plugins).filter(k=>k.indexOf('Coach')===0)`.
|
||
|
||
## 📍 État natif actuel (2026-06-05)
|
||
- **Capacitor 8 n'auto-découvre PAS les plugins in-app** → tous enregistrés dans `MainViewController.capacitorDidLoad()` : CoachWorkoutKit, CoachHealthRoute, CoachWorkoutObserver, CoachLiveBridge, **CoachAppleAuth**, **CoachGoogleAuth**.
|
||
- **Login natif Apple + Google = OK, validé device.** Apple via `AuthenticationServices` (intégré). Google via **SDK GoogleSignIn** (SPM, lié à la target App) + `GIDClientID`/URL scheme dans Info.plist. Les deux POSTent l'id_token sur `/auth/apple|google/web` (backend) → cookie de session.
|
||
- watchOS `CoachWatch` + Live Activity en place (chantiers antérieurs, cf. `HANDOFF-WATCHOS.md`).
|
||
- **Widgets natifs iPhone + complication Watch (2026-06-29)** : code écrit côté dev, **reste le câblage Mac**.
|
||
- iPhone : plugin `CoachWidgetBridge` (enregistré dans `capacitorDidLoad()`), modèle partagé `CoachWidgetSnapshot.swift`, widgets `CoachWidgets.swift` (Séance du jour + Score de forme) dans l'extension `CoachLiveActivity`. App Group `group.ch.hypnotruck.coach`. **Hook web déjà en prod** (`widget-bridge.js`).
|
||
- Watch : `CoachWidgetBridge.pushToWatch` (WCSession) → `CoachWatch/ConnectivityManager` reçoit → App Group montre + reload. Complications `CoachWatchWidgets/CoachWatchComplications.swift` (Forme circular/corner, Séance rectangular/inline). **Nouvelle target widget watchOS à créer dans Xcode.**
|
||
- ⚠️ Étapes Mac obligatoires (App Group portail + capabilities + Target Membership + target Watch + build) : **TOUT est détaillé dans `docs/widgets-runbook-mac.md`.**
|
||
|
||
## 🆕 Routine quotidienne sur la Watch (2026-08-03, dev — non compilé)
|
||
|
||
Nouveau chantier, code complet côté dev, **build Mac requis**. Tout est détaillé
|
||
dans **`docs/routine-watch-runbook-mac.md`** (archi, étapes, débogage).
|
||
|
||
- **Nouveaux fichiers, DÉJÀ référencés dans `project.pbxproj`** (aucun « Add
|
||
Files » à faire) : `App/CoachRoutineBridge.swift` (target App),
|
||
`CoachWatch/RoutineStore.swift` + `CoachWatch/RoutineView.swift` (target
|
||
CoachWatch). Backup : `project.pbxproj.bak-routine`.
|
||
- **Modifs** : `MainViewController.capacitorDidLoad()` enregistre
|
||
`CoachRoutineBridgePlugin` ; `CoachWatch/ContentView` ouvre `RoutineView` en
|
||
sheet ; `ConnectivityManager` (watch) gagne `sendRoutine()` + réception du
|
||
`routineSnapshot`.
|
||
- ⚠️ **`CoachLiveBridge` touché** — un seul ajout : `routeIfNotLiveSample()`
|
||
aiguille les messages `routineDone` vers `NotificationCenter` au lieu de les
|
||
émettre comme samples. Le délégué `WCSession` est unique côté iPhone, on ne
|
||
pouvait pas en ajouter un second. **À re-vérifier au build** que le live
|
||
workout (validé E2E le 31/05) fonctionne toujours.
|
||
- Côté backend `coach_sportif` : **déjà en prod** (`/api/routine`,
|
||
`/api/routine/check`, `/api/routine/day`, `data/routine_log.json`).
|
||
|
||
## 🔗 Universal Links (2026-08-06, dev — non compilé)
|
||
|
||
Constat : **aucun** `.widgetURL` ne ramenait dans l'app. La Live Activity
|
||
(`CoachLiveActivityWidget`), les deux widgets iPhone (`CoachWidgets` → `/calendar`
|
||
et `/forme`) et les deux complications Watch pointent tous sur des URL
|
||
`https://coach.hypnotruck.ch/…`, mais l'entitlement `associated-domains` était
|
||
absent et `/.well-known/apple-app-site-association` renvoyait 404 → **un tap
|
||
ouvrait Safari**, où le cookie d'auth injecté dans la WKWebView n'existe pas :
|
||
écran de login.
|
||
|
||
- **Backend — déjà en prod** (commit `8ec0047`) : route AASA publique déclarant
|
||
`TZ2PVTRKNY.ch.hypnotruck.coach` sur `/live`, `/routine`, `/forme`,
|
||
`/calendar` (chemins énumérés un par un pour ne pas détourner
|
||
`/calendar.ics`), et `static/deeplink.js` qui écoute `appUrlOpen` et amène la
|
||
WebView sur le bon chemin — Capacitor ne le fait pas tout seul.
|
||
- **Côté ce repo** : `App.entitlements` gagne
|
||
`com.apple.developer.associated-domains = applinks:coach.hypnotruck.ch`.
|
||
- ⚠️ **Étape Mac** : ouvrir la cible **App** → *Signing & Capabilities* →
|
||
vérifier qu'**Associated Domains** apparaît avec `applinks:coach.hypnotruck.ch`
|
||
(l'entitlement est là, Xcode doit régénérer le profil ; si erreur de signing,
|
||
décocher/recocher *Automatically manage signing*). Puis rebuild.
|
||
- Effet attendu : tap sur la Live Activity → app → `/live` → la vue Liquid Glass
|
||
`CoachLiveView` s'ouvre d'elle-même (le JS de `/live` appelle
|
||
`openNativeLive`). C'est le point d'entrée qui manquait.
|
||
|
||
## 🆕 Chantier montre outdoor (2026-08-20, dev — non compilé)
|
||
|
||
Branche **`feat/watch-outdoor`**, commits `f2fa3cc` (moteur d'intervalles) et
|
||
`a04dfc4` (trace GPS). `IntervalEngine` guide les intervalles d'une séance au
|
||
poignet ; `RouteFilter` + `LocationTracker` enregistrent le parcours.
|
||
|
||
**Pourquoi ce code existe** : WorkoutKit ne sait pas exécuter une séance
|
||
structurée dans une app tierce — son seul point d'exécution public est
|
||
`WorkoutPlan.openInWorkoutApp()`, qui ouvre l'app Exercice d'Apple. Il faut donc
|
||
écrire la machine à états nous-mêmes.
|
||
|
||
**Le moteur est volontairement pur** : aucun HealthKit, aucun timer, aucune
|
||
horloge interne. Il répond à « où en sommes-nous ? » à partir du temps et de la
|
||
distance qu'on lui pousse. Le temps de référence est `elapsedTime` du builder,
|
||
qui exclut déjà les pauses.
|
||
|
||
✅ **57 tests Swift verts sur Linux** (`./tests-linux/run.sh`, toolchain
|
||
`~/workspace/toolchains/bin`) : 17 pour `IntervalEngine`, 15 pour `RouteFilter`.
|
||
Les deux ont été **vérifiés rouges** en neutralisant leur correction (5 échecs
|
||
sans les frontières théoriques du moteur, 4 sans le lissage du dénivelé). La
|
||
logique est donc validée — reste l'intégration, que Linux ne peut pas compiler.
|
||
|
||
✅ **Plus d'« Add Files » à faire** — correction du 2026-08-20 après-midi. Une
|
||
note antérieure de ce fichier annonçait `IntervalEngine` absent du projet : ce
|
||
n'est plus vrai. Les **trois** fichiers sont déclarés dans la cible `CoachWatch`
|
||
directement dans le `pbxproj` (sauvegarde `project.pbxproj.bak-outdoor`).
|
||
|
||
Contrôle avant de builder — chacun doit rendre **2** (déclaration + phase de
|
||
compilation, soit 1 cible) :
|
||
```bash
|
||
for f in IntervalEngine RouteFilter LocationTracker; do
|
||
echo "$f: $(grep -c "$f.swift in Sources" ios/App/App.xcodeproj/project.pbxproj)"
|
||
done
|
||
```
|
||
|
||
⚠️ **À vérifier au premier build, dans cet ordre** :
|
||
1. **Une séance extérieure demande la localisation** (feuille « lorsque l'app
|
||
est active »). Si rien n'apparaît, la trace ne partira pas.
|
||
2. **La sortie a bien un parcours dans Santé** à la fin. C'est le test qui
|
||
compte : `finishRoute` doit être appelé APRÈS `finishWorkout`, sinon la
|
||
trace existe sans être associée à la séance.
|
||
3. **La FC remonte toujours sur `/live`** — `WorkoutManager` a été modifié,
|
||
c'est le test de non-régression prioritaire.
|
||
4. Écran éteint, poignet baissé : les positions continuent d'arriver. Si elles
|
||
s'arrêtent, chercher du côté du CPU (une boucle d'affichage trop rapide fait
|
||
suspendre l'app), pas du côté des autorisations.
|
||
|
||
⚠️ `Info.plist` de `CoachWatch` gagne `UIBackgroundModes = [location]` et
|
||
`NSLocationWhenInUseUsageDescription`. **La clé de background est vitale** :
|
||
armer `allowsBackgroundLocationUpdates` sans elle termine l'app. Un garde-fou
|
||
la vérifie au démarrage, mais si elle disparaissait du build, le suivi
|
||
retomberait silencieusement au premier plan seul.
|
||
|
||
## 🔜 Câbler IntervalEngine — analyse du 2026-08-20 (rien de codé)
|
||
|
||
`IntervalEngine` **compile sur device** (build validé le 20/08, l'app tourne),
|
||
mais **aucune vue ne l'appelle** : le moteur est un guide sans itinéraire.
|
||
|
||
### Le chaînon manquant
|
||
|
||
Le moteur attend un `CoachSessionPlan` (liste d'étapes : durée, zone, bornes
|
||
bpm). **Rien ne le lui fournit** — `ConnectivityManager` reçoit les zones, le
|
||
`CoachWidgetSnapshot` et la routine, mais aucun plan structuré. Vérifié par
|
||
lecture du fichier, pas supposé.
|
||
|
||
En revanche **le serveur sait déjà produire ces étapes** :
|
||
`coach_sportif/web/fitness_export.py::_blocks_from_session` déplie une séance
|
||
en blocs chronométrés, répétitions comprises (un bug de prod du 12/08 avait
|
||
fait disparaître un 7×(1'C/1'M) : les `repeat` sont désormais dépliés), et
|
||
`_zone_bpm(zone, hr_zones)` convertit une zone en bornes bpm. Ces briques
|
||
servent déjà à l'export TCX Workout.
|
||
|
||
Indice que le chemin était prévu : les `CodingKeys` de `CoachPlanStep` sont en
|
||
snake_case (`duration_sec`, `hr_zone`, `hr_bpm_min`…) — le type a été écrit
|
||
pour décoder du JSON venant du serveur.
|
||
|
||
### Les trois couches, dans l'ordre
|
||
|
||
1. ✅ **Serveur — FAIT le 2026-08-21** (`coach_sportif` `c8d1541`, déployé et
|
||
vérifié en prod). **`GET /api/plan/session[?date=YYYY-MM-DD][&type=...]`**
|
||
rend la séance dépliée au format `CoachSessionPlan` : clés snake_case,
|
||
`hr_zone` en entier, durées en secondes. Mesuré en prod le 21/08 sur la
|
||
séance du jour (CDC J13, 15×(1'/1')) : **32 étapes, 40 min**, alternance
|
||
`work`/`recovery` correcte, bornes Z1 100-109 / Z2 109-118.
|
||
- Les bornes BPM viennent des **zones datées** (`current_zones`), jamais des
|
||
`hr_min`/`hr_max` du plan (hérités d'avant l'unification du 12/08).
|
||
- Les `repeat` sont **dépliés** — le moteur ne sait pas répéter.
|
||
- `404` = pas de séance ce jour · `422` = séance non chronométrée (repos,
|
||
durée ouverte) : rien à dérouler, à distinguer côté montre.
|
||
- ⚠️ Route volontairement **hors `/api/v1/*`** (JWT que la WebView n'a pas).
|
||
Un test le verrouille : ne pas la « ranger » dans l'API native.
|
||
2. **iPhone** — pousser ce plan vers la montre par `WCSession`, à côté de ce
|
||
qui part déjà.
|
||
3. **Montre** — `ConnectivityManager` publie un `sessionPlan` ; une vue appelle
|
||
`engine.update(elapsed:distance:)` à chaque tick et joue une haptique sur
|
||
`events.transitions`.
|
||
|
||
### ⚠️ Le piège à trancher AVANT de câbler
|
||
|
||
L'en-tête d'`IntervalEngine.swift` affirmait que
|
||
`HKLiveWorkoutBuilder.elapsedTime` « exclut déjà les pauses ». **La doc Apple
|
||
dit l'inverse**, vérifié à la source le 20/08 : *« The elapsed time for the
|
||
workout based on the builder's current contents, including pauses. »*
|
||
|
||
Câbler le moteur dessus telle quelle ferait avancer le déroulé **pendant les
|
||
pauses** — un fractionné mis en pause pour traverser une route se déroulerait
|
||
à l'arrêt. La propriété qui exclut réellement les pauses est
|
||
`HKWorkoutBuilder.elapsedTime(at:)`. Les deux textes d'Apple se contredisent
|
||
frontalement ; le commentaire du fichier a été corrigé, **le choix de la source
|
||
de temps reste à faire**.
|
||
|
||
⚠️ Et jamais un `Timer` de vue SwiftUI : il ne tourne pas écran éteint, donc
|
||
pendant l'essentiel d'une séance (même famille de piège que `VmaTestView`).
|
||
|
||
## 👉 À reprendre
|
||
- ✅ **Phase 4 login — FAIT & DÉPLOYÉ** (révocation Apple à la suppression de compte) : code complet côté backend `coach_sportif` (commit `5ae6e2d`) + clé `.p8` déployée sur le VPS prod (vérifié 2026-06-26). Rien à coder. Détails : `coach_sportif/COWORK.md`.
|
||
- **iOS — bloqué Mac** : builder + uploader TestFlight le natif accumulé (login Apple+Google natif, watchOS live, HeartRateRangeAlert). Sur le Mac : `cd ~/coach-ios && git pull && npm install && npx cap sync ios && open ios/App/App.xcodeproj` → Clean Build Folder → Archive → Upload.
|
||
- ⚠️ **Corrigé le 2026-08-20** : cette ligne disait `open ios/App/App.xcworkspace`. **Ce fichier n'existe pas** (vérifié : le dépôt ne contient que `App.xcodeproj`). Depuis **Capacitor 8, les dépendances passent par SPM** et il n'y a plus de workspace CocoaPods. La commande échouait donc telle quelle.
|
||
- ⚠️ **Fixes robustesse 2026-07-01 (dev, non compilés)** à valider au prochain build : `CoachWidgetBridge` (score lu en `NSNumber.intValue` → corrige un score float perdu ; `save` renvoie Bool → `call.reject` si App Group KO), `CoachAppleAuth` (double-tap : rejette l'ancien `pendingCall` ; erreur testée par domaine `ASAuthorizationError`). Swift pur, aucun nouveau fichier ni capability.
|
||
- ⚠️ **Checklist pré-App-Store (audit sécu 2026-07-01)** : `ios/App/App/App.entitlements` a `aps-environment = development`. Pour la soumission **App Store/TestFlight**, l'APNs prod exige `production`. NON changé côté dev (casserait le push en dev device — arbitrage de signing à faire sur Mac : soit basculer `production` avant l'archive de distribution, soit laisser Xcode le gérer via le profil de distribution auto). À trancher/tester au moment du build release.
|
||
- **watchOS — bloqué Mac** : test device + TestFlight de la cible `CoachWatch` (Phases 1-3 réalisées, build vert sim). Cf. `HANDOFF-WATCHOS.md`.
|
||
- **Android — bloqué externe** : Play Console ($25 validé), upload AAB + Internal Testing depuis machine avec Android SDK.
|
||
- Backend/web : voir `coach_sportif/COWORK.md`.
|