diff --git a/docs/watch-plan-origin-runbook-mac.md b/docs/watch-plan-origin-runbook-mac.md new file mode 100644 index 0000000..467a02a --- /dev/null +++ b/docs/watch-plan-origin-runbook-mac.md @@ -0,0 +1,83 @@ +# Runbook Mac — le build qui rend l'envoi vérifiable + +**Écrit le 03/09/2026, à suivre sur le Mac mini.** Compte 20 minutes, dont la +moitié d'attente. Ce build ne change rien à ce que la montre reçoit : il rend +l'app capable de **dire** ce qu'elle a fait, ce qu'elle ne savait pas faire. + +Le dossier complet — deux épisodes de « la montre ne dit plus rien », leur +chronologie et ce qu'elle élimine — est dans `coach_sportif`, +[`docs/GUIDE-MONTRE.md`](../../coach_sportif/docs/GUIDE-MONTRE.md) §5quater et +§5quinquies. Ici : les gestes. + +## Ce qui change + +Deux fichiers, **déjà déclarés dans la target App** — donc rien à ajouter dans +Xcode, pas de Target Membership à cocher, pas de nouveau framework à lier +(WorkoutKit était déjà importé par `CoachWorkoutKit.swift`) : + +- `ios/App/App/CoachWorkoutKit.swift` +- `ios/App/App/CoachWorkoutObserver.swift` + +Branche : `feat/watch-outdoor`. Le côté serveur est **déjà en prod**, il attend +juste que l'app l'appelle. + +## 1. Récupérer et compiler + +```bash +cd ~/coach-ios # ou l'emplacement du clone sur le Mac +git checkout feat/watch-outdoor +git pull +npx cap sync ios +open ios/App/App.xcworkspace +``` + +Dans Xcode : cible **App**, ton iPhone connecté, ▶︎. Rien d'autre à toucher. + +⚠️ **Ce code n'a jamais été compilé** — WorkoutKit et HealthKit n'existent pas +dans Swift pour Linux, et `tests-linux/` ne couvre que ce qui n'importe que +Foundation. Les signatures Apple ont été vérifiées une par une dans la doc DocC +avant d'être écrites, mais si quelque chose casse, c'est là que ça cassera : + +| symptôme | où regarder | +|---|---| +| `value of type 'HKWorkout' has no member 'workoutPlan'` | l'extension vient de WorkoutKit : vérifier que `import WorkoutKit` est bien pris (il est sous `#if canImport`) | +| erreur sur `case .custom(let custom)` | `WorkoutPlan.workout` est l'enum `WorkoutPlan.Workout` — cas `custom`, `goal`, `pacer`, `swimBikeRun` | +| erreur de conversion sur `sw.date` | c'est un `DateComponents`, pas une `Date` — d'où le `Calendar.current.date(from:)` | + +## 2. Vérifier sans courir (2 minutes, dans le canapé) + +1. Ouvre **Programme**, la séance du jour, et appuie sur l'envoi Apple Watch. + Le message doit maintenant **nommer la séance programmée** : + « Programmée sur la montre : CDC · … ». + - S'il dit « la montre ne la liste pas encore » : le scheduler n'a pas encore + publié — regarde *Exercice → Programmées* au poignet avant de conclure. + - S'il dit « Autorisation « Séances programmées » absente » : **c'est la + réponse au silence**. Réglages iPhone → coach → autoriser, puis renvoyer. + C'est exactement ce qu'une réinstallation de l'app remet à zéro, et + l'ancienne version affichait « Envoyé » quand même. + +2. Console Xcode (facultatif), filtre `CoachWorkoutKit` : `schedule OK`, + `authorizationState`. + +## 3. Après ta prochaine séance + +Le plugin remonte tout seul, en fin de séance, si elle venait de la séance +programmée ou d'un départ à la main. Pour le lire : + +```bash +# sur nexus, là où vivent les données (node n'y est pas installé) +venv/bin/python3 tools/watch_alert_check.py --dump /tmp/wac.json +# puis en local, là où node existe +python3 tools/watch_alert_check.py --from /tmp/wac.json +``` + +Trois réponses possibles, et une seule était disponible jusqu'ici : + +- **« Séance lancée depuis la séance programmée »** → les alertes comptées + étaient bien dues. Si tu n'as rien entendu, le défaut est ailleurs que dans + l'envoi et le lancement. +- **« Séance lancée À LA MAIN »** → aucune alerte n'était possible, le silence + est normal. C'est le cas que rien ne savait détecter, et qui devient plausible + depuis que la Polar est la montre de sport. +- **« Origine inconnue »** → séance antérieure au build, ou lecture en échec. + Ce n'est **pas** un « non » : l'app préfère se taire qu'affirmer à tort. diff --git a/ios/App/App/CoachWorkoutObserver.swift b/ios/App/App/CoachWorkoutObserver.swift index dbf52fc..c1d2be4 100644 --- a/ios/App/App/CoachWorkoutObserver.swift +++ b/ios/App/App/CoachWorkoutObserver.swift @@ -27,7 +27,11 @@ import HealthKit import UserNotifications // Pour l'extension `HKWorkout.workoutPlan` (iOS 17+), qui rend la composition // du plan dont une séance est issue — ou nil si elle a été lancée à la main. +// Même garde que `CoachWorkoutKit.swift` : sur un SDK sans WorkoutKit, le +// fichier doit continuer à compiler, quitte à ne rien remonter. +#if canImport(WorkoutKit) import WorkoutKit +#endif @objc(CoachWorkoutObserverPlugin) public class CoachWorkoutObserverPlugin: CAPPlugin, CAPBridgedPlugin { @@ -392,6 +396,7 @@ public class CoachWorkoutObserverPlugin: CAPPlugin, CAPBridgedPlugin { /// Même parti pris que `reportToRoutineIfStrength` : on n'envoie que des /// faits bruts, l'interprétation vit côté serveur donc sans rebuild iOS. private func reportPlanOrigin(_ workout: HKWorkout) { + #if canImport(WorkoutKit) guard #available(iOS 17.0, *) else { return } Task { var fromPlan = false @@ -431,6 +436,7 @@ public class CoachWorkoutObserverPlugin: CAPPlugin, CAPBridgedPlugin { } }.resume() } + #endif } private func scheduleNotification(for workout: HKWorkout) {