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>
9.9 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.xcodeproj (pas de .xcworkspace : Capacitor 8 passe par
Swift Package Manager, plus par CocoaPods), 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.