Commit Graph

156 Commits

Author SHA1 Message Date
Sylvain Bettinelli
54108381dd Widget Saisie rapide : bundle, schéma d'URL et synchro câblés
Trois des étapes du runbook sont des fichiers texte, donc faisables hors Xcode :

* `CoachQuickWidget()` enregistré dans `CoachLiveActivityBundle`, sous
  `if #available(iOS 17.0, *)` — le bundle sert aussi des cibles antérieures ;
* schéma `coachapp://` déclaré dans l'Info.plist de l'App. Il n'y était pas :
  seul le schéma Google était présent, et les boutons Photo et Scanner du
  widget n'auraient donc rien ouvert ;
* `CoachQuickSync.flush()` appelé depuis `applicationDidBecomeActive`, qui
  était un corps vide.

Ne restent en manuel que les gestes qui touchent le projet Xcode lui-même :
Target Membership des trois fichiers Swift, puis le build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 13:24:01 +00:00
Sylvain Bettinelli
823bfb2ab9 Synchronisation des saisies du widget vers le serveur
`CoachQuickSync.flush()` envoie la file partagée vers POST /api/drinks et retire
les entrées transmises. Cible App uniquement : l'extension n'appelle jamais ce
code, elle n'a ni session ni droit de tenir une requête — c'est toute la raison
d'être de la file.

Le cookie de session du WebView est réutilisé plutôt que de refabriquer une
authentification qui divergerait.

Trois comportements voulus, documentés pour qu'on ne les « corrige » pas :
une entrée qui échoue reste en file et repartira — mieux vaut un doublon visible
qu'une saisie disparue ; un 400 est considéré comme traité, sans quoi un payload
refusé bloquerait la file indéfiniment ; la date est formatée en fuseau local,
pour qu'un verre bu à 23 h compte pour le jour où il a été bu.

Reste un appel à poser dans AppDelegate.applicationDidBecomeActive — le
one-liner est dans le runbook.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 13:06:28 +00:00
Sylvain Bettinelli
7778e62c40 docs(widgets): runbook Mac du widget Saisie rapide
Target Membership, enregistrement dans le WidgetBundle, schéma d'URL, iOS 17
minimum, et le câblage de synchronisation restant côté app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 13:03:35 +00:00
Sylvain Bettinelli
9fda989afc Widget natif « Saisie rapide » : eau et café sans ouvrir l'app
Boutons d'App Intent (iOS 17+) pour l'eau et le café : ils s'exécutent dans
l'extension, écrivent dans l'App Group et rafraîchissent la timeline. Aucun
appel réseau — une extension n'a ni session authentifiée ni droit de tenir une
requête.

Photo et code-barres OUVRENT l'application, par `coachapp://meals?photo=1` et
`?scan=1` : la caméra exige le premier plan, aucun widget ne peut y échapper.
Le widget économise les navigations, pas le lancement.

`CoachQuickLog` : file d'attente partagée app ↔ widget ↔ montre. Les saisies
partent au serveur à la prochaine ouverture de l'app, et sont retirées PAR
IDENTIFIANT — vider la file perdrait une saisie faite pendant la
synchronisation, sans laisser de trace.

Le widget affiche son propre total du jour : les boissons déjà connues du
serveur (nouveaux champs `waterMlToday` / `coffeeCountToday` du snapshot, tous
deux optionnels pour rester lisibles depuis une version antérieure) PLUS ce qui
attend dans la file. Sans cela, il afficherait un total périmé juste après une
saisie et l'utilisateur douterait de ce qu'il vient de faire. Ce qui n'est pas
encore parti est signalé par une flèche, pas masqué.

Un échec d'écriture de l'App Group remonte en erreur d'intent : afficher
« ajouté » sur une saisie perdue est pire que ne rien afficher.

Runbook Mac complété — Target Membership, enregistrement dans le WidgetBundle,
schéma d'URL, iOS 17 minimum, et le câblage de synchronisation restant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 13:03:16 +00:00
Sylvain Bettinelli
f480c36dab feat(deeplink): entitlement associated-domains pour coach.hypnotruck.ch
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>
2026-08-06 09:28:21 +00:00
Sylvain
37d52bf18d chore(xcode): signing et capabilities 2026-08-06 11:07:02 +02:00
Sylvain Bettinelli
3e3351f3ca feat(healthkit): lit le score d'effort saisi sur l'Apple Watch
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>
2026-08-03 21:12:53 +00:00
Sylvain Bettinelli
f032ee2126 feat(watch): séance guidée au poignet
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>
2026-08-03 16:32:29 +00:00
Sylvain Bettinelli
21d819a52b feat(watch): remonte les séances de renfo pour la complétion auto de la routine
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>
2026-08-03 16:13:02 +00:00
Sylvain Bettinelli
d5e42bf662 feat(watch): coches au grain exercice sur la montre
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>
2026-08-03 16:01:40 +00:00
Sylvain Bettinelli
f9f8f95642 fix(watch): transmet les blocs de routine, pas seulement l'état coché
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>
2026-08-03 15:53:08 +00:00
Sylvain Bettinelli
df843368ce fix(ios): injecte le cookie d'auth dans le store de la WKWebView
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>
2026-08-03 15:21:28 +00:00
Sylvain
95d2961a56 chore(xcode): App Groups sur App, CoachLiveActivity et CoachWatch 2026-08-03 17:01:53 +02:00
Sylvain Bettinelli
1e7adb709e docs(session-xcode): exclut build/ de la vérif de câblage
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>
2026-08-03 14:44:09 +00:00
Sylvain Bettinelli
66d1877449 docs: guide pas à pas de la session Xcode (9 étapes)
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>
2026-08-03 14:31:45 +00:00
Sylvain Bettinelli
f30cc60da5 chore(xcode): câble les fichiers Swift des widgets aux bonnes targets
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>
2026-08-03 14:30:09 +00:00
Sylvain Bettinelli
caeac37f38 feat(watch): routine quotidienne cochable sur l'Apple Watch
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>
2026-08-03 14:22:33 +00:00
Sylvain Bettinelli
5f8f3edcd9 docs(cowork): checklist pré-App-Store — aps-environment production (audit sécu 2026-07-01)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 07:11:16 +00:00
Sylvain Bettinelli
e1adbbb61f fix(ios): robustesse bridge widget + Apple Sign-In
- 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>
2026-07-01 06:37:42 +00:00
Sylvain Bettinelli
ec975a9a0d docs(ios): corrige la note capacitor.config — fichier gitignored/généré, .ts déjà OK
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>
2026-06-29 09:20:14 +00:00
Sylvain Bettinelli
4153446de2 fix(ios): lot 5 audit — réaligne capacitor.config.json sur le .ts (régression visuelle)
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>
2026-06-29 09:19:20 +00:00
Sylvain Bettinelli
cc88431f6b feat(watch): complications Apple Watch (Forme + Séance du jour)
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>
2026-06-29 05:50:29 +00:00
Sylvain Bettinelli
a6bdebcfc8 feat(widgets): widgets natifs iPhone (Séance du jour + Score de forme)
Pont WebView→App Group→WidgetKit :
- CoachWidgetSnapshot.swift : modèle partagé App↔extension (App Group group.ch.hypnotruck.coach)
- CoachWidgetBridge.swift : plugin Capacitor setSnapshot/clear + WidgetCenter.reloadAllTimelines
- CoachWidgets.swift : 2 widgets (TimelineProvider + vues SwiftUI, small/medium)
- bundle + enregistrement plugin + entitlements App Group (App + CoachLiveActivity)

⚠️ Reste câblage Mac (App Group portail + capability + Target Membership) :
docs/widgets-runbook-mac.md. Hook web côté coach_sportif.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 05:18:17 +00:00
Sylvain Bettinelli
edf3783fb5 docs: analyse migration iOS natif vs hybride (aide à la décision, ancrée inventaire réel)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 05:05:14 +00:00
Sylvain Bettinelli
fe268a88d1 docs(cowork): Phase 4 révocation Apple FAIT & déployé prod; clarifie les blocages Mac/Android restants
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 07:04:35 +00:00
Sylvain Bettinelli
a5f418e7f0 docs: COWORK.md — coordination multi-Claude (git + workflow build) 2026-06-05 06:59:22 +00:00
Sylvain
728d7fb288 chore(ios): link GoogleSignIn to App target 2026-06-05 08:42:31 +02:00
Sylvain
120adbed26 chore(ios): GoogleSignIn SPM 2026-06-05 08:38:47 +02:00
Sylvain Bettinelli
622cc03692 feat(ios): GIDClientID + URL scheme (reversed client id) pour GoogleSignIn
Info.plist : GIDClientID (client iOS Google) + CFBundleURLTypes avec le reversed
client id (com.googleusercontent.apps.817483900105-amoanmsold3j3k71retpjuhvrknsuunh)
pour le retour OAuth Google natif.
2026-06-05 06:20:24 +00:00
Sylvain Bettinelli
947d76714e feat(ios): plugin natif Sign in with Google (CoachGoogleAuth)
- CoachGoogleAuth.swift : plugin Capacitor (SDK GoogleSignIn) → signIn() renvoie
  { idToken, email?, givenName? }, POSTé sur /auth/google/web (aud = client iOS).
  Garde #if canImport(GoogleSignIn) → le repo build même avant l'ajout du SDK.
- MainViewController : enregistre CoachGoogleAuthPlugin.
- AppDelegate : GIDSignIn.sharedInstance.handle(url) dans application(_:open:).
- project.pbxproj : CoachGoogleAuth.swift dans la target App.

⚠️ Xcode (Mac) : ajouter le SDK GoogleSignIn-iOS via SPM + GIDClientID/URL
scheme (reversed client id) dans Info.plist (cf. HANDOFF). Sans le SDK, le
bouton ne s'affiche pas (signIn rejette proprement).
2026-06-05 06:15:17 +00:00
Sylvain Bettinelli
eee199a5ed fix(ios): enregistre le plugin CoachAppleAuth dans capacitorDidLoad
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>
2026-06-04 19:38:55 +00:00
Sylvain
c407aa8364 fix(workoutkit): efface les séances programmées avant d'en pousser une nouvelle
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>
2026-06-04 21:15:55 +02:00
Sylvain Bettinelli
bf975ce8e6 feat(ios): plugin natif Sign in with Apple (CoachAppleAuth)
- 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>
2026-06-04 19:05:52 +00:00
Sylvain
585e1233db feat(workoutkit): pousse les zones cardiaques en plage BPM explicite (HeartRateRangeAlert)
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>
2026-05-31 12:11:27 +02:00
Sylvain
5a1568ef2f perf(watchos-live): FC poussée en temps réel à chaque échantillon (throttle 2s -> envoi immédiat)
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>
2026-05-31 12:05:27 +02:00
Sylvain
51cbce86ef docs(watchos): chantier CLOS — test E2E FC réelle Watch Ultra 3 -> /live PASSÉ (2026-05-31)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 12:02:39 +02:00
Sylvain
3bafecf5fc docs(watchos): re-vérif build green 2026-05-31 (App + Watch + Live Activity)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 11:54:22 +02:00
Sylvain
a41a1b1aa9 feat(live-native): cardiac-aware zones temps réel dans CoachLiveView
CoachLiveBridge.openNativeLive lit maintenant le token JS et le passe à
CoachLiveView(authToken:). La vue fetch /api/cardiac-zones au .onAppear
+ stocke en @State.

CardiacZones struct (Decodable) calque la payload du backend :
- méthode (karvonen / fcmax_pct), beta-bloquant, fcmax/fcrest observées, HRR
- Z1-Z5 lo/hi + clinical_target_bpm (HRKarv0.6 sous bb, Díaz-Buschmann 2014)
- zoneFor(hr:) -> ZoneMatch {id, lo, hi, color}

HRMetricCard (variante de MetricCard pour FC) :
- Chiffre + glow change de couleur dynamiquement selon zone courante
  (cyan Z1 récup -> rouge Z5 VO2max). Animation .easeOut 0.25s.
- Pill "Z3 · 128-145" sous l'unité bpm.
- "✓ Utile" si beta-bloqué + HR ≥ clinical_target_bpm.

Méthode 100% alignée Apple Watch Workout app (Karvonen HRR, recalc mensuel
max/rest, 5 zones) + ACSM. Aucune extrapolation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 11:39:16 +02:00
Sylvain
98705981ba feat(ios): active allowsBackForwardNavigationGestures sur WKWebView
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>
2026-05-28 10:31:37 +02:00
Sylvain
fac7d95cd1 feat(healthkit): deleteWorkout dans CoachHealthRoute (supprime workout d'Apple Santé)
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>
2026-05-26 20:07:17 +02:00
Sylvain
4484b3ea47 feat(live-activity): widgetURL -> tap ouvre /live (vue native CoachLiveView)
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>
2026-05-26 19:55:27 +02:00
Sylvain
a5795bf83f feat(live): Live Activity / Dynamic Island (WidgetKit extension iOS 16.2+/17+)
Nouvelle target "CoachLiveActivity" (Widget Extension, bundle
ch.hypnotruck.coach.LiveActivity, iOS 17.0) embarquée dans App via copy phase
"Embed App Extensions" (dstSubfolderSpec=13). API ActivityKit + WidgetKit
vérifiées sur SDK iOS 26.5 swiftinterface (Activity.request(content:),
ActivityConfiguration, DynamicIsland(expanded:compactLeading:trailing:minimal:)).

Architecture :
- CoachLiveActivityAttributes (modèle partagé App ↔ extension)
- CoachLiveActivityWidget (Lock Screen + Dynamic Island UI)
- CoachLiveActivityBundle (@main WidgetBundle)
- LiveActivityManager (App side, wrap Activity.request/update/end)
- LiveStore hook : start sur 1er sample, update à chaque ingest, end après
  60s "lost". Tout guardé @available(iOS 16.2, *) pour préserver deployment iOS 15.
- Info.plist App : NSSupportsLiveActivities = YES.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:51:57 +02:00
Sylvain
f65d96e908 feat(live): vue native SwiftUI avec vrai Liquid Glass iOS 26
- 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>
2026-05-26 18:05:21 +02:00
Sylvain
3c0e266cdc chore(ios): recommended build settings (Xcode) + WKRunsIndependentlyOfCompanionApp=false
- "Update to recommended settings" d'Xcode (CLANG_WARN_*, ENABLE_USER_SCRIPT_SANDBOXING,
  STRING_CATALOG_GENERATE_SYMBOLS, BuildIndependentTargetsInParallel, LastUpgradeCheck)
- Info.plist watch : WKRunsIndependentlyOfCompanionApp=false (app watch dépendante du compagnon)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 21:36:09 +02:00
Sylvain
b92e4d5ce6 docs: point 1 (/live nav) DÉPLOYÉ en prod (coach_sportif ecf334a)
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>
2026-05-25 21:06:54 +02:00
Sylvain
5bb979d9bc docs(handoff): état fix /live (branche feat/live-nav-tab) + infra déploiement VPS
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>
2026-05-25 20:47:49 +02:00
Sylvain
6a4e3f7768 docs: handoff backend /live (Mac->Linux) + pont watch->iPhone prouvé E2E
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>
2026-05-25 20:39:47 +02:00
Sylvain
678b5a41d0 chore: logs os_log bout-en-bout (watch sendSample + iPhone liveSample) + hook AUTOSIM
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>
2026-05-25 20:37:03 +02:00
Sylvain
9f0d90f563 chore(ios): log CoachLiveBridge à la réception d'un sample Watch
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>
2026-05-25 20:26:53 +02:00
Sylvain
3ce8ca9e28 feat(watchos): mode Test (FC simulée) — valide le flux sans HealthKit
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>
2026-05-25 20:21:24 +02:00