diff --git a/COWORK.md b/COWORK.md index df70c25..e1f58d9 100644 --- a/COWORK.md +++ b/COWORK.md @@ -129,6 +129,59 @@ 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. diff --git a/ios/App/CoachWatch/IntervalEngine.swift b/ios/App/CoachWatch/IntervalEngine.swift index 81445d4..f61b0a6 100644 --- a/ios/App/CoachWatch/IntervalEngine.swift +++ b/ios/App/CoachWatch/IntervalEngine.swift @@ -15,9 +15,32 @@ import Foundation * * - il se teste intégralement sur Linux (cf. `tests-linux/`), là où * `WorkoutManager` ne le peut pas ; - * - le temps de référence est `HKLiveWorkoutBuilder.elapsedTime`, qui - * **exclut déjà les pauses** : le moteur n'a donc rien à savoir de la - * pause, ni de l'auto-pause de la montre ; + * - le temps de référence est poussé par l'appelant ; + * + * ⚠️ **CE COMMENTAIRE AFFIRMAIT LE CONTRAIRE DE SA SOURCE** (relevé le + * 2026-08-20, avant tout câblage). Il disait que + * `HKLiveWorkoutBuilder.elapsedTime` « exclut déjà les pauses », donc que + * le moteur n'avait rien à savoir de la pause. La doc Apple dit + * l'inverse, mot pour mot : « The elapsed time for the workout based on + * the builder's current contents, **including pauses**. » + * (developer.apple.com/documentation/healthkit/hkliveworkoutbuilder/elapsedtime) + * + * Conséquence si on câble le moteur sur cette propriété 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 tout + * seul, à 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. » Ce n'est + * PAS la même API, et les deux textes d'Apple se contredisent + * frontalement sur ce point : à trancher avant le câblage. + * + * Le moteur, lui, reste correct : il ne fait qu'intégrer ce qu'on lui + * pousse. C'est l'appelant qui devra fournir un temps réellement actif ; + * + * - il ignore donc l'auto-pause de la montre, à condition que la source de + * temps ci-dessus soit correcte ; * - il est rejouable : réinjecter la même suite de ticks redonne le même * déroulé, ce qui rend la reprise après crash triviale * (`handleActiveWorkoutRecovery`).