mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-21 14:34:32 +00:00
fix(cli): bucket daily history by local date
The daily cache keyed days by UTC while period ranges used local midnight, so any user outside UTC lost or double-counted a whole day in history.daily and the menubar trend, forecast, and stats disagreed with the period totals. Use the local date everywhere and bump the cache version to rebuild.
This commit is contained in:
parent
ebfa3b8748
commit
fa9c9edfd0
5 changed files with 16 additions and 10 deletions
15
src/cli.ts
15
src/cli.ts
|
|
@ -4,7 +4,7 @@ import { exportCsv, exportJson, type PeriodExport } from './export.js'
|
|||
import { loadPricing } from './models.js'
|
||||
import { parseAllSessions, filterProjectsByName } from './parser.js'
|
||||
import { convertCost } from './currency.js'
|
||||
import { renderStatusBar } from './format.js'
|
||||
import { localDateString, 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'
|
||||
|
|
@ -25,7 +25,12 @@ const MS_PER_DAY = 24 * 60 * 60 * 1000
|
|||
const BACKFILL_DAYS = 365
|
||||
|
||||
function toDateString(date: Date): string {
|
||||
return date.toISOString().slice(0, 10)
|
||||
return localDateString(date)
|
||||
}
|
||||
|
||||
function localMidnightAfter(dateStr: string): Date {
|
||||
const [y, m, d] = dateStr.split('-').map(Number)
|
||||
return new Date(y!, m! - 1, d! + 1)
|
||||
}
|
||||
|
||||
function getDateRange(period: string): { range: DateRange; label: string } {
|
||||
|
|
@ -35,12 +40,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)
|
||||
|
|
@ -342,7 +347,7 @@ program
|
|||
const cache = await withDailyCacheLock(async () => {
|
||||
let c = await loadDailyCache()
|
||||
const gapStart = c.lastComputedDate
|
||||
? new Date(new Date(`${c.lastComputedDate}T00:00:00.000Z`).getTime() + MS_PER_DAY)
|
||||
? localMidnightAfter(c.lastComputedDate)
|
||||
: new Date(todayStart.getTime() - BACKFILL_DAYS * MS_PER_DAY)
|
||||
|
||||
if (gapStart.getTime() <= yesterdayEnd.getTime()) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { mkdir, open, readFile, rename, unlink } from 'fs/promises'
|
|||
import { homedir } from 'os'
|
||||
import { join } from 'path'
|
||||
|
||||
export const DAILY_CACHE_VERSION = 2
|
||||
export const DAILY_CACHE_VERSION = 3
|
||||
const DAILY_CACHE_FILENAME = 'daily-cache.json'
|
||||
|
||||
export type DailyEntry = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { DailyEntry } from './daily-cache.js'
|
||||
import { localDateString } from './format.js'
|
||||
import type { PeriodData } from './menubar-json.js'
|
||||
import { CATEGORY_LABELS, type ProjectSummary, type TaskCategory } from './types.js'
|
||||
|
||||
|
|
@ -21,7 +22,7 @@ function emptyEntry(date: string): DailyEntry {
|
|||
}
|
||||
|
||||
function dateKey(iso: string): string {
|
||||
return iso.slice(0, 10)
|
||||
return localDateString(new Date(iso))
|
||||
}
|
||||
|
||||
export function aggregateProjectsIntoDays(projects: ProjectSummary[]): DailyEntry[] {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export function formatTokens(n: number): string {
|
|||
/// out to Intl.DateTimeFormat for every turn in a loop and avoids the UTC drift that bites
|
||||
/// `Date.toISOString().slice(0,10)` whenever the user runs this between local midnight and
|
||||
/// UTC midnight.
|
||||
function localDateString(d: Date): string {
|
||||
export function localDateString(d: Date): string {
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
|
|
|
|||
|
|
@ -135,8 +135,8 @@ describe('aggregateProjectsIntoDays', () => {
|
|||
sessions: [{
|
||||
sessionId: 's1',
|
||||
project: 'p',
|
||||
firstTimestamp: '2026-04-09T23:59:00Z',
|
||||
lastTimestamp: '2026-04-10T00:10:00Z',
|
||||
firstTimestamp: new Date(2026, 3, 9, 23, 59).toISOString(),
|
||||
lastTimestamp: new Date(2026, 3, 10, 0, 10).toISOString(),
|
||||
totalCostUSD: 1,
|
||||
totalInputTokens: 0, totalOutputTokens: 0, totalCacheReadTokens: 0, totalCacheWriteTokens: 0,
|
||||
apiCalls: 0,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue