mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-03 05:15:24 +00:00
* fix(app,dash): render pre-history days as no data, not a currency zero Days before the first recorded day in history.daily were painted as a real 0.00/0 calls in the daily activity heatmap and the daily spend charts. That zero is unknown, not measured, so those cells and bars now read "No data recorded" instead of a currency zero. Genuinely idle days within recorded history stay as real zeros. - app: heatmap cells, the daily spend bars, and the daily-by-model columns get a distinct no-data style plus a "No data recorded" hover for days before the first recorded day. - dash: the granular line chart drops buckets before the first recorded day so no flat zero line is drawn before any history exists. - data-start is derived in the UI layer from history.daily; payload shape is unchanged. Verified: app vitest 418 pass (7 new for the no-data behavior); app, dash, and root typecheck clean; dash and app renderer vite builds succeed. * no-data: cap guard, timezone-free dash trim, StackedBars aria Three hardening fixes from adversarial review of the no-data rendering: dataStartKey returns null at the payload's 365-entry history cap, where the oldest retained entry is no longer the true data start; classifying past it would label real aged-out history as no data on long custom ranges. Documented that the install-to-first-use gap reading as no data is literally accurate: nothing was recorded then either. The dash granular chart now trims leading zero-only buckets by value instead of comparing bucket timestamps against date keys, so producer and viewer timezone skew can never drop a real first-day bucket, and an all-zero series lands in the established empty state. StackedBars no-data columns expose their state via aria-label, not only a title on a non-focusable div.
30 lines
1.2 KiB
TypeScript
30 lines
1.2 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { dataStartKey } from './period'
|
|
import type { DailyHistoryEntry } from './types'
|
|
|
|
function day(date: string): DailyHistoryEntry {
|
|
return { date, cost: 1, savingsUSD: 0, calls: 1, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, topModels: [] }
|
|
}
|
|
|
|
describe('dataStartKey', () => {
|
|
it('returns the earliest recorded day of a sparse history', () => {
|
|
expect(dataStartKey([day('2026-05-03'), day('2026-04-24'), day('2026-06-01')])).toBe('2026-04-24')
|
|
})
|
|
|
|
it('returns null for an empty history (no classification possible)', () => {
|
|
expect(dataStartKey([])).toBeNull()
|
|
})
|
|
|
|
it('returns null at the server-side 365-entry cap', () => {
|
|
// At the cap the oldest retained entry is not the true data start, so
|
|
// classification must switch off instead of labeling real aged-out
|
|
// history as "no data recorded" on long custom ranges.
|
|
const capped = Array.from({ length: 365 }, (_, i) => {
|
|
const d = new Date(Date.UTC(2026, 0, 1) + i * 24 * 60 * 60 * 1000)
|
|
return day(d.toISOString().slice(0, 10))
|
|
})
|
|
expect(dataStartKey(capped)).toBeNull()
|
|
expect(dataStartKey(capped.slice(0, 364))).toBe('2026-01-01')
|
|
})
|
|
})
|