mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-17 03:56:45 +00:00
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.
This commit is contained in:
parent
474c71a77b
commit
18c3c8f908
4 changed files with 26 additions and 6 deletions
|
|
@ -19,6 +19,8 @@
|
||||||
|
|
||||||
### Fixed (macOS menubar)
|
### 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.
|
- **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
|
## 0.9.6 - 2026-05-03
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ final class AppStore {
|
||||||
var capacityEstimates: [String: CapacityEstimate] = [:]
|
var capacityEstimates: [String: CapacityEstimate] = [:]
|
||||||
|
|
||||||
private var cache: [PayloadCacheKey: CachedPayload] = [:]
|
private var cache: [PayloadCacheKey: CachedPayload] = [:]
|
||||||
|
private var cacheDate: String = ""
|
||||||
private var switchTask: Task<Void, Never>?
|
private var switchTask: Task<Void, Never>?
|
||||||
|
|
||||||
private var currentKey: PayloadCacheKey {
|
private var currentKey: PayloadCacheKey {
|
||||||
|
|
@ -99,18 +100,33 @@ final class AppStore {
|
||||||
|
|
||||||
private var inFlightKeys: Set<PayloadCacheKey> = []
|
private var inFlightKeys: Set<PayloadCacheKey> = []
|
||||||
|
|
||||||
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
|
let key = currentKey
|
||||||
if !force, cache[key]?.isFresh == true { return }
|
if !force, cache[key]?.isFresh == true { return }
|
||||||
if !force, inFlightKeys.contains(key) { return }
|
if !force, inFlightKeys.contains(key) { return }
|
||||||
inFlightKeys.insert(key)
|
inFlightKeys.insert(key)
|
||||||
let showedLoading = cache[key] == nil
|
let didShowLoading = showLoading || cache[key] == nil
|
||||||
if showedLoading {
|
if didShowLoading {
|
||||||
loadingCount += 1
|
loadingCount += 1
|
||||||
}
|
}
|
||||||
defer {
|
defer {
|
||||||
inFlightKeys.remove(key)
|
inFlightKeys.remove(key)
|
||||||
if showedLoading { loadingCount = max(loadingCount - 1, 0) }
|
if didShowLoading { loadingCount = max(loadingCount - 1, 0) }
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
let fresh = try await DataClient.fetch(period: key.period, provider: key.provider, includeOptimize: includeOptimize)
|
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.
|
/// Does not toggle isLoading, so the popover's loading overlay is unaffected.
|
||||||
/// Always uses the .all provider since the menubar badge shows total spend.
|
/// Always uses the .all provider since the menubar badge shows total spend.
|
||||||
func refreshQuietly(period: Period) async {
|
func refreshQuietly(period: Period) async {
|
||||||
|
invalidateStaleDayCache()
|
||||||
do {
|
do {
|
||||||
let fresh = try await DataClient.fetch(period: period, provider: .all, includeOptimize: false)
|
let fresh = try await DataClient.fetch(period: period, provider: .all, includeOptimize: false)
|
||||||
cache[PayloadCacheKey(period: period, provider: .all)] = CachedPayload(payload: fresh, fetchedAt: Date())
|
cache[PayloadCacheKey(period: period, provider: .all)] = CachedPayload(payload: fresh, fetchedAt: Date())
|
||||||
|
|
|
||||||
|
|
@ -170,8 +170,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
||||||
guard now.timeIntervalSince(lastRefreshTime) > 5 else { return }
|
guard now.timeIntervalSince(lastRefreshTime) > 5 else { return }
|
||||||
lastRefreshTime = now
|
lastRefreshTime = now
|
||||||
|
|
||||||
|
store.invalidateCache()
|
||||||
Task {
|
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)
|
async let today: Void = store.refreshQuietly(period: .today)
|
||||||
_ = await (main, today)
|
_ = await (main, today)
|
||||||
refreshStatusButton()
|
refreshStatusButton()
|
||||||
|
|
|
||||||
|
|
@ -407,7 +407,7 @@ struct FooterBar: View {
|
||||||
.fixedSize()
|
.fixedSize()
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
Task { await store.refresh(includeOptimize: false, force: true) }
|
Task { await store.refresh(includeOptimize: false, force: true, showLoading: true) }
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: store.isLoading ? "arrow.triangle.2.circlepath" : "arrow.clockwise")
|
Image(systemName: store.isLoading ? "arrow.triangle.2.circlepath" : "arrow.clockwise")
|
||||||
.font(.system(size: 11, weight: .medium))
|
.font(.system(size: 11, weight: .medium))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue