From 72ccf34a5a056404b5b20b75eb1f32d770cc4ca8 Mon Sep 17 00:00:00 2001 From: iamtoruk Date: Sun, 19 Apr 2026 03:18:38 -0700 Subject: [PATCH] fix: use local timezone for daily date bucketing instead of UTC Timestamps in session files are UTC ISO strings. Several code paths extracted the date via .slice(0, 10) which gives the UTC date, while date range filtering uses local-time boundaries. This caused turns between UTC midnight and local midnight to be bucketed under the wrong day -- the menubar showed lower today cost than the TUI because those turns were attributed to tomorrow (UTC) but filtered as today (local). format.ts already had a localDateString fix; this applies the same pattern everywhere via dateKey() in day-aggregator.ts. --- src/cli.ts | 14 +++++++------- src/dashboard.tsx | 3 ++- src/day-aggregator.ts | 5 +++-- src/export.ts | 3 ++- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 494e313..6090495 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -8,7 +8,7 @@ import { renderStatusBar } from './format.js' import { type PeriodData, type ProviderCost } from './menubar-json.js' import { buildMenubarPayload } from './menubar-json.js' import { addNewDays, getDaysInRange, loadDailyCache, saveDailyCache, withDailyCacheLock } from './daily-cache.js' -import { aggregateProjectsIntoDays, buildPeriodDataFromDays } from './day-aggregator.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' import { parseDateRangeFlags } from './cli-date.js' @@ -25,7 +25,7 @@ const MS_PER_DAY = 24 * 60 * 60 * 1000 const BACKFILL_DAYS = 365 function toDateString(date: Date): string { - return date.toISOString().slice(0, 10) + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}` } function getDateRange(period: string): { range: DateRange; label: string } { @@ -35,12 +35,12 @@ function getDateRange(period: string): { range: DateRange; label: string } { switch (period) { case 'today': { const start = new Date(now.getFullYear(), now.getMonth(), now.getDate()) - return { range: { start, end }, label: `Today (${start.toISOString().slice(0, 10)})` } + return { range: { start, end }, label: `Today (${toDateString(start)})` } } case 'yesterday': { const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1) const yesterdayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 23, 59, 59, 999) - return { range: { start, end: yesterdayEnd }, label: `Yesterday (${start.toISOString().slice(0, 10)})` } + return { range: { start, end: yesterdayEnd }, label: `Yesterday (${toDateString(start)})` } } case 'week': { const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7) @@ -123,7 +123,7 @@ function buildJsonReport(projects: ProjectSummary[], period: string, periodKey: for (const sess of sessions) { for (const turn of sess.turns) { if (!turn.timestamp) { continue } - const day = turn.timestamp.slice(0, 10) + const day = dateKey(turn.timestamp) if (!dailyMap[day]) { dailyMap[day] = { cost: 0, calls: 0 } } for (const call of turn.assistantCalls) { dailyMap[day].cost += call.costUSD @@ -204,7 +204,7 @@ function buildJsonReport(projects: ProjectSummary[], period: string, periodKey: Object.entries(m).sort(([, a], [, b]) => b - a).map(([name, calls]) => ({ name, calls })) const topSessions = projects - .flatMap(p => p.sessions.map(s => ({ project: p.project, sessionId: s.sessionId, date: s.firstTimestamp?.slice(0, 10) ?? null, cost: convertCost(s.totalCostUSD), calls: s.apiCalls }))) + .flatMap(p => p.sessions.map(s => ({ project: p.project, sessionId: s.sessionId, date: s.firstTimestamp ? dateKey(s.firstTimestamp) : null, cost: convertCost(s.totalCostUSD), calls: s.apiCalls }))) .sort((a, b) => b.cost - a.cost) .slice(0, 5) @@ -545,7 +545,7 @@ program return } - const defaultName = `codeburn-${new Date().toISOString().slice(0, 10)}` + const defaultName = `codeburn-${toDateString(new Date())}` const outputPath = opts.output ?? `${defaultName}.${opts.format}` let savedPath: string diff --git a/src/dashboard.tsx b/src/dashboard.tsx index a4e5cde..99163aa 100644 --- a/src/dashboard.tsx +++ b/src/dashboard.tsx @@ -9,6 +9,7 @@ import { loadPricing } from './models.js' import { getAllProviders } from './providers/index.js' import { scanAndDetect, type WasteFinding, type WasteAction, type OptimizeResult } from './optimize.js' import { estimateContextBudget, discoverProjectCwd, type ContextBudget } from './context-budget.js' +import { dateKey } from './day-aggregator.js' import { join } from 'path' type Period = 'today' | 'week' | '30days' | 'month' | 'all' @@ -195,7 +196,7 @@ function DailyActivity({ projects, days = 14, pw, bw }: { projects: ProjectSumma for (const session of project.sessions) { for (const turn of session.turns) { if (!turn.timestamp) continue - const day = turn.timestamp.slice(0, 10) + const day = dateKey(turn.timestamp) dailyCosts[day] = (dailyCosts[day] ?? 0) + turn.assistantCalls.reduce((s, c) => s + c.costUSD, 0) dailyCalls[day] = (dailyCalls[day] ?? 0) + turn.assistantCalls.length } diff --git a/src/day-aggregator.ts b/src/day-aggregator.ts index 5030f8d..bc63fa6 100644 --- a/src/day-aggregator.ts +++ b/src/day-aggregator.ts @@ -20,8 +20,9 @@ function emptyEntry(date: string): DailyEntry { } } -function dateKey(iso: string): string { - return iso.slice(0, 10) +export function dateKey(iso: string): string { + const d = new Date(iso) + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` } export function aggregateProjectsIntoDays(projects: ProjectSummary[]): DailyEntry[] { diff --git a/src/export.ts b/src/export.ts index 58f250e..2ef75bd 100644 --- a/src/export.ts +++ b/src/export.ts @@ -3,6 +3,7 @@ import { dirname, join, resolve } from 'path' import { CATEGORY_LABELS, type ProjectSummary, type TaskCategory } from './types.js' import { getCurrency, convertCost } from './currency.js' +import { dateKey } from './day-aggregator.js' function escCsv(s: string): string { const sanitized = /^[=+\-@]/.test(s) ? `'${s}` : s @@ -48,7 +49,7 @@ function buildDailyRows(projects: ProjectSummary[], period: string): Row[] { for (const session of project.sessions) { for (const turn of session.turns) { if (!turn.timestamp) continue - const day = turn.timestamp.slice(0, 10) + const day = dateKey(turn.timestamp) if (!daily[day]) { daily[day] = { cost: 0, calls: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, sessions: new Set() } }