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:
@@ -69,6 +69,10 @@ struct CoachWidgetSnapshot: Codable {
|
||||
var kcalToday: Double? = nil
|
||||
var kcalGoal: Double? = nil
|
||||
|
||||
/// Boutons de saisie rapide, poussés depuis les préréglages de boisson.
|
||||
/// `nil` ⇒ repli aligné sur les préréglages par défaut du serveur.
|
||||
var quickDrinks: [QuickDrink]? = nil
|
||||
|
||||
|
||||
/// Activité RÉELLEMENT réalisée aujourd'hui, indépendante du plan.
|
||||
///
|
||||
@@ -132,15 +136,77 @@ struct CoachWidgetSnapshot: Codable {
|
||||
}
|
||||
|
||||
/// Score de forme / récupération (nil = pas encore de score).
|
||||
/// Un bouton de saisie rapide du widget, tel que l'utilisateur l'a réglé.
|
||||
///
|
||||
/// ⚠️ Les volumes étaient en dur dans le widget, dont un café à 100 ml qui
|
||||
/// ne correspondait à aucun préréglage : le serveur avait tranché que 60 ml
|
||||
/// est un expresso et 250 ml un mug, précisément parce qu'un volume unique
|
||||
/// pour tous les cafés faussait le journal. Ces préréglages sont en outre
|
||||
/// personnalisables (`/api/drinks/presets`) — les figer ici les ignorait.
|
||||
struct QuickDrink: Codable, Equatable, Sendable {
|
||||
var ref: String // "water" | "coffee" | … (drink_type de l'API)
|
||||
var label: String
|
||||
var volumeMl: Double
|
||||
}
|
||||
|
||||
/// Bande de lecture du score de forme, telle que le serveur la nomme
|
||||
/// (`app.RECOVERY_BANDS`). Le natif la traduit en couleur ; il ne la décide
|
||||
/// plus.
|
||||
enum FormeBand: String, Codable, Sendable {
|
||||
case great, good, medium, low
|
||||
}
|
||||
|
||||
struct FormeScore: Codable {
|
||||
var score: Int? // 0–100
|
||||
var label: String? // "Prêt", "Récup", "Fatigue"…
|
||||
var label: String? // "Excellente", "Bonne", "Correcte", "Basse"
|
||||
/// Bande servie par le serveur. `nil` pour un snapshot écrit par une
|
||||
/// version antérieure : on retombe alors sur `resolvedBand`.
|
||||
var band: String? = nil
|
||||
|
||||
/// Seuils de repli — **ceux du serveur**, pas les anciens.
|
||||
///
|
||||
/// ⚠️ La complication et les widgets coloraient le score avec un barème
|
||||
/// à eux (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] par construction : 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 suivi jusqu'ici,
|
||||
/// le binaire ne se déployant qu'au build suivant.
|
||||
static let fallbackThresholds: [(min: Int, band: FormeBand)] = [
|
||||
(65, .great), (55, .good), (40, .medium),
|
||||
]
|
||||
|
||||
/// Bande à afficher : celle du serveur, sinon déduite du score.
|
||||
var resolvedBand: FormeBand? {
|
||||
if let raw = band, let known = FormeBand(rawValue: raw) { return known }
|
||||
guard let score = score else { return nil }
|
||||
for step in Self.fallbackThresholds where score >= step.min { return step.band }
|
||||
return .low
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Péremption au changement de jour
|
||||
|
||||
extension CoachWidgetSnapshot {
|
||||
/// Repli des boutons de saisie rapide — **les préréglages par défaut du
|
||||
/// serveur** (`drink_presets.DEFAULT_PRESETS`), jamais des valeurs inventées.
|
||||
/// Trois boutons : c'est ce que la largeur du widget permet.
|
||||
static let fallbackQuickDrinks: [QuickDrink] = [
|
||||
QuickDrink(ref: "water", label: "Verre", volumeMl: 200),
|
||||
QuickDrink(ref: "water", label: "Bouteille", volumeMl: 500),
|
||||
QuickDrink(ref: "coffee", label: "Expresso", volumeMl: 60),
|
||||
]
|
||||
|
||||
/// Boutons à afficher : ceux de l'utilisateur, sinon le repli.
|
||||
///
|
||||
/// Un préréglage sans volume plausible est écarté : un bouton qui
|
||||
/// enregistrerait 0 ml serait pire que pas de bouton.
|
||||
var resolvedQuickDrinks: [QuickDrink] {
|
||||
let usable = (quickDrinks ?? []).filter { $0.volumeMl > 0 && $0.volumeMl <= 2000 }
|
||||
return usable.isEmpty ? Self.fallbackQuickDrinks : Array(usable.prefix(3))
|
||||
}
|
||||
|
||||
/// Le snapshot tel qu'il doit s'afficher le jour `day`.
|
||||
///
|
||||
/// ⚠️ **Le widget n'a aucun accès réseau.** Il rend ce que l'app lui a
|
||||
|
||||
Reference in New Issue
Block a user