Commit Graph

69 Commits

Author SHA1 Message Date
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
Sylvain
fef7781db7 chore(watchos): renomme l'app watch en "Coach Watch" pour la distinguer dans Santé
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>
2026-05-25 20:03:44 +02:00
Sylvain
d1c1b88da4 feat(watchos): message clair si accès Santé refusé (au lieu de Error(7))
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>
2026-05-25 19:54:48 +02:00
Sylvain
6cb89359c6 chore(watchos): logs diagnostics séance (autorisation + erreur HK domaine/code)
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>
2026-05-25 19:50:56 +02:00
Sylvain
a4f8276b13 fix(watchos): empêche les sessions HK multiples + nettoie après échec/fin
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>
2026-05-25 19:47:17 +02:00
Sylvain
ef69240af2 chore(watchos): supprime le warning AccentColor manquant
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME vidé sur la target CoachWatch
(pas de couleur d'accent custom, l'UI utilise des couleurs explicites). Build
Release device + Debug sim : 0 warning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 19:15:09 +02:00
Sylvain
ae5ac7ec25 feat(watchos): AppIcon CoachWatch — prérequis TestFlight
Asset catalog mono-taille 1024 (source resources/icon.png, comme l'app iOS),
ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon. Vérifié: Assets.car + CFBundleIconName
dans l'Info.plist final. Sans icône, l'upload TestFlight est refusé.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 19:09:23 +02:00
Sylvain
0bd5d5c5f8 docs(watchos): avancement Phases 1-3 réalisées + deltas watchOS 26
Statut chantier, commits, et 3 deltas vs pseudocode (ConnectivityManager non
@MainActor, WorkoutManager @MainActor + délégués nonisolated, registerPluginInstance
Capacitor 8). Reste : test device + TestFlight + cross-check JS /live.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 19:03:20 +02:00
Sylvain
1cd96de7c6 fix(watchos): WKCompanionAppBundleIdentifier — l'app watch refusait de s'installer
Sans cette clé (ou WKWatchOnly), iOS rejette l'install d'une app watch compagnon
(erreur "WKCompanionAppBundleIdentifier missing or malformed"), sur simulateur
comme sur device/TestFlight. Détecté au smoke test simctl install. Pointe vers
le bundle iOS ch.hypnotruck.coach.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 18:59:07 +02:00
Sylvain
b2e9836b95 feat(ios): plugin Capacitor CoachLiveBridge (Phase 3) — relais WCSession -> /live
CAPPlugin + CAPBridgedPlugin (jsName CoachLiveBridge). Active WCSession au load(),
reçoit didReceiveMessage (temps réel) + didReceiveUserInfo (lot bufferisé) et
relaie au JS via notifyListeners("liveSample"). Enregistré dans
MainViewController.capacitorDidLoad(). Méthode getStatus pour debug JS. Build OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 16:27:15 +02:00
Sylvain
518529891b feat(watchos): UI live workout + entry point (Phase 2)
CoachWatchApp: WKApplicationDelegateAdaptor active WCSession au boot, injecte les
managers. ContentView: picker 4 activités (course/vélo/marche/renfo) -> vue live
(FC/calories/distance/durée + état liaison iPhone) -> bouton Terminer. Build OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 16:23:06 +02:00
Sylvain
167c1e4c27 fix(watchos): WorkoutManager @MainActor — concurrency Swift 6 clean
Classe @MainActor (implicitement Sendable) ; callbacks HealthKit nonisolated qui
calculent les valeurs hors-main puis hoppent via Task { @MainActor }. Récupère le
builder depuis la session passée au délégué (plus de capture de self non-Sendable).
Build watchsimulator : 0 warning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 16:22:17 +02:00
Sylvain
c6f5a7ffc9 feat(watchos): WorkoutManager (HKLiveWorkoutBuilder) — séance live + push iPhone
HKWorkoutSession(healthStore:configuration:) + associatedWorkoutBuilder +
HKLiveWorkoutDataSource. Collecte FC (mostRecent), calories/distance (sum) via
statistics(for:), publication @Published sur main, envoi throttlé (2s) vers
ConnectivityManager. Clôture async endCollection(at:)/finishWorkout(). Build OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 16:20:11 +02:00
Sylvain
5864cfcbef feat(watchos): ConnectivityManager (WCSession) — envoi des samples vers l'iPhone
sendMessage si iPhone joignable, sinon buffer + transferUserInfo (livraison
différée). Délégué WCSession hors-main, hop vers main pour @Published isReachable,
buffer sérialisé sur file dédiée. Build watchsimulator OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 16:11:58 +02:00
Sylvain
5d60a4049e feat(watchos): target CoachWatch (Phase 1) — app watchOS embarquée
- nouvelle target watchOS app CoachWatch (ch.hypnotruck.coach.watchkitapp), SwiftUI, SDK watchos
- entitlement HealthKit + Info.plist FR (NSHealthShare/UpdateUsageDescription) + WKBackgroundModes workout-processing
- "Embed Watch Content" + dépendance App -> CoachWatch (CoachWatch.app dans App.app/Watch)
- schemes partagés App + CoachWatch
- CoachWatchApp.swift + ContentView.swift minimaux

Build validé: target watchsimulator + embed dans App.app (iphonesimulator).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 16:09:37 +02:00
Sylvain
ec26ae0ca7 chore(ios): healthkit background-delivery + bump version 1.2
- entitlement com.apple.developer.healthkit.background-delivery (CoachWorkoutObserver)
- registration CoachWorkoutObserver.swift dans la target App
- LSApplicationCategoryType = healthcare-fitness
- MARKETING_VERSION 1.1 -> 1.2, CURRENT_PROJECT_VERSION 2 -> 22

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:50:25 +02:00
Sylvain Bettinelli
6e4011db3f 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).
2026-05-24 13:25:07 +00:00
Sylvain Bettinelli
f315c5471e feat(workout-observer): bypass @capgo via requestAllHealthPermissions
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.
2026-05-18 16:35:42 +00:00
Sylvain Bettinelli
1393cc3dcf fix(ios26): contentInset 'never' (default) au lieu de 'always'
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.
2026-05-18 15:27:15 +00:00
Sylvain Bettinelli
8134b843f7 fix(ui): désactive bounce iOS WKWebView (bottom tab bar semblait remonter)
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.
2026-05-18 14:42:57 +00:00
Sylvain Bettinelli
a21b067eac fix(workout-observer): HKAnchoredObjectQuery au lieu de predicate 1h
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) ✓
2026-05-18 12:54:22 +00:00
Sylvain Bettinelli
e62e4803e4 feat(workout-observer): debug status + test notif + timeSensitive
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.
2026-05-18 12:29:22 +00:00
Sylvain Bettinelli
33ed74ca63 feat(workout-observer): notif locale 'Séance terminée' via HKObserverQuery
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.
2026-05-18 10:56:20 +00:00
Sylvain Bettinelli
934dc82aa6 fix(workoutkit): fallback vers strength au lieu de running
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.
2026-05-18 10:33:40 +00:00
Sylvain Bettinelli
5a91d0b9be feat(workoutkit): whitelist activities + isInspectable WKWebView
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.
2026-05-18 10:23:11 +00:00
Sylvain Bettinelli
80567d4611 fix(workoutkit): warmup/cooldown nil quand durée 0 (évite assertion crash)
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.
2026-05-18 09:51:31 +00:00
Sylvain Bettinelli
94f5beebcb fix(ios): bande blanche bounce — force scrollView.backgroundColor noir
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>
2026-05-16 12:32:50 +00:00
Sylvain Bettinelli
a9d17a80c8 fix(capacitor): backgroundColor noir partout (anti flash blanc)
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>
2026-05-14 14:18:58 +00:00
Sylvain Bettinelli
4581852cf5 fix(workoutkit): guard displayName par #available(iOS 18.0)
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>
2026-05-13 04:46:08 +00:00
Sylvain Bettinelli
e371d0a561 feat(health): expose getCharacteristics (DOB / sexe / groupe sanguin)
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>
2026-05-13 04:43:30 +00:00
Sylvain Bettinelli
84bf4c0d67 feat(workoutkit): support label per step (WorkoutStep.displayName)
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>
2026-05-12 22:14:37 +00:00
Sylvain Bettinelli
97e640fb57 fix(plugins): register CoachHealthRoutePlugin dans capacitorDidLoad
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.
2026-05-11 16:53:27 +00:00
Sylvain Bettinelli
35cf2b53ce fix(plugin): lazy HKHealthStore + load() override pour debug
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.
2026-05-11 16:42:27 +00:00
Sylvain Bettinelli
8d011610be feat(ios): plugin natif CoachHealthRoute (HKWorkoutRouteQuery)
@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).
2026-05-11 14:25:37 +00:00
Sylvain Bettinelli
908c6b2141 feat(ios): add capacitor-native-settings plugin pour deep-link iOS Settings
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>
2026-05-10 19:13:39 +00:00
Sylvain Bettinelli
a251d369df Bump v1.1 (build 2) + ITSAppUsesNonExemptEncryption=false pour TestFlight
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)
2026-05-08 10:54:26 +00:00
Sylvain Bettinelli
0bd3c88300 WorkoutKit Swift : sendInterval accepte blockSteps[] (N steps custom)
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.
2026-05-08 10:38:45 +00:00
Sylvain Bettinelli
05f13ba2a5 WorkoutKit : étend support strength/core/yoga/HIIT/flexibility + location auto
Mapping HKWorkoutActivityType élargi :
- strength → .functionalStrengthTraining
- traditional_strength → .traditionalStrengthTraining
- core, core_training → .coreTraining (utile pour X Core REHAB)
- hiit, high_intensity → .highIntensityIntervalTraining
- cross_training → .crossTraining
- yoga → .yoga
- pilates → .pilates
- flexibility, stretching, mobility → .flexibility
- swimming → .swimming
- mixed → .mixedCardio

Location auto :
- strength/core/yoga/pilates/flexibility/HIIT/cross → .indoor
- cardio outdoor (running, cycling, walking, hiking, swimming) → .outdoor

→ Permet de pousser des séances strength/core/yoga structurées en blocs
intervalles (60s travail / 15s récup × N) sur l'Apple Watch. Idéal pour
X Core REHAB et autres séances time-based de Sylvain (gainage, holds, circuit).
2026-05-08 10:27:40 +00:00