mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-04-28 06:59:37 +00:00
fix(menubar): prefetch periods and align dashboard dates with local timezone
Loading overlay no longer flashes on every 15s poll. isLoading now only toggles when the cache is cold, and all periods prefetch once on launch so tab switching is instant. Heatmap tooltip, trend bars, forecast, and all-time stats were computing on UTC dates while the CLI reports on local dates, so the two disagreed at day boundaries. Switched every date formatter and calendar in these paths to .current so the menubar matches codeburn today output.
This commit is contained in:
parent
988060cd09
commit
3c2aab2207
3 changed files with 35 additions and 21 deletions
|
|
@ -73,10 +73,11 @@ final class AppStore {
|
|||
let key = currentKey
|
||||
guard !inFlightKeys.contains(key) else { return }
|
||||
inFlightKeys.insert(key)
|
||||
isLoading = true
|
||||
let showLoading = cache[key] == nil
|
||||
if showLoading { isLoading = true }
|
||||
defer {
|
||||
inFlightKeys.remove(key)
|
||||
isLoading = false
|
||||
if showLoading { isLoading = false }
|
||||
}
|
||||
do {
|
||||
let fresh = try await DataClient.fetch(period: key.period, provider: key.provider, includeOptimize: includeOptimize)
|
||||
|
|
@ -88,6 +89,15 @@ final class AppStore {
|
|||
}
|
||||
}
|
||||
|
||||
/// Prefetch all periods so tab switching is instant. Skips any period already cached.
|
||||
func prefetchAll() async {
|
||||
for period in Period.allCases {
|
||||
let key = PayloadCacheKey(period: period, provider: .all)
|
||||
if cache[key] != nil { continue }
|
||||
await refreshQuietly(period: period)
|
||||
}
|
||||
}
|
||||
|
||||
/// Background refresh for a period other than the visible one (e.g. keeping today fresh for the menubar badge).
|
||||
/// Does not toggle isLoading, so the popover's loading overlay is unaffected.
|
||||
/// Always uses the .all provider since the menubar badge shows total spend.
|
||||
|
|
|
|||
|
|
@ -71,17 +71,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|||
|
||||
private func startRefreshLoop() {
|
||||
refreshTask = Task { [weak self] in
|
||||
guard let s = self else { return }
|
||||
// First cycle: fetch current view, then prefetch all periods in background
|
||||
await s.store.refreshQuietly(period: .today)
|
||||
s.refreshStatusButton()
|
||||
await s.store.refresh(includeOptimize: true)
|
||||
s.refreshStatusButton()
|
||||
await s.store.prefetchAll()
|
||||
|
||||
while !Task.isCancelled {
|
||||
guard let self else { return }
|
||||
// Always keep the (today, all) payload warm. The menubar title and the
|
||||
// agent tab strip both read from it, so it has to refresh every cycle
|
||||
// regardless of whether the user is currently viewing Today or a
|
||||
// different period / provider.
|
||||
await self.store.refreshQuietly(period: .today)
|
||||
self.refreshStatusButton()
|
||||
await self.store.refresh(includeOptimize: true)
|
||||
self.refreshStatusButton()
|
||||
try? await Task.sleep(nanoseconds: refreshIntervalNanos)
|
||||
guard let s = self else { return }
|
||||
await s.store.refreshQuietly(period: .today)
|
||||
s.refreshStatusButton()
|
||||
await s.store.refresh(includeOptimize: true)
|
||||
s.refreshStatusButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -344,7 +344,7 @@ private struct BarTooltipCard: View {
|
|||
private func prettyDate(_ ymd: String) -> String {
|
||||
let parser = DateFormatter()
|
||||
parser.dateFormat = "yyyy-MM-dd"
|
||||
parser.timeZone = TimeZone(identifier: "UTC")
|
||||
parser.timeZone = .current
|
||||
guard let date = parser.date(from: ymd) else { return ymd }
|
||||
let display = DateFormatter()
|
||||
display.dateFormat = "EEE MMM d"
|
||||
|
|
@ -392,11 +392,11 @@ private struct TrendStats {
|
|||
|
||||
private func buildTrendBars(from days: [DailyHistoryEntry]) -> [TrendBar] {
|
||||
var calendar = Calendar(identifier: .gregorian)
|
||||
calendar.timeZone = TimeZone(identifier: "UTC")!
|
||||
calendar.timeZone = .current
|
||||
let formatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "yyyy-MM-dd"
|
||||
f.timeZone = TimeZone(identifier: "UTC")
|
||||
f.timeZone = .current
|
||||
return f
|
||||
}()
|
||||
let entryByDate = Dictionary(uniqueKeysWithValues: days.map { ($0.date, $0) })
|
||||
|
|
@ -427,11 +427,11 @@ private func computeTrendStats(bars: [TrendBar], allDays: [DailyHistoryEntry]) -
|
|||
let peak = bars.filter { $0.cost > 0 }.max(by: { $0.cost < $1.cost })
|
||||
|
||||
var calendar = Calendar(identifier: .gregorian)
|
||||
calendar.timeZone = TimeZone(identifier: "UTC")!
|
||||
calendar.timeZone = .current
|
||||
let formatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "yyyy-MM-dd"
|
||||
f.timeZone = TimeZone(identifier: "UTC")
|
||||
f.timeZone = .current
|
||||
return f
|
||||
}()
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
|
|
@ -547,11 +547,11 @@ private struct ForecastStats {
|
|||
|
||||
private func computeForecast(days: [DailyHistoryEntry]) -> ForecastStats {
|
||||
var calendar = Calendar(identifier: .gregorian)
|
||||
calendar.timeZone = TimeZone(identifier: "UTC")!
|
||||
calendar.timeZone = .current
|
||||
let formatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "yyyy-MM-dd"
|
||||
f.timeZone = TimeZone(identifier: "UTC")
|
||||
f.timeZone = .current
|
||||
return f
|
||||
}()
|
||||
let now = Date()
|
||||
|
|
@ -798,17 +798,17 @@ private func computeAllStats(payload: MenubarPayload) -> AllStats {
|
|||
let favoriteModel = payload.current.topModels.first?.name ?? "—"
|
||||
|
||||
var calendar = Calendar(identifier: .gregorian)
|
||||
calendar.timeZone = TimeZone(identifier: "UTC")!
|
||||
calendar.timeZone = .current
|
||||
let formatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "yyyy-MM-dd"
|
||||
f.timeZone = TimeZone(identifier: "UTC")
|
||||
f.timeZone = .current
|
||||
return f
|
||||
}()
|
||||
let displayFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "MMM d"
|
||||
f.timeZone = TimeZone(identifier: "UTC")
|
||||
f.timeZone = .current
|
||||
return f
|
||||
}()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue