# 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 .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`.