Files
coach-ios/COWORK.md

18 KiB
Raw Blame History

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).

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) :

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).

📌 Point du 2026-08-21 — build OK, vérifs GPS EN SUSPENS

Build Xcode passé sur iPhone (iOS 26.6), après un git pull qui partait de be1ffec : le Mac n'avait donc ni LocationTracker, ni RouteFilter, ni le correctif du doublon pbxproj (19fbfd1) — le build du 20/08 ne les contenait pas. Contrôles avant build : 2/2/2 déclarations, UIBackgroundModes = [location] et NSLocationWhenInUseUsageDescription présents, un seul @main par cible.

Une erreur de compilation, corrigée (3e657f5) : finishRoute(with:) n'accepte pas d'optionnel — vérifié dans la doc Apple : func finishRoute(with workout: HKWorkout, metadata:), « You must have already saved this workout to the HealthKit store ». Le commentaire du fichier promettait une trace « sauvegardée sans association » : c'est impossible, aucune API ne clôt une route orpheline. Traité en amont : quand finishWorkout() rend nil (montre verrouillée), la séance EST dans HealthKit et WorkoutManager.recentlySavedWorkout() va la rechercher — dernier workout de HKSource.default() croisant les 5 dernières minutes (chevauchement, jamais startDate : filtrer sur le début raterait toute séance longue). Sinon discardRoute() jette la trace explicitement et le journalise comme une perte.

⏳ Les 4 vérifications au poignet n'ont PAS encore été faites — la séance doit être lancée depuis l'app sur la montre, les logs du 21/08 ne montrent que l'app iPhone (sync HealthKit complète et saine : 7 séances, 7 traces, 13 scores d'effort, App Group en écriture ok:true).

Relevé au passage, non traité : avertissement UIScene lifecycle will soon be required (Capacitor, échéance future) ; un échantillon de pas venant de "iPhone de …" et non de la Watch — à surveiller, la cadence étant dérivée des pas relus par séance, deux sources sur une même séance la fausseraient.

🖥️ Vues d'entraînement au choix (WorkOutDoors) — cadré le 21/08, rien codé

Cadrage complet : docs/watch-training-screens.md. En deux lignes : LiveWorkoutView a cinq métriques en dur, aucune notion de champ ni de page. Décision prise : la configuration s'édite côté web (réutiliser TileManager) et voyage par WCSession — donc aucun rebuild pour changer ses écrans. Le modèle et le catalogue sont du Foundation pur, donc écrits et testés sur Linux ; seul le rendu TabView exige Xcode. Châssis ≈ 1 jour.

📋 Pré-requis feat/watch-outdoor — checklist dev (2026-09-20)

✅ 34 commits accumulés depuis main. Code complet & testé (57 tests verts Linux).

  • IntervalEngine.swift + RouteFilter.swift + LocationTracker.swift — déclarés dans CoachWatch (3 fichiers × 2 déclarations = 6 attendues)
  • CoachWorkoutKit.swift et CoachWorkoutObserver.swift — modifiés pour route (enregistrement, finition, sauvegarde)
  • Info.plist (CoachWatch) — UIBackgroundModes = [location] + NSLocationWhenInUseUsageDescription présents
  • Toutes les corrections BLE/Polar revertées (chantier jeté, n'impacte pas feat/watch-outdoor)
  • Branche stable, dernière corr le 20/08 soir

À tester côté Mac avant toute release (4 points critiques) :

  1. Permission localisation — une sortie extérieure demande "Localisation lorsque l'app est active". Si rien, pas de trace.
  2. Trace dans Santé — à la fin du workout, vérifier Santé > Données > Athlétisme > Trajets : la sortie doit apparaître.
  3. FC remonte sur /live — WorkoutManager modifié, test de régression prioritaire.
  4. Écran verrouillé — positions continuent d'arriver (test suivi GPS en background). S'arrêtent = boucle d'affichage trop rapide (CPU), pas permission.

👉 À 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 feat/watch-outdoor — prêt Mac : builder + tester 4 points critiques (voir checklist ci-dessus). Workflow exact ligne 20.
    • Corrigé le 2026-08-20 : open ios/App/App.xcworkspace n'existe pas. Depuis Capacitor 8 : open ios/App/App.xcodeproj (SPM, pas CocoaPods).
    • Fixes robustesse 2026-07-01 (non compilés avant) à valider : CoachWidgetBridge (score Float ↔ NSNumber.intValue ; KO App Group → reject), CoachAppleAuth (double-tap → rejette ancien call).
    • Checklist pré-App-Store : ios/App/App/App.entitlements a aps-environment = development. TestFlight réclame production — à basculer avant archive (Xcode peut le gérer via profil dist auto, à trancher).
  • 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.