docs(watchos): plan technique + handoff Claude Code Mac mini

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).
This commit is contained in:
Sylvain Bettinelli
2026-05-24 13:25:07 +00:00
parent f315c5471e
commit 6e4011db3f
3 changed files with 711 additions and 0 deletions

86
CLAUDE.md Normal file
View File

@@ -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).

181
HANDOFF-WATCHOS.md Normal file
View File

@@ -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. 🚀

View File

@@ -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 %}
<header class="ap-page-header --hr">
<svg class="ap-page-header__icon"><use href="#icon-hr"/></svg>
<h1 class="ap-page-header__title">Live workout</h1>
</header>
<section class="ap-card --hr live-workout-card">
<div class="live-stat">
<span class="live-stat__label">FC</span>
<span class="live-stat__value" id="live-hr"></span>
<span class="live-stat__unit">bpm</span>
</div>
<div class="live-stat">
<span class="live-stat__label">Calories</span>
<span class="live-stat__value" id="live-kcal"></span>
<span class="live-stat__unit">kcal</span>
</div>
<div class="live-stat">
<span class="live-stat__label">Distance</span>
<span class="live-stat__value" id="live-dist"></span>
<span class="live-stat__unit">km</span>
</div>
<div class="live-stat">
<span class="live-stat__label">Durée</span>
<span class="live-stat__value" id="live-elapsed"></span>
</div>
<div class="live-status" id="live-status">En attente de la Watch…</div>
</section>
<script type="module">
import { registerPlugin } from '/static/vendor/capacitor-core.js';
const CoachLiveBridge = registerPlugin('CoachLiveBridge');
let lastTs = 0;
CoachLiveBridge.addListener('liveSample', (s) => {
document.getElementById('live-hr').textContent = Math.round(s.heartRate || 0);
document.getElementById('live-kcal').textContent = Math.round(s.activeEnergyKcal || 0);
document.getElementById('live-dist').textContent = ((s.distanceMeters || 0) / 1000).toFixed(2);
document.getElementById('live-elapsed').textContent = fmtMS(s.elapsedSec || 0);
document.getElementById('live-status').textContent = 'En direct';
lastTs = Date.now();
});
function fmtMS(sec) {
const h = Math.floor(sec / 3600);
const m = Math.floor((sec % 3600) / 60);
const s = Math.floor(sec % 60);
return h ? `${h}h${String(m).padStart(2,'0')}` : `${m}:${String(s).padStart(2,'0')}`;
}
setInterval(() => {
if (Date.now() - lastTs > 8000) {
document.getElementById('live-status').textContent = 'Connexion perdue avec la Watch';
}
}, 1000);
</script>
{% 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.