mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-17 03:56:45 +00:00
Fix timezone handling: menubar UTC bugs, --timezone flag, DST-safe dates
Three fixes for issue #184: 1. Menubar Swift code used UTC instead of local timezone in two places: computeHistoryStats hardcoded TimeZone("UTC") and effectiveTokensInLast7Days used ISO8601DateFormatter (UTC default). Both now use .current to match CLI-produced local date keys. 2. Add --timezone flag and CODEBURN_TZ env var to override the system timezone for all date grouping. Sets process.env.TZ before any Date operations so all existing local-timezone code works unchanged. 3. Replace MS_PER_DAY arithmetic with Date constructor day-of-month math for yesterday/backfill computations. Subtracting 86400000ms from midnight skips a day on DST spring-forward (23-hour day). Fixes #184
This commit is contained in:
parent
f35400f199
commit
68c6f2c710
4 changed files with 22 additions and 8 deletions
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
17
src/cli.ts
17
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 <zone>', '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() }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue