fix(widget): le total retombait à zéro après synchronisation

Le widget affiche « snapshot + file d'attente ». La synchronisation vidait la
file sans créditer le snapshot : le total repassait donc à zéro juste après que
la saisie ait été enregistrée pour de bon. Le pire moment pour douter de son
journal.

`flush()` reporte désormais dans le snapshot ce qu'il vient d'envoyer, AVANT de
vider la file. Seules les entrées du jour sont créditées — le snapshot porte des
totaux quotidiens, y verser une saisie d'hier les fausserait. Le pont web
repoussera un snapshot exact au prochain chargement de page ; ce report garde
l'affichage juste entre-temps.

Les optionnels du snapshot reçoivent `= nil` explicitement : sans valeur par
défaut, l'init memberwise de Swift les aurait exigés à l'appel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Sylvain Bettinelli
2026-08-06 13:51:22 +00:00
parent b2b3c10807
commit d8adad466b
2 changed files with 33 additions and 8 deletions

View File

@@ -54,14 +54,39 @@ enum CoachQuickSync {
}
if !sent.isEmpty {
/* Le snapshot AVANT de vider la file. Le widget affiche
« snapshot + file » : vider la file sans créditer le snapshot
ferait retomber son total à zéro alors que la saisie vient
justement d'être enregistrée. C'est exactement ce qui se passait.
Le pont web repoussera un snapshot exact au prochain chargement
de page ; en attendant, ce report garde l'affichage juste. */
credit(queue.filter { sent.contains($0.id) })
CoachQuickLog.remove(ids: sent)
// Le widget doit cesser d'afficher la flèche d'attente, et son
// total doit repasser sur la source serveur.
WidgetCenter.shared.reloadAllTimelines()
}
return sent.count
}
/// Reporte dans le snapshot ce qui vient de partir au serveur.
///
/// Seules les entrées du JOUR comptent : le snapshot porte des totaux
/// quotidiens, et y verser une saisie d'hier les fausserait.
private static func credit(_ entries: [CoachQuickEntry]) {
let calendar = Calendar.current
let today = entries.filter { calendar.isDateInToday($0.at) }
guard !today.isEmpty else { return }
var snapshot = CoachWidgetStore.load()
?? CoachWidgetSnapshot(updatedAt: Date(), today: nil, forme: nil)
let water = today.filter { $0.ref == "water" }.reduce(0) { $0 + $1.amount }
let coffees = today.filter { $0.ref == "coffee" }.count
if water > 0 { snapshot.waterMlToday = (snapshot.waterMlToday ?? 0) + water }
if coffees > 0 { snapshot.coffeeCountToday = (snapshot.coffeeCountToday ?? 0) + coffees }
snapshot.updatedAt = Date()
CoachWidgetStore.save(snapshot)
}
private static func post(_ entry: CoachQuickEntry) async -> Bool {
var request = URLRequest(url: base.appendingPathComponent("api/drinks"))
request.httpMethod = "POST"

View File

@@ -41,8 +41,8 @@ enum CoachWidgetStore {
/// le widget n'a pas d'accès réseau, tout vient de ce snapshot poussé par l'app.
struct CoachWidgetSnapshot: Codable {
var updatedAt: Date
var today: TodaySession?
var forme: FormeScore?
var today: TodaySession? = nil
var forme: FormeScore? = nil
/// Boissons du jour DÉJÀ connues du serveur, en millilitres et en nombre de
/// cafés. Le widget de saisie rapide y ajoute ce qui attend encore dans la
@@ -50,14 +50,14 @@ struct CoachWidgetSnapshot: Codable {
/// « 1,5 L » tant que l'app n'a pas resynchronisé, et l'utilisateur
/// douterait de saisies pourtant enregistrées.
/// Optionnels : un snapshot écrit par une version antérieure reste lisible.
var waterMlToday: Double?
var coffeeCountToday: Int?
var waterMlToday: Double? = nil
var coffeeCountToday: Int? = nil
/// É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) :
/// il n'est pas écrit en dur ici, pour qu'un changement d'objectif suive.
var kcalToday: Double?
var kcalGoal: Double?
var kcalToday: Double? = nil
var kcalGoal: Double? = nil
/// Séance planifiée du jour (nil = jour OFF / pas de séance).