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).
Le plugin tiers @capgo/capacitor-health v8.4.9 a un bug iOS 26 :
Health.requestAuthorization pend indéfiniment (Promise jamais
résolue, confirmé empiriquement 2026-05-18 — timeout 8s même avec
1 seul type 'steps' + désinstall/réinstall complet inefficace).
Workaround : nouvelle méthode requestAllHealthPermissions exposée
sur notre plugin custom CoachWorkoutObserver (qui fonctionne
empiriquement — fireCount=10 confirme que HealthKit native marche).
Appelle directement HKHealthStore.requestAuthorization avec un
Set<HKObjectType> construit en Swift (14 quantity + 1 category
+ workout). Pas de string→enum mapping intermédiaire, pas de
parsing TypeScript→Swift, donc pas de chaîne où ça pourrait
silently fail comme dans @capgo.
Bump build requis côté Mac avant rebuild TestFlight build 22.
Cause root du bug nav bar qui flash en bas pendant le scroll iOS 26 :
ios.contentInset = 'always' faisait que iOS WKWebView ajustait
dynamiquement le contentInset pendant le scroll → window.innerHeight
shrinkait de 902 → 868 (perte safe-area-inset-bottom = 34px) → la
nav `position: fixed; bottom: 0` suivait le shrink → flash visible.
Diagnostic empirique 2026-05-18 via debug overlay JS qui montrait :
- État repos : innerH=902, nav.bottom=902 ✅
- État scroll : innerH=868, nav.bottom=868 ❌ (34px plus haut)
Sources doc officielles vérifiées :
- capacitorjs.com/docs/config : default Capacitor iOS = 'never'
- developer.apple.com/documentation/uikit/uiscrollview/
contentinsetadjustmentbehavior :
'never' = aucun ajustement automatique → évite le shrink dynamique
→ recommandé pour web apps fullscreen
'always' = force ajustement sur tous axes → cause de notre shrink
Le CSS coach_sportif gère déjà manuellement env(safe-area-inset-*)
dans critical.css (top, bottom, left, right). Combiné avec
StatusBar.overlaysWebView=false (status bar a son espace dédié,
n'overlay pas le WebView), tout reste correct sans contentInset.
Bump build requis côté Mac avant rebuild TestFlight.
Workaround JS RAF loop (commit d8b928a) conservé en safety net :
quand contentInset='never' tient sa promesse, innerHeight est stable,
maxInnerH-h=0, pas de transform appliqué. Si jamais iOS 26 régresse,
le workaround prend le relais.
Symptôme rapporté par Sylvain 2026-05-18 : sur toutes les pages, quand
on scroll vers le bas et qu'on arrive en fin de page, la bottom tab bar
(#bottom-tabs en position: fixed) "remonte" visuellement avec le rebond
élastique iOS.
Cause : WKWebView UIScrollView a `bounces = true` par défaut. Pendant
le rebond élastique en fin de scroll, le viewport iOS bouge et les
éléments position:fixed suivent visuellement le rebond pour quelques
millisecondes — donnant l'impression que la nav bar n'est pas vraiment
fixe.
Fix natif Swift (doc Apple-recommandée pour ce cas) :
- webView.scrollView.bounces = false
- webView.scrollView.alwaysBounceVertical = false
- webView.scrollView.alwaysBounceHorizontal = false
Plus fiable que le hack JS preventDefault touchmove existant dans
_layout.html (qui restera comme fallback web pur).
Bump build requis côté Mac avant rebuild TestFlight.
Diagnostic via getStatus build 18 : fireCount=10 mais lastNotifAt=""
et lastWorkoutUUID="" → l'observer fire mais fetchLatestWorkout ne
trouve aucun workout. Le predicate
HKQuery.predicateForSamples(withStart: now-1h, end: nil) filtre par
startDate, ce qui rate les workouts dont startDate est antérieur à
now-1h (ex: workout long en cours, ou workouts sync depuis iCloud
historique).
Refactor : HKAnchoredObjectQuery (API Apple-recommandée pour ce cas).
- Au boot, charge l'anchor stocké via NSKeyedArchiver/Unarchiver
- Au 1er fire (anchor nil), enregistre le nouvel anchor SANS notifier
(skip back-historize : évite spam si HealthKit sync un gros
historique iCloud)
- Aux fires suivants, retourne uniquement les samples ajoutés depuis
l'anchor précédent → notif sur le plus récent par endDate
- Persistance anchor via UserDefaults (NSKeyedArchiver requiringSecureCoding)
Nouvelle méthode resetAnchor() exposée au JS pour permettre le reset
côté debug — utile pour tester avec un workout pré-existant.
Signatures vérifiées via JSON DocC Apple :
- HKAnchoredObjectQuery(type:predicate:anchor:limit:resultsHandler:) ✓
- HKObjectQueryNoLimit (var global Int) ✓
Instrumentation pour debug visible côté JS sans Console.app.
Nouvelles méthodes exposées :
- getStatus() → { observerActive, bgDeliveryEnabled, fireCount,
lastFireAt, lastNotifAt, lastWorkoutUUID, lastError,
healthDataAvailable }
- testNotification() → force une notif locale immédiate via
UNUserNotificationCenter pour valider le pipeline notif natif,
indépendant de l'observer HealthKit.
Persistance UserDefaults :
- fireCount incrémenté à chaque observer.update callback
- lastFireAt / lastNotifAt / lastError timestamps ISO8601
- observerActive / bgDeliveryEnabled bools pour état courant
Fix notif :
- interruptionLevel = .timeSensitive (iOS 15+) sur les notifs
"Séance terminée" pour qu'iOS les affiche même en foreground app
ou Focus modéré. Sans ça les bannières peuvent être suppress
silencieusement par iOS.
Bump version requis côté Mac avant rebuild TestFlight.
Backlog #3-C — Détecte la fin d'une séance Apple Watch (ajout d'un
nouveau HKWorkout au HealthStore) et push une notif locale enrichie
'🎉 Séance {sport} terminée — {min}min · {kcal}kcal · {km}km'.
Nouveau plugin natif CoachWorkoutObserver.swift :
- requestAuthorization (lecture HKWorkout) — idempotent, ne re-prompt
pas si déjà autorisé via @capgo/capacitor-health.
- enableBackgroundDelivery(.immediate) sur HKWorkoutType — iOS réveille
l'app dès qu'un nouveau workout est ajouté.
- HKObserverQuery callback → fetchLatestWorkout (filtre 1h) → schedule
UNNotificationRequest immédiat.
- Dédup via UserDefaults (lastNotifiedUUID) pour éviter doublons quand
iOS rappelle l'observer plusieurs fois pour le même workout.
- Workaround Swift 15.0 deployment target : withCheckedThrowingContinuation
pour wrapper les APIs callback-only (requestAuthorization /
enableBackgroundDelivery existent en async/await mais iOS 15.4+ only).
Signatures Apple vérifiées via JSON DocC :
- requestAuthorization(toShare:read:completion:) ✓
- enableBackgroundDelivery(for:frequency:withCompletion:) ✓
- HKObserverQuery(sampleType:predicate:updateHandler:) ✓
- HKSampleSortIdentifierEndDate (global String) ✓
Bump version requis côté Mac avant rebuild TestFlight.
⚠️ Capability Xcode requise lors du rebuild Mac : Signing & Capabilities
→ HealthKit → cocher 'Background Delivery'. Sans ça, iOS ne réveille
pas l'app quand un workout se termine.
Pour les activities hors whitelist (yoga, hiit, crossTraining, core,
pilates, flexibility, etc.), on bascule vers .functionalStrengthTraining
au lieu de .running. Sémantiquement plus juste pour du non-cardio —
sur la Watch l'icône sera "Entraînement musculaire" au lieu de
"Course", plus représentative.
functionalStrengthTraining est déjà whitelistée (testée empiriquement
build 15, 2026-05-18) donc safe en fallback. Ne re-déclenche pas le
precondition crash de CustomWorkout.init.
Le rawValue exact est retourné côté JS via { activityFallback: true,
activityUsed: <int> } pour permettre au web d'afficher un avertissement
informatif si besoin.
Backlog #1 — WKWebView debugging via Safari Web Inspector :
- MainViewController.swift : webView?.isInspectable = true (iOS 16.4+)
- Permet de débugger l'app TestFlight depuis Safari Web Inspector
Mac (menu Développement → iPhone → coach.hypnotruck.ch). Sans ça,
la WebView ne remonte pas dans la liste des cibles inspectables.
Backlog #2 — Whitelist défensive activities CustomWorkout :
- Nouvelle fonction isSafeForCustomWorkout(HKWorkoutActivityType) -> Bool
- Whitelist : running, cycling, swimming, walking, hiking, rowing,
skatingSports, paddleSports (confirmées doc Apple WWDC23+24) +
functionalStrengthTraining, traditionalStrengthTraining (confirmées
empiriquement le 2026-05-18 build 15).
- Pour toute activity hors whitelist (yoga, pilates, hiit, crossTraining,
coreTraining, etc.) : fallback automatique sur .running avec NSLog
warning. Évite les precondition crashes CustomWorkout.init pour
activities non testées.
- Retour JS enrichi : { sent, activityUsed (rawValue), activityFallback (bool) }
pour permettre au web d'afficher un avertissement si fallback.
Bump version requis côté Mac avant rebuild TestFlight.
La signature CustomWorkout(warmup: WorkoutStep?, cooldown: WorkoutStep?)
accepte nil. Or notre code passait toujours un WorkoutStep(goal: .time(
warmupMin * 60, .seconds)) — avec warmupMin = 0 on créait un step de
durée zéro qui déclenchait un assertion failure interne dans
CustomWorkout.init (precondition Apple non catchable depuis try/catch
Swift, crash dur).
Confirmé via crash report iOS sur build 1.2 (14) :
Thread 5 crashed → _assertionFailure
→ CustomWorkout.init(activity:location:displayName:warmup:blocks:
cooldown:)
EXC_BREAKPOINT (SIGTRAP)
Fix : let warmupStep: WorkoutStep? = warmupMin > 0 ? WorkoutStep(...)
: nil — idem cooldown. Permet aux apps web de pousser des séances 100%
décrites en blockSteps (renforcement avec items warmup + main + aux +
cooldown détaillés et nommés via displayName).
Bump version requis côté Mac avant rebuild TestFlight.
Cause définitive identifiée après plusieurs tentatives CSS/JS vaines :
WKWebView a un scrollView.backgroundColor blanc par défaut, exposé
lors du bounce iOS quand on pulle au sommet ou pousse en bas. Aucune
propriété CSS (overscroll-behavior, background body/html) ne peut le
changer car c'est une couche native UIKit AU-DESSUS du DOM.
Fix : override viewDidLoad() de MainViewController pour set :
- view.backgroundColor = .black
- webView.isOpaque = false
- webView.backgroundColor = .black
- webView.scrollView.backgroundColor = .black
Rebuild Xcode + push sur l'iPhone requis pour appliquer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tous les backgroundColor passent de #ffffff à #000000 :
- ios.backgroundColor (WebView natif entre 2 navigations)
- SplashScreen.backgroundColor (cohérence avec splash dark)
- StatusBar.backgroundColor (cohérence avec app dark)
StatusBar.style passe de DEFAULT à LIGHT (icônes/texte blanc sur fond
noir, lisible sur app dark).
À synchroniser au prochain rebuild :
npx cap sync ios
(régénère ios/App/App/capacitor.config.json)
puis archive Xcode.
Pendant ce temps, fix web déjà déployé (style inline html/body) atténue
le flash sans rebuild requis.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WorkoutStep.displayName est dispo uniquement à partir d'iOS 18.
Notre fonction tolère iOS 17+ donc Swift refusait la compilation.
Fix : guarde l'assignation par #available(iOS 18.0, *). Sur iOS 17,
l'Apple Watch garde l'affichage natif Apple "Travail"/"Récupération".
Sur iOS 18+, les labels custom Course/Marche/Vélo etc. s'appliquent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nouvelle méthode CoachHealthRoutePlugin.getCharacteristics qui lit
les caractéristiques read-only depuis HealthKit :
- dateOfBirth (ISO8601 date)
- biologicalSex (male/female/other)
- bloodType (A+/A-/B+/B-/AB+/AB-/O+/O-)
Auth étendue : on demande aussi read sur dateOfBirth, biologicalSex
et bloodType en plus de workoutType + workoutRoute.
Utilisé côté web par la page Profil pour auto-remplir les champs
sans saisie manuelle (cf. /api/profile/healthkit-suggestions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plugin accepte maintenant un champ optionnel `label` sur chaque step
(work/rest ou blockSteps[]). Si fourni, override l'affichage natif
Apple "Travail"/"Récupération" sur l'Apple Watch par le label custom
(ex. "Course"/"Marche").
API étendue :
- work: { duration_min, hr_zone, label? }
- rest: { duration_min, hr_zone, label? }
- blockSteps: [{ type, duration_min, hr_zone, label? }]
Aucune régression : si label absent ou vide, comportement inchangé.
Nécessite rebuild iOS pour prendre effet (Mac mini en attente macOS 26).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Capacitor 8 n'auto-découvre PAS les plugins in-app (définis dans le
target App, pas en npm package). Faut les passer explicitement à
bridge.registerPluginInstance() dans MainViewController.capacitorDidLoad().
Le commentaire du fichier expliquait déjà la règle, mais on l'avait
oubliée pour CoachHealthRoute → window.Capacitor.Plugins.CoachHealthRoute
restait undefined.
Hypothèse : HKHealthStore() à l'init de classe peut causer un échec
silencieux qui empêche Capacitor d'enregistrer le plugin. Lazy init.
Aussi : public override func load() avec NSLog pour qu'on voie dans
les logs natifs si Capacitor charge bien la classe au boot.
@capgo/capacitor-health n'expose pas les routes GPS des workouts. Ce
plugin Swift custom appelle HKSampleQuery + HKWorkoutRouteQuery pour
récupérer la liste des CLLocation associées à un workout (par UUID).
API JS :
CoachHealthRoute.getRoute({workoutUUID})
→ {available: bool, route: [{latitude, longitude, altitude, timestamp, speed}], count}
Format payload aligné sur HAE pour réutiliser le parser backend
existant (tools/health_parser.py).
Ajoute le plugin natif capacitor-native-settings@8.1.0 qui permet
d'ouvrir directement la page Localisation dans Réglages iPhone via
UIApplication.shared.open() (API officielle Apple).
Cas d'usage : depuis /settings → Météo → bouton 'Gérer la permission
iOS' → ouvre Réglages → Confidentialité → Localisation → Coach
Hypnotruck. C'est ce qu'utilisent Strava, Komoot, Uber, etc.
Sync ios/App/CapApp-SPM/Package.swift via npx cap sync ios — le
plugin est maintenant inclus dans le build iOS.
⚠️ Requiert rebuild TestFlight v1.2 :
cd /Users/sylvainbettinelli/Documents/coach-ios/ios/App
pod install (ou via Xcode si SPM)
Xcode → Product → Archive → upload TestFlight
Côté webapp, le bouton tombera en fallback (instructions texte) tant
que la nouvelle build TestFlight n'est pas installée. Le code détecte
window.Capacitor.Plugins.NativeSettings et bascule selon dispo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Préparation upload TestFlight v1.1 avec :
- 12 phases d'évolution depuis v1.0 du 7 mai (bilan IA hebdo, /calendar
refonte vue semaine, /workouts page, /profile, plan additions custom,
WorkoutKit Apple Watch 6 commits, builder chips blocs interactif)
- MARKETING_VERSION 1.0 → 1.1 (visible côté user TestFlight)
- CURRENT_PROJECT_VERSION 1 → 2 (build number, doit être strictement
croissant côté Apple)
- ITSAppUsesNonExemptEncryption=false : évite le questionnaire crypto
au moment de l'upload (l'app n'utilise pas d'algorithmes hors crypto
iOS standard)
Avant : work + rest fixes (2 steps) avec repeats sur le couple.
Maintenant : payload optionnel blockSteps = [{type, duration_min, hr_zone},
...] qui permet N steps custom dans le bloc à répéter. Backward compat :
si pas de blockSteps, lit work + rest comme avant.
Refactor de sendCustomWorkout : prend stepsSpec: [(min, hrZone, isWork)]
au lieu de work*/rest* params séparés. Génère N IntervalSteps depuis le
spec, met dans 1 IntervalBlock avec iterations=repeats.
Le compile error venait de let scheduled = ... .id ... .date qui suppose un
ScheduledWorkoutPlan avec ces propriétés. Probablement le type retourné par
schedule() dans le SDK actuel a une signature différente (ou retourne Void).
Simplification : ne pas capturer le résultat, juste try await + log statique.
preview() ne compile pas dans le SDK actuel (iOS 17.4+ pas dispo).
On revient sur schedule() qui marche, avec 2 améliorations :
- Schedule à Date()+60s au lieu de Date() pour éviter qu'iOS ignore
un workout dont la date passe au moment du traitement (race condition).
- NSLog du résultat (id + date) pour diagnostic Console.app si l'user
ne voit toujours rien sur la Watch malgré schedule succès.
Limitation : avec schedule(), le workout apparaît dans la liste 'Programmées'
au TOP de l'app Workout sur l'Apple Watch (avec icône ⏰), PAS dans la
sous-section 'Custom Workouts'. C'est l'API qu'Apple expose actuellement
pour pousser des CustomWorkout via WorkoutKit — la sous-section Custom
Workouts ne semble pas writable depuis 3rd-party apps directement.
schedule() (iOS 17.0+) que j'utilisais met le workout dans la Schedule (le
calendrier Fitness iOS) → utile pour 'demain à 14h je fais cette séance' mais
pas pour 'mets cette séance dans ma bibliothèque, je la lancerai quand'.
preview() (iOS 17.4+) ouvre une sheet système 'Ajouter à mes workouts' qui
ajoute la séance dans **Custom Workouts** de l'Apple Watch — accessible via
Workout app → Course → Custom Workouts → [séance].
Ajouts :
- requestAuthorization() en amont (idempotent, déclenche dialog iOS la 1re fois)
- if #available(iOS 17.4) → preview(plan)
else → schedule(plan, at: now) en fallback (rare car iOS 17.0-17.3)
- NSLog('[CoachWorkoutKit] ...') pour visibilité Console.app au cas où
L'erreur compile précédente sur preview() venait probablement des autres
problèmes API (WorkoutAlertEnumeration, IntervalStep init), maintenant tous
réglés. preview() devrait compiler proprement avec ce build.
Capacitor 8 requiert que les plugins implémentent CAPBridgedPlugin pour
déclarer explicitement identifier + jsName + pluginMethods. Sans ça,
registerPluginInstance() registre l'instance mais le bridge ne sait pas
quelles méthodes exposer → Capacitor.Plugins.CoachWorkoutKit reste
undefined côté WebView.
Avec ce fix : Capacitor.Plugins.CoachWorkoutKit dispo en JS, avec
méthodes isAvailable() et sendInterval() callable.
Capacitor 8 n'auto-découvre pas les plugins in-app Swift (qui ne sont pas
des npm packages). Les plugins définis dans le target App doivent être
enregistrés manuellement via une sous-classe de CAPBridgeViewController
qui override capacitorDidLoad().
Ajouts :
- ios/App/App/MainViewController.swift : sous-classe qui registers
CoachWorkoutKitPlugin() dans capacitorDidLoad()
- Main.storyboard : customClass pointe sur MainViewController (module
'App' target) au lieu de CAPBridgeViewController (module Capacitor)
- project.pbxproj : 4 entrées pour MainViewController.swift (BuildFile,
FileReference, group, Sources phase)
Après rebuild, Capacitor.Plugins.CoachWorkoutKit sera dispo en JS.
L'API WorkoutScheduler.shared.preview(plan) n'existe pas (ou pas avec cette
signature) dans le SDK iOS de l'utilisateur — compile error 'Value of type
WorkoutScheduler has no...'.
On garde uniquement WorkoutScheduler.shared.schedule(plan, at: now), qui
est l'API stable depuis iOS 17.0. Effet : le workout devient immédiatement
disponible dans Apple Watch → Exercice → Workouts personnalisés.
Le 1er schedule déclenche automatiquement la dialog d'autorisation iOS
'Coach Hypnotruck souhaite ajouter des workouts à votre Apple Watch' si pas
encore accordée. Plus besoin du authorizationState query manuel.
Trade-off : pas de sheet de prévisualisation système avant ajout (l'user ne
voit pas la structure dans une UI native avant que le workout soit ajouté).
Mais c'est le bon compromis pour avoir un build qui marche. On pourra
réintroduire preview() plus tard si on bump le deployment target.
Compile errors :
- WorkoutAlertEnumeration n'existe pas → c'est WorkoutAlert (et il faut
passer l'alert via WorkoutStep.alert, pas en param d'IntervalStep)
- IntervalStep init est .init(_ purpose:, step: WorkoutStep) — pas de goal
ni alert direct, faut wrapper dans WorkoutStep d'abord
- WorkoutScheduler.shared.preview(plan) ajouté en iOS 17.4+ → wrap dans
if #available(17.4) avec fallback schedule() pour 17.0-17.3
Ajouts :
- requestAuthorization() avant preview/schedule (idempotent si déjà accordée)
- HeartRateZoneAlert appliqué sur work + rest steps via .alert (style mutable)
Plugin Capacitor Swift in-app (ios/App/App/CoachWorkoutKit.swift) qui utilise
le framework Apple WorkoutKit (iOS 17+ / watchOS 10+) pour construire un
CustomWorkout structuré (warmup + IntervalBlock work/rest répété + cooldown)
et présenter la sheet système iOS qui propose à l'user d'ajouter le workout
à ses workouts personnalisés Apple Watch.
API JS (côté WebView) :
Capacitor.Plugins.CoachWorkoutKit.isAvailable()
Capacitor.Plugins.CoachWorkoutKit.sendInterval({
activity, displayName, warmupMin, work, rest, repeats, cooldownMin
})
Activity supportées : running / cycling / walking / hiking
HR zones (1-5) optionnelles via HeartRateZoneAlert sur work + rest steps.
Workflow attendu user :
1. coach.hypnotruck.ch → /settings ou bouton 'Envoyer Apple Watch' depuis /calendar
2. JS appelle WK.sendInterval(...)
3. Plugin Swift construit CustomWorkout + IntervalBlock + alerts
4. WorkoutScheduler.shared.preview(plan) ouvre la sheet iOS native
5. User tape 'Ajouter aux workouts' → workout dispo dans Apple Watch
→ Exercice → Course → Workouts personnalisés → [displayName]
Build à faire sur le Mac (cap sync ios + Xcode build → install iPhone).
Phase 1 = MVP plugin + bouton test sur /settings de coach.hypnotruck.ch.
Phase 2 = bouton 'Envoyer Apple Watch' sur card du jour /calendar (convertit
la séance prévue en CustomWorkout).
Phase 3 = bibliothèque templates intervalles (10x400m, fartlek, etc.).
Project.pbxproj : ajouté 4 entrées (PBXBuildFile + PBXFileReference + group +
Sources phase) avec IDs D1BD5B1990CF3F2B9C5D000{1,2}.
- package.json : name 'coach-mobile', description mentionne iOS+Android,
scripts 'sync', 'sync:ios', 'sync:android', 'open:android', 'build:android'
- README.md : titre "Coach Hypnotruck — Mobile (iOS + Android)", section
Architecture mentionne Coque Android Capacitor + Health Connect, commande
cap-assets sans --ios pour générer les deux plateformes en une fois
- coach-ios.local.json filename CONSERVÉ pour ne pas casser les references
dans le code Swift natif AppDelegate.swift / CoachAuth.swift
Action user pour finaliser :
1. Renommer le repo Gitea : Settings → Repository name → coach-mobile
2. Local : `mv ~/coach-ios ~/coach-mobile && cd ~/coach-mobile`
3. Update remote : `git remote set-url origin git@gitea:sylvain/coach-mobile.git`
@capacitor/assets v3.0.5 → 87 assets Android générés à partir des sources
1024x1024 (icon) et 2732x2732 (splash) déjà présentes :
- mipmap-{m,h,xh,xxh,xxxh}dpi : ic_launcher.png + _round + _foreground
(foreground/background layers pour adaptive icons Android 8+)
- drawable-{land,port}-{l,m,h,xh,xxh,xxxh}dpi : splash.png
- mipmap-anydpi-v26 : ic_launcher.xml et ic_launcher_round.xml
(XML adaptive icon refs vers les layers PNG)
iOS pbxproj : strip leading zero `0920` → `920` (effet de bord du tool, pas
de comportement Xcode modifié — juste format normalisé).
Reste assets marketing pour Play Console (Feature Graphic 1024x500,
screenshots phone) — à générer au moment du upload, pas dans le repo.
@capgo/capacitor-health plugin (déjà dans package.json) requiert min SDK 26
(Android 8.0+) — Health Connect n'existe pas en deçà. Bump explicite avec
commentaire pour ne pas se faire piéger plus tard si on baisse le min sdk.
Privacy Policy URL pointée vers https://coach.hypnotruck.ch/privacy (route
déjà servie par FastAPI). C'est la page que Health Connect ouvre dans une
WebView quand l'user clique "Politique de confidentialité" dans la dialog
de permissions natives.
iOS déjà 100% configuré côté Health (entitlement + NSHealthShareUsage +
NSHealthUpdateUsage en FR). Rien à toucher.
Génère la structure Capacitor Android (gradle + manifest + activity Java/Kotlin)
avec applicationId 'ch.hypnotruck.coach' (cohérent avec iOS), minSdk 24, target 36.
8 plugins synchronisés au passage (déjà déclarés dans package.json) :
@capacitor/{app,geolocation,haptics,local-notifications,push-notifications,
splash-screen,status-bar} et @capgo/capacitor-health.
Build APK toujours bloqué côté infra (Android SDK non installé sur cette machine,
+ attente validation Play Console pour signing/upload). Mais la structure est
prête : `cap sync` après modification du web puis `./gradlew assembleDebug`
sur une machine avec Android SDK.
Étape suivante : configurer permissions Health Connect dans AndroidManifest +
sync iOS pour cohérence cross-platform.
Évite le redirect 303 du backend /login qui casse WKWebView (WebKitErrorDomain
code 102, frame load interrupted by policy change). Le cookie est posé
directement dans HTTPCookieStorage.shared avec les bons attributs (domain
coach.hypnotruck.ch, path /, secure, expire 1 an).
Token lu depuis CoachAuth.kCoachWebToken défini dans CoachAuth.swift
(gitignored). Template fourni dans CoachAuth.swift.example.
Setup côté Mac mini :
1. cp ios/App/App/CoachAuth.swift.example ios/App/App/CoachAuth.swift
2. Éditer pour mettre la vraie valeur du token
3. Dans Xcode, Right-click App folder → Add Files → sélectionner CoachAuth.swift
WebKit error code 102 (Frame load interrupted by policy change) sur le redirect
303 retourné par /login après pose du cookie. Solution Phase 0 : laisser le
user saisir le token une fois via le formulaire /login standard, le cookie
coach_web_token (1 an, secure, httponly, samesite=lax) doit persister dans
WKWebView pour les lancements suivants.
Si la persistance cookie échoue : inject natif via AppDelegate.swift à venir.
- capacitor.config.ts charge coachWebToken depuis coach-ios.local.json (gitignored)
- server.url devient https://coach.hypnotruck.ch/login?token=... → backend pose le cookie 1 an, redirige vers /
- Au prochain lancement, le cookie tient → /login?token redirige direct vers /
- coach-ios.local.json.example fourni pour setup Mac mini
- Capacitor 8 + plugins natifs (push, local-notif, geoloc, HealthKit Capgo, splash, status bar, haptics)
- Config pointe vers https://coach.hypnotruck.ch (server.url)
- Icône 1024×1024 + splash 2732×2732 depuis icon-512 PWA coach_sportif
- README complet avec procédure build sur Mac mini
- Bundle ID : ch.hypnotruck.coach, App name : Coach Hypnotruck