Les macros arrivent au poignet — affichage, pas saisie

Sylvain, après avoir reçu le widget Hydratation : « il manque juste le suivi
des repas calories et lipides etc ».

⚠️ Sa demande est plus fine que ce que j'avais écarté. J'avais dit non à la
SAISIE des repas sur la montre — pas de caméra, pas de scanner, pas de saisie
de texte : un bouton n'aurait fait que renvoyer vers l'iPhone. Mais VOIR où
l'on en est n'a aucune de ces limites.

Ce qui manquait, et où
-----------------------
Le snapshot portait `kcalToday`/`kcalGoal` et rien d'autre. Les macros
existaient pourtant côté serveur, agrégées et servies :

  /api/meals            -> kcal, protein_g, carbs_g, fat_g, fiber_g
  /api/nutrition/goals  -> cibles {target, min, max} pour chacune

Il manquait uniquement le transport. Trois maillons ajoutés : `widget-bridge.js`
les pousse, `CoachWidgetSnapshot` les porte, `CoachWatchNutritionWidget` les
affiche.

⛔ Rien n'est calculé côté natif. Les totaux viennent du journal, les cibles de
`nutrition_goals.py`. Le seul calcul est un reste (cible − consommé), la même
soustraction que /meals affiche déjà.

Le piège évité, parce qu'il est documenté dans ce fichier
---------------------------------------------------------
`asOf(_:)` remet les totaux du jour à zéro quand le snapshot date d'hier. Les
macros y sont ajoutées : les oublier aurait affiché les grammes de la veille
sous la date du jour — le défaut même que cette fonction existe pour empêcher,
et qui avait survécu un temps sur les totaux de boisson.

⚠️ Les CIBLES ne se périment pas, elles : une cible n'est pas une mesure. Sans
ça, la jauge disparaîtrait chaque nuit jusqu'à la prochaine ouverture de l'app.
Deux tests Linux le verrouillent (`testMacrosResetLikeEnergy`,
`testMacroGoalsSurviveTheNight`) — 93 au vert.

⚠️ Une cible de type « min » (les protéines) se lit comme un plancher atteint,
pas comme un plafond à ne pas dépasser. `widget-bridge.js` ne pousse que
`target` ; le widget n'en sait pas plus et ne prétend pas le contraire.

Le widget compile-t-il ? Inconnu, comme les précédents : WidgetKit n'existe pas
sur Linux. Vérifié en revanche que chaque champ appelé existe, et qu'aucun
symbole n'est déclaré deux fois.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Bettinelli
2026-09-25 07:46:32 +00:00
parent d41e8a457b
commit 6aa7265147
4 changed files with 195 additions and 0 deletions

View File

@@ -41,8 +41,14 @@ final class DayRolloverTests: XCTestCase {
s.waterMlToday = 1660
s.coffeeCountToday = 3
s.kcalToday = 1186
s.proteinToday = 74
s.fatToday = 41
s.carbsToday = 158
s.waterGoalMl = 2750
s.kcalGoal = 1950
s.proteinGoal = 118
s.fatGoal = 59
s.carbsGoal = 225
s.zones = HeartRateZones(z2: [109, 118], fcmaxUsed: 152, clinicalTargetBpm: 112,
zonesSince: "2026-08-12", zonesFrom: "frozen")
return s
@@ -69,6 +75,28 @@ final class DayRolloverTests: XCTestCase {
XCTAssertEqual(vu.kcalToday, 0)
}
func testMacrosResetLikeEnergy() {
// ⚠️ Ajoutées le 25/09/2026 pour le suivi au poignet. Les oublier dans
// `asOf` afficherait les grammes d'hier sous la date du jour — le
// défaut même que cette fonction existe pour empêcher, et qui avait
// survécu un temps sur les totaux de boisson.
let vu = snapshot(at: "2026-08-17 22:00").asOf(date("2026-08-18 00:01"), calendar: zurich)
XCTAssertEqual(vu.proteinToday, 0)
XCTAssertEqual(vu.fatToday, 0)
XCTAssertEqual(vu.carbsToday, 0)
}
func testMacroGoalsSurviveTheNight() {
// Une CIBLE n'est pas une mesure : elle ne se périme pas à minuit.
// Sans elle, la jauge disparaîtrait chaque nuit jusqu'à la prochaine
// ouverture de l'app.
let vu = snapshot(at: "2026-08-17 22:00").asOf(date("2026-08-18 00:01"), calendar: zurich)
XCTAssertEqual(vu.proteinGoal, 118)
XCTAssertEqual(vu.fatGoal, 59)
XCTAssertEqual(vu.carbsGoal, 225)
XCTAssertEqual(vu.kcalGoal, 1950)
}
func testYesterdaysDayScopedFieldsAreDropped() {
let vu = snapshot(at: "2026-08-17 22:00").asOf(date("2026-08-18 00:01"), calendar: zurich)
XCTAssertNil(vu.routine, "la routine se recoche chaque jour")