diff --git a/mac/Sources/CodeBurnMenubar/AppStore.swift b/mac/Sources/CodeBurnMenubar/AppStore.swift index 4ac3948..3eec2b2 100644 --- a/mac/Sources/CodeBurnMenubar/AppStore.swift +++ b/mac/Sources/CodeBurnMenubar/AppStore.swift @@ -174,7 +174,10 @@ final class AppStore { /// last 7 days of dailyHistory. Used as the "tokens consumed in 7-day window" reading paired /// with the API-reported percent for capacity estimation. private func effectiveTokensInLast7Days(history: [DailyHistoryEntry], asOf now: Date) -> Double { - let cutoff = ISO8601DateFormatter().string(from: now.addingTimeInterval(-7 * 86400)).prefix(10) + let f = DateFormatter() + f.dateFormat = "yyyy-MM-dd" + f.timeZone = .current + let cutoff = f.string(from: now.addingTimeInterval(-7 * 86400)) return history .filter { $0.date >= cutoff } .reduce(0.0) { $0 + $1.effectiveTokens } diff --git a/mac/Sources/CodeBurnMenubar/Views/FindingsSection.swift b/mac/Sources/CodeBurnMenubar/Views/FindingsSection.swift index 86f174c..aff1e83 100644 --- a/mac/Sources/CodeBurnMenubar/Views/FindingsSection.swift +++ b/mac/Sources/CodeBurnMenubar/Views/FindingsSection.swift @@ -213,11 +213,11 @@ private struct HistoryStats { private func computeHistoryStats(history: [DailyHistoryEntry]) -> HistoryStats { 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() diff --git a/src/cli.ts b/src/cli.ts index 116866c..368cbbc 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -7,7 +7,7 @@ import { convertCost } from './currency.js' import { renderStatusBar } from './format.js' import { type PeriodData, type ProviderCost } from './menubar-json.js' import { buildMenubarPayload } from './menubar-json.js' -import { getDaysInRange, ensureCacheHydrated, emptyCache, MS_PER_DAY, BACKFILL_DAYS, toDateString } from './daily-cache.js' +import { getDaysInRange, ensureCacheHydrated, emptyCache, BACKFILL_DAYS, toDateString } from './daily-cache.js' import { aggregateProjectsIntoDays, buildPeriodDataFromDays, dateKey } from './day-aggregator.js' import { CATEGORY_LABELS, type DateRange, type ProjectSummary, type TaskCategory } from './types.js' import { renderDashboard } from './dashboard.js' @@ -141,8 +141,19 @@ const program = new Command() .description('See where your AI coding tokens go - by task, tool, model, and project') .version(version) .option('--verbose', 'print warnings to stderr on read failures and skipped files') + .option('--timezone ', 'IANA timezone for date grouping (e.g. Asia/Tokyo, America/New_York)') program.hook('preAction', async (thisCommand) => { + const tz = thisCommand.opts<{ timezone?: string }>().timezone ?? process.env['CODEBURN_TZ'] + if (tz) { + try { + Intl.DateTimeFormat(undefined, { timeZone: tz }) + } catch { + console.error(`\n Invalid timezone: "${tz}". Use an IANA timezone like "America/New_York" or "Asia/Tokyo".\n`) + process.exit(1) + } + process.env.TZ = tz + } const config = await readConfig() setModelAliases(config.modelAliases ?? {}) if (thisCommand.opts<{ verbose?: boolean }>().verbose) { @@ -383,7 +394,7 @@ program const periodInfo = getDateRange(opts.period) const now = new Date() const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()) - const yesterdayStr = toDateString(new Date(todayStart.getTime() - MS_PER_DAY)) + const yesterdayStr = toDateString(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1)) const isAllProviders = pf === 'all' const cache = await hydrateCache() @@ -454,7 +465,7 @@ program // Cache stores per-provider cost+calls per day in DailyEntry.providers, so we can derive // a provider-filtered history without re-parsing. Tokens aren't broken down per provider // in the cache, so the filtered view shows zero tokens (heatmap/trend still works on cost). - const historyStartStr = toDateString(new Date(todayStart.getTime() - BACKFILL_DAYS * MS_PER_DAY)) + const historyStartStr = toDateString(new Date(now.getFullYear(), now.getMonth(), now.getDate() - BACKFILL_DAYS)) const allCacheDays = getDaysInRange(cache, historyStartStr, yesterdayStr) // Parse only today for history; historical days come from cache const todayRangeForHistory: DateRange = { start: todayStart, end: new Date() } diff --git a/src/daily-cache.ts b/src/daily-cache.ts index 6e30727..096e2c6 100644 --- a/src/daily-cache.ts +++ b/src/daily-cache.ts @@ -165,7 +165,7 @@ export async function ensureCacheHydrated( const now = new Date() const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()) const yesterdayEnd = new Date(todayStart.getTime() - 1) - const yesterdayStr = toDateString(new Date(todayStart.getTime() - MS_PER_DAY)) + const yesterdayStr = toDateString(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1)) return withDailyCacheLock(async () => { let c = await loadDailyCache() @@ -183,7 +183,7 @@ export async function ensureCacheHydrated( parseInt(c.lastComputedDate.slice(5, 7)) - 1, parseInt(c.lastComputedDate.slice(8, 10)) + 1 ) - : new Date(todayStart.getTime() - BACKFILL_DAYS * MS_PER_DAY) + : new Date(now.getFullYear(), now.getMonth(), now.getDate() - BACKFILL_DAYS) if (gapStart.getTime() <= yesterdayEnd.getTime()) { const gapRange: DateRange = { start: gapStart, end: yesterdayEnd }