mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-02 00:40:14 +00:00
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:
parent
888030fce3
commit
72ccf34a5a
4 changed files with 14 additions and 11 deletions
14
src/cli.ts
14
src/cli.ts
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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[] {
|
||||||
|
|
|
||||||
|
|
@ -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() }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue