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>
14 KiB
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)
git pullAVANT de toucher au projet ;git pushaprès chaque changement.- ⚠️ Ne jamais laisser Xcode ouvert pendant un
git pull— Xcode garde/réécrit lepbxprojen mémoire → fichiers Swift non compilés + conflits (ça nous a bloqués 2× le 2026-06-05). Quitter Xcode (Cmd+Q), pull, rouvrir. - Après toute modif faite dans Xcode (package SPM, capability, signing) → commit + push
project.pbxproj(+Package.resolved) immédiatement, sinon le prochaingit pullest bloqué par les modifs locales. - 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 »
- Quitter Xcode.
git pull- 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). - Ouvrir Xcode → Clean Build Folder (
Cmd+Shift+K) → Run (Cmd+R) sur l'iPhone. - Erreurs → corriger, committer le fix, push.
- 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é danscapacitorDidLoad()), modèle partagéCoachWidgetSnapshot.swift, widgetsCoachWidgets.swift(Séance du jour + Score de forme) dans l'extensionCoachLiveActivity. App Groupgroup.ch.hypnotruck.coach. Hook web déjà en prod (widget-bridge.js). - Watch :
CoachWidgetBridge.pushToWatch(WCSession) →CoachWatch/ConnectivityManagerreçoit → App Group montre + reload. ComplicationsCoachWatchWidgets/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.
- iPhone : plugin
🆕 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()enregistreCoachRoutineBridgePlugin;CoachWatch/ContentViewouvreRoutineViewen sheet ;ConnectivityManager(watch) gagnesendRoutine()+ réception duroutineSnapshot. - ⚠️
CoachLiveBridgetouché — un seul ajout :routeIfNotLiveSample()aiguille les messagesroutineDoneversNotificationCenterau lieu de les émettre comme samples. Le déléguéWCSessionest 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éclarantTZ2PVTRKNY.ch.hypnotruck.coachsur/live,/routine,/forme,/calendar(chemins énumérés un par un pour ne pas détourner/calendar.ics), etstatic/deeplink.jsqui écouteappUrlOpenet amène la WebView sur le bon chemin — Capacitor ne le fait pas tout seul. - Côté ce repo :
App.entitlementsgagnecom.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 GlassCoachLiveViews'ouvre d'elle-même (le JS de/liveappelleopenNativeLive). 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 :
- Une séance extérieure demande la localisation (feuille « lorsque l'app est active »). Si rien n'apparaît, la trace ne partira pas.
- La sortie a bien un parcours dans Santé à la fin. C'est le test qui
compte :
finishRoutedoit être appelé APRÈSfinishWorkout, sinon la trace existe sans être associée à la séance. - La FC remonte toujours sur
/live—WorkoutManagera été modifié, c'est le test de non-régression prioritaire. - É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
- Serveur (Python, testable sans Mac) — une route rendant la séance du
jour au format
CoachSessionPlan, en réutilisant_blocks_from_sessionetapp.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. - iPhone — pousser ce plan vers la montre par
WCSession, à côté de ce qui part déjà. - Montre —
ConnectivityManagerpublie unsessionPlan; une vue appelleengine.update(elapsed:distance:)à chaque tick et joue une haptique surevents.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(commit5ae6e2d) + clé.p8dé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 queApp.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 enNSNumber.intValue→ corrige un score float perdu ;saverenvoie Bool →call.rejectsi App Group KO),CoachAppleAuth(double-tap : rejette l'ancienpendingCall; erreur testée par domaineASAuthorizationError). Swift pur, aucun nouveau fichier ni capability. - ⚠️ Checklist pré-App-Store (audit sécu 2026-07-01) :
ios/App/App/App.entitlementsaaps-environment = development. Pour la soumission App Store/TestFlight, l'APNs prod exigeproduction. NON changé côté dev (casserait le push en dev device — arbitrage de signing à faire sur Mac : soit basculerproductionavant l'archive de distribution, soit laisser Xcode le gérer via le profil de distribution auto). À trancher/tester au moment du build release.
- ⚠️ Corrigé le 2026-08-20 : cette ligne disait
- 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.