Commit Graph

31 Commits

Author SHA1 Message Date
Sylvain Bettinelli
891a8bddce IntervalEngine : le commentaire sur les pauses disait l'inverse d'Apple
L'en-tête affirmait que `HKLiveWorkoutBuilder.elapsedTime` « exclut déjà les
pauses », et en tirait que le moteur n'avait rien à savoir de la pause ni de
l'auto-pause.

La doc Apple dit le contraire, vérifié à la source le 2026-08-20 :
« The elapsed time for the workout based on the builder's current contents,
including pauses. »

Conséquence si on câblait le moteur dessus telle quelle : une pause de 5 min
ferait avancer le déroulé de 5 min d'effort — un fractionné mis en pause pour
traverser une route se déroulerait à l'arrêt.

La propriété qui exclut réellement les pauses est
`HKWorkoutBuilder.elapsedTime(at:)` : « The duration of a workout doesn't
include intervals between pause and resume events. » Les deux textes d'Apple
se contredisent frontalement ; le choix de la source de temps reste à faire
avant le câblage.

Le moteur lui-même reste correct : il n'intègre que ce qu'on lui pousse.
Aucun comportement modifié, 57 tests Swift toujours verts.

COWORK gagne l'analyse complète du câblage (3 couches, le serveur sait déjà
produire les étapes via _blocks_from_session) pour reprise à froid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 16:52:21 +00:00
Sylvain Bettinelli
acbb2c6896 La montre enregistre enfin sa trace GPS
La cible watchOS ne contenait pas une ligne de localisation : ni
CLLocationManager, ni HKWorkoutRouteBuilder, et aucune clé NSLocation dans son
Info.plist. Tant que les séances partaient de l'app Exercice d'Apple, c'est
elle qui écrivait la trace et CoachHealthRoute la relisait après coup. Dès
qu'une séance démarre depuis CoachWatch, plus personne ne l'écrit : la sortie
n'aurait ni carte, ni parcours dans Santé.

Le filtrage vit dans `RouteFilter`, en Foundation pur, donc testable ici :
rejet des points imprécis (> 50 m, seuil de l'exemple Apple), des sauts
impossibles, des doublons et des points arrivés dans le désordre, puis cumul de
la distance (haversine) et du dénivelé.

Le dénivelé a demandé deux passes. L'hystérésis seule laissait passer 297 m de
D+ sur un parcours PLAT : une oscillation d'amplitude égale au seuil est
comptée à chaque alternance, ce qui est inhérent à tout seuil. D'où un lissage
préalable, qui annule le bruit alternant. Contrepartie assumée et testée : une
pointe franchie en quelques points est écrêtée, donc sous-comptée — sans
conséquence, le D+ qui fait foi restant celui, barométrique, qu'Apple écrit
dans les métadonnées de la séance.

Quatre pièges documentés, tous traités dans le code :
- `allowsBackgroundLocationUpdates` sans `UIBackgroundModes` = `location`
  TERMINE l'app. Un garde-fou vérifie le plist avant d'armer le drapeau.
- Ne jamais demander « Always » sur watchOS : Apple décrit ce prompt comme
  « mostly a placeholder » et le parcours comme un comportement indéfini.
- Interdit de relancer la localisation depuis l'arrière-plan : la pause garde
  le flux ouvert et ignore les points, au lieu de couper le manager.
- Le CPU tue le GPS — d'où l'insertion des points par lots et la trace
  d'affichage bornée à 1500 points.

Corrigé après lecture de la doc : `finishRoute` s'appelle APRÈS `finishWorkout`
et reçoit le workout pour s'y associer. J'avais écrit l'inverse, avec nil, ce
qui aurait perdu l'association à chaque sortie. Et `finishWorkout()` rend nil
sans erreur quand la montre est verrouillée : on sauvegarde alors la trace sans
association plutôt que de la jeter.

15 tests, dont 4 vérifiés rouges en désactivant le lissage. Les trois fichiers
sont déclarés dans la cible CoachWatch (pbxproj sauvegardé en .bak-outdoor) :
aucun « Add Files » à faire sur le Mac.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 16:37:24 +00:00
Sylvain Bettinelli
f2fa3cccb0 Le moteur d'intervalles, pour guider une séance dans notre app
WorkoutKit ne sait pas exécuter une séance structurée dans une app tierce :
son seul point d'exécution public est `WorkoutPlan.openInWorkoutApp()`, qui
ouvre l'app Exercice d'Apple. Guider les intervalles du plan coach au poignet
impose donc d'écrire la machine à états nous-mêmes.

`IntervalEngine` est volontairement pur — aucun HealthKit, aucun timer, aucune
horloge interne. Il répond à « où en sommes-nous ? » à partir du temps et de la
distance qu'on lui pousse. Le temps de référence est `elapsedTime` du builder,
qui exclut déjà les pauses : le moteur n'a rien à savoir de la pause ni de
l'auto-pause. Et comme il est une fonction de la suite des ticks, rejouer
celle-ci redonne le même déroulé — ce qui rend la reprise après crash triviale.

Le point délicat est la dérive. Quand un tick arrive en retard (app suspendue
poignet baissé, collecte irrégulière), l'étape suivante démarre à la frontière
THÉORIQUE de la précédente, jamais à l'instant du tick. Sinon chaque
répétition offre quelques secondes de rab et l'erreur s'accumule : sur un
9×(1'/1'), le décalage final se compte en dizaines de secondes. Pour la même
raison `update` boucle au lieu de tester une fois — un tick manqué peut
franchir plusieurs étapes courtes d'un coup.

17 tests, dont 5 vérifiés rouges en neutralisant les frontières théoriques.
Rien n'est encore câblé : le fichier n'est pas membre de la cible CoachWatch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:50:27 +00:00
Sylvain Bettinelli
852e12526a La montre avait les données du jour et n'en montrait rien
Le snapshot poussé par l'iPhone (séance prévue, forme, hydratation, énergie,
routine, séance de demain) arrive sur la montre depuis WatchConnectivity et
vit dans son App Group. Les complications le lisent ; l'app, elle, ouvrait
sur le lanceur d'activité et n'en affichait pas une ligne.

Aucun nouveau pont : `ConnectivityManager` publie le snapshot qu'il persistait
déjà, l'accueil en montre un résumé, et une feuille « Aujourd'hui » donne le
détail. Même `asOf(_:)` que les complications, pour qu'un snapshot d'hier se
vide au lieu de mentir.

Les vues vivent dans ContentView.swift, déjà membre de la cible CoachWatch :
pas de Target Membership à régler dans Xcode.

⚠️ Non compilé — SwiftUI et WatchKit n'existent pas sur Linux. Seuls
`swiftc -parse` et les tests Linux existants sont passés. À builder sur le Mac.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 20:34:31 +00:00
Sylvain Bettinelli
fb4033130f Watch : cible clinique, couleurs alignées sur l'iPhone, aperçus Xcode
- `HeartRateZones` porte `clinicalTargetBpm` et `isUseful(bpm:)`. Sous
  bêtabloquant, le repère utile n'est PAS le bas de Z4 mais HRKarv0,60
  (Díaz-Buschmann 2014, ESC : la Karvonen standard sous-évalue le seuil chez le
  patient traité). Un sceau vert l'indique sous la plage, et VoiceOver le dit —
  une icône seule ne suffit pas.
- Z1 passe de `.blue` à `.cyan` : `CoachLiveView` (iPhone) utilise déjà cyan.
  Le même effort doit avoir la même teinte sur les deux écrans de la même app ;
  j'avais introduit la divergence en écrivant le modèle sans regarder l'existant.
- Deux `#Preview` avec les zones RÉELLES de production (Z1 100-109 … Z5
  134-143) : les cinq états d'un coup, plus le cas « zones inconnues ». 109 bpm
  y figure exprès — c'est le plancher de Z2, il vérifie la règle du
  chevauchement d'un battement.

⚠️ `CoachLiveView.CardiacZones` reste une SECONDE implémentation de la même
règle, avec sa propre table de zones. Non fusionnée ici pour ne pas mélanger
deux chantiers ; c'est le même motif que les deux traductions WorkoutKit
divergentes corrigées ce matin.

⚠️ NON COMPILÉ — build Xcode requis.
2026-08-12 19:20:16 +00:00
Sylvain Bettinelli
eb819b68ca Watch : afficher la zone cardiaque pendant la séance
L'app Coach de la montre n'affichait que la FC brute — aucune notion de zone
dans `ContentView.swift`. C'est pourtant au poignet que « 142 bpm » a besoin
d'être traduit ; l'écran iPhone portait déjà l'information, mais on ne le
regarde pas en courant.

- `HeartRateZones.swift` : modèle partagé (App + CoachLiveActivity + les deux
  cibles Watch), câblé dans les 4 phases Sources du pbxproj comme
  `CoachWidgetSnapshot.swift`. ⚠️ Les bornes viennent de `/api/cardiac-zones`,
  version FIGÉE et datée — jamais recalculées côté natif, ce serait rouvrir les
  jeux de zones concurrents supprimés le 12/08.
- `zone(for:)` retient la zone la plus HAUTE qui contient la valeur : les
  bornes se chevauchent d'un battement (Z1 100-109, Z2 109-118), et 109 bpm
  doit se lire Z2, pas Z1. Au-delà du haut de Z5, on reste en Z5 — un effort
  maximal n'est pas une absence de zone.
- `ConnectivityManager` publie les zones reçues avec le snapshot et repart du
  dernier connu au lancement (la montre a pu redémarrer). Un snapshot partiel
  n'efface pas celles en place.
- `HeartRateMetric` : pastille « Z3 », valeur colorée, et la plage sous le
  chiffre. ⚠️ La couleur ne porte jamais l'information seule — illisible pour
  un daltonien, et le contraste d'un écran de montre en plein soleil ne se
  prête pas aux nuances. Libellé VoiceOver explicite.

`switch` en `return` explicites plutôt qu'en expression : la syntaxe courte
exige Swift 5.9 pile, et je ne peux pas compiler ici.

⚠️ NON COMPILÉ — build Xcode requis, avec `getSnapshot` (`8a25f36`) en attente.
2026-08-12 19:13:07 +00:00
Sylvain Bettinelli
c74faeaad5 Marche 6 minutes, et la vraie recuperation cardiaque
Deux ajouts lies par le meme constat : ce qu'on peut mesurer honnetement.

MARCHE 6 MINUTES — troisieme protocole de la vue de test. Sous-maximal, sans
risque, c'est la reference en cardiologie et le seul des trois qui ne demande
aucun effort maximal. Type d'activite HealthKit .walking (la distance et la
vitesse ne vivent pas sur le meme quantity type que la course).

RECUPERATION CARDIAQUE — le pont collecte desormais
heartRateRecoveryOneMinute, la valeur calculee par la Watch elle-meme.

Pourquoi ne pas la deriver de la serie de FC post-seance, deja en base :
mesure faite sur les 19 seances exploitables de la prod, la baisse mediane a
1 minute ressort a 2 bpm, avec 7 valeurs nulles ou negatives (min -10, max 15).
Ce n'est pas physiologique : la fin de seance HealthKit ne coincide pas avec
l'arret de l'effort. Une courbe batie la-dessus monterait et descendrait sans
rien mesurer.

⚠️ Le sample n'est PAS rattache au workout : predicateForObjects(from:) ne le
rendrait jamais. Apple l'ecrit comme un echantillon independant horodate juste
apres la seance, d'ou une requete par fenetre temporelle.

⚠️ NON COMPILE. La courbe de tendance cote web attend ce build : sans lui, le
champ reste absent et il n'y a rien a tracer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 18:58:24 +00:00
Sylvain Bettinelli
a61ba25590 Test de VMA guide au poignet
Pendant un test, le telephone est dans une poche : la montre est le seul ecran
consultable en courant, et c'est elle qui porte deja vitesse, distance et FC via
WorkoutManager.

Deux protocoles. Paliers d'une minute de 8 km/h par pas de 0,5, avec haptique au
changement et l'ecart a l'allure cible affiche — la vitesse brute demanderait un
calcul mental a chaque foulee. Ou six minutes, la distance parcourue donnant
directement la VMA.

⚠️ AUCUN calcul cote montre : elle transmet des mesures (paliers tenus,
distance, FC max), le serveur calcule. Deux implementations d'une meme formule
divergent toujours. Le POST passe par CoachRoutineBridge, qui a deja
l'abonnement NotificationCenter, la base d'API et le cookie — un fichier de plus
devrait etre cable a la main dans le pbxproj.

⚠️ Les paliers avancent sur `elapsedSec` de HKLiveWorkoutBuilder, PAS sur un
compteur accumule au fil du Timer de la vue : ce Timer ne tourne pas quand
l'ecran s'eteint, c'est-a-dire des que le poignet retombe — soit pendant
l'essentiel du test. Meme famille de piege que la Live Activity fantome.

L'envoi reprend la livraison differee de la routine : un test se fait dehors,
l'iPhone peut etre hors de portee, et refaire un test maximal parce qu'un
message s'est perdu n'est pas acceptable. Si l'envoi echoue, les chiffres
restent affiches pour une saisie manuelle.

VmaTestView.swift est un fichier NOUVEAU : cable dans le pbxproj (4 entrees,
sauvegarde project.pbxproj.bak-vma). Verification : `grep -c "VmaTestView.swift
in Sources"` doit rendre 2 (declaration + phase de compilation = 1 cible).

⚠️ NON COMPILE — a builder au prochain passage sur le Mac.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 18:47:03 +00:00
Sylvain Bettinelli
4f8699fd37 Vitesse en km/h sur la montre et dans la Live Activity
Suite de la demande : voir des km/h en courant, ce que l'app Exercice
d'Apple ne permet pas (vitesse réservée au vélo, allure min/km imposée en
course, aucun réglage).

La vitesse instantanée vient de HealthKit lui-même : HKLiveWorkoutDataSource
collecte .runningSpeed d'office en course extérieure (Series 6+) et
.cyclingSpeed à vélo (watchOS 11+). Elle est lue en mostRecentQuantity —
c'est la vitesse à l'instant t qu'on veut afficher, pas la moyenne de la
séance. Sur un appareil qui ne la produit pas, repli sur distance/temps.

Affichée sur les trois surfaces : écran de séance de la montre, Live
Activity (bannière + île dépliée) et vue native /live.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 13:31:13 +00:00
Sylvain Bettinelli
21d39f2e98 Piloter la séance depuis la Live Activity (Pause / Terminer)
Comme l'app Exercice : deux boutons sur la bannière de l'écran verrouillé et
dans l'île dépliée. iOS n'expose aucun geste de balayage pour révéler des
actions — depuis iOS 17, ce sont des boutons intégrés (App Intents), et c'est
le seul mécanisme public.

- Intents `CoachTogglePauseIntent` / `CoachEndWorkoutIntent`, conformes à
  LiveActivityIntent (sans quoi `perform()` n'est jamais appelé).
- Ils vivent dans CoachLiveActivityAttributes.swift, seul fichier déjà membre
  des deux targets : un fichier neuf imposerait une manip Target Membership
  dans Xcode, source d'erreurs répétées ici.
- Commandes relayées à la montre par WCSession, avec repli transferUserInfo :
  isReachable retombe à false quand la séance tourne en arrière-plan profond,
  la commande est alors différée au réveil de l'app montre.
- L'état de pause remonte depuis HKWorkoutSession (seule source fiable : la
  montre peut mettre en pause d'elle-même) et bascule le libellé du bouton.
- Le watchdog de LiveStore est neutralisé pendant la pause : sans ça, l'absence
  de samples aurait affiché « connexion perdue » puis terminé l'activité.
- L'app watchOS gagne le bouton Pause qui lui manquait, aligné sur le même état.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 13:25:14 +00:00
Sylvain Bettinelli
5e9b4a3df6 La Live Activity restait affichée après la fin de séance
La montre n'annonçait pas la fin : le flux de samples s'arrêtait, et
l'iPhone ne pouvait pas distinguer « séance terminée » de « montre hors de
portée ». La Live Activity restait donc figée sur la dernière FC reçue —
un cœur à 74 bpm en permanence dans la Dynamic Island.

Trois causes cumulées, toutes corrigées :

1. Aucun message de fin. WorkoutManager envoie maintenant `liveEnded` à
   l'arrêt de la session HealthKit, en cas d'échec, et en mode test.
2. Le seul chemin de terminaison côté iPhone était le watchdog de
   LiveStore, un Timer du main runloop — qui ne tourne pas quand l'app est
   suspendue en arrière-plan, c'est-à-dire pendant toute la séance.
   `endLive()` termine désormais l'activité dès réception du message.
3. `dismissalPolicy: .default` laissait la bannière ~4 h APRÈS la fin.
   Passé en `.immediate`.

Filet de sécurité : `adoptExisting()` au lancement reprend la main sur une
activité orpheline (app tuée pendant une séance, `current` perdu) et
termine celles de plus de 6 h.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 12:50:32 +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
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
95d2961a56 chore(xcode): App Groups sur App, CoachLiveActivity et CoachWatch 2026-08-03 17:01:53 +02: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
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
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
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
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
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
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
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
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