Sans lecture de l'App Group, un widget qui n'affiche pas la bonne chose laisse
trois hypotheses ouvertes — rien d'ecrit, ecrit sans le champ, ou ecrit mais
pas reaffiche — et le JS ne peut en departager aucune : l'App Group lui est
invisible.
Vecu ce soir : tout etait correct jusqu'au stockage, seul le rafraichissement
de la timeline manquait. getSnapshot rend hasDoneToday, hasToday, l'horodatage
et le contenu, ce qui designe directement le maillon fautif.
⚠️ NON COMPILE — a embarquer au prochain build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
Les familles accessory* n'etaient declarees que cote Watch, alors que ce sont
exactement celles de l'ecran verrouille iOS 16+. Les vues existaient donc
deja : il ne manquait que la declaration cote iPhone.
Ajoutes sur l'ecran verrouille : Forme (circulaire/rectangulaire/inline),
Seance (rectangulaire/inline, avec la bascule doneToday du commit precedent)
et Routine du jour. Sur la Watch : complication Routine, et la seance gagne la
famille circulaire (icone du sport, verte quand elle est faite).
Le snapshot gagne RoutineProgress {done, total} au grain EXERCICE — celui de
la page web et de l'app Watch depuis le 03/08. `fraction` borne le ratio et
protege de la division par zero ; un total nul ne doit pas remplir la jauge.
Meme garde du jour que doneToday : la routine repart a zero au reveil, un
report aveugle afficherait 24/24 des le matin.
Vues dupliquees plutot que partagees entre les deux extensions : elles ne
partagent aucun fichier et l'App Group n'est pas commun entre appareils. Trois
petites vues coutent moins qu'un fichier de plus a cabler dans deux cibles —
et pour la meme raison tout est ajoute a CoachWidgets.swift, deja membre de sa
cible : AUCUN fichier nouveau, donc rien a cabler dans le pbxproj.
⚠️ NON COMPILE — a builder au prochain passage sur le Mac, avec le correctif
« JOUR OFF » (12b56a4).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pendant du commit web 25f53b5. Quand la seance prevue est remplacee par une
autre activite, le plan du jour se vide : le widget iPhone affichait « JOUR
OFF / Pas de seance prevue » et la complication Watch « Jour OFF » — le jour
meme d'une sortie de 20 km. Le snapshot ne portait que le PLAN.
CoachWidgetSnapshot gagne doneToday (sport, titre, sous-titre), optionnel :
un snapshot ecrit par une version anterieure reste decodable. today decrit le
plan, doneToday decrit la journee ; les deux peuvent coexister.
CoachWidgetBridge le conserve quand l'appel l'omet, comme la nutrition — mais
seulement si le snapshot precedent date d'aujourd'hui, sinon la sortie de la
veille resterait affichee indefiniment.
Les deux vues basculent sur l'activite realisee, avec la pastille verte
« Realisee » deja utilisee pour une seance planifiee faite. Le sous-titre
(« 20,3 km · 56 min ») passe devant le titre : le titre d'une seance importee
est souvent vide.
⚠️ NON COMPILE — a builder au prochain passage sur le Mac. Aucun fichier
nouveau, donc rien a cabler dans le pbxproj : les quatre fichiers touches sont
deja membres de leurs cibles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
En corrigeant le build de la nouvelle target CoachWatchWidgets (creee en 1 par
Xcode, refusee a l'installation), la valeur a aussi ete changee sur
ch.hypnotruck.coach.LiveActivity - l'extension qui porte les widgets iPhone, la
Live Activity et le widget Saisie rapide. Elle se retrouvait en 23 quand son app
parente reste en 22.
Verifie : avant cette session, aucune ligne du pbxproj n'etait en 23.
En debug sur device, l'ecart passe. A l'archivage, Apple rejette l'upload pour
CFBundleVersion Mismatch - une extension doit porter le meme CFBundleVersion que
l'app qui la contient. Ca se serait vu dans l'Organizer, apres l'archive, au
moment le plus penible.
Les huit configurations sont ramenees a 22. Au prochain TestFlight, monter les
quatre targets ENSEMBLE (App, CoachWatch, CoachWatchWidgets, LiveActivity) -
c'est ce que l'etape 7 du guide prevoit avec 1.3 / build 23.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Propose puis mis de cote aujourd'hui. Ce qui est consigne, c'est l'etat verifie
dans le code, pour que la reprise ne recommence pas l'enquete.
Les complications portent deja un widgetURL, mais CoachWatch n'implemente aucun
onOpenURL : le tap ouvre l'app sur le choix d'activite, pas sur la seance. Et la
donnee est deja sur la montre - CoachWidgetSnapshot.TodaySession y arrive par
WCSession, l'app ne la lit simplement pas. Le travail se reduit donc a lire
l'URL entrante et mapper le sport vers le HKWorkoutActivityType, environ 1 h.
La limite qui cadre le sujet : une complication ne peut pas ouvrir l'app
Exercice d'Apple sur une seance programmee, il n'existe aucune URL publique pour
ca. Le fractionne structure envoye par WorkoutKit se lancera toujours a la main
depuis Exercice > Programmees. Reimplementer le guidage d'intervalles chez nous
doublonnerait ce qu'Apple fait deja bien.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
`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>
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>
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>
Aucun .widgetURL ne ramenait dans l'app : la Live Activity, les deux widgets
iPhone et les deux complications Watch pointent sur des URL https, mais sans
entitlement associated-domains ni fichier d'association servi, iOS ouvrait
Safari — où le cookie d'auth de la WKWebView n'existe pas.
Le versant serveur est déjà en prod (coach_sportif 8ec0047 : route AASA +
deeplink.js). Reste la capability à vérifier dans Xcode côté Mac, cf. COWORK.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
watchOS demande 'Quel a été votre effort ?' après chaque séance et l'user y
répond déjà. La réponse est stockée dans HealthKit en workoutEffortScore
(iOS 18+, vérifié dans la doc Apple) : on la récupère au lieu de la redemander
dans l'app.
- getEffortScores({days}) : parcourt les séances de la fenêtre et, pour chacune,
interroge les samples d'effort via le prédicat officiel
predicateForWorkoutEffortSamplesRelated(workout:activity:). Pas de
rapprochement approximatif par horodatage.
- unité HKUnit.appleEffortScore(), score arrondi sur 1-10
- requestAuthorization demande désormais la permission de lecture du type :
sans elle, la requête renvoie une liste vide SANS erreur — HealthKit ne
distingue pas 'refusé' de 'aucune donnée'
- guard iOS 18 : sur plus ancien, renvoie [] avec une raison explicite
Le pendant serveur est dans coach_sportif (commit 0e79e74) :
POST /api/feedback/device/import, qui n'écrase jamais une saisie manuelle.
NON COMPILÉ — à inclure au prochain build Xcode.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pendant natif du lecteur web. Bouton Démarrer par bloc : un exercice à la fois,
décompte en anneau, passage auto, haptique à chaque transition, pause et
navigation. Pendant une routine au sol, la montre est bien plus pratique que
d'aller chercher le téléphone — c'est tout l'intérêt de la version watchOS.
Chaque exercice écoulé est coché via RoutineStore.toggle, donc renvoyé à
l'iPhone puis au serveur : à la fin le bloc est à jour sur les trois surfaces.
La durée par exercice est désormais transmise dans le snapshot (duration_sec,
optionnel pour rester compatible avec les snapshots déjà persistés) : la vue
guidée utilise la vraie durée au lieu de répartir celle du bloc.
RoutineGuidedView.swift est déjà référencé dans le pbxproj (target CoachWatch),
aucun Add Files à faire. Backup : project.pbxproj.bak-guided
NON COMPILÉ — à inclure au prochain build Xcode.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pendant natif de coach_sportif c12caef. À la détection d'un HKWorkout de type
renforcement, POST /api/routine/auto-complete avec les faits bruts.
Volontairement bête côté natif : aucune règle de correspondance ici. Fenêtre de
12 h depuis l'envoi explicite, tolérance de durée, choix du bloc — tout vit
côté serveur et reste donc ajustable sans rebuild iOS.
NON COMPILÉ — à inclure au prochain build Xcode.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pendant natif du commit coach_sportif 01bad9e. Les 24 exercices sont
cochables un par un pendant la séance, comme sur le web.
- RoutineExercise + items dans RoutineBlock ; les exercices voyagent avec le
snapshot, rien n'est codé en dur côté watchOS
- l'en-tête de bloc est tappable : coche/décoche tous ses exercices d'un coup,
pour enchaîner une série sans s'arrêter à chaque ligne
- compteur n/N par bloc + progression globale en exercices
- CoachRoutineBridge.sanitizeBlocks transmet désormais les items
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pushToWatch ne lisait que `date` et `done` dans l'appel JS et ignorait
`blocks`. La montre recevait donc un snapshot sans aucun bloc et restait sur
son écran vide « Ouvre l'app Coach sur l'iPhone » — alors que le log confirmait
que l'envoi partait bien (constaté sur device le 2026-08-03).
sanitizeBlocks ne garde que les clés attendues en types property-list stricts :
updateApplicationContext lève une exception ObjC sur tout ce qui ne l'est pas,
et les entiers venus du JS arrivent en NSNumber — un `as? Int` direct échoue
quand JavaScriptCore les passe en double (même piège que le score perdu dans
CoachWidgetBridge).
Le log distingue désormais blocs transmis et blocs faits : « 0 blocs » était
ambigu et désignait le compte des cochés, pas celui des blocs envoyés.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AppDelegate écrivait dans HTTPCookieStorage.shared, qui est le store
d'URLSession — pas celui que lit la WKWebView (WKWebsiteDataStore). Ça
fonctionnait jusqu'en mai parce que Capacitor recopiait alors les cookies au
démarrage ; ce n'est plus le cas, et l'app retombait sur l'écran de login
malgré un token valide (constaté sur device le 2026-08-03).
MainViewController.viewDidLoad injecte désormais dans le store de la WebView,
puis recharge une fois pour rejouer la première requête avec le cookie. Pas de
boucle : au passage suivant le cookie existe et on sort avant.
L'injection AppDelegate est conservée : elle sert aux appels natifs URLSession.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La commande listait tout DerivedData et les checkouts SPM en « NON CÂBLÉ »
(15 faux positifs constatés sur le Mac le 2026-08-03). Le dossier build/
n'existe pas côté serveur dev, d'où l'angle mort.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rassemble tout le natif accumulé depuis le TestFlight de mai en une seule
procédure ordonnée par dépendances : pull, token, App Group, capabilities,
target complications (facultative), build+tests device, puis distribution.
Point d'arrêt explicite après l'étape 6 : l'app tourne sur l'iPhone, la
publication TestFlight peut attendre un autre jour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Réduit le travail manuel de la session Xcode : CoachWidgetBridge (App),
CoachWidgets (CoachLiveActivity) et CoachWidgetSnapshot (partagé App +
CoachLiveActivity + CoachWatch, un PBXBuildFile par target = ce que fait
"Target Membership") n'étaient référencés nulle part depuis le 2026-06-29.
Reste manuel sur le Mac, car il faut créer une target :
CoachWatchWidgets/CoachWatchComplications.swift
Backup : project.pbxproj.bak-widgets
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pendant natif du backend coach_sportif (commit 772385b, déjà en prod).
- CoachRoutineBridge (target App) : pousse blocs + état du jour vers la Watch
via updateApplicationContext, relaie les coches Watch vers /api/routine/day
puis notifie la WebView (event routineUpdated)
- CoachWatch/RoutineStore : état persisté en UserDefaults, reset au changement
de jour. Les blocs viennent de l'iPhone — rien codé en dur côté watchOS, donc
modifier la routine côté web ne demande aucun rebuild
- CoachWatch/RoutineView : liste cochable, progression, haptique, bouton de
renvoi si la synchro a échoué
- ConnectivityManager (watch) : sendRoutine() en sendMessage avec repli
transferUserInfo (coche faite iPhone hors de portée -> livraison différée)
⚠️ CoachLiveBridge touché : routeIfNotLiveSample() aiguille les messages
routineDone vers NotificationCenter. WCSession.delegate est unique côté iPhone,
impossible d'en ajouter un second. Le flux live workout n'est pas modifié mais
doit être re-vérifié au build.
Les 3 nouveaux fichiers sont référencés dans project.pbxproj (bonne target) :
aucun "Add Files" à faire sur le Mac. Runbook : docs/routine-watch-runbook-mac.md
NON COMPILÉ — nécessite une session Xcode sur le Mac mini.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- CoachWidgetBridge : lit le score en NSNumber.intValue (le cast `as? Int`
renvoyait nil quand le web envoie un float → score widget perdu par
intermittence). CoachWidgetStore.save renvoie un Bool ; le plugin fait
call.reject si la sauvegarde échoue au lieu de résoudre ok:true à tort.
- CoachAppleAuth : sur double-tap, rejette l'ancien pendingCall (Promise JS
ne pend plus indéfiniment) ; accès pendingCall confiné au main thread ;
erreur d'annulation testée par domaine ASAuthorizationError (pas rawValue nu).
Swift pur, non compilé ici (pas de Xcode) — à valider au prochain build Mac.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rectification : capacitor.config.json n'est PAS suivi (gitignored, généré par cap
sync). Le .ts source a déjà les bonnes valeurs → le finding audit 'JSON divergent'
était un artefact local périmé, pas un bug du repo. cap sync sur le Mac régénère
correctement. Pas d'action de versionnement.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Le JSON embarqué avait divergé du .ts source (cap sync non rejoué) :
contentInset always->never, fonds #ffffff->#000000, StatusBar DEFAULT->LIGHT.
Corrige la régression bottom-nav + flash blanc ré-introduite en prod. JSON validé.
aps-environment (dev->prod) NON flippé depuis Linux (casserait le dev ; fix par
config Release sur le Mac) — documenté dans docs/widgets-runbook-mac.md avec le
reste des findings iOS de l'audit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
L'App Group n'étant pas partagé entre appareils, le snapshot transite par
WatchConnectivity : iPhone CoachWidgetBridge.pushToWatch (updateApplicationContext)
-> CoachWatch/ConnectivityManager reçoit -> App Group de la montre + reload des
complications. Nouvelles complications (CoachWatchWidgets/CoachWatchComplications.swift) :
Forme (accessoryCircular/corner, Gauge) + Séance du jour (accessoryRectangular/inline).
⚠️ Reste Mac : créer la target widget watchOS + App Group/capability + Target
Membership. Runbook complet dans docs/widgets-runbook-mac.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>