Files
coach-ios/COWORK.md
Sylvain Bettinelli a44fa0046a Cadrage des vues d'entraînement au choix, et point d'étape du 21/08
Le chantier « écrans configurables façon WorkOutDoors » est cadré, rien n'est
codé. docs/watch-training-screens.md fixe l'analyse pour qu'une session
ultérieure n'ait pas à la refaire.

Ce qui a été établi :

- LiveWorkoutView a cinq métriques ÉCRITES EN DUR ; il n'existe aucune notion
  de champ, de page ni de configuration. Tout est à créer.
- Neuf champs sont disponibles sans aucune collecte nouvelle (dont allure, D+,
  précision GPS, étape du fractionné) ; FC moyenne, temps en zone, cadence,
  puissance et laps demandent du travail.
- Décision : la configuration s'édite côté WEB (TileManager existe déjà) et
  voyage par WCSession — donc changer ses écrans ne demandera aucun rebuild
  Xcode, comme pour la routine.
- Le modèle et le catalogue sont du Foundation pur : écrits et testés sur
  Linux, seul le rendu TabView exige le Mac. Ne pas commencer par le rendu.
- Deux contraintes tenues dès la conception : 1 Hz maximum et page visible
  seule (le CPU suspend l'app et arrête le GPS sans erreur), et espacement des
  mises à jour en luminance réduite.

COWORK gagne le point d'étape du 21/08 : build passé, correctif finishRoute, et
surtout le fait que les 4 vérifications au poignet ne sont PAS encore faites —
les logs du jour ne montrent que l'app iPhone.

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

17 KiB
Raw Permalink 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 AppAdd 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 AppSigning & 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 /liveWorkoutManager 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 fournitConnectivityManager 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. MontreConnectivityManager 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.

👉 À 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.