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

9.7 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.xcworkspace, 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 de l'App (CFBundleURLTypes). Les boutons pointent sur coachapp://meals?photo=1 et coachapp://meals?scan=1.
  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.