Commit Graph

11 Commits

Author SHA1 Message Date
Sylvain Bettinelli
b8eb1e1b1e Le widget ne passait pas minuit : personne ne lisait updatedAt
Signale par Sylvain le 2026-08-18 : passe minuit, l'hydratation, les calories
et la seance de la veille restaient affichees comme celles du jour. Il fallait
ouvrir l'app pour les voir retomber a zero.

Le widget n'a AUCUN acces reseau : il rend ce que l'app lui a pousse la
derniere fois qu'elle a tourne. Le snapshot portait deja `updatedAt` — rien
ne le lisait. CoachQuickWidget programmait meme un reveil a minuit, avec le
bon commentaire, mais relisait ensuite les memes chiffres sans regarder leur
date : le rechargement avait lieu, la remise a zero non.

- `CoachWidgetSnapshot.asOf(_:)` : vide ce qui decrit UNE journee (eau, cafes,
  kcal, routine, activite faite, score de forme) quand le snapshot date d'un
  autre jour. Conserve ce qui traverse les jours — objectifs et zones
  cardiaques. Les compteurs repartent a 0 et non a nil : au matin, « rien bu »
  est vrai, « je ne sais pas » ne l'est pas.
- Les trois providers emettent une 2e entree datee de minuit. WidgetKit
  bascule seul, sans reveiller l'app.
- `tomorrow` : la seance du lendemain, poussee par le pont web. Sans elle,
  vider la seance ferait annoncer JOUR OFF chaque nuit — l'agacement d'un
  incident precedent, repete tous les jours.

⚠️ Deuxieme occurrence trouvee en verifiant, et pire : CoachQuickSync.credit()
ajoutait au total EXISTANT puis rehorodatait a maintenant. Sur un snapshot de
la veille, 200 ml bus le matin donnaient « 2 200 ml » estampilles du jour — un
total faux, que la peremption ne pouvait plus rattraper.

⚠️ NON COMPILE : pas de toolchain Swift ici. Build Xcode requis sur le Mac
mini pour que le correctif arrive sur l'iPhone et la Watch.
2026-08-18 07:11:15 +00:00
Sylvain Bettinelli
5bf3e91ad2 Widget Boissons : jauge d'hydratation + ouverture sur /hydratation
Le widget affichait un total sans jauge, et c'était une décision documentée
et juste : « aucun objectif de boisson n'est fixé, et en inventer un
donnerait un pourcentage qui n'a pas de sens ». Une cible existe maintenant
côté serveur (`/api/drinks` renvoie `water_target_ml`), donc la jauge peut
apparaître — sans cible, on retombe exactement sur l'ancien comportement.

⚠️ Cette cible n'a **aucune source** : c'est un objectif personnel, pas une
recommandation médicale. D'où le libellé « objectif » et non « besoin » ou
« recommandé ».

⚠️ Les deux côtés du pont bougent ensemble : `widget-bridge.js` pose
`waterGoalMl`, `CoachWidgetBridge` la lit sous le même nom. Le widget
« JOUR OFF » a menti pendant des jours parce que le pont lisait une clé
absente de la réponse serveur.

Ajoute aussi un `widgetURL` vers /hydratation : un tap hors bouton ouvrait
l'app sur sa dernière page consultée, au hasard.

⚠️ **NON COMPILÉ** — pas de Mac ici. À vérifier au prochain build Xcode,
avec les zones cardiaques Watch et `getSnapshot` déjà en attente.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 08:33:40 +00:00
Sylvain Bettinelli
eb819b68ca Watch : afficher la zone cardiaque pendant la séance
L'app Coach de la montre n'affichait que la FC brute — aucune notion de zone
dans `ContentView.swift`. C'est pourtant au poignet que « 142 bpm » a besoin
d'être traduit ; l'écran iPhone portait déjà l'information, mais on ne le
regarde pas en courant.

- `HeartRateZones.swift` : modèle partagé (App + CoachLiveActivity + les deux
  cibles Watch), câblé dans les 4 phases Sources du pbxproj comme
  `CoachWidgetSnapshot.swift`. ⚠️ Les bornes viennent de `/api/cardiac-zones`,
  version FIGÉE et datée — jamais recalculées côté natif, ce serait rouvrir les
  jeux de zones concurrents supprimés le 12/08.
- `zone(for:)` retient la zone la plus HAUTE qui contient la valeur : les
  bornes se chevauchent d'un battement (Z1 100-109, Z2 109-118), et 109 bpm
  doit se lire Z2, pas Z1. Au-delà du haut de Z5, on reste en Z5 — un effort
  maximal n'est pas une absence de zone.
- `ConnectivityManager` publie les zones reçues avec le snapshot et repart du
  dernier connu au lancement (la montre a pu redémarrer). Un snapshot partiel
  n'efface pas celles en place.
- `HeartRateMetric` : pastille « Z3 », valeur colorée, et la plage sous le
  chiffre. ⚠️ La couleur ne porte jamais l'information seule — illisible pour
  un daltonien, et le contraste d'un écran de montre en plein soleil ne se
  prête pas aux nuances. Libellé VoiceOver explicite.

`switch` en `return` explicites plutôt qu'en expression : la syntaxe courte
exige Swift 5.9 pile, et je ne peux pas compiler ici.

⚠️ NON COMPILÉ — build Xcode requis, avec `getSnapshot` (`8a25f36`) en attente.
2026-08-12 19:13:07 +00:00
Sylvain Bettinelli
3202e03a63 Trois widgets d'ecran verrouille et une complication Routine
Les familles accessory* n'etaient declarees que cote Watch, alors que ce sont
exactement celles de l'ecran verrouille iOS 16+. Les vues existaient donc
deja : il ne manquait que la declaration cote iPhone.

Ajoutes sur l'ecran verrouille : Forme (circulaire/rectangulaire/inline),
Seance (rectangulaire/inline, avec la bascule doneToday du commit precedent)
et Routine du jour. Sur la Watch : complication Routine, et la seance gagne la
famille circulaire (icone du sport, verte quand elle est faite).

Le snapshot gagne RoutineProgress {done, total} au grain EXERCICE — celui de
la page web et de l'app Watch depuis le 03/08. `fraction` borne le ratio et
protege de la division par zero ; un total nul ne doit pas remplir la jauge.
Meme garde du jour que doneToday : la routine repart a zero au reveil, un
report aveugle afficherait 24/24 des le matin.

Vues dupliquees plutot que partagees entre les deux extensions : elles ne
partagent aucun fichier et l'App Group n'est pas commun entre appareils. Trois
petites vues coutent moins qu'un fichier de plus a cabler dans deux cibles —
et pour la meme raison tout est ajoute a CoachWidgets.swift, deja membre de sa
cible : AUCUN fichier nouveau, donc rien a cabler dans le pbxproj.

⚠️ NON COMPILE — a builder au prochain passage sur le Mac, avec le correctif
« JOUR OFF » (12b56a4).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 18:08:13 +00:00
Sylvain Bettinelli
12b56a4d39 Le widget disait « JOUR OFF » les jours de sortie non planifiee
Pendant du commit web 25f53b5. Quand la seance prevue est remplacee par une
autre activite, le plan du jour se vide : le widget iPhone affichait « JOUR
OFF / Pas de seance prevue » et la complication Watch « Jour OFF » — le jour
meme d'une sortie de 20 km. Le snapshot ne portait que le PLAN.

CoachWidgetSnapshot gagne doneToday (sport, titre, sous-titre), optionnel :
un snapshot ecrit par une version anterieure reste decodable. today decrit le
plan, doneToday decrit la journee ; les deux peuvent coexister.

CoachWidgetBridge le conserve quand l'appel l'omet, comme la nutrition — mais
seulement si le snapshot precedent date d'aujourd'hui, sinon la sortie de la
veille resterait affichee indefiniment.

Les deux vues basculent sur l'activite realisee, avec la pastille verte
« Realisee » deja utilisee pour une seance planifiee faite. Le sous-titre
(« 20,3 km · 56 min ») passe devant le titre : le titre d'une seance importee
est souvent vide.

⚠️ NON COMPILE — a builder au prochain passage sur le Mac. Aucun fichier
nouveau, donc rien a cabler dans le pbxproj : les quatre fichiers touches sont
deja membres de leurs cibles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 17:56:53 +00:00
Sylvain Bettinelli
d8adad466b fix(widget): le total retombait à zéro après synchronisation
Le widget affiche « snapshot + file d'attente ». La synchronisation vidait la
file sans créditer le snapshot : le total repassait donc à zéro juste après que
la saisie ait été enregistrée pour de bon. Le pire moment pour douter de son
journal.

`flush()` reporte désormais dans le snapshot ce qu'il vient d'envoyer, AVANT de
vider la file. Seules les entrées du jour sont créditées — le snapshot porte des
totaux quotidiens, y verser une saisie d'hier les fausserait. Le pont web
repoussera un snapshot exact au prochain chargement de page ; ce report garde
l'affichage juste entre-temps.

Les optionnels du snapshot reçoivent `= nil` explicitement : sans valeur par
défaut, l'init memberwise de Swift les aurait exigés à l'appel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 13:51:22 +00:00
Sylvain Bettinelli
b2b3c10807 L'hydratation est un total, pas une jauge
Décision de l'utilisateur : pas d'objectif de boisson, juste la consommation du
jour. Le code qui attendait un objectif pour afficher une barre est retiré,
champ de snapshot compris — un conditionnel qui ne se déclenchera jamais est une
fausse promesse, et le prochain lecteur y verrait une fonctionnalité en attente.

Le widget affiche « 1,2 L bue aujourd'hui ». Seule l'énergie garde sa jauge :
elle est la seule à avoir une cible fixée (1950 kcal).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 13:47:05 +00:00
Sylvain Bettinelli
5eafa14b99 Jauges d'hydratation et de calories dans le widget
Deux barres sous le titre : eau bue sur objectif, énergie consommée sur objectif
calorique. La barre des calories vire à l'orange au-dessus de la cible.

La jauge n'apparaît QUE si l'objectif correspondant existe. Aucun objectif
d'hydratation n'est défini dans les objectifs nutritionnels : tant qu'il vaut
nil, le widget affiche le volume bu sans barre. Une jauge sur un objectif
inventé donnerait un pourcentage qui ne veut rien dire — et il aurait l'air
d'un réglage validé.

Les trois champs du snapshot sont optionnels : un instantané écrit par une
version antérieure reste lisible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 13:44:40 +00:00
Sylvain Bettinelli
9fda989afc Widget natif « Saisie rapide » : eau et café sans ouvrir l'app
Boutons d'App Intent (iOS 17+) pour l'eau et le café : ils s'exécutent dans
l'extension, écrivent dans l'App Group et rafraîchissent la timeline. Aucun
appel réseau — une extension n'a ni session authentifiée ni droit de tenir une
requête.

Photo et code-barres OUVRENT l'application, par `coachapp://meals?photo=1` et
`?scan=1` : la caméra exige le premier plan, aucun widget ne peut y échapper.
Le widget économise les navigations, pas le lancement.

`CoachQuickLog` : file d'attente partagée app ↔ widget ↔ montre. Les saisies
partent au serveur à la prochaine ouverture de l'app, et sont retirées PAR
IDENTIFIANT — vider la file perdrait une saisie faite pendant la
synchronisation, sans laisser de trace.

Le widget affiche son propre total du jour : les boissons déjà connues du
serveur (nouveaux champs `waterMlToday` / `coffeeCountToday` du snapshot, tous
deux optionnels pour rester lisibles depuis une version antérieure) PLUS ce qui
attend dans la file. Sans cela, il afficherait un total périmé juste après une
saisie et l'utilisateur douterait de ce qu'il vient de faire. Ce qui n'est pas
encore parti est signalé par une flèche, pas masqué.

Un échec d'écriture de l'App Group remonte en erreur d'intent : afficher
« ajouté » sur une saisie perdue est pire que ne rien afficher.

Runbook Mac complété — Target Membership, enregistrement dans le WidgetBundle,
schéma d'URL, iOS 17 minimum, et le câblage de synchronisation restant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 13:03:16 +00:00
Sylvain Bettinelli
e1adbbb61f fix(ios): robustesse bridge widget + Apple Sign-In
- CoachWidgetBridge : lit le score en NSNumber.intValue (le cast `as? Int`
  renvoyait nil quand le web envoie un float → score widget perdu par
  intermittence). CoachWidgetStore.save renvoie un Bool ; le plugin fait
  call.reject si la sauvegarde échoue au lieu de résoudre ok:true à tort.
- CoachAppleAuth : sur double-tap, rejette l'ancien pendingCall (Promise JS
  ne pend plus indéfiniment) ; accès pendingCall confiné au main thread ;
  erreur d'annulation testée par domaine ASAuthorizationError (pas rawValue nu).

Swift pur, non compilé ici (pas de Xcode) — à valider au prochain build Mac.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 06:37:42 +00:00
Sylvain Bettinelli
a6bdebcfc8 feat(widgets): widgets natifs iPhone (Séance du jour + Score de forme)
Pont WebView→App Group→WidgetKit :
- CoachWidgetSnapshot.swift : modèle partagé App↔extension (App Group group.ch.hypnotruck.coach)
- CoachWidgetBridge.swift : plugin Capacitor setSnapshot/clear + WidgetCenter.reloadAllTimelines
- CoachWidgets.swift : 2 widgets (TimelineProvider + vues SwiftUI, small/medium)
- bundle + enregistrement plugin + entitlements App Group (App + CoachLiveActivity)

⚠️ Reste câblage Mac (App Group portail + capability + Target Membership) :
docs/widgets-runbook-mac.md. Hook web côté coach_sportif.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 05:18:17 +00:00