Commit Graph

121 Commits

Author SHA1 Message Date
Sylvain Bettinelli
089b6fbde6 Ce que la session Xcode du 11 aout a rencontre, et que le guide ne disait pas
L'etape 5 est faite. Cinq ecarts entre le guide et Xcode reel, tous corriges
dans le doc pour que la prochaine session ne les repaye pas :

Le template ne s'appelle plus CoachWatchWidgets.swift mais
CoachWatchWidgetsBundle.swift. Comme il porte un @main et que
CoachWatchComplications.swift en a deja un, l'oublier fait echouer la
compilation sur deux @main dans la meme target.

Les nouvelles targets sont creees en dossier synchronise : tout fichier du
dossier en est membre automatiquement, sans etre liste dans project.pbxproj.
CoachWatchComplications.swift etait donc deja inclus - d'ou son absence du
dialogue Add Files, qui ressemble a une panne et n'en est pas une. L'etape 6
du guide est desormais sans objet, et un grep qui renvoie 0 sur ce fichier
n'est plus un signal d'alarme.

Xcode cree la target en build 1 quand le projet est en 22, ce qui bloque a
l'installation. Les cases du template ont change (Include Control et Include
Configuration App Intent au lieu de Include Live Activity) : les deux se
decochent, nos complications sont des StaticConfiguration.

Et la Watch n'a pas besoin d'etre visible comme destination dans Xcode : la
phase Embed Watch Content embarque l'app Watch dans l'app iPhone, builder le
schema App suffit. Chercher la montre dans la liste des devices est une impasse
qui coute un mode developpeur et un redemarrage pour rien.

Ajout d'un script de verification de l'assemblage : il dit a quelle target
appartient chaque phase Embed. Embed Foundation Extensions doit pointer sur
CoachWatch - rattachee a App, l'extension resterait sur l'iPhone et aucune
complication n'apparaitrait.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 14:34:28 +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
ce048ee634 Runbook Mac pour builder le correctif Live Activity
Build simple (aucun fichier neuf, aucune capability), mais avec un piège :
la montre doit être rebuildée elle aussi, sinon rien ne change — le message
de fin de séance part de CoachWatch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 12:54:40 +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
051c8e8ff4 Le guide faisait ouvrir un workspace qui n'existe pas
Trois docs demandaient d'ouvrir ios/App/App.xcworkspace. Ce fichier n'existe
pas : Capacitor 8 passe par Swift Package Manager (ios/App/CapApp-SPM), il n'y a
plus ni Podfile ni workspace CocoaPods. La session s'arretait sur « The file
does not exist » des l'ouverture de Xcode.

C'est App.xcodeproj qu'il faut ouvrir. Corrige dans SESSION-XCODE.md,
widgets-runbook-mac.md et routine-watch-runbook-mac.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 11:59:29 +00:00
Sylvain Bettinelli
ddfd95cbd7 Le guide Xcode annoncait un fichier a ajouter, il y en a quatre
SESSION-XCODE.md disait « une seule exception : CoachWatchComplications.swift ».
C'etait vrai le 3 aout, jour ou il a ete ecrit. Le widget de saisie rapide est
arrive le 6 aout avec trois fichiers Swift de plus, et aucun n'appartient a une
target : CoachQuickLog.swift, CoachQuickSync.swift, CoachQuickWidget.swift.
Verifie dans project.pbxproj, pas suppose.

Consequence si on suivait le guide tel quel : l'extension ne compile pas, et le
widget « Saisie des repas et boissons » n'apparait nulle part - sans que rien
n'explique pourquoi, puisque le guide affirmait qu'il n'y avait rien a ajouter.

Une etape 4b liste les trois fichiers avec leur Target Membership exact.
CoachQuickLog va dans DEUX cibles, l'extension ecrivant la file que l'app vide.
Et ce qui est deja fait est dit comme tel, pour ne pas le refaire : le widget
est enregistre dans le WidgetBundle, et CoachQuickSync.flush() est bien appele
par AppDelegate.

Le point 4 du runbook widgets demandait de verifier le schema coachapp:// dans
l'Info.plist. Il est barre : depuis e715804 les boutons pointent sur des URL
https routees par Universal Links, precisement parce que coachapp:// n'etait
routé nulle part et ouvrait l'app sur sa derniere page consultee.

Aucun code touche, uniquement de la documentation de session.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 11:40:49 +00:00
Sylvain Bettinelli
ebb9273dc3 Le scan de code-barres ne pouvait pas fonctionner sur Android
Le manifest ne declarait que INTERNET. Or /meals scanne via
navigator.mediaDevices.getUserMedia() : Capacitor 8 relaie bien la demande
(BridgeWebChromeClient.onPermissionRequest mappe VIDEO_CAPTURE sur
Manifest.permission.CAMERA, puis lance le permissionLauncher), mais il ne peut
demander a l'utilisateur que ce qui est declare dans le manifest. La permission
etant absente, le scan etait le seul chemin casse - <input capture> continuait
de marcher, puisqu'il delegue a l'app appareil photo par intent.

CAMERA implique android.hardware.camera ET android.hardware.camera.autofocus en
fonctionnalites REQUISES, donc filtrage au Play Store, tant qu'on ne les
redeclare pas en required="false". Les deux sont donc declarees explicitement :
le scan demande facingMode ideal, pas exact, l'app reste utilisable sans.

Cote iOS, les libelles de permission ne decrivaient que le scan de code-barres
et la photo de profil, alors que la camera et la photothèque servent aussi a
photographier une assiette et une etiquette nutritionnelle pour analyse. Un
libelle qui ne couvre pas l'usage reel est un motif classique de rejet App
Store. NSPhotoLibraryAddUsageDescription reste inutile : rien n'ecrit dans la
photothèque (aucun PHPhotoLibrary ni UIImageWriteToSavedPhotosAlbum dans ios/).

Les deux fichiers sont bien formes (xml.etree, plistlib). Rien n'est compile
ici : reste a verifier sur un appareil que le dialogue camera apparait et que
le flux video s'affiche.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 08:10:10 +00:00
Sylvain Bettinelli
1c0e0320a9 Le plugin santé expose les métriques que seule la montre mesure
getWorkoutDetails lit, pour une liste de séances : les métadonnées du
HKWorkout (dénivelé de l'altimètre barométrique, température, humidité,
indoor, METs) et la dynamique de course de watchOS 9+ (puissance, longueur
de foulée, oscillation verticale, temps de contact au sol, vitesse max).

@capgo/capacitor-health n'expose aucun de ces types, et le dénivelé
barométrique est plus juste que le cumul d'altitude GPS que le serveur
calculait faute de mieux. Sur une montre antérieure à la Series 6, les
champs de dynamique sont simplement absents de la réponse.

Le web appelle déjà la méthode derrière un test d'existence : elle ne
prendra effet qu'après ce build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 06:49:28 +00:00
Sylvain Bettinelli
9790f0fa8a fix(widget): le pont effaçait l'hydratation à chaque chargement de page
Voilà la vraie cause du total qui retombait à zéro. `setSnapshot` reconstruisait
le snapshot **de zéro** avec `today` et `forme` seulement : tout ce que
`CoachQuickSync` venait de créditer — hydratation, cafés, calories — était
écrasé au premier chargement de page suivant.

Mon correctif précédent créditait donc un snapshot que le pont effaçait dans la
foulée. Il reste utile (il tient l'affichage entre la synchro et le prochain
chargement), mais il ne pouvait pas suffire seul.

Les champs de nutrition sont désormais FACULTATIFS dans l'appel : absents, on
conserve ceux du snapshot existant au lieu de les vider. Un pont qui n'envoie
qu'une partie des données ne doit pas détruire le reste.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 13:54:32 +00:00
Sylvain Bettinelli
d8adad466b fix(widget): le total retombait à zéro après synchronisation
Le widget affiche « snapshot + file d'attente ». La synchronisation vidait la
file sans créditer le snapshot : le total repassait donc à zéro juste après que
la saisie ait été enregistrée pour de bon. Le pire moment pour douter de son
journal.

`flush()` reporte désormais dans le snapshot ce qu'il vient d'envoyer, AVANT de
vider la file. Seules les entrées du jour sont créditées — le snapshot porte des
totaux quotidiens, y verser une saisie d'hier les fausserait. Le pont web
repoussera un snapshot exact au prochain chargement de page ; ce report garde
l'affichage juste entre-temps.

Les optionnels du snapshot reçoivent `= nil` explicitement : sans valeur par
défaut, l'init memberwise de Swift les aurait exigés à l'appel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 13:51:22 +00:00
Sylvain Bettinelli
b2b3c10807 L'hydratation est un total, pas une jauge
Décision de l'utilisateur : pas d'objectif de boisson, juste la consommation du
jour. Le code qui attendait un objectif pour afficher une barre est retiré,
champ de snapshot compris — un conditionnel qui ne se déclenchera jamais est une
fausse promesse, et le prochain lecteur y verrait une fonctionnalité en attente.

Le widget affiche « 1,2 L bue aujourd'hui ». Seule l'énergie garde sa jauge :
elle est la seule à avoir une cible fixée (1950 kcal).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 13:47:05 +00:00
Sylvain Bettinelli
5eafa14b99 Jauges d'hydratation et de calories dans le widget
Deux barres sous le titre : eau bue sur objectif, énergie consommée sur objectif
calorique. La barre des calories vire à l'orange au-dessus de la cible.

La jauge n'apparaît QUE si l'objectif correspondant existe. Aucun objectif
d'hydratation n'est défini dans les objectifs nutritionnels : tant qu'il vaut
nil, le widget affiche le volume bu sans barre. Une jauge sur un objectif
inventé donnerait un pourcentage qui ne veut rien dire — et il aurait l'air
d'un réglage validé.

Les trois champs du snapshot sont optionnels : un instantané écrit par une
version antérieure reste lisible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 13:44:40 +00:00
Sylvain Bettinelli
e4d317f781 Titre dans le widget : « Saisie des repas et boissons »
Un widget se lit hors contexte, au milieu d'autres tuiles : sans titre,
« 0,3 L · 2 cafés » ne dit ni de quoi il s'agit, ni à quoi servent les boutons.

Le nom affiché dans la galerie de widgets est aligné sur ce titre — deux libellés
différents pour la même chose obligeraient à les rapprocher de tête.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 13:40:38 +00:00
Sylvain Bettinelli
e715804dd9 fix(widget): Photo et Scanner ouvraient une page au hasard
Les deux liens utilisaient un schéma custom `coachapp://` que rien ne route :
le projet passe par les Universal Links (entitlement associated-domains), comme
les widgets Séance du jour et Score de forme, qui pointent tous deux sur des URL
https. L'app s'ouvrait donc sur sa dernière page consultée — le score de sommeil
en l'occurrence.

Les liens passent en https://coach.hypnotruck.ch/meals?photo=1 et ?scan=1, et le
schéma custom que j'avais ajouté à l'Info.plist est retiré : il ne servait à
rien et aurait laissé une fausse piste.

J'avais écrit ces URL sans lire comment les widgets voisins s'y prenaient — même
faute que pour `todayISO`, supposer un mécanisme au lieu de le vérifier.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 13:40:10 +00:00
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