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.
This commit is contained in:
iamtoruk 2026-04-19 03:18:38 -07:00 committed by AgentSeal
parent 888030fce3
commit 72ccf34a5a
4 changed files with 14 additions and 11 deletions

View file

@ -8,7 +8,7 @@ import { renderStatusBar } from './format.js'
import { type PeriodData, type ProviderCost } from './menubar-json.js' import { type PeriodData, type ProviderCost } from './menubar-json.js'
import { buildMenubarPayload } from './menubar-json.js' import { buildMenubarPayload } from './menubar-json.js'
import { addNewDays, getDaysInRange, loadDailyCache, saveDailyCache, withDailyCacheLock } from './daily-cache.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 { CATEGORY_LABELS, type DateRange, type ProjectSummary, type TaskCategory } from './types.js'
import { renderDashboard } from './dashboard.js' import { renderDashboard } from './dashboard.js'
import { parseDateRangeFlags } from './cli-date.js' import { parseDateRangeFlags } from './cli-date.js'
@ -25,7 +25,7 @@ const MS_PER_DAY = 24 * 60 * 60 * 1000
const BACKFILL_DAYS = 365 const BACKFILL_DAYS = 365
function toDateString(date: Date): string { 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 } { function getDateRange(period: string): { range: DateRange; label: string } {
@ -35,12 +35,12 @@ function getDateRange(period: string): { range: DateRange; label: string } {
switch (period) { switch (period) {
case 'today': { case 'today': {
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate()) 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': { case 'yesterday': {
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1) 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) 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': { case 'week': {
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7) 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 sess of sessions) {
for (const turn of sess.turns) { for (const turn of sess.turns) {
if (!turn.timestamp) { continue } 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 } } if (!dailyMap[day]) { dailyMap[day] = { cost: 0, calls: 0 } }
for (const call of turn.assistantCalls) { for (const call of turn.assistantCalls) {
dailyMap[day].cost += call.costUSD 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 })) Object.entries(m).sort(([, a], [, b]) => b - a).map(([name, calls]) => ({ name, calls }))
const topSessions = projects 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) .sort((a, b) => b.cost - a.cost)
.slice(0, 5) .slice(0, 5)
@ -545,7 +545,7 @@ program
return return
} }
const defaultName = `codeburn-${new Date().toISOString().slice(0, 10)}` const defaultName = `codeburn-${toDateString(new Date())}`
const outputPath = opts.output ?? `${defaultName}.${opts.format}` const outputPath = opts.output ?? `${defaultName}.${opts.format}`
let savedPath: string let savedPath: string

View file

@ -9,6 +9,7 @@ import { loadPricing } from './models.js'
import { getAllProviders } from './providers/index.js' import { getAllProviders } from './providers/index.js'
import { scanAndDetect, type WasteFinding, type WasteAction, type OptimizeResult } from './optimize.js' import { scanAndDetect, type WasteFinding, type WasteAction, type OptimizeResult } from './optimize.js'
import { estimateContextBudget, discoverProjectCwd, type ContextBudget } from './context-budget.js' import { estimateContextBudget, discoverProjectCwd, type ContextBudget } from './context-budget.js'
import { dateKey } from './day-aggregator.js'
import { join } from 'path' import { join } from 'path'
type Period = 'today' | 'week' | '30days' | 'month' | 'all' 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 session of project.sessions) {
for (const turn of session.turns) { for (const turn of session.turns) {
if (!turn.timestamp) continue 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) dailyCosts[day] = (dailyCosts[day] ?? 0) + turn.assistantCalls.reduce((s, c) => s + c.costUSD, 0)
dailyCalls[day] = (dailyCalls[day] ?? 0) + turn.assistantCalls.length dailyCalls[day] = (dailyCalls[day] ?? 0) + turn.assistantCalls.length
} }

View file

@ -20,8 +20,9 @@ function emptyEntry(date: string): DailyEntry {
} }
} }
function dateKey(iso: string): string { export function dateKey(iso: string): string {
return iso.slice(0, 10) 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[] { export function aggregateProjectsIntoDays(projects: ProjectSummary[]): DailyEntry[] {

View file

@ -3,6 +3,7 @@ import { dirname, join, resolve } from 'path'
import { CATEGORY_LABELS, type ProjectSummary, type TaskCategory } from './types.js' import { CATEGORY_LABELS, type ProjectSummary, type TaskCategory } from './types.js'
import { getCurrency, convertCost } from './currency.js' import { getCurrency, convertCost } from './currency.js'
import { dateKey } from './day-aggregator.js'
function escCsv(s: string): string { function escCsv(s: string): string {
const sanitized = /^[=+\-@]/.test(s) ? `'${s}` : s 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 session of project.sessions) {
for (const turn of session.turns) { for (const turn of session.turns) {
if (!turn.timestamp) continue if (!turn.timestamp) continue
const day = turn.timestamp.slice(0, 10) const day = dateKey(turn.timestamp)
if (!daily[day]) { if (!daily[day]) {
daily[day] = { cost: 0, calls: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, sessions: new Set() } daily[day] = { cost: 0, calls: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, sessions: new Set() }
} }