Files
coach-ios/docs/widgets-runbook-mac.md
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

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É par cap sync. La source versionnée capacitor.config.ts a déjà les bonnes valeurs (contentInset: never, fonds #000000, StatusBar LIGHT). Le finding audit « JSON embarqué divergent » était un artefact local périmé, pas un problème du code. → Sur le Mac : un simple npx cap sync ios régénère le JSON correctement depuis le .ts. (Aucune action de versionnement nécessaire.)
  • aps-environment dans ios/App/App/App.entitlements = developmentpush cassés en prod/TestFlight. NON modifié depuis Linux (flipper en production casserait 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 Capacitor setSnapshot/clear + WidgetCenter.reloadAllTimelines().
  • ios/App/CoachLiveActivity/CoachWidgets.swift — 2 widgets (TimelineProvider + vues SwiftUI).
  • ios/App/CoachLiveActivity/CoachLiveActivityBundle.swift — widgets ajoutés au WidgetBundle.
  • ios/App/App/MainViewController.swift — plugin enregistré dans capacitorDidLoad().
  • ios/App/App/App.entitlements — App Group group.ch.hypnotruck.coach ajouté.
  • ios/App/CoachLiveActivity/CoachLiveActivity.entitlementscréé avec l'App Group.

coach_sportif (web, déjà en prod via webhook) :

  • web/static/widget-bridge.js + include dans _layout.html — pousse today (depuis /api/today) + forme (recovery_score). No-op hors app native.
  • web/templates/home.htmldata-form-score exposé sur le hero.

Étapes Mac à faire (dans l'ordre)

1. App Group sur le portail Apple

developer.apple.com → Certificates, IDs & Profiles → IdentifiersApp 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+ CapabilityApp Groups → cocher group.ch.hypnotruck.coach.
  • Vérifier que CODE_SIGN_ENTITLEMENTS pointe 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.swiftcocher App ET CoachLiveActivity (fichier partagé).
  • CoachWidgetBridge.swiftcible App uniquement.
  • CoachWidgets.swiftcible 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.js pousse 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 /calendar resp. /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.pushToWatchupdateApplicationContextCoachWatch/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/ConnectivityManager gère didReceiveApplicationContext/didReceiveUserInfoCoachWidgetStore.save + WidgetCenter.reloadAllTimelines().
  • ios/App/CoachWatchWidgets/CoachWatchComplications.swift — 2 complications (Forme : circular/corner ; Séance : rectangular/inline) + @main WidgetBundle.

Étapes Mac

  1. Créer la target : File → New → Target → Widget Extension (plateforme watchOS), nom CoachWatchWidgets, embarquée dans l'app Watch CoachWatch. Supprimer le fichier template généré, puis Add Files CoachWatchComplications.swift à cette target.
  2. Target Membership de CoachWidgetSnapshot.swift : cocher CoachWatch (l'app Watch, pour ConnectivityManager) ET CoachWatchWidgets (l'extension complication).
  3. 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).
  4. 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).
  5. Si la complication reste vide : vérifier que updateApplicationContext arrive (log widgetSnapshot reçu de l'iPhone cô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

  1. Prérequis : App Group group.ch.hypnotruck.coach créé et actif sur les deux cibles (étapes plus haut). Rien ne fonctionne sans.
  2. Ajouter les deux fichiers au projet Xcode, avec le Target Membership ci-dessus.
  3. Enregistrer le widget dans le WidgetBundle de CoachLiveActivity : ajouter CoachQuickWidget() à côté des widgets existants.
  4. Schéma d'URL : vérifier coachapp:// dans le Info.plist. Sans objet depuis e715804 : les boutons pointent sur https://coach.hypnotruck.ch/meals?photo=1 et ?scan=1, routés par Universal Links comme les autres widgets. Un coachapp:// n'était routé nulle part et ouvrait l'app sur sa dernière page consultée, au hasard.
  5. Cible de déploiement iOS 17 minimum : en dessous, le widget compile mais les boutons d'App Intent n'apparaissent pas.
  6. 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.