Strip optimize from menubar, fix stuck loading spinner

The menubar ran --optimize on every 30-second CLI invocation. As
sessions accumulated throughout the day, optimize got heavier until
it exceeded the 45-second timeout. When the fetch failed with no
cached data, the loading overlay had no escape hatch and stayed
forever.

- Never pass includeOptimize from the menubar (background loop,
  forceRefresh, tab/period switches, manual refresh button)
- On fetch failure with empty cache, retry without optimize as
  fallback so the spinner always clears
- refreshQuietly also skips optimize
This commit is contained in:
iamtoruk 2026-05-04 23:11:42 -07:00
parent e7560052ed
commit c706cd2de2
3 changed files with 21 additions and 9 deletions

View file

@ -71,9 +71,9 @@ final class AppStore {
switchTask?.cancel()
switchTask = Task {
if selectedProvider == .all {
await refresh(includeOptimize: true, force: true)
await refresh(includeOptimize: false, force: true)
} else {
async let main: Void = refresh(includeOptimize: true, force: true)
async let main: Void = refresh(includeOptimize: false, force: true)
async let all: Void = refreshQuietly(period: period)
_ = await (main, all)
}
@ -88,9 +88,9 @@ final class AppStore {
switchTask?.cancel()
switchTask = Task {
if provider == .all {
await refresh(includeOptimize: true, force: true)
await refresh(includeOptimize: false, force: true)
} else {
async let main: Void = refresh(includeOptimize: true, force: true)
async let main: Void = refresh(includeOptimize: false, force: true)
async let all: Void = refreshQuietly(period: selectedPeriod)
_ = await (main, all)
}
@ -119,8 +119,20 @@ final class AppStore {
lastError = nil
} catch {
if Task.isCancelled { return }
lastError = String(describing: error)
NSLog("CodeBurn: fetch failed for \(key.period.rawValue)/\(key.provider.rawValue): \(error)")
if includeOptimize, cache[key] == nil {
do {
let fallback = try await DataClient.fetch(period: key.period, provider: key.provider, includeOptimize: false)
guard !Task.isCancelled else { return }
cache[key] = CachedPayload(payload: fallback, fetchedAt: Date())
lastError = nil
return
} catch {
if Task.isCancelled { return }
NSLog("CodeBurn: fallback fetch also failed: \(error)")
}
}
lastError = String(describing: error)
}
let allKey = PayloadCacheKey(period: selectedPeriod, provider: .all)
@ -134,7 +146,7 @@ final class AppStore {
/// Always uses the .all provider since the menubar badge shows total spend.
func refreshQuietly(period: Period) async {
do {
let fresh = try await DataClient.fetch(period: period, provider: .all, includeOptimize: true)
let fresh = try await DataClient.fetch(period: period, provider: .all, includeOptimize: false)
cache[PayloadCacheKey(period: period, provider: .all)] = CachedPayload(payload: fresh, fetchedAt: Date())
} catch {
NSLog("CodeBurn: quiet refresh failed for \(period.rawValue): \(error)")

View file

@ -171,7 +171,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
lastRefreshTime = now
Task {
async let main: Void = store.refresh(includeOptimize: true, force: true)
async let main: Void = store.refresh(includeOptimize: false, force: true)
async let today: Void = store.refreshQuietly(period: .today)
_ = await (main, today)
refreshStatusButton()
@ -207,7 +207,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
if self.store.selectedPeriod != .today || self.store.selectedProvider != .all {
await self.store.refreshQuietly(period: .today)
}
await self.store.refresh(includeOptimize: true, force: true)
await self.store.refresh(includeOptimize: false, force: true)
self.refreshStatusButton()
try? await Task.sleep(nanoseconds: refreshIntervalNanos)
}

View file

@ -407,7 +407,7 @@ struct FooterBar: View {
.fixedSize()
Button {
Task { await store.refresh(includeOptimize: true, force: true) }
Task { await store.refresh(includeOptimize: false, force: true) }
} label: {
Image(systemName: store.isLoading ? "arrow.triangle.2.circlepath" : "arrow.clockwise")
.font(.system(size: 11, weight: .medium))