Aucun .widgetURL ne ramenait dans l'app : la Live Activity, les deux widgets
iPhone et les deux complications Watch pointent sur des URL https, mais sans
entitlement associated-domains ni fichier d'association servi, iOS ouvrait
Safari — où le cookie d'auth de la WKWebView n'existe pas.
Le versant serveur est déjà en prod (coach_sportif 8ec0047 : route AASA +
deeplink.js). Reste la capability à vérifier dans Xcode côté Mac, cf. COWORK.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
watchOS demande 'Quel a été votre effort ?' après chaque séance et l'user y
répond déjà. La réponse est stockée dans HealthKit en workoutEffortScore
(iOS 18+, vérifié dans la doc Apple) : on la récupère au lieu de la redemander
dans l'app.
- getEffortScores({days}) : parcourt les séances de la fenêtre et, pour chacune,
interroge les samples d'effort via le prédicat officiel
predicateForWorkoutEffortSamplesRelated(workout:activity:). Pas de
rapprochement approximatif par horodatage.
- unité HKUnit.appleEffortScore(), score arrondi sur 1-10
- requestAuthorization demande désormais la permission de lecture du type :
sans elle, la requête renvoie une liste vide SANS erreur — HealthKit ne
distingue pas 'refusé' de 'aucune donnée'
- guard iOS 18 : sur plus ancien, renvoie [] avec une raison explicite
Le pendant serveur est dans coach_sportif (commit 0e79e74) :
POST /api/feedback/device/import, qui n'écrase jamais une saisie manuelle.
NON COMPILÉ — à inclure au prochain build Xcode.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pendant natif du lecteur web. Bouton Démarrer par bloc : un exercice à la fois,
décompte en anneau, passage auto, haptique à chaque transition, pause et
navigation. Pendant une routine au sol, la montre est bien plus pratique que
d'aller chercher le téléphone — c'est tout l'intérêt de la version watchOS.
Chaque exercice écoulé est coché via RoutineStore.toggle, donc renvoyé à
l'iPhone puis au serveur : à la fin le bloc est à jour sur les trois surfaces.
La durée par exercice est désormais transmise dans le snapshot (duration_sec,
optionnel pour rester compatible avec les snapshots déjà persistés) : la vue
guidée utilise la vraie durée au lieu de répartir celle du bloc.
RoutineGuidedView.swift est déjà référencé dans le pbxproj (target CoachWatch),
aucun Add Files à faire. Backup : project.pbxproj.bak-guided
NON COMPILÉ — à inclure au prochain build Xcode.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pendant natif de coach_sportif c12caef. À la détection d'un HKWorkout de type
renforcement, POST /api/routine/auto-complete avec les faits bruts.
Volontairement bête côté natif : aucune règle de correspondance ici. Fenêtre de
12 h depuis l'envoi explicite, tolérance de durée, choix du bloc — tout vit
côté serveur et reste donc ajustable sans rebuild iOS.
NON COMPILÉ — à inclure au prochain build Xcode.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pendant natif du commit coach_sportif 01bad9e. Les 24 exercices sont
cochables un par un pendant la séance, comme sur le web.
- RoutineExercise + items dans RoutineBlock ; les exercices voyagent avec le
snapshot, rien n'est codé en dur côté watchOS
- l'en-tête de bloc est tappable : coche/décoche tous ses exercices d'un coup,
pour enchaîner une série sans s'arrêter à chaque ligne
- compteur n/N par bloc + progression globale en exercices
- CoachRoutineBridge.sanitizeBlocks transmet désormais les items
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pushToWatch ne lisait que `date` et `done` dans l'appel JS et ignorait
`blocks`. La montre recevait donc un snapshot sans aucun bloc et restait sur
son écran vide « Ouvre l'app Coach sur l'iPhone » — alors que le log confirmait
que l'envoi partait bien (constaté sur device le 2026-08-03).
sanitizeBlocks ne garde que les clés attendues en types property-list stricts :
updateApplicationContext lève une exception ObjC sur tout ce qui ne l'est pas,
et les entiers venus du JS arrivent en NSNumber — un `as? Int` direct échoue
quand JavaScriptCore les passe en double (même piège que le score perdu dans
CoachWidgetBridge).
Le log distingue désormais blocs transmis et blocs faits : « 0 blocs » était
ambigu et désignait le compte des cochés, pas celui des blocs envoyés.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AppDelegate écrivait dans HTTPCookieStorage.shared, qui est le store
d'URLSession — pas celui que lit la WKWebView (WKWebsiteDataStore). Ça
fonctionnait jusqu'en mai parce que Capacitor recopiait alors les cookies au
démarrage ; ce n'est plus le cas, et l'app retombait sur l'écran de login
malgré un token valide (constaté sur device le 2026-08-03).
MainViewController.viewDidLoad injecte désormais dans le store de la WebView,
puis recharge une fois pour rejouer la première requête avec le cookie. Pas de
boucle : au passage suivant le cookie existe et on sort avant.
L'injection AppDelegate est conservée : elle sert aux appels natifs URLSession.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La commande listait tout DerivedData et les checkouts SPM en « NON CÂBLÉ »
(15 faux positifs constatés sur le Mac le 2026-08-03). Le dossier build/
n'existe pas côté serveur dev, d'où l'angle mort.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rassemble tout le natif accumulé depuis le TestFlight de mai en une seule
procédure ordonnée par dépendances : pull, token, App Group, capabilities,
target complications (facultative), build+tests device, puis distribution.
Point d'arrêt explicite après l'étape 6 : l'app tourne sur l'iPhone, la
publication TestFlight peut attendre un autre jour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Réduit le travail manuel de la session Xcode : CoachWidgetBridge (App),
CoachWidgets (CoachLiveActivity) et CoachWidgetSnapshot (partagé App +
CoachLiveActivity + CoachWatch, un PBXBuildFile par target = ce que fait
"Target Membership") n'étaient référencés nulle part depuis le 2026-06-29.
Reste manuel sur le Mac, car il faut créer une target :
CoachWatchWidgets/CoachWatchComplications.swift
Backup : project.pbxproj.bak-widgets
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pendant natif du backend coach_sportif (commit 772385b, déjà en prod).
- CoachRoutineBridge (target App) : pousse blocs + état du jour vers la Watch
via updateApplicationContext, relaie les coches Watch vers /api/routine/day
puis notifie la WebView (event routineUpdated)
- CoachWatch/RoutineStore : état persisté en UserDefaults, reset au changement
de jour. Les blocs viennent de l'iPhone — rien codé en dur côté watchOS, donc
modifier la routine côté web ne demande aucun rebuild
- CoachWatch/RoutineView : liste cochable, progression, haptique, bouton de
renvoi si la synchro a échoué
- ConnectivityManager (watch) : sendRoutine() en sendMessage avec repli
transferUserInfo (coche faite iPhone hors de portée -> livraison différée)
⚠️ CoachLiveBridge touché : routeIfNotLiveSample() aiguille les messages
routineDone vers NotificationCenter. WCSession.delegate est unique côté iPhone,
impossible d'en ajouter un second. Le flux live workout n'est pas modifié mais
doit être re-vérifié au build.
Les 3 nouveaux fichiers sont référencés dans project.pbxproj (bonne target) :
aucun "Add Files" à faire sur le Mac. Runbook : docs/routine-watch-runbook-mac.md
NON COMPILÉ — nécessite une session Xcode sur le Mac mini.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- CoachWidgetBridge : lit le score en NSNumber.intValue (le cast `as? Int`
renvoyait nil quand le web envoie un float → score widget perdu par
intermittence). CoachWidgetStore.save renvoie un Bool ; le plugin fait
call.reject si la sauvegarde échoue au lieu de résoudre ok:true à tort.
- CoachAppleAuth : sur double-tap, rejette l'ancien pendingCall (Promise JS
ne pend plus indéfiniment) ; accès pendingCall confiné au main thread ;
erreur d'annulation testée par domaine ASAuthorizationError (pas rawValue nu).
Swift pur, non compilé ici (pas de Xcode) — à valider au prochain build Mac.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rectification : capacitor.config.json n'est PAS suivi (gitignored, généré par cap
sync). Le .ts source a déjà les bonnes valeurs → le finding audit 'JSON divergent'
était un artefact local périmé, pas un bug du repo. cap sync sur le Mac régénère
correctement. Pas d'action de versionnement.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Le JSON embarqué avait divergé du .ts source (cap sync non rejoué) :
contentInset always->never, fonds #ffffff->#000000, StatusBar DEFAULT->LIGHT.
Corrige la régression bottom-nav + flash blanc ré-introduite en prod. JSON validé.
aps-environment (dev->prod) NON flippé depuis Linux (casserait le dev ; fix par
config Release sur le Mac) — documenté dans docs/widgets-runbook-mac.md avec le
reste des findings iOS de l'audit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
L'App Group n'étant pas partagé entre appareils, le snapshot transite par
WatchConnectivity : iPhone CoachWidgetBridge.pushToWatch (updateApplicationContext)
-> CoachWatch/ConnectivityManager reçoit -> App Group de la montre + reload des
complications. Nouvelles complications (CoachWatchWidgets/CoachWatchComplications.swift) :
Forme (accessoryCircular/corner, Gauge) + Séance du jour (accessoryRectangular/inline).
⚠️ Reste Mac : créer la target widget watchOS + App Group/capability + Target
Membership. Runbook complet dans docs/widgets-runbook-mac.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Info.plist : GIDClientID (client iOS Google) + CFBundleURLTypes avec le reversed
client id (com.googleusercontent.apps.817483900105-amoanmsold3j3k71retpjuhvrknsuunh)
pour le retour OAuth Google natif.
Capacitor 8 n'auto-découvre pas les plugins in-app : il faut les registrer
explicitement via bridge.registerPluginInstance(). CoachAppleAuthPlugin avait
été créé mais pas ajouté à la liste → window.Capacitor.Plugins.CoachAppleAuth
restait undefined → bouton Apple natif absent. Corrigé.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WorkoutScheduler.schedule() empilait : chaque envoi ajoutait un workout sans
retirer le précédent → un ancien restait dans Exercice → Programmées et on
risquait maxAllowedScheduledWorkoutCount. Ajout de removeAllWorkouts() avant
schedule() (ne touche que nos workouts) : 1 envoi = la séance courante remplace
la précédente. API vérifiée swiftinterface WorkoutKit (iOS 26.5 SDK).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- CoachAppleAuth.swift : plugin Capacitor (AuthenticationServices) → signIn()
renvoie { identityToken, user, email?, givenName? } au JS, POSTé sur
/auth/apple/web (aud = bundle id ch.hypnotruck.coach).
- App.entitlements : capability com.apple.developer.applesignin (Default).
- project.pbxproj : enregistre CoachAppleAuth.swift dans la target App.
- AppDelegate : l'injection du cookie bypass n'écrase plus une session
existante (sinon le login natif serait remplacé à chaque lancement).
Côté Xcode : ajouter la capability 'Sign in with Apple' puis build device.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Le plugin accepte hr_bpm_min/hr_bpm_max par step et pousse un HeartRateRangeAlert
réel -> ce sont les zones Karvonen HRR (sous bêta-bloquant) qui s'appliquent dans
l'app Exercice de la Watch, pas les zones génériques %FCmax d'Apple. hr_zone reste
en fallback. API vérifiée dans le SDK WorkoutKit (WorkoutAlertMetric.countPerMinute).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Le throttle fixe de 2s ajoutait jusqu'à 2s de latence sur /live vs l'app
Exercice native d'Apple (instantanée). Désormais chaque nouvel échantillon FC
est envoyé immédiatement ; throttle léger 0.5s conservé uniquement pour
calories/distance (anti-saturation BLE).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Active le swipe-back natif iOS (depuis le bord gauche) dans la WebView.
Indispensable pour pouvoir retirer les flèches de retour du chrome web
(cf. coach_sportif _layout.html commit lié) : sans ce geste, plus aucun
moyen de revenir sur les drill-downs (workout detail, etc.).
Distinct du swipe-to-delete des séances (qui démarre depuis le corps des
cartes, pas l'edge d'écran) -> aucun conflit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nouveau plugin method @objc deleteWorkout(workoutUUID:) -> retrouve le HKWorkout
par UUID puis HKHealthStore.delete. requestAuthorization étendu pour inclure
share auth de HKWorkoutType (requise pour delete). Utilisé par la page /workouts
au swipe-to-delete pour synchroniser la suppression avec Apple Santé.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sur la Dynamic Island compact/minimal ET sur la bannière Lock Screen,
.widgetURL(https://coach.hypnotruck.ch/live) fait que le tap ouvre l'app
sur /live, qui appelle openNativeLive et présente CoachLiveView (Liquid
Glass plein écran) avec toutes les mesures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- LiveStore.swift : bus singleton main-actor, alimenté par CoachLiveBridge,
observable par SwiftUI. Watchdog 8s pour status .lost.
- CoachLiveView.swift : vue SwiftUI @available(iOS 26.0,*) avec
.glassEffect()/GlassEffectContainer (vrai Liquid Glass), aurora background,
4 cartes métriques teintées, pulse heartbeat sur FC en direct.
- CoachLiveBridge : nouvelle méthode plugin openNativeLive() qui présente
CoachLiveView en plein écran (fallback web pour iOS < 26). emit() alimente
aussi LiveStore.shared.
- pbxproj : registration des 2 nouveaux .swift dans la target App.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Onglet "Live workout" -> /live ajouté dans NAV_ITEMS, mergé sur main (ff) et
déployé via webhook (service coach-web actif). /live était déjà OK côté
route/auth/template (Phase 4) ; manquait juste le lien nav. "token invalide"
= accès /live non authentifié (pas de lien -> redirect login).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Branche poussée, infra (VPS ubuntu@83.228.246.229, prod/staging, make deploy*,
auto-deploy webhook), et étapes pour merger sur main depuis l'env Linux/VPS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HANDOFF-LIVE-BACKEND.md : tâches coach_sportif pour finir /live (auth cookie de
session, lien d'accès depuis l'app, JS template) + contrat exact du plugin
(event liveSample, champs heartRate/activeEnergyKcal/distanceMeters/elapsedSec/ts/sim).
Plan mis à jour : pont watch->iPhone validé end-to-end sur simulateur.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Logger unified (capturable via simctl) côté watch (envoi) et iPhone (réception)
pour valider le pont WCSession. Hook AUTOSIM (env var) démarre le mode test au
lancement, pour validation automatisée sur simulateur. Pont prouvé E2E sur sim
pair iPhone+Watch : HR identiques watch->iPhone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CAPLog à chaque liveSample reçu via WCSession -> permet de prouver que le flux
watch -> iPhone fonctionne dans la console, indépendamment du rendu de /live
(dont l'auth backend peut bloquer).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bouton "Test (FC simulée)" qui envoie des samples synthétiques (FC ~110-150,
calories, distance, durée) via WCSession toutes les 2s, sans démarrer de séance
HealthKit. Permet de valider watch -> iPhone -> /live indépendamment de
l'autorisation Santé (bloquée par un sous-système Santé coincé sur la Watch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Les deux apps s'appelaient "Coach" (iPhone + watch, bundles distincts) -> impossible
d'identifier laquelle régler dans l'app Santé. Le nom distinct permet d'accorder
l'autorisation HealthKit à la bonne app.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Si authorizationStatus(workoutType) == .sharingDenied après requestAuthorization,
on n'essaie pas de démarrer la séance (qui échouerait en HKErrorAuthorizationDenied)
et on affiche un message demandant d'activer Coach dans Réglages → Santé.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Logger os "ch.hypnotruck.coach.watchkitapp/workout" : trace requestAuthorization,
statut de partage, startActivity/beginCollection, transitions de session et
surtout didFailWithError (domaine+code) pour identifier la cause de l'Error(7).
Logger en constante de fichier (Sendable) pour rester accessible aux délégués
nonisolated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Garde anti double-démarrage (guard !isRunning, session == nil) pour éviter la
cascade de HKLiveWorkoutBuilder en Error(7) quand on retape l'activité. Reset
session/builder à nil après échec, fin et erreur de séance pour permettre une
nouvelle tentative propre. Le statusMessage d'erreur reste donc visible sur le
picker.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>