Le widget ne passait pas minuit : personne ne lisait updatedAt
Signale par Sylvain le 2026-08-18 : passe minuit, l'hydratation, les calories et la seance de la veille restaient affichees comme celles du jour. Il fallait ouvrir l'app pour les voir retomber a zero. Le widget n'a AUCUN acces reseau : il rend ce que l'app lui a pousse la derniere fois qu'elle a tourne. Le snapshot portait deja `updatedAt` — rien ne le lisait. CoachQuickWidget programmait meme un reveil a minuit, avec le bon commentaire, mais relisait ensuite les memes chiffres sans regarder leur date : le rechargement avait lieu, la remise a zero non. - `CoachWidgetSnapshot.asOf(_:)` : vide ce qui decrit UNE journee (eau, cafes, kcal, routine, activite faite, score de forme) quand le snapshot date d'un autre jour. Conserve ce qui traverse les jours — objectifs et zones cardiaques. Les compteurs repartent a 0 et non a nil : au matin, « rien bu » est vrai, « je ne sais pas » ne l'est pas. - Les trois providers emettent une 2e entree datee de minuit. WidgetKit bascule seul, sans reveiller l'app. - `tomorrow` : la seance du lendemain, poussee par le pont web. Sans elle, vider la seance ferait annoncer JOUR OFF chaque nuit — l'agacement d'un incident precedent, repete tous les jours. ⚠️ Deuxieme occurrence trouvee en verifiant, et pire : CoachQuickSync.credit() ajoutait au total EXISTANT puis rehorodatait a maintenant. Sur un snapshot de la veille, 200 ml bus le matin donnaient « 2 200 ml » estampilles du jour — un total faux, que la peremption ne pouvait plus rattraper. ⚠️ NON COMPILE : pas de toolchain Swift ici. Build Xcode requis sur le Mac mini pour que le correctif arrive sur l'iPhone et la Watch.
This commit is contained in:
@@ -95,18 +95,25 @@ enum CoachQuickLog {
|
|||||||
/// snapshot) ; on y ajoute ce qui attend encore. Sans cela, le widget
|
/// snapshot) ; on y ajoute ce qui attend encore. Sans cela, le widget
|
||||||
/// afficherait un total périmé juste après une saisie, et l'utilisateur
|
/// afficherait un total périmé juste après une saisie, et l'utilisateur
|
||||||
/// douterait de ce qu'il vient de faire.
|
/// douterait de ce qu'il vient de faire.
|
||||||
static func todayTotal(kind ref: String, syncedMl: Double = 0) -> Double {
|
/// ⚠️ **`day` est explicite, et ce n'est pas de la coquetterie.**
|
||||||
|
///
|
||||||
|
/// Ces deux fonctions filtraient sur `isDateInToday`, c'est-à-dire sur
|
||||||
|
/// l'instant du CALCUL. Or un widget construit sa timeline à l'avance :
|
||||||
|
/// l'entrée datée de minuit est fabriquée la veille au soir, et « today »
|
||||||
|
/// y désignait encore la veille. La file locale se serait donc réinitialisée
|
||||||
|
/// avec un jour de retard, comme les totaux serveur.
|
||||||
|
static func todayTotal(kind ref: String, syncedMl: Double = 0, day: Date = Date()) -> Double {
|
||||||
let calendar = Calendar.current
|
let calendar = Calendar.current
|
||||||
let waiting = pending()
|
let waiting = pending()
|
||||||
.filter { $0.kind == .drink && $0.ref == ref && calendar.isDateInToday($0.at) }
|
.filter { $0.kind == .drink && $0.ref == ref && calendar.isDate($0.at, inSameDayAs: day) }
|
||||||
.reduce(0) { $0 + $1.amount }
|
.reduce(0) { $0 + $1.amount }
|
||||||
return syncedMl + waiting
|
return syncedMl + waiting
|
||||||
}
|
}
|
||||||
|
|
||||||
static func todayCount(kind ref: String, syncedCount: Int = 0) -> Int {
|
static func todayCount(kind ref: String, syncedCount: Int = 0, day: Date = Date()) -> Int {
|
||||||
let calendar = Calendar.current
|
let calendar = Calendar.current
|
||||||
let waiting = pending()
|
let waiting = pending()
|
||||||
.filter { $0.kind == .drink && $0.ref == ref && calendar.isDateInToday($0.at) }
|
.filter { $0.kind == .drink && $0.ref == ref && calendar.isDate($0.at, inSameDayAs: day) }
|
||||||
.count
|
.count
|
||||||
return syncedCount + waiting
|
return syncedCount + waiting
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,14 @@ enum CoachQuickSync {
|
|||||||
let today = entries.filter { calendar.isDateInToday($0.at) }
|
let today = entries.filter { calendar.isDateInToday($0.at) }
|
||||||
guard !today.isEmpty else { return }
|
guard !today.isEmpty else { return }
|
||||||
|
|
||||||
var snapshot = CoachWidgetStore.load()
|
/* ⚠️ **`asOf` AVANT de créditer, sinon on additionne deux journées.**
|
||||||
|
Cette fonction ajoute au total existant puis réhorodate le snapshot à
|
||||||
|
maintenant. Sur un snapshot de la veille, 200 ml bus ce matin
|
||||||
|
donneraient « 2 200 ml » estampillés d'aujourd'hui — et la péremption
|
||||||
|
ne pourrait plus rien y voir, puisque `updatedAt` serait à jour.
|
||||||
|
C'est la même cause que le widget figé au jour précédent, à un endroit
|
||||||
|
où elle produit un total faux plutôt qu'un total périmé. */
|
||||||
|
var snapshot = CoachWidgetStore.load()?.asOf(Date())
|
||||||
?? CoachWidgetSnapshot(updatedAt: Date(), today: nil, forme: nil)
|
?? CoachWidgetSnapshot(updatedAt: Date(), today: nil, forme: nil)
|
||||||
let water = today.filter { $0.ref == "water" }.reduce(0) { $0 + $1.amount }
|
let water = today.filter { $0.ref == "water" }.reduce(0) { $0 + $1.amount }
|
||||||
let coffees = today.filter { $0.ref == "coffee" }.count
|
let coffees = today.filter { $0.ref == "coffee" }.count
|
||||||
|
|||||||
@@ -37,6 +37,18 @@ public class CoachWidgetBridgePlugin: CAPPlugin, CAPBridgedPlugin {
|
|||||||
done: t["done"] as? Bool ?? false
|
done: t["done"] as? Bool ?? false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
/// Séance de DEMAIN — jamais affichée aujourd'hui. Elle permet au
|
||||||
|
/// widget de basculer seul à minuit sans annoncer « JOUR OFF » :
|
||||||
|
/// cf. `CoachWidgetSnapshot.asOf(_:)`.
|
||||||
|
var tomorrow: CoachWidgetSnapshot.TodaySession?
|
||||||
|
if let t = call.getObject("tomorrow") {
|
||||||
|
tomorrow = CoachWidgetSnapshot.TodaySession(
|
||||||
|
sport: t["sport"] as? String ?? "rest",
|
||||||
|
title: t["title"] as? String ?? "",
|
||||||
|
subtitle: t["subtitle"] as? String,
|
||||||
|
done: t["done"] as? Bool ?? false
|
||||||
|
)
|
||||||
|
}
|
||||||
var forme: CoachWidgetSnapshot.FormeScore?
|
var forme: CoachWidgetSnapshot.FormeScore?
|
||||||
if let f = call.getObject("forme") {
|
if let f = call.getObject("forme") {
|
||||||
// `as? Int` échoue si le JS envoie un float (72.0) — le nombre transite
|
// `as? Int` échoue si le JS envoie un float (72.0) — le nombre transite
|
||||||
@@ -90,6 +102,14 @@ public class CoachWidgetBridgePlugin: CAPPlugin, CAPBridgedPlugin {
|
|||||||
// réveil, un report aveugle afficherait 24/24 dès le matin.
|
// réveil, un report aveugle afficherait 24/24 dès le matin.
|
||||||
snapshot.routine = routine ?? (Calendar.current.isDateInToday(previous?.updatedAt ?? .distantPast)
|
snapshot.routine = routine ?? (Calendar.current.isDateInToday(previous?.updatedAt ?? .distantPast)
|
||||||
? previous?.routine : nil)
|
? previous?.routine : nil)
|
||||||
|
/* ⚠️ **Garde du jour OBLIGATOIRE ici**, et pour une raison qui n'est pas
|
||||||
|
celle de `doneToday`. « Demain » est relatif à `updatedAt` : un
|
||||||
|
`tomorrow` poussé hier désigne AUJOURD'HUI. Le reporter tel quel dans
|
||||||
|
un snapshot daté de maintenant le ferait passer pour la séance de
|
||||||
|
demain — le widget afficherait la séance du jour avec 24 h de retard
|
||||||
|
au minuit suivant. On ne reporte donc que dans la même journée. */
|
||||||
|
snapshot.tomorrow = tomorrow ?? (Calendar.current.isDateInToday(previous?.updatedAt ?? .distantPast)
|
||||||
|
? previous?.tomorrow : 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
|
// 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
|
// partiel ne doit pas effacer la cible, sinon la jauge disparaîtrait au
|
||||||
|
|||||||
@@ -91,6 +91,15 @@ struct CoachWidgetSnapshot: Codable {
|
|||||||
/// côté natif (cf. l'en-tête de ce fichier).
|
/// côté natif (cf. l'en-tête de ce fichier).
|
||||||
var zones: HeartRateZones? = nil
|
var zones: HeartRateZones? = nil
|
||||||
|
|
||||||
|
/// Séance planifiée de DEMAIN. N'est jamais affichée le jour même : elle
|
||||||
|
/// sert au widget à passer minuit sans accès réseau.
|
||||||
|
///
|
||||||
|
/// ⚠️ Sans elle, `asOf(_:)` n'aurait d'autre choix que de vider la séance
|
||||||
|
/// au changement de jour — et le widget annoncerait « JOUR OFF » chaque
|
||||||
|
/// nuit jusqu'à ce que l'app soit rouverte, y compris la veille d'une
|
||||||
|
/// sortie longue. Poussée par `widget-bridge.js` depuis `tomorrow_session`.
|
||||||
|
var tomorrow: TodaySession? = nil
|
||||||
|
|
||||||
/// Séance planifiée du jour (nil = jour OFF / pas de séance).
|
/// Séance planifiée du jour (nil = jour OFF / pas de séance).
|
||||||
struct TodaySession: Codable {
|
struct TodaySession: Codable {
|
||||||
var sport: String // running, cycling, strength, mobility, hiking, rest…
|
var sport: String // running, cycling, strength, mobility, hiking, rest…
|
||||||
@@ -128,3 +137,64 @@ struct CoachWidgetSnapshot: Codable {
|
|||||||
var label: String? // "Prêt", "Récup", "Fatigue"…
|
var label: String? // "Prêt", "Récup", "Fatigue"…
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Péremption au changement de jour
|
||||||
|
|
||||||
|
extension CoachWidgetSnapshot {
|
||||||
|
/// 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
|
||||||
|
/// poussé la dernière fois qu'elle a tourné — et rien ne l'obligeait à
|
||||||
|
/// vérifier de QUAND datait ce qu'il affichait. Passé minuit, l'hydratation,
|
||||||
|
/// les calories, la routine et la séance de la veille restaient à l'écran,
|
||||||
|
/// présentées comme celles du jour. Il fallait ouvrir l'app pour les voir
|
||||||
|
/// retomber à zéro. Signalé le 2026-08-18.
|
||||||
|
///
|
||||||
|
/// `updatedAt` existait déjà : **personne ne le lisait**. C'est tout le
|
||||||
|
/// correctif.
|
||||||
|
///
|
||||||
|
/// Ce qui est vidé est ce qui décrit UNE journée. Ce qui traverse les jours
|
||||||
|
/// est conservé, et la distinction n'est pas cosmétique :
|
||||||
|
///
|
||||||
|
/// | conservé | pourquoi |
|
||||||
|
/// |---|---|
|
||||||
|
/// | `waterGoalMl`, `kcalGoal` | ce sont des objectifs, pas des mesures |
|
||||||
|
/// | `zones` | version figée, valable des semaines |
|
||||||
|
///
|
||||||
|
/// Les compteurs repartent à **0 et non à `nil`** : au matin d'un jour
|
||||||
|
/// neuf, « rien bu » est une information vraie, alors que `nil` signifierait
|
||||||
|
/// « je ne sais pas ».
|
||||||
|
func asOf(_ day: Date, calendar: Calendar = .current) -> CoachWidgetSnapshot {
|
||||||
|
if calendar.isDate(updatedAt, inSameDayAs: day) { return self }
|
||||||
|
|
||||||
|
var s = self
|
||||||
|
s.waterMlToday = 0
|
||||||
|
s.coffeeCountToday = 0
|
||||||
|
s.kcalToday = 0
|
||||||
|
s.routine = nil
|
||||||
|
// L'activité de la veille n'est pas celle du jour, et le score de forme
|
||||||
|
// du jour n'est calculé qu'après la nuit : afficher celui d'hier comme
|
||||||
|
// étant celui d'aujourd'hui serait faux, pas seulement périmé.
|
||||||
|
s.doneToday = nil
|
||||||
|
s.forme = nil
|
||||||
|
|
||||||
|
// La séance : c'est ici que `tomorrow` évite le « JOUR OFF » nocturne.
|
||||||
|
// Elle ne vaut que pour le LENDEMAIN du snapshot — au-delà, on ne sait
|
||||||
|
// plus, et un plan inventé serait pire qu'une case vide.
|
||||||
|
let lendemain = calendar.date(byAdding: .day, value: 1, to: calendar.startOfDay(for: updatedAt))
|
||||||
|
if let lendemain, calendar.isDate(lendemain, inSameDayAs: day) {
|
||||||
|
s.today = tomorrow
|
||||||
|
} else {
|
||||||
|
s.today = nil
|
||||||
|
}
|
||||||
|
s.tomorrow = nil
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prochain minuit strictement après `date`. Sert aux trois widgets à
|
||||||
|
/// programmer l'entrée qui fait basculer l'affichage sans réveiller l'app.
|
||||||
|
static func nextMidnight(after date: Date, calendar: Calendar = .current) -> Date {
|
||||||
|
calendar.date(byAdding: .day, value: 1, to: calendar.startOfDay(for: date))
|
||||||
|
?? date.addingTimeInterval(86_400)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -77,27 +77,37 @@ struct CoachQuickProvider: TimelineProvider {
|
|||||||
completion(current())
|
completion(current())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ⚠️ **Le rendez-vous de minuit existait déjà — et ne servait à rien.**
|
||||||
|
///
|
||||||
|
/// La version précédente programmait bien `.after(midnight)`, avec le bon
|
||||||
|
/// commentaire (« pour que les totaux du jour repartent de zéro »). Mais au
|
||||||
|
/// réveil, `current()` relisait `snapshot.waterMlToday` **sans regarder de
|
||||||
|
/// quand datait ce snapshot** : elle réaffichait donc exactement les mêmes
|
||||||
|
/// chiffres. Le rechargement avait lieu, la remise à zéro non.
|
||||||
|
///
|
||||||
|
/// C'est `asOf(_:)` qui manquait, pas le réveil.
|
||||||
func getTimeline(in context: Context, completion: @escaping (Timeline<CoachQuickEntryTimeline>) -> Void) {
|
func getTimeline(in context: Context, completion: @escaping (Timeline<CoachQuickEntryTimeline>) -> Void) {
|
||||||
// Une seule entrée, rechargée à chaque saisie par `reloadAllTimelines`.
|
let now = Date()
|
||||||
// Rafraîchissement de sécurité au prochain minuit, pour que les totaux
|
let midnight = CoachWidgetSnapshot.nextMidnight(after: now)
|
||||||
// du jour repartent de zéro même sans interaction.
|
completion(Timeline(entries: [current(at: now), current(at: midnight)],
|
||||||
let midnight = Calendar.current.nextDate(
|
policy: .after(midnight.addingTimeInterval(60))))
|
||||||
after: Date(), matching: DateComponents(hour: 0, minute: 0), matchingPolicy: .nextTime
|
|
||||||
) ?? Date().addingTimeInterval(3600)
|
|
||||||
completion(Timeline(entries: [current()], policy: .after(midnight)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func current() -> CoachQuickEntryTimeline {
|
private func current(at day: Date = Date()) -> CoachQuickEntryTimeline {
|
||||||
let snapshot = CoachWidgetStore.load()
|
// `asOf` vide les totaux du jour quand le snapshot date d'un autre jour.
|
||||||
|
let snapshot = CoachWidgetStore.load()?.asOf(day)
|
||||||
// Le serveur connaît peut-être déjà des boissons du jour : le snapshot
|
// Le serveur connaît peut-être déjà des boissons du jour : le snapshot
|
||||||
// les porte. On y ajoute ce qui attend dans la file.
|
// les porte. On y ajoute ce qui attend dans la file.
|
||||||
let syncedWater = snapshot?.waterMlToday ?? 0
|
let syncedWater = snapshot?.waterMlToday ?? 0
|
||||||
let syncedCoffee = snapshot?.coffeeCountToday ?? 0
|
let syncedCoffee = snapshot?.coffeeCountToday ?? 0
|
||||||
return CoachQuickEntryTimeline(
|
return CoachQuickEntryTimeline(
|
||||||
date: Date(),
|
date: day,
|
||||||
waterMl: CoachQuickLog.todayTotal(kind: "water", syncedMl: syncedWater),
|
waterMl: CoachQuickLog.todayTotal(kind: "water", syncedMl: syncedWater, day: day),
|
||||||
waterGoal: snapshot?.waterGoalMl,
|
waterGoal: snapshot?.waterGoalMl,
|
||||||
coffeeCount: CoachQuickLog.todayCount(kind: "coffee", syncedCount: syncedCoffee),
|
coffeeCount: CoachQuickLog.todayCount(kind: "coffee", syncedCount: syncedCoffee, day: day),
|
||||||
|
// ⚠️ `pending` compte ce qui attend d'être envoyé, TOUS jours
|
||||||
|
// confondus : une saisie d'hier non synchronisée doit encore partir,
|
||||||
|
// et le badge doit continuer à le signaler.
|
||||||
pending: CoachQuickLog.pending().count,
|
pending: CoachQuickLog.pending().count,
|
||||||
kcal: snapshot?.kcalToday,
|
kcal: snapshot?.kcalToday,
|
||||||
kcalGoal: snapshot?.kcalGoal
|
kcalGoal: snapshot?.kcalGoal
|
||||||
|
|||||||
@@ -23,16 +23,35 @@ struct CoachWidgetProvider: TimelineProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getSnapshot(in context: Context, completion: @escaping (CoachSnapshotEntry) -> Void) {
|
func getSnapshot(in context: Context, completion: @escaping (CoachSnapshotEntry) -> Void) {
|
||||||
completion(CoachSnapshotEntry(date: Date(), snapshot: CoachWidgetStore.load()))
|
let now = Date()
|
||||||
|
completion(CoachSnapshotEntry(date: now, snapshot: CoachWidgetStore.load()?.asOf(now)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ⚠️ **DEUX entrées, et c'est le correctif.**
|
||||||
|
///
|
||||||
|
/// L'ancienne version n'en produisait qu'une, avec le snapshot brut, et
|
||||||
|
/// repassait une heure plus tard. Or repasser ne sert à rien : elle relisait
|
||||||
|
/// le MÊME snapshot périmé dans l'App Group et le réaffichait à l'identique.
|
||||||
|
/// Les chiffres de la veille restaient donc à l'écran jusqu'à ce que l'app
|
||||||
|
/// soit rouverte à la main.
|
||||||
|
///
|
||||||
|
/// La seconde entrée est datée de minuit et porte le snapshot passé par
|
||||||
|
/// `asOf(_:)`. WidgetKit bascule dessus tout seul à l'heure dite, **sans
|
||||||
|
/// réveiller l'app ni accéder au réseau** — ce qu'un widget ne peut de
|
||||||
|
/// toute façon pas faire.
|
||||||
func getTimeline(in context: Context, completion: @escaping (Timeline<CoachSnapshotEntry>) -> Void) {
|
func getTimeline(in context: Context, completion: @escaping (Timeline<CoachSnapshotEntry>) -> Void) {
|
||||||
let entry = CoachSnapshotEntry(date: Date(), snapshot: CoachWidgetStore.load())
|
let now = Date()
|
||||||
|
let stored = CoachWidgetStore.load()
|
||||||
|
let midnight = CoachWidgetSnapshot.nextMidnight(after: now)
|
||||||
|
let entries = [
|
||||||
|
CoachSnapshotEntry(date: now, snapshot: stored?.asOf(now)),
|
||||||
|
CoachSnapshotEntry(date: midnight, snapshot: stored?.asOf(midnight)),
|
||||||
|
]
|
||||||
// L'app rafraîchit explicitement (WidgetCenter.reloadAllTimelines) à
|
// L'app rafraîchit explicitement (WidgetCenter.reloadAllTimelines) à
|
||||||
// chaque setSnapshot. On planifie en plus un refresh horaire de courtoisie.
|
// chaque setSnapshot ; on redemande la main peu après minuit pour que
|
||||||
let next = Calendar.current.date(byAdding: .hour, value: 1, to: Date())
|
// le jour d'APRÈS soit à son tour préparé.
|
||||||
?? Date().addingTimeInterval(3600)
|
completion(Timeline(entries: entries,
|
||||||
completion(Timeline(entries: [entry], policy: .after(next)))
|
policy: .after(midnight.addingTimeInterval(60))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,13 +51,24 @@ struct WatchProvider: TimelineProvider {
|
|||||||
WatchEntry(date: Date(), snapshot: nil)
|
WatchEntry(date: Date(), snapshot: nil)
|
||||||
}
|
}
|
||||||
func getSnapshot(in context: Context, completion: @escaping (WatchEntry) -> Void) {
|
func getSnapshot(in context: Context, completion: @escaping (WatchEntry) -> Void) {
|
||||||
completion(WatchEntry(date: Date(), snapshot: CoachWidgetStore.load()))
|
let now = Date()
|
||||||
|
completion(WatchEntry(date: now, snapshot: CoachWidgetStore.load()?.asOf(now)))
|
||||||
}
|
}
|
||||||
|
/// Même correctif que les widgets iPhone : une entrée maintenant, une à
|
||||||
|
/// minuit avec le snapshot périmé remis à zéro. Sans elle, la complication
|
||||||
|
/// affichait les chiffres de la veille jusqu'à réouverture de l'app —
|
||||||
|
/// et sur une montre, c'est encore plus visible : elle est au poignet
|
||||||
|
/// avant que l'iPhone ne soit déverrouillé.
|
||||||
func getTimeline(in context: Context, completion: @escaping (Timeline<WatchEntry>) -> Void) {
|
func getTimeline(in context: Context, completion: @escaping (Timeline<WatchEntry>) -> Void) {
|
||||||
let entry = WatchEntry(date: Date(), snapshot: CoachWidgetStore.load())
|
let now = Date()
|
||||||
let next = Calendar.current.date(byAdding: .hour, value: 2, to: Date())
|
let stored = CoachWidgetStore.load()
|
||||||
?? Date().addingTimeInterval(7200)
|
let midnight = CoachWidgetSnapshot.nextMidnight(after: now)
|
||||||
completion(Timeline(entries: [entry], policy: .after(next)))
|
let entries = [
|
||||||
|
WatchEntry(date: now, snapshot: stored?.asOf(now)),
|
||||||
|
WatchEntry(date: midnight, snapshot: stored?.asOf(midnight)),
|
||||||
|
]
|
||||||
|
completion(Timeline(entries: entries,
|
||||||
|
policy: .after(midnight.addingTimeInterval(60))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user