From 18c3c8f9086b68bac05ece8bc476515ec682ede8 Mon Sep 17 00:00:00 2001 From: iamtoruk Date: Tue, 5 May 2026 11:35:38 -0700 Subject: [PATCH] Fix stale menubar data after sleep and silent refresh button Cache now tracks the calendar date and clears on day rollover so overnight sleep no longer shows yesterday's numbers. Wake-from-sleep invalidates the entire cache before fetching. Manual refresh and wake explicitly request loading feedback so the spinner is visible even when stale data exists. --- CHANGELOG.md | 2 ++ mac/Sources/CodeBurnMenubar/AppStore.swift | 25 ++++++++++++++++--- mac/Sources/CodeBurnMenubar/CodeBurnApp.swift | 3 ++- .../Views/MenuBarContent.swift | 2 +- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eec81a1..47a2ed6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ ### Fixed (macOS menubar) - **Stuck loading spinner.** The menubar ran `--optimize` on every 30-second background refresh. As sessions accumulated, optimize exceeded the 45-second timeout, and the loading overlay stayed forever with no fallback. Optimize is now stripped from all menubar fetches (use `codeburn optimize` in the CLI instead). On fetch failure with empty cache, the app retries without optimize so the spinner always clears. +- **Stale data after overnight sleep.** Cache keys used the period enum (`.today`) not a calendar date, so data from yesterday persisted after midnight. Cache now tracks the current date and clears itself on day rollover. Wake-from-sleep additionally clears all cached entries before fetching fresh data. +- **Refresh button appeared to do nothing.** Clicking refresh with stale cached data never showed the loading overlay because loading state only triggered on empty cache. Manual refresh and wake-from-sleep now explicitly request loading feedback. ## 0.9.6 - 2026-05-03 diff --git a/mac/Sources/CodeBurnMenubar/AppStore.swift b/mac/Sources/CodeBurnMenubar/AppStore.swift index 27ef0a4..af1947a 100644 --- a/mac/Sources/CodeBurnMenubar/AppStore.swift +++ b/mac/Sources/CodeBurnMenubar/AppStore.swift @@ -34,6 +34,7 @@ final class AppStore { var capacityEstimates: [String: CapacityEstimate] = [:] private var cache: [PayloadCacheKey: CachedPayload] = [:] + private var cacheDate: String = "" private var switchTask: Task? private var currentKey: PayloadCacheKey { @@ -99,18 +100,33 @@ final class AppStore { private var inFlightKeys: Set = [] - func refresh(includeOptimize: Bool, force: Bool = false) async { + private func invalidateStaleDayCache() { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + let today = formatter.string(from: Date()) + if cacheDate != today { + cache.removeAll() + cacheDate = today + } + } + + func invalidateCache() { + cache.removeAll() + } + + func refresh(includeOptimize: Bool, force: Bool = false, showLoading: Bool = false) async { + invalidateStaleDayCache() let key = currentKey if !force, cache[key]?.isFresh == true { return } if !force, inFlightKeys.contains(key) { return } inFlightKeys.insert(key) - let showedLoading = cache[key] == nil - if showedLoading { + let didShowLoading = showLoading || cache[key] == nil + if didShowLoading { loadingCount += 1 } defer { inFlightKeys.remove(key) - if showedLoading { loadingCount = max(loadingCount - 1, 0) } + if didShowLoading { loadingCount = max(loadingCount - 1, 0) } } do { let fresh = try await DataClient.fetch(period: key.period, provider: key.provider, includeOptimize: includeOptimize) @@ -145,6 +161,7 @@ final class AppStore { /// Does not toggle isLoading, so the popover's loading overlay is unaffected. /// Always uses the .all provider since the menubar badge shows total spend. func refreshQuietly(period: Period) async { + invalidateStaleDayCache() do { let fresh = try await DataClient.fetch(period: period, provider: .all, includeOptimize: false) cache[PayloadCacheKey(period: period, provider: .all)] = CachedPayload(payload: fresh, fetchedAt: Date()) diff --git a/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift b/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift index dbc2e66..bf5bacf 100644 --- a/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift +++ b/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift @@ -170,8 +170,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { guard now.timeIntervalSince(lastRefreshTime) > 5 else { return } lastRefreshTime = now + store.invalidateCache() Task { - async let main: Void = store.refresh(includeOptimize: false, force: true) + async let main: Void = store.refresh(includeOptimize: false, force: true, showLoading: true) async let today: Void = store.refreshQuietly(period: .today) _ = await (main, today) refreshStatusButton() diff --git a/mac/Sources/CodeBurnMenubar/Views/MenuBarContent.swift b/mac/Sources/CodeBurnMenubar/Views/MenuBarContent.swift index 59ffdbb..2315353 100644 --- a/mac/Sources/CodeBurnMenubar/Views/MenuBarContent.swift +++ b/mac/Sources/CodeBurnMenubar/Views/MenuBarContent.swift @@ -407,7 +407,7 @@ struct FooterBar: View { .fixedSize() Button { - Task { await store.refresh(includeOptimize: false, force: true) } + Task { await store.refresh(includeOptimize: false, force: true, showLoading: true) } } label: { Image(systemName: store.isLoading ? "arrow.triangle.2.circlepath" : "arrow.clockwise") .font(.system(size: 11, weight: .medium))