From 6e4011db3fe0f38aaceb6f256d70c930f79f6988 Mon Sep 17 00:00:00 2001 From: Sylvain Bettinelli Date: Sun, 24 May 2026 13:25:07 +0000 Subject: [PATCH] docs(watchos): plan technique + handoff Claude Code Mac mini MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Préparation du chantier watchOS Live Workout (live FC/calories/distance sur l'iPhone pendant que Sylvain fait du sport sur l'Apple Watch). Décision archi : la phase native Swift/watchOS nécessite Xcode + Apple Watch device, indispo dans la session Linux où ces docs ont été écrites. D'où le handoff vers Claude Code lancé sur le Mac mini Sylvain. Fichiers ajoutés : - `docs/watchos-live-workout-plan.md` (444 lignes) Plan technique complet en 5 phases : 1. Création target watchOS Xcode + entitlements 2. Code Swift watchOS (4 fichiers : CoachWatchApp, WorkoutManager, ConnectivityManager, ContentView) 3. Plugin Capacitor iOS (CoachLiveBridge.swift) qui reçoit les samples WCSession et les publie au JS via notifyListeners 4. Route web /live + template (squelette prêt à coller dans coach_sportif/web/) 5. Tests device + TestFlight internal Inclut décisions d'archi (HKLiveWorkoutBuilder, sendMessage avec fallback transferUserInfo, pas de Live Activity V1), pseudocode Swift, estimation effort 12-20h sur 3-5 jours, et risques + mitigations. - `HANDOFF-WATCHOS.md` (181 lignes) Instructions étape par étape pour Claude Code Mac. Liste les memories à charger (project_coach_watchos_live, project_coach_ios, project_coach_app_commercial, user_sante_cardio, feedback_inline_display_none_toggle), workflow Step 1..6 (setup env, création target, lecture docs Apple, implémentation Swift, route web, tests device), garde-fous et règle "qui possède quoi" pendant le chantier (Mac = Swift natif, Linux = backend Python). - `CLAUDE.md` (1ère commit, fichier existait en local non-tracked) Pointeur ajouté dans la section Backlog vers les 2 nouveaux fichiers. Aucun fichier Swift n'a été écrit ici (décision conjointe avec Sylvain pour ne pas risquer du code à l'aveugle sans Xcode pour valider les APIs HealthKit/WCSession à jour). --- CLAUDE.md | 86 ++++++ HANDOFF-WATCHOS.md | 181 ++++++++++++ docs/watchos-live-workout-plan.md | 444 ++++++++++++++++++++++++++++++ 3 files changed, 711 insertions(+) create mode 100644 CLAUDE.md create mode 100644 HANDOFF-WATCHOS.md create mode 100644 docs/watchos-live-workout-plan.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0caf1e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,86 @@ +# CLAUDE.md — coach-ios (wrapper mobile Capacitor) + +Instructions pour Claude Code sur ce projet. Lis ce fichier en début de session. + +## 🎯 Contexte projet + +- **Nature** : wrapper Capacitor 8 (iOS + Android) qui charge `https://coach.hypnotruck.ch` dans une WebView +- **Pas de code web embarqué** — `server.url` dans `capacitor.config.ts` pointe vers la prod → déploiement web = mise à jour app sans review Apple/Google +- **Bundle ID** : `ch.hypnotruck.coach` (iOS + Android) +- **Repo Gitea** : encore nommé `coach-ios` (renommage `coach-mobile` à faire) ; fichier `coach-ios.local.json` conservé pour ne pas casser les refs Swift +- **Plateformes** : iOS 13+ et Android 8.0+ (API 26+, requis par Health Connect) +- **Build iOS** : nécessite le Mac mini (macOS 13+, Xcode 15+). Pas de Mac cloud nécessaire. +- **État** : iOS prêt (HealthKit + cookie inject AppDelegate). Android prêt. Bloqué externe : upgrade macOS 26 pour build iOS + Play Console $25 ~3-5j. + +## 🔒 Règles non négociables + +1. **Le code web n'est PAS ici** — pour modifier l'UI/UX, c'est `coach_sportif/web/`. Ce repo ne touche QUE le natif (Swift, Kotlin, plugins Capacitor, config). +2. **Cookie inject AppDelegate** (iOS) : mécanisme critique pour l'auth WKWebView. Toute modif `AppDelegate.swift` doit préserver ce flux. +3. **Health Connect Android** : nécessite la privacy policy URL dans le manifest (validation Play Console). +4. **Pas d'extrapolation** : root cause vérifiable avant tout fix natif. iOS+Android = doubles surfaces de bug. +5. **Default yes** : décider et exécuter (sauf actions destructives). +6. **Pousser après commit** : `git push` systématique. +7. **Descriptions Info.plist en FR** (NSHealthShareUsageDescription etc.) — App Store FR. + +## 🛠️ Workflow par phase + +### Avant toute feature native +- `brainstorming` — explorer l'intent (souvent : "ce truc marche sur web, comment le porter en natif ?") +- `grill-with-docs` — challenger : est-ce vraiment du natif ou du web qui marcherait via WebView ? + +### Code Swift / iOS +- `swiftui-pro` — quand le chantier **watchOS live workout** démarrera (HKWorkoutPlan + WatchConnectivity) +- Vérifier `CoachHealthRoute.swift`, `CoachWorkoutKit.swift`, `CoachWorkoutObserver.swift` avant de toucher au flow HealthKit + +### Code Kotlin / Android +- `compose-state-hoisting` — si plugin Capacitor custom avec UI Compose +- `kotlin-coroutines-structured-concurrency` — appels Health Connect async +- `kotlin-flow-state-event-modeling` — observations Health Connect + +### Bug / régression natif +- `investigate` — root cause discipline (cassures fréquentes : permissions, signatures, entitlements) +- `diagnose` — reproduire sur device réel, pas juste simulator + +### QA / ship +- `qa` (gstack) — l'app pointant vers la prod web, tester le web couvre 80% +- `webapp-testing` — Playwright sur `coach.hypnotruck.ch` (le contenu réel de la WebView) +- `cso` — audit signatures, entitlements, secrets dans `coach-ios.local.json` +- `ship` — workflow PR (mais les releases App Store/Play Store restent manuelles depuis Mac mini) + +## 📂 Conventions code + +- **iOS natif** : `ios/App/App/*.swift` + - `AppDelegate.swift` — bootstrap + cookie inject (NE PAS CASSER) + - `MainViewController.swift` — WKWebView config + - `CoachAuth.swift.example` → copier en `CoachAuth.swift` (gitignored, contient secret partagé) + - `CoachHealthRoute.swift` / `CoachWorkoutKit.swift` / `CoachWorkoutObserver.swift` — pont HealthKit ↔ web +- **Android natif** : `android/app/src/main/java/ch/...` +- **Config Capacitor** : `capacitor.config.ts` (TS, source de vérité) → généré vers `ios/App/App/capacitor.config.json` +- **Resources** : `resources/` → assets (icons, splash) générés par `@capacitor/assets` + +## 🔗 Plugins Capacitor utilisés + +- `@capacitor/push-notifications` +- `@capacitor/local-notifications` +- `@capacitor/geolocation` +- `@capgo/capacitor-health` — HealthKit (iOS) / Health Connect (Android) +- `@capacitor/splash-screen` +- `@capacitor/status-bar` +- `@capacitor/haptics` + +## 🚧 Backlog notable + +- Renommer le repo Gitea `coach-ios` → `coach-mobile` +- Upgrade macOS 26 sur Mac mini pour reprendre les builds iOS +- Play Console validation (~3-5j, $25 one-shot) pour upload Android +- **watchOS live workout compagnon** (chantier ACTIF depuis 2026-05-24) — plan + technique complet dans `docs/watchos-live-workout-plan.md`. Handoff pour + Claude Code Mac dans `HANDOFF-WATCHOS.md` (à lire en 1er si tu reprends ce + chantier). +- Push workouts vers Apple Watch (HKWorkoutPlan) / Polar (Training Targets API) / Garmin — backlog 2026-05-12 + +## ⚠️ Limites connues sync + +- **Apple Watch sync** : impossible via API tierce — le wrapper HealthKit dans cette app est obligatoire pour récupérer les données Watch. +- **Polar AccessLink** : pas d'historique pré-enregistrement (limitation API). +- **Push training-targets vers Polar** : nécessiterait migration AccessLink v4 (backlog). diff --git a/HANDOFF-WATCHOS.md b/HANDOFF-WATCHOS.md new file mode 100644 index 0000000..a049349 --- /dev/null +++ b/HANDOFF-WATCHOS.md @@ -0,0 +1,181 @@ +# Handoff — Chantier watchOS Live Workout + +**Pour** : Claude Code lancé sur le Mac mini Sylvain (`cd ~/coach-ios && claude`). +**De** : Claude Code session Linux du 2026-05-24 (sandbox sans Xcode). +**Pourquoi le handoff** : la phase native Swift/watchOS nécessite Xcode + +Apple Watch device, indispo sur le sandbox Linux. + +## Première chose à faire avant de coder + +1. **Lis** `coach-ios/CLAUDE.md` (règles projet). +2. **Lis** `coach-ios/docs/watchos-live-workout-plan.md` (plan technique complet). +3. **Vérifie** que tu es bien sur le Mac mini avec Xcode 16+ : `xcodebuild -version`. +4. **Vérifie** que `coach-ios` est à jour : `git pull` (les commits préparatoires + sont déjà poussés sur `main`). + +## Contexte session Linux (2026-05-24) + +Sylvain a confirmé vouloir attaquer le chantier watchOS Live Workout +(memory `project_coach_watchos_live.md`). On a estimé ensemble qu'écrire +du Swift à l'aveugle depuis le sandbox Linux était risqué (training data +peut diverger des dernières APIs Apple, pas de compile Xcode pour valider). + +D'où ce handoff : toute la phase native (Phases 1-3 du plan) doit être +exécutée par toi sur le Mac avec lecture des docs Apple en direct. + +Memories à charger pour contexte complet (probablement déjà dans `~/.claude/projects/-home-coder/memory/` synchronisé via Gitea ou autre, sinon +demande à Sylvain de te briefer) : + +- `project_coach_watchos_live.md` — chantier original +- `project_coach_ios.md` — état wrapper Capacitor +- `project_coach_app_commercial.md` — pivot commercial (le watchOS live est un + différenciateur clé pour vendre à 39 ou 99 CHF/mois) +- `user_sante_cardio.md` — FCmax Sylvain 145-155 bpm (à respecter dans tests) +- `feedback_inline_display_none_toggle.md` — règle apprise hier : grep JS avant + de migrer du `style="display:none"` initial vers du CSS + +## État actuel à ton arrivée + +``` +coach-ios/ +├── CLAUDE.md ............. Règles projet (à jour) +├── HANDOFF-WATCHOS.md .... Ce fichier +├── docs/ +│ └── watchos-live-workout-plan.md ... Plan technique complet +├── ios/App/App/ +│ ├── AppDelegate.swift .......... Bootstrap + cookie inject (NE PAS CASSER) +│ ├── MainViewController.swift ... WKWebView config +│ ├── CoachHealthRoute.swift ..... HealthKit web bridge existant +│ ├── CoachWorkoutKit.swift ...... HKWorkoutPlan push (iOS 17+) +│ └── CoachWorkoutObserver.swift . Observer fin de séance +└── ios/App/App.xcodeproj ......... Projet Xcode (PAS de target watchOS encore) +``` + +**Aucun fichier Swift watchOS écrit pour l'instant** — décision conjointe : +tu écris depuis zéro après avoir lu les docs Apple à jour. + +## Workflow recommandé sur ton Mac + +### Step 1 — Setup environnement (30 min) + +```bash +cd ~/coach-ios +git pull origin main +xcodebuild -version # Xcode 16+ requis (watchOS 11+ SDK) +xcrun simctl list devicetypes | grep Watch # voir simulateurs watchOS dispo +``` + +Vérifie aussi que ton Apple Watch est jumelée à l'iPhone et tournée vers +watchOS 11+ (cf. About sur la Watch). + +### Step 2 — Création de la target watchOS (1-2h) + +**2 voies possibles** : + +**Voie A — Manuelle dans Xcode UI** (recommandée pour la 1ère fois) : +1. Ouvre `ios/App/App.xcworkspace` +2. `File > New > Target...` → watchOS → App +3. Suis le plan section "Phase 1.1" du fichier `docs/watchos-live-workout-plan.md` +4. Commit le diff sur `project.pbxproj` + +**Voie B — Édition pbxproj directe** (si tu veux être 100% reproducible) : +- Édite `ios/App/App.xcodeproj/project.pbxproj` à la main +- Risque : format propriétaire Xcode, fragile. À éviter sauf si Voie A + bloquée. + +### Step 3 — Lecture des docs Apple à jour (45 min) + +Avant de coder, lis ces 4 pages doc Apple **dans ton browser** ou via +`mcp__claude_ai_*` si tu as un MCP fetch dispo : + +1. `developer.apple.com/documentation/healthkit/hkliveworkoutbuilder` +2. `developer.apple.com/documentation/watchconnectivity/wcsession` +3. `developer.apple.com/documentation/healthkit/hkworkoutsession` +4. `capacitorjs.com/docs/plugins/creating-plugins/ios-guide` + +Note les **signatures exactes** qui peuvent avoir changé. Le plan technique +du fichier `docs/watchos-live-workout-plan.md` contient du pseudocode qui +peut être périmé sur des détails (par exemple async/await sur certaines +méthodes en watchOS 11+). + +### Step 4 — Implémentation Swift (6-8h sur 1-2 jours) + +Suis les Phases 2 et 3 du plan technique. Commit petit par petit : + +- Commit 1 : `CoachWatchApp.swift` + `ContentView.swift` minimal + (juste affichage statique, vérifie que la target builde sur simulator). +- Commit 2 : `WorkoutManager.swift` (HKLiveWorkoutBuilder seul, sans + Connectivity). Vérifie sur Apple Watch device que les samples HR arrivent. +- Commit 3 : `ConnectivityManager.swift` (WCSession). Test envoi simple. +- Commit 4 : `CoachLiveBridge.swift` côté iPhone (Capacitor plugin). +- Commit 5 : Tests + fixes itératifs. + +Règle clé : **chaque commit doit compiler** (Watch + iPhone targets). + +### Step 5 — Route web `/live` + template + +Cette partie peut être faite par toi OU déléguée à une session Claude Code +sur le repo `coach_sportif/` (Linux Linux sandbox d'origine). Le code FastAPI ++ Jinja est trivial une fois le plugin Capacitor publie l'event JS +`liveSample`. + +Si tu veux le faire toi-même sur Mac : Phase 4 du plan technique a le +squelette prêt à coller. + +### Step 6 — Tests device + TestFlight internal (4-8h) + +- Test E2E : lance app CoachWatch sur Apple Watch → choisis activité + Course → vérifie /live sur iPhone affiche le BPM en moins de 10 sec. +- Test fallback : Bluetooth off pendant 30 sec, vérifie que les samples + arrivent quand BLE revient. +- Test batterie : 1h de workout, vérifier % drop sur Watch (cible <15%). +- Upload TestFlight internal (juste pour Sylvain) avant la 1ère review + publique. + +## Garde-fous communs à respecter (depuis les memories) + +1. **Pas d'extrapolation** : si une API Apple n'est pas vérifiée dans la doc + live, **ne pas l'inventer**. Préfère commenter "TODO: vérifier signature" + et demander à Sylvain. +2. **Default yes** : décide et exécute sauf actions destructives (push force, + delete entitlements, etc.). +3. **Git push après chaque commit** atomique qui compile. +4. **descriptions Info.plist en FR** (App Store FR). +5. **`raise X from e`** ou `from None` côté Python (B904 fixé ce matin, + préserver la norme). +6. **Tests à chaque feature** ; pas de merge sans test minimum. +7. **CLAUDE.md à jour** : si tu décides quelque chose qui change l'archi + (ex : passer à Live Activity au lieu de webview /live), document-le. + +## Quand revenir vers la session Linux + +Le repo `coach_sportif/` (Linux sandbox source) reste mon terrain : + +- Backend FastAPI (route `/live`, helpers) +- Template Jinja (`live_workout.html`) +- Tests pytest backend +- Refactor CSS / scope global + +Quand tu finis le plugin Capacitor `CoachLiveBridge` et que tu as besoin +de la route `/live` côté serveur, soit tu la code toi-même (Phase 4), soit +tu demandes à Sylvain de relancer la session Linux pour que je l'écrive. + +## Référence "qui possède quoi" pendant le chantier + +| Composant | Owner | Notes | +|---|---|---| +| `coach-ios/ios/App/App/Coach*Watch.swift` | Toi (Mac) | Swift natif iOS | +| `coach-ios/watch/CoachWatch/*.swift` | Toi (Mac) | Swift natif watchOS | +| `coach-ios/ios/App/App/CoachLiveBridge.swift` | Toi (Mac) | Plugin Capacitor iOS | +| `coach-ios/project.pbxproj` | Toi (Mac) | Édition prudente | +| `coach_sportif/web/routers/misc.py` route /live | Moi (Linux) ou toi | Trivial | +| `coach_sportif/web/templates/live_workout.html` | Moi (Linux) ou toi | Squelette dans plan | +| `coach_sportif/web/static/style.css` ajouts .live-* | Moi (Linux) | Si je fais le template | +| Tests pytest backend | Moi (Linux) | | +| Tests Xcode XCTest | Toi (Mac) | | + +## Bon courage + +Quand tu auras fini : commit final qui résume l'état + ferme ce chantier +dans la memory (`project_coach_watchos_live.md` à update). Et appelle +Sylvain pour test device. 🚀 diff --git a/docs/watchos-live-workout-plan.md b/docs/watchos-live-workout-plan.md new file mode 100644 index 0000000..af26794 --- /dev/null +++ b/docs/watchos-live-workout-plan.md @@ -0,0 +1,444 @@ +# Chantier watchOS — Live Workout sur iPhone + +**Statut** : plan validé, prêt à exécuter sur Mac mini avec Xcode. +**Owner exécution** : Claude Code lancé sur le Mac (cd `~/coach-ios && claude`). +**Origine** : memory `project_coach_watchos_live.md` + session Linux 2026-05-24. + +## Objectif fonctionnel + +Pendant que Sylvain fait une séance de sport sur son Apple Watch, +l'iPhone (et la web app `/live`) affiche en temps réel : FC bpm, calories, +distance parcourue, durée écoulée, allure (si applicable). + +Comportement de référence : app **Apple Workout / Vélo native** qui le +fait déjà côté iPhone via APIs privées. On reproduit avec APIs publiques. + +## Architecture cible (5 couches) + +``` +┌─────────────────────────────────────────────┐ +│ Apple Watch │ +│ ┌─────────────────────────────────────┐ │ +│ │ CoachWatch (watchOS app native) │ │ +│ │ ┌─────────────────────────────┐ │ │ +│ │ │ WorkoutManager (HKLiveWB) │←──┼───┼── L1 +│ │ └─────────────────────────────┘ │ │ +│ │ ↓ heart rate / │ │ +│ │ ↓ calories / │ │ +│ │ ↓ distance │ │ +│ │ ┌─────────────────────────────┐ │ │ +│ │ │ ConnectivityManager (WCSession) ←──┼── L2 +│ │ └─────────────────────────────┘ │ │ +│ └─────────────────────────────────────┘ │ +└──────────────────────┬──────────────────────┘ + │ WatchConnectivity + │ sendMessage (1-3 sec) + ↓ +┌─────────────────────────────────────────────┐ +│ iPhone │ +│ ┌─────────────────────────────────────┐ │ +│ │ App.xcodeproj (Capacitor) │ │ +│ │ ┌─────────────────────────────┐ │ │ +│ │ │ CoachLiveBridge.swift │←──┼───┼── L3 +│ │ │ (Capacitor Plugin natif) │ │ │ +│ │ └─────────────────────────────┘ │ │ +│ │ ↓ event JS │ │ +│ │ ┌─────────────────────────────┐ │ │ +│ │ │ WKWebView (loads /live) │←──┼───┼── L4 +│ │ └─────────────────────────────┘ │ │ +│ └─────────────────────────────────────┘ │ +└──────────────────────┬──────────────────────┘ + │ HTTP (au démarrage seulement) + ↓ +┌─────────────────────────────────────────────┐ +│ Backend coach.hypnotruck.ch │ +│ ┌─────────────────────────────────────┐ │ +│ │ /live route (FastAPI + template) │←──── L5 +│ └─────────────────────────────────────┘ │ +└─────────────────────────────────────────────┘ +``` + +## Décisions d'archi (à respecter) + +### Décision 1 — App watchOS native, pas SwiftUI Capacitor + +Le live workout nécessite `HKLiveWorkoutBuilder` qui n'est dispo que sur watchOS +natif (Xcode target Watch App). Capacitor ne tourne PAS sur watchOS. + +### Décision 2 — `sendMessage` plutôt que `transferUserInfo` + +`sendMessage(_:replyHandler:errorHandler:)` est temps réel mais nécessite +les 2 devices reachable. Si la Watch perd la connexion à l'iPhone (BLE down), +on passe en fallback `transferUserInfo` (queued, livraison différée). + +Stratégie : tenter `sendMessage` toutes les 1-3 sec. Si `WCSession.isReachable == false`, +buffer 30 sec puis dump via `transferUserInfo` au retour de connexion. + +### Décision 3 — Pas de Live Activity iOS pour le MVP + +Live Activity (Dynamic Island + Lock Screen) = ActivityKit framework, plus +de complexité, plus de surface de bug, validation Apple Store plus stricte. +Reporté à la V2 si la V1 cartonne en TestFlight. + +V1 = juste page web `/live` qui affiche les données. iPhone doit être déverrouillé +et l'app au premier plan pour voir le live. Comportement acceptable car le user +est en train de faire du sport — il a son iPhone à côté de lui (vélo, treadmill). + +### Décision 4 — Démarrage du workout depuis l'app Watch (pas l'iPhone) + +`HKLiveWorkoutBuilder` doit être démarré côté watchOS. Donc l'user **lance la +séance depuis l'app CoachWatch sur sa montre**, pas depuis l'app native Workout. + +Trade-off accepté : moins ergonomique (l'user pourrait préférer Workout native), +mais c'est la seule voie publique. Workaround possible plus tard : +**WatchOS App Intent / Complication** pour "Lancer séance Coach" depuis le +cadran (1 tap). + +### Décision 5 — Bundle ID watchOS + +`ch.hypnotruck.coach.watchkitapp` (extension du bundle iOS `ch.hypnotruck.coach`). +Signing automatique via Apple Developer Account déjà actif. + +## Phase 1 — Création de la cible watchOS dans Xcode + +**Owner** : Sylvain (manuel UI Xcode) ou Claude Code Mac via édition +`project.pbxproj` direct. + +### Étape 1.1 — Add target Watch App + +Dans Xcode : +1. `File > New > Target` +2. Choisir **watchOS > App** (pas "App with Companion" car notre iPhone est Capacitor) +3. Product Name : `CoachWatch` +4. Interface : **SwiftUI** +5. Language : Swift +6. Bundle ID : `ch.hypnotruck.coach.watchkitapp` +7. Embed in Application : **App** (l'app iPhone existante) +8. Finish + +### Étape 1.2 — Entitlements watchOS + +Dans Signing & Capabilities de la target CoachWatch : +1. `+ Capability` → **HealthKit** + - Cocher : Workout Processing +2. `+ Capability` → **Background Modes** + - Cocher : Workout processing +3. Vérifier le provisioning profile (auto-managed devrait suffire avec Apple Dev Account) + +### Étape 1.3 — Info.plist watchOS + +Ajouter 2 clés (FR pour App Store FR) : +- `NSHealthShareUsageDescription` : "Coach Hypnotruck lit ta fréquence cardiaque et tes calories pendant la séance pour les afficher en direct sur ton iPhone." +- `NSHealthUpdateUsageDescription` : "Coach Hypnotruck enregistre ta séance dans l'app Santé." + +## Phase 2 — Code Swift watchOS + +**4 fichiers Swift à créer** dans la target CoachWatch : + +### `CoachWatchApp.swift` (entry point SwiftUI) + +```swift +import SwiftUI +import WatchKit + +@main +struct CoachWatchApp: App { + @WKApplicationDelegateAdaptor private var appDelegate: AppDelegate + + var body: some Scene { + WindowGroup { + ContentView() + .environmentObject(WorkoutManager.shared) + .environmentObject(ConnectivityManager.shared) + } + } +} + +class AppDelegate: NSObject, WKApplicationDelegate { + func applicationDidFinishLaunching() { + // Activate WCSession dès le boot pour ne pas rater les early messages + _ = ConnectivityManager.shared + } +} +``` + +### `WorkoutManager.swift` (HKLiveWorkoutBuilder) + +Pattern : +- Singleton `@MainActor` +- `@Published` pour les samples (heartRate, activeEnergyKcal, distanceMeters, elapsedSec) +- `func startWorkout(activityType: HKWorkoutActivityType)` qui demande auth HK, crée la session + builder, configure le delegate +- `HKLiveWorkoutBuilderDelegate` : `workoutBuilder(_:didCollectDataOf:)` qui pull les samples + push via Connectivity +- `func endWorkout()` qui clôture proprement + +APIs minimales requises : +- `HKHealthStore` +- `HKWorkoutSession(healthStore:configuration:)` +- `HKLiveWorkoutBuilder(...)` +- `HKQuantityTypeIdentifier.heartRate`, `.activeEnergyBurned`, `.distanceWalkingRunning` (ou `.distanceCycling`) +- `HKWorkoutConfiguration` avec `activityType` + `locationType` (outdoor/indoor) + +### `ConnectivityManager.swift` (WCSession) + +Pattern : +- Singleton avec `WCSession.default` activé +- `func sendSample(...)` qui try `sendMessage` si reachable, sinon buffer en RAM + flush via `transferUserInfo` +- Throttle : un sample max toutes les 1-3 sec pour ne pas saturer BLE + +```swift +import WatchConnectivity + +@MainActor +final class ConnectivityManager: NSObject, ObservableObject, WCSessionDelegate { + static let shared = ConnectivityManager() + private var buffer: [[String: Any]] = [] + + override init() { + super.init() + guard WCSession.isSupported() else { return } + WCSession.default.delegate = self + WCSession.default.activate() + } + + func sendSample(_ payload: [String: Any]) { + let session = WCSession.default + if session.isReachable { + session.sendMessage(payload, replyHandler: nil) { [weak self] _ in + // Fallback queue + self?.buffer.append(payload) + } + } else { + buffer.append(payload) + if buffer.count >= 10 || buffer.count == 1 { + flushBuffer() + } + } + } + + private func flushBuffer() { + guard !buffer.isEmpty else { return } + WCSession.default.transferUserInfo(["samples": buffer]) + buffer.removeAll() + } + + // WCSessionDelegate stubs + func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {} + func sessionReachabilityDidChange(_ session: WCSession) { + if session.isReachable { flushBuffer() } + } +} +``` + +### `ContentView.swift` (UI SwiftUI watchOS) + +3 vues : +- **Picker activité** : Course / Vélo / Marche / Renfo (les 4 sports principaux du plan) +- **Live workout view** : grosses metrics centrées (FC, calories, durée), bouton Pause/Resume + End +- **Summary view** : après End, montre les totaux + bouton "Retour" qui ferme l'app + +Patterns SwiftUI iOS HIG conformes : +- `TabView` avec `.tabViewStyle(.page)` pour swipe entre vues +- `.digitalCrown` rotation pour ajuster nothing (skip) +- Haptic feedback sur start/pause/end (`WKInterfaceDevice.current().play(.start)` etc.) +- Always-on display via `.scenePhase` (preserve simplified UI quand `.inactive`) + +## Phase 3 — Plugin Capacitor iOS (côté iPhone) + +### `CoachLiveBridge.swift` + +À placer dans `ios/App/App/CoachLiveBridge.swift`. C'est un plugin +Capacitor natif qui : +1. Active `WCSession` côté iPhone au boot +2. Implémente `WCSessionDelegate.session(_:didReceiveMessage:)` et `didReceiveUserInfo:` +3. Pousse les samples au JS via `notifyListeners("liveSample", payload)` + +```swift +import Foundation +import Capacitor +import WatchConnectivity + +@objc(CoachLiveBridge) +public class CoachLiveBridge: CAPPlugin, WCSessionDelegate { + + override public func load() { + super.load() + guard WCSession.isSupported() else { return } + WCSession.default.delegate = self + WCSession.default.activate() + } + + public func session(_ session: WCSession, + activationDidCompleteWith activationState: WCSessionActivationState, + error: Error?) {} + public func sessionDidBecomeInactive(_ session: WCSession) {} + public func sessionDidDeactivate(_ session: WCSession) { + // Re-activer pour les sessions futures (pattern recommandé) + WCSession.default.activate() + } + + public func session(_ session: WCSession, didReceiveMessage message: [String: Any]) { + DispatchQueue.main.async { [weak self] in + self?.notifyListeners("liveSample", data: message) + } + } + + public func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) { + if let samples = userInfo["samples"] as? [[String: Any]] { + DispatchQueue.main.async { [weak self] in + for s in samples { self?.notifyListeners("liveSample", data: s) } + } + } + } +} +``` + +### Enregistrement du plugin + +Dans `Podfile` (déjà géré par Capacitor build) et dans le JS web : + +```javascript +// Côté web (à ajouter dans /live template) +import { registerPlugin } from '@capacitor/core'; +const CoachLiveBridge = registerPlugin('CoachLiveBridge'); +CoachLiveBridge.addListener('liveSample', (sample) => { + // sample.heartRate, sample.activeEnergyKcal, sample.distanceMeters, sample.elapsedSec + updateLiveUI(sample); +}); +``` + +## Phase 4 — Route web `/live` côté backend + +À ajouter dans `coach_sportif/web/routers/misc.py` ou nouveau router : + +```python +@router.get("/live", response_class=HTMLResponse) +def live_workout(request: Request, token: str | None = Query(default=None)): + """Page affichage live workout : reçoit les samples push de la Watch via + CoachLiveBridge (plugin Capacitor) et affiche les metrics en temps réel. + + Cette page n'est utile QUE dans le wrapper Capacitor iOS — en navigateur + pur, aucun WCSession donc 0 samples reçus.""" + check_auth(request, token) + return templates.TemplateResponse( + request, "live_workout.html", + {"token": token or "", "v": assets_version()}, + ) +``` + +Template `coach_sportif/web/templates/live_workout.html` (squelette) : + +```html +{% extends "_layout.html" %} +{% block title %}Live workout — Coach{% endblock %} +{% block content %} +
+ +

Live workout

+
+
+
+ FC + + bpm +
+
+ Calories + + kcal +
+
+ Distance + + km +
+
+ Durée + +
+
En attente de la Watch…
+
+ +{% endblock %} +``` + +## Phase 5 — Tests + +### Tests unitaires possibles +- `WorkoutManager` : impossible sans device (HKHealthStore mock = expérimental) +- `ConnectivityManager` : peut tester le buffer logique sans réseau +- Plugin iOS : pareil — buffer + notifyListeners testable en unit + +### Tests device (obligatoires) +- iPhone + Apple Watch jumelée déverrouillées +- Lancer app CoachWatch sur la montre +- Choisir activité (Course par ex) +- Vérifier que FC commence à s'afficher dans 5-10 sec +- Ouvrir Coach Hypnotruck iPhone → `/live` → vérifier que les chiffres défilent +- Test fallback : couper le Bluetooth pendant 30 sec, voir que le buffer flush au retour + +### Test simulator (limité) +- Simulator watchOS supporte HKLiveWorkoutBuilder en mock mais valeurs aléatoires +- WatchConnectivity entre simulators iPhone + Watch supportée depuis Xcode 14 + +## Estimation effort (révisée après recherche) + +| Phase | Effort | Owner | +|---|---|---| +| 1 — Création target Xcode + entitlements | 1-2h | Sylvain (UI Xcode) ou Claude Mac via pbxproj | +| 2 — Code Swift watchOS (4 fichiers) | 4-6h | Claude Mac (compile, lit erreurs) | +| 3 — Plugin Capacitor iOS | 2-3h | Claude Mac | +| 4 — Route web `/live` + template | 1h | Faisable depuis n'importe où (Linux ou Mac) | +| 5 — Tests device + fixes | 4-8h | Sylvain + Claude Mac | +| **Total réaliste** | **12-20h sur 3-5 jours** | | + +## Risques connus + mitigations + +| Risque | Mitigation | +|---|---| +| HKLiveWorkoutBuilder permissions refusées par user | Doc claire dans onboarding + bouton "Réessayer" qui force re-prompt | +| WCSession.isReachable false en permanence (BLE flaky) | Fallback `transferUserInfo` queued (livraison différée mais garantie) | +| Sample rate trop élevé → batterie Watch crame | Throttle à 1-3 sec, agréger côté Watch avant push | +| User lance Workout natif Apple au lieu de CoachWatch | Doc + onboarding pédagogique. V2 : App Intent / Complication | +| Apple rejette en TestFlight pour signing/entitlements | Tester en internal TestFlight d'abord, pas production directe | +| `notifyListeners` Capacitor ne reçoit pas en background | App doit rester au premier plan iPhone (acceptable pendant séance) | + +## Pas dans le scope V1 + +- Live Activity iOS (Dynamic Island) — V2 si V1 cartonne +- App Intent / Complication "Lancer séance" — V2 +- Sync vers Polar / Garmin pendant la séance — backlog séparé +- Multi-user / Apple Watch partagée — irrelevant pour Sylvain +- Workouts piscine (HKWorkoutSwimmingLocationType) — exclu, Sylvain ne nage pas + +## Références à valider sur Mac (les liens Apple ne se rendent pas via WebFetch Linux) + +- HKLiveWorkoutBuilder : `developer.apple.com/documentation/healthkit/hkliveworkoutbuilder` +- WCSession : `developer.apple.com/documentation/watchconnectivity/wcsession` +- HKWorkoutSession : `developer.apple.com/documentation/healthkit/hkworkoutsession` +- Capacitor Custom Plugin iOS : `capacitorjs.com/docs/plugins/creating-plugins/ios-guide` + +Le Claude Mac doit lire ces 4 pages dans sa session pour valider les +signatures avant de les utiliser.