Files
coach-ios/COWORK.md
Sylvain Bettinelli 891a8bddce IntervalEngine : le commentaire sur les pauses disait l'inverse d'Apple
L'en-tête affirmait que `HKLiveWorkoutBuilder.elapsedTime` « exclut déjà les
pauses », et en tirait que le moteur n'avait rien à savoir de la pause ni de
l'auto-pause.

La doc Apple dit le contraire, vérifié à la source le 2026-08-20 :
« The elapsed time for the workout based on the builder's current contents,
including pauses. »

Conséquence si on câblait le moteur dessus telle quelle : une pause de 5 min
ferait avancer le déroulé de 5 min d'effort — 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:)` : « The duration of a workout doesn't
include intervals between pause and resume events. » Les deux textes d'Apple
se contredisent frontalement ; le choix de la source de temps reste à faire
avant le câblage.

Le moteur lui-même reste correct : il n'intègre que ce qu'on lui pousse.
Aucun comportement modifié, 57 tests Swift toujours verts.

COWORK gagne l'analyse complète du câblage (3 couches, le serveur sait déjà
produire les étapes via _blocks_from_session) pour reprise à froid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 16:52:21 +00:00

194 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (Python, testable sans Mac)** — une route rendant la séance du
jour au format `CoachSessionPlan`, en réutilisant `_blocks_from_session` et
`app.current_zones()` (règle 9 du CLAUDE.md : jamais un % de FCmax
générique). ✅ Commencer par là : le JSON se juge dans un navigateur avant
qu'une ligne de Swift n'en dépende.
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`.