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:
@@ -91,6 +91,11 @@ public class CoachWidgetBridgePlugin: CAPPlugin, CAPBridgedPlugin {
|
|||||||
snapshot.routine = routine ?? (Calendar.current.isDateInToday(previous?.updatedAt ?? .distantPast)
|
snapshot.routine = routine ?? (Calendar.current.isDateInToday(previous?.updatedAt ?? .distantPast)
|
||||||
? previous?.routine : nil)
|
? previous?.routine : nil)
|
||||||
snapshot.waterMlToday = number("waterMlToday") ?? previous?.waterMlToday
|
snapshot.waterMlToday = number("waterMlToday") ?? previous?.waterMlToday
|
||||||
|
// Même règle de report que les autres champs nutrition : un appel
|
||||||
|
// partiel ne doit pas effacer la cible, sinon la jauge disparaîtrait au
|
||||||
|
// premier rafraîchissement incomplet. La clé est posée par
|
||||||
|
// `widget-bridge.js` — les deux côtés doivent bouger ensemble.
|
||||||
|
snapshot.waterGoalMl = number("waterGoalMl") ?? previous?.waterGoalMl
|
||||||
snapshot.coffeeCountToday =
|
snapshot.coffeeCountToday =
|
||||||
(call.getValue("coffeeCountToday") as? NSNumber)?.intValue ?? previous?.coffeeCountToday
|
(call.getValue("coffeeCountToday") as? NSNumber)?.intValue ?? previous?.coffeeCountToday
|
||||||
snapshot.kcalToday = number("kcalToday") ?? previous?.kcalToday
|
snapshot.kcalToday = number("kcalToday") ?? previous?.kcalToday
|
||||||
|
|||||||
@@ -53,6 +53,16 @@ struct CoachWidgetSnapshot: Codable {
|
|||||||
var waterMlToday: Double? = nil
|
var waterMlToday: Double? = nil
|
||||||
var coffeeCountToday: Int? = nil
|
var coffeeCountToday: Int? = nil
|
||||||
|
|
||||||
|
/// Objectif d'hydratation du jour, servi par `/api/drinks` depuis la refonte
|
||||||
|
/// de la page /hydratation (2026-08-13).
|
||||||
|
///
|
||||||
|
/// ⚠️ **Cette cible n'a aucune source** : c'est un objectif personnel, pas
|
||||||
|
/// une recommandation médicale (voir `nutrition_goals.HYDRATION_DEFAULT_ML`
|
||||||
|
/// côté serveur). Le widget peut donc l'afficher comme une jauge, jamais
|
||||||
|
/// comme un seuil de santé. `nil` ⇒ jauge masquée, total seul — c'était le
|
||||||
|
/// comportement d'origine et il reste le repli.
|
||||||
|
var waterGoalMl: Double? = nil
|
||||||
|
|
||||||
/// Énergie du jour et objectif, poussés par l'app depuis le journal.
|
/// Énergie du jour et objectif, poussés par l'app depuis le journal.
|
||||||
/// `kcalGoal` vient des objectifs nutritionnels réels (1950 au 06/08/2026) :
|
/// `kcalGoal` vient des objectifs nutritionnels réels (1950 au 06/08/2026) :
|
||||||
/// il n'est pas écrit en dur ici, pour qu'un changement d'objectif suive.
|
/// il n'est pas écrit en dur ici, pour qu'un changement d'objectif suive.
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ enum CoachQuickError: Error, CustomLocalizedStringResourceConvertible {
|
|||||||
struct CoachQuickEntryTimeline: TimelineEntry {
|
struct CoachQuickEntryTimeline: TimelineEntry {
|
||||||
let date: Date
|
let date: Date
|
||||||
let waterMl: Double
|
let waterMl: Double
|
||||||
|
let waterGoal: Double?
|
||||||
let coffeeCount: Int
|
let coffeeCount: Int
|
||||||
let pending: Int
|
let pending: Int
|
||||||
let kcal: Double?
|
let kcal: Double?
|
||||||
@@ -67,7 +68,7 @@ struct CoachQuickEntryTimeline: TimelineEntry {
|
|||||||
struct CoachQuickProvider: TimelineProvider {
|
struct CoachQuickProvider: TimelineProvider {
|
||||||
func placeholder(in context: Context) -> CoachQuickEntryTimeline {
|
func placeholder(in context: Context) -> CoachQuickEntryTimeline {
|
||||||
CoachQuickEntryTimeline(
|
CoachQuickEntryTimeline(
|
||||||
date: Date(), waterMl: 1250, coffeeCount: 2, pending: 0,
|
date: Date(), waterMl: 1250, waterGoal: 2750, coffeeCount: 2, pending: 0,
|
||||||
kcal: 1420, kcalGoal: 1950
|
kcal: 1420, kcalGoal: 1950
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -95,6 +96,7 @@ struct CoachQuickProvider: TimelineProvider {
|
|||||||
return CoachQuickEntryTimeline(
|
return CoachQuickEntryTimeline(
|
||||||
date: Date(),
|
date: Date(),
|
||||||
waterMl: CoachQuickLog.todayTotal(kind: "water", syncedMl: syncedWater),
|
waterMl: CoachQuickLog.todayTotal(kind: "water", syncedMl: syncedWater),
|
||||||
|
waterGoal: snapshot?.waterGoalMl,
|
||||||
coffeeCount: CoachQuickLog.todayCount(kind: "coffee", syncedCount: syncedCoffee),
|
coffeeCount: CoachQuickLog.todayCount(kind: "coffee", syncedCount: syncedCoffee),
|
||||||
pending: CoachQuickLog.pending().count,
|
pending: CoachQuickLog.pending().count,
|
||||||
kcal: snapshot?.kcalToday,
|
kcal: snapshot?.kcalToday,
|
||||||
@@ -124,20 +126,29 @@ struct CoachQuickWidgetView: View {
|
|||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.minimumScaleFactor(0.8)
|
.minimumScaleFactor(0.8)
|
||||||
|
|
||||||
// Hydratation en total, énergie en jauge : seule l'énergie a un
|
/* Le total reste le chiffre principal ; la jauge n'apparaît que si
|
||||||
// objectif fixé (1950 kcal). Une barre suppose une cible ; sans
|
le serveur a envoyé une cible.
|
||||||
// cible, un chiffre dit plus qu'un pourcentage inventé.
|
|
||||||
|
⚠️ **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) {
|
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) {
|
HStack(spacing: 6) {
|
||||||
Text("\(litres) L")
|
Text("\(litres) L")
|
||||||
.font(.system(.subheadline, design: .rounded).weight(.semibold))
|
.font(.system(.subheadline, design: .rounded).weight(.semibold))
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
Text("bue aujourd'hui")
|
if let goal = entry.waterGoal, goal > 0 {
|
||||||
.font(.caption2).foregroundStyle(.secondary)
|
ProgressView(value: min(entry.waterMl / goal, 1))
|
||||||
Spacer()
|
.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 {
|
if let kcal = entry.kcal {
|
||||||
@@ -208,6 +219,11 @@ struct CoachQuickWidgetView: View {
|
|||||||
}
|
}
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
.containerBackground(.fill.tertiary, for: .widget)
|
.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"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user