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>
9.8 KiB
Runbook Mac — widgets natifs iPhone (Séance du jour + Score de forme)
⚠️ Fixes audit iOS 2026-06-29 — à finir sur le Mac
- ✅ capacitor.config.json : RIEN à corriger dans le repo. Vérifié 2026-06-29 : ce fichier est gitignored / non suivi (
ios/.gitignore) — c'est un artefact GÉNÉRÉ parcap sync. La source versionnéecapacitor.config.tsa déjà les bonnes valeurs (contentInset: never, fonds#000000, StatusBarLIGHT). Le finding audit « JSON embarqué divergent » était un artefact local périmé, pas un problème du code. → Sur le Mac : un simplenpx cap sync iosrégénère le JSON correctement depuis le.ts. (Aucune action de versionnement nécessaire.) - ⏳
aps-environmentdansios/App/App/App.entitlements=development→ push cassés en prod/TestFlight. NON modifié depuis Linux (flipper enproductioncasserait les builds dev locaux ; le bon fix est par configuration Release sur le Mac, ou via la capability « Push Notifications » qui le gère automatiquement). À traiter sur le Mac AVANT toute distribution utilisant le push distant. - ⏳ Entitlements widget + cible Watch : cf. sections plus bas (App Group / CODE_SIGN_ENTITLEMENTS / Target Membership) — c'est aussi un finding de l'audit (widgets muets sans App Group câblé).
Pour le Claude « Cowork » (Mac) / Sylvain. Le code Swift + entitlements + le
hook web sont écrits côté coach_sportif/coach-ios par le Claude dev (Linux).
Il reste les étapes Mac/portail Apple ci-dessous — elles ne peuvent pas se
faire depuis Linux (provisioning + ajout de fichiers aux cibles dans Xcode).
Ce qui est déjà fait (committé)
coach-ios (natif) :
ios/App/App/CoachWidgetSnapshot.swift— modèle partagé App ↔ extension (App Group).ios/App/App/CoachWidgetBridge.swift— plugin CapacitorsetSnapshot/clear+WidgetCenter.reloadAllTimelines().ios/App/CoachLiveActivity/CoachWidgets.swift— 2 widgets (TimelineProvider + vues SwiftUI).ios/App/CoachLiveActivity/CoachLiveActivityBundle.swift— widgets ajoutés auWidgetBundle.ios/App/App/MainViewController.swift— plugin enregistré danscapacitorDidLoad().ios/App/App/App.entitlements— App Groupgroup.ch.hypnotruck.coachajouté.ios/App/CoachLiveActivity/CoachLiveActivity.entitlements— créé avec l'App Group.
coach_sportif (web, déjà en prod via webhook) :
web/static/widget-bridge.js+ include dans_layout.html— poussetoday(depuis/api/today) +forme(recovery_score). No-op hors app native.web/templates/home.html—data-form-scoreexposé sur le hero.
Étapes Mac à faire (dans l'ordre)
1. App Group sur le portail Apple
developer.apple.com → Certificates, IDs & Profiles → Identifiers → App Groups →
créer group.ch.hypnotruck.coach (si pas déjà créé).
2. Activer la capability sur les 2 cibles (Xcode)
⚠️ Quitter puis git pull d'abord (règle COWORK : jamais de pull Xcode ouvert).
Ouvrir ios/App/App.xcworkspace, puis pour App ET CoachLiveActivity :
- onglet Signing & Capabilities → + Capability → App Groups →
cocher
group.ch.hypnotruck.coach. - Vérifier que
CODE_SIGN_ENTITLEMENTSpointe bien sur le bon.entitlements(App →App/App.entitlements; CoachLiveActivity →CoachLiveActivity/CoachLiveActivity.entitlements, à régler si Xcode ne l'a pas pris).
3. Ajouter les nouveaux fichiers aux bonnes cibles (Target Membership)
Les fichiers existent sur disque mais doivent être référencés par le projet :
CoachWidgetSnapshot.swift→ cocher App ET CoachLiveActivity (fichier partagé).CoachWidgetBridge.swift→ cible App uniquement.CoachWidgets.swift→ cible CoachLiveActivity uniquement.CoachLiveActivity.entitlements→ pas une source (référencé par le build setting).
Méthode : clic droit sur le dossier de la cible → Add Files to "App"… (décocher Copy items, cocher la bonne cible), ou sélectionner le fichier → inspecteur de droite → Target Membership.
4. Build + test device
- Clean Build Folder (
Cmd+Shift+K) → Run sur l'iPhone. - Ouvrir l'app (charge la prod) une fois pour que
widget-bridge.jspousse le snapshot. - Écran d'accueil → ajouter les widgets Coach : « Séance du jour » (small/medium) et « Score de forme » (small).
- Vérifier qu'ils affichent les vraies données du jour. Tap → ouvre
/calendarresp./forme.
5. Debug si le widget reste vide
- Vérifier le snapshot écrit : Safari → Développement → iPhone → console →
await Capacitor.Plugins.CoachWidgetBridge.setSnapshot({today:{sport:"running",title:"Test",done:false}})puis long-press l'écran d'accueil → le widget doit se mettre à jour. - Si rien : l'App Group n'est pas partagé (étape 2) →
UserDefaults(suiteName:)renvoie nil.
Modèle de données (contrat web ↔ widget)
{
"today": { "sport": "running", "title": "Sortie longue", "subtitle": "90 min · Z2", "done": false },
"forme": { "score": 72, "label": "Prêt" } // null si pas de score
}
sport ∈ running | cycling | strength | mobility | hiking | walking | rest.
Complication Apple Watch (code écrit — reste la target Xcode)
⚠️ Rappel archi : l'App Group n'est PAS partagé entre iPhone et Watch (conteneurs
distincts). Le flux est : iPhone CoachWidgetBridge.pushToWatch → updateApplicationContext
→ CoachWatch/ConnectivityManager reçoit → écrit dans l'App Group de la MONTRE →
la complication lit ce conteneur. Tout ce code est déjà écrit & committé.
Déjà fait (committé)
- iPhone :
CoachWidgetBridge.pushToWatch()envoie le snapshot via WCSession (sans être delegate). - Watch :
CoachWatch/ConnectivityManagergèredidReceiveApplicationContext/didReceiveUserInfo→CoachWidgetStore.save+WidgetCenter.reloadAllTimelines(). ios/App/CoachWatchWidgets/CoachWatchComplications.swift— 2 complications (Forme : circular/corner ; Séance : rectangular/inline) +@mainWidgetBundle.
Étapes Mac
- Créer la target : File → New → Target → Widget Extension (plateforme watchOS),
nom
CoachWatchWidgets, embarquée dans l'app WatchCoachWatch. Supprimer le fichier template généré, puis Add FilesCoachWatchComplications.swiftà cette target. - Target Membership de
CoachWidgetSnapshot.swift: cocher CoachWatch (l'app Watch, pourConnectivityManager) ET CoachWatchWidgets (l'extension complication). - App Group
group.ch.hypnotruck.coach: activer la capability sur CoachWatch ET CoachWatchWidgets (même ID que l'iPhone ; conteneur physique distinct côté montre). - Build la target Watch + extension, lancer sur la montre. Ajouter les complications sur un cadran (Forme = circular ; Séance = rectangular). Ouvrir l'app iPhone une fois pour pousser le 1er snapshot (l'app Watch doit être installée & appairée).
- Si la complication reste vide : vérifier que
updateApplicationContextarrive (logwidgetSnapshot reçu de l'iPhonecôté watch) et que l'App Group est bien partagé sur les deux targets watch.
Suite possible (backlog)
- Lockscreen widgets iPhone (
accessoryRectangular/accessoryCircular) — ajouter les familles aux widgets iOS. transferCurrentComplicationUserInfo(réveil complication, budget ~50/j) si besoin de fraîcheur en arrière-plan.- Background refresh iPhone (App Background Tasks) pour un widget à jour sans ouvrir l'app.
Widget « Saisie rapide » (ajouté 2026-08-06)
Noter l'eau et le café sans ouvrir l'app ; photo et code-barres l'ouvrent — la caméra exige le premier plan, aucun widget ne peut y échapper.
Fichiers et Target Membership
| Fichier | Cibles |
|---|---|
ios/App/App/CoachQuickLog.swift |
App + CoachLiveActivity (+ CoachWatch et CoachWatchWidgets pour la complication) |
ios/App/CoachLiveActivity/CoachQuickWidget.swift |
CoachLiveActivity seule |
ios/App/App/CoachWidgetSnapshot.swift |
inchangé — déjà App + CoachLiveActivity |
CoachQuickLog.swift doit être dans les deux cibles : l'extension écrit la
file, l'app la vide.
À faire sur le Mac
- Prérequis : App Group
group.ch.hypnotruck.coachcréé et actif sur les deux cibles (étapes plus haut). Rien ne fonctionne sans. - Ajouter les deux fichiers au projet Xcode, avec le Target Membership ci-dessus.
- Enregistrer le widget dans le
WidgetBundledeCoachLiveActivity: ajouterCoachQuickWidget()à côté des widgets existants. Schéma d'URL : vérifierSans objet depuiscoachapp://dans leInfo.plist.e715804: les boutons pointent surhttps://coach.hypnotruck.ch/meals?photo=1et?scan=1, routés par Universal Links comme les autres widgets. Uncoachapp://n'était routé nulle part et ouvrait l'app sur sa dernière page consultée, au hasard.- Cible de déploiement iOS 17 minimum : en dessous, le widget compile mais les boutons d'App Intent n'apparaissent pas.
- Builder sur un device réel — les App Intents ne s'exécutent pas de façon fiable en simulateur.
Synchronisation (fait — CoachQuickSync.swift, cible App seule)
CoachQuickSync.flush() envoie la file vers POST /api/drinks et retire les
entrées transmises par identifiant. Reste à l'appeler depuis AppDelegate :
func applicationDidBecomeActive(_ application: UIApplication) {
Task { await CoachQuickSync.flush() }
}
Comportements voulus, à ne pas « corriger » :
- 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é : le réessayer indéfiniment bloquerait la file ;
- la date est formatée en fuseau local — un verre bu à 23 h compte pour le jour où il a été bu.