From 28fef67a453fc50cbbca03d2e8b51ce63e2a983f Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 9 Jun 2026 18:32:26 -0400 Subject: [PATCH] fix(bar-app): periodic background refresh so the glance self-heals The menu only re-polled on open or after a mutation, so if the server briefly dropped rows (e.g. a backend restart mid-session) the dropdown could stay stuck showing stale/missing rows until reopened. Add a 60s background poll that reconnects if the client was lost and reloads non-force (respecting server-side caches, so it never hammers providers). As a bonus, alerts now evaluate every interval instead of only on menu-open, making them genuinely proactive. --- .../Sources/CCSBarApp/BarViewModel.swift | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/macos-bar/Sources/CCSBarApp/BarViewModel.swift b/macos-bar/Sources/CCSBarApp/BarViewModel.swift index a1843f50..b8106ef4 100644 --- a/macos-bar/Sources/CCSBarApp/BarViewModel.swift +++ b/macos-bar/Sources/CCSBarApp/BarViewModel.swift @@ -37,6 +37,12 @@ final class BarViewModel: ObservableObject { private let home: String private var client: CCSBarClient? private var debouncer = RefreshDebouncer(interval: 15) + /// Periodic background refresh so the glance self-heals from a transient + /// server gap (e.g. a momentary backend restart that dropped the native rows) + /// without the user having to reopen the menu. Cheap + safe: it reads the + /// local server's caches and never hammers providers (native quota is + /// TTL-gated server-side). + private var pollTask: Task? private let prefs: BarPreferences private let notifier: NotificationDelivering @@ -58,6 +64,23 @@ final class BarViewModel: ObservableObject { self.glanceMode = prefs.load().glanceMode self.spendChartStyle = SpendChartStyleStore.load() reconnect() + startBackgroundPolling() + } + + /// Periodically re-poll for the app's lifetime so a transient empty/missing-row + /// state recovers on its own within one interval — each tick reconnects if the + /// client/discovery was lost, then loads (non-force, so it respects the + /// server-side caches). This is what prevents the menu from getting stuck after + /// the server momentarily restarts. + private func startBackgroundPolling() { + pollTask?.cancel() + pollTask = Task { [weak self] in + while !Task.isCancelled { + try? await Task.sleep(for: .seconds(60)) + guard let self else { return } + await self.load(force: false) + } + } } /// Re-read prefs after the preferences sheet writes through, so the next poll