Les widgets reçoivent la bande et les préréglages au lieu de les inventer

La couleur du score de forme était décidée ici, avec un barème périmé
(vert >= 75, jaune >= 50). Le serveur avait recalé le sien sur la
distribution réelle (65/55/40) parce que le score est borné à [25, 75] :
une bande haute à 75 est inatteignable. Mesuré sur 115 jours de
production, le vert n'est jamais sorti et le rouge couvrait 68 jours.
La correction n'avait pas atteint le binaire.

`FormeScore` porte désormais la bande servie par le serveur, et
`resolvedBand` ne sert que de repli — aligné sur les seuils serveur, pas
sur les anciens.

Le widget de saisie rapide enregistrait un café à 100 ml, volume qui ne
correspond à aucun préréglage : le serveur avait séparé l'expresso (60)
du mug (250) parce qu'un volume unique faussait le journal, et ces
préréglages sont personnalisables. Les boutons viennent maintenant du
snapshot ; `fallbackQuickDrinks` reprend les préréglages par défaut du
serveur.

15 tests ajoutés au banc d'essai Linux, qui compile réellement ces deux
fichiers : 25 tests verts. Le reste (SwiftUI, WidgetKit) n'est pas
compilable ici et n'a été que relu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Sylvain Bettinelli
2026-08-19 11:42:19 +00:00
parent a965c1ad2b
commit 4cff1d4603
7 changed files with 304 additions and 24 deletions

View File

@@ -63,13 +63,16 @@ struct CoachQuickEntryTimeline: TimelineEntry {
let pending: Int
let kcal: Double?
let kcalGoal: Double?
/// Boutons de saisie rapide, résolus depuis les préréglages de l'utilisateur.
var quickDrinks: [CoachWidgetSnapshot.QuickDrink] = CoachWidgetSnapshot.fallbackQuickDrinks
}
struct CoachQuickProvider: TimelineProvider {
func placeholder(in context: Context) -> CoachQuickEntryTimeline {
CoachQuickEntryTimeline(
date: Date(), waterMl: 1250, waterGoal: 2750, coffeeCount: 2, pending: 0,
kcal: 1420, kcalGoal: 1950
kcal: 1420, kcalGoal: 1950,
quickDrinks: CoachWidgetSnapshot.fallbackQuickDrinks
)
}
@@ -110,7 +113,8 @@ struct CoachQuickProvider: TimelineProvider {
// et le badge doit continuer à le signaler.
pending: CoachQuickLog.pending().count,
kcal: snapshot?.kcalToday,
kcalGoal: snapshot?.kcalGoal
kcalGoal: snapshot?.kcalGoal,
quickDrinks: snapshot?.resolvedQuickDrinks ?? CoachWidgetSnapshot.fallbackQuickDrinks
)
}
}
@@ -121,6 +125,8 @@ struct CoachQuickProvider: TimelineProvider {
struct CoachQuickWidgetView: View {
var entry: CoachQuickEntryTimeline
private var quickDrinks: [CoachWidgetSnapshot.QuickDrink] { entry.quickDrinks }
private var litres: String {
String(format: "%.1f", entry.waterMl / 1000).replacingOccurrences(of: ".", with: ",")
}
@@ -193,15 +199,18 @@ struct CoachQuickWidgetView: View {
}
}
// Volumes pilotés par les préréglages de l'utilisateur
// (`/api/drinks/presets`), plus écrits ici. Le café valait 100 ml en
// dur, un volume qui ne correspond à aucun préréglage : le serveur
// avait justement séparé l'expresso (60) du mug (250) parce qu'un
// volume unique faussait le journal. Repli documenté dans
// `CoachWidgetSnapshot.fallbackQuickDrinks`.
HStack(spacing: 6) {
Button(intent: LogDrinkIntent(ref: "water", amountMl: 250)) {
Label("25 cl", systemImage: "drop.fill")
}
Button(intent: LogDrinkIntent(ref: "water", amountMl: 500)) {
Label("50 cl", systemImage: "drop.fill")
}
Button(intent: LogDrinkIntent(ref: "coffee", amountMl: 100)) {
Label("Café", systemImage: "cup.and.saucer.fill")
ForEach(Array(quickDrinks.enumerated()), id: \.offset) { _, drink in
Button(intent: LogDrinkIntent(ref: drink.ref, amountMl: drink.volumeMl)) {
Label(drink.label,
systemImage: drink.ref == "coffee" ? "cup.and.saucer.fill" : "drop.fill")
}
}
}
.buttonStyle(.bordered)