Files
coach-ios/docs/widgets-runbook-mac.md
Sylvain Bettinelli ddfd95cbd7 Le guide Xcode annoncait un fichier a ajouter, il y en a quatre
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>
2026-08-11 11:40:49 +00:00

164 lines
9.8 KiB
Markdown

# 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` = `development`**push 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.entitlements`**créé** 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.html``data-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 → **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_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.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.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)
```jsonc
{
"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/ConnectivityManager` gère `didReceiveApplicationContext`/`didReceiveUserInfo`
`CoachWidgetStore.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` :
```swift
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.