feat(bar): live Codex usage sync with force refresh and spend-chart periods

Codex quota in the menu bar came only from frozen local session logs, so it
showed stale data ("older session") that no refresh could update. Fetch it live
from the same source the dashboard uses, under the existing Claude-style safety
controls (10-min TTL, in-flight coalescing, 429 backoff, circuit breaker,
serve-stale), falling back to local logs when offline or rate-limited. A success
with no usable 5h/weekly window keeps the local reading rather than caching a
contentless row. The footer refresh and a new inline button on the Codex card
force a live pull past the open debounce, and a forced /summary re-pulls native
rows while serving the last-known cached rows if the live pull overruns its
budget, so the Claude/Codex cards never blank mid-refresh.

The spend strip gains a Today / 7d / 30d selector (default 7d), a taller chart,
and bottom time-axis labels (local hours for today, weekday for 7d, dates for
30d). The analytics endpoint now exposes a 24-bucket hourly series for today,
converted from the pipeline's UTC hour keys to the user's local clock so the
intra-day chart matches the dashboard.
This commit is contained in:
Tam Nhu Tran committed 2026-06-20 18:37:18 -04:00
1 parent 9977c49fab
commit 6ed5b2d62b
13 files changed
+1263 -53

No files matched your search

@@ -33,6 +33,11 @@ final class BarViewModel: ObservableObject {
@Published var spendChartStyle: SpendChartStyle {
didSet { SpendChartStyleStore.save(spendChartStyle) }
}
/// Active time window for the spend sparkline (today/7d/30d). Persisted via
/// SpendPeriodStore; didSet mirrors the spendChartStyle pattern.
@Published var spendPeriod: SpendPeriod {
didSet { SpendPeriodStore.save(spendPeriod) }
}
/// The alerts the most recent evaluation wanted delivered, surfaced in the
/// dropdown so users who deny notifications still see the conditions.
@Published var activeAlerts: [BarNotification] = []
@@ -71,6 +76,7 @@ final class BarViewModel: ObservableObject {
self.appearance = BarAppearanceStore.load()
self.glanceMode = prefs.load().glanceMode
self.spendChartStyle = SpendChartStyleStore.load()
self.spendPeriod = SpendPeriodStore.load()
reconnect()
startBackgroundPolling()
}
@@ -219,6 +225,16 @@ final class BarViewModel: ObservableObject {
reconnectAndLoad(force: force)
}
// MARK: - Force refresh (bypasses the 15s debounce)
/// Unconditional force-refresh: skips the debouncer and calls
/// `reconnectAndLoad(force: true)` directly. Used by the footer Refresh button
/// and by the Codex stale-footnote inline action so those never silently no-op
/// inside the debounce window.
func forceRefresh() {
reconnectAndLoad(force: true)
}
// MARK: - Start CCS (called from offline UI button)
/// Trigger a full connect sequence (probe → launch → poll). Used by the