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>
This commit is contained in:
Sylvain Bettinelli
2026-08-13 08:33:40 +00:00
parent fb4033130f
commit 5bf3e91ad2
3 changed files with 41 additions and 10 deletions

View File

@@ -58,6 +58,7 @@ enum CoachQuickError: Error, CustomLocalizedStringResourceConvertible {
struct CoachQuickEntryTimeline: TimelineEntry {
let date: Date
let waterMl: Double
let waterGoal: Double?
let coffeeCount: Int
let pending: Int
let kcal: Double?
@@ -67,7 +68,7 @@ struct CoachQuickEntryTimeline: TimelineEntry {
struct CoachQuickProvider: TimelineProvider {
func placeholder(in context: Context) -> CoachQuickEntryTimeline {
CoachQuickEntryTimeline(
date: Date(), waterMl: 1250, coffeeCount: 2, pending: 0,
date: Date(), waterMl: 1250, waterGoal: 2750, coffeeCount: 2, pending: 0,
kcal: 1420, kcalGoal: 1950
)
}
@@ -95,6 +96,7 @@ struct CoachQuickProvider: TimelineProvider {
return CoachQuickEntryTimeline(
date: Date(),
waterMl: CoachQuickLog.todayTotal(kind: "water", syncedMl: syncedWater),
waterGoal: snapshot?.waterGoalMl,
coffeeCount: CoachQuickLog.todayCount(kind: "coffee", syncedCount: syncedCoffee),
pending: CoachQuickLog.pending().count,
kcal: snapshot?.kcalToday,
@@ -124,20 +126,29 @@ struct CoachQuickWidgetView: View {
.lineLimit(1)
.minimumScaleFactor(0.8)
// Hydratation en total, énergie en jauge : seule l'énergie a un
// objectif fixé (1950 kcal). Une barre suppose une cible ; sans
// cible, un chiffre dit plus qu'un pourcentage inventé.
/* Le total reste le chiffre principal ; la jauge n'apparaît que si
le serveur a envoyé une cible.
**La cible n'a aucune source** : c'est l'objectif personnel de
`nutrition_goals.HYDRATION_DEFAULT_ML`, pas une recommandation.
D'où le libellé « objectif » et non « besoin » ou « recommandé ».
Sans cible (`nil`), on retombe sur le comportement d'origine un
total seul, jamais un pourcentage inventé. */
VStack(alignment: .leading, spacing: 3) {
// Hydratation : un TOTAL, pas une jauge. Décision assumée
// aucun objectif de boisson n'est fixé, et en inventer un
// donnerait un pourcentage qui n'a pas de sens.
HStack(spacing: 6) {
Text("\(litres) L")
.font(.system(.subheadline, design: .rounded).weight(.semibold))
.monospacedDigit()
Text("bue aujourd'hui")
.font(.caption2).foregroundStyle(.secondary)
Spacer()
if let goal = entry.waterGoal, goal > 0 {
ProgressView(value: min(entry.waterMl / goal, 1))
.tint(.cyan)
Text("objectif \(String(format: "%.1f", goal / 1000).replacingOccurrences(of: ".", with: ",")) L")
.font(.caption2).foregroundStyle(.tertiary).monospacedDigit()
} else {
Text("bue aujourd'hui")
.font(.caption2).foregroundStyle(.secondary)
Spacer()
}
}
if let kcal = entry.kcal {
@@ -208,6 +219,11 @@ struct CoachQuickWidgetView: View {
}
.padding(.vertical, 2)
.containerBackground(.fill.tertiary, for: .widget)
/* Taper le fond du widget ouvre la page où l'on corrige : /hydratation
depuis la scission de /meals. Sans `widgetURL`, un tap hors bouton
ouvrait l'app sur sa dernière page consultée, au hasard.
https, pas de schéma custom le projet route par Universal Links. */
.widgetURL(URL(string: "https://coach.hypnotruck.ch/hydratation"))
}
}