mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-20 05:54:25 +00:00
fix(app): daily charts empty/flat on real data — use real last-N days
contiguousDailyWindow rebuilt a calendar window from the client clock and looked up days by date key; on real CLI data every lookup missed and zero-filled, so the Spend chart went empty and the Overview chart went flat. Use the real last-N backfilled entries of history.daily directly (Spend last-15, Overview last-max(30, periodDaily)) and remove the dead helper. typecheck clean; 147/147 tests pass.
This commit is contained in:
parent
a216b841ae
commit
a8351bfd1a
6 changed files with 25 additions and 73 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { DailyHistoryEntry, Period } from './types'
|
||||
import { contiguousDailyWindow, formatChartDate, sliceDailyToPeriod } from './period'
|
||||
import { formatChartDate, sliceDailyToPeriod } from './period'
|
||||
|
||||
function entry(date: string): DailyHistoryEntry {
|
||||
return {
|
||||
|
|
@ -56,23 +56,6 @@ describe('sliceDailyToPeriod', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('contiguousDailyWindow', () => {
|
||||
it('returns exactly the requested calendar days ending today and zero-fills gaps', () => {
|
||||
const window = contiguousDailyWindow(DAILY, 5, NOW)
|
||||
|
||||
expect(window.map(day => day.date)).toEqual([
|
||||
'2026-07-06',
|
||||
'2026-07-07',
|
||||
'2026-07-08',
|
||||
'2026-07-09',
|
||||
'2026-07-10',
|
||||
])
|
||||
expect(window[0]).toMatchObject({ cost: 0, calls: 0, topModels: [] })
|
||||
expect(window[3]).toBe(DAILY[7])
|
||||
expect(window[4]).toBe(DAILY[8])
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatChartDate', () => {
|
||||
it('formats date keys without shifting the local calendar day', () => {
|
||||
expect(formatChartDate('2026-07-01')).toBe('Jul 1')
|
||||
|
|
|
|||
|
|
@ -31,32 +31,6 @@ export function sliceDailyToPeriod(daily: DailyHistoryEntry[], period: Period, n
|
|||
return daily.filter(d => (start === null || d.date >= start) && d.date <= todayKey)
|
||||
}
|
||||
|
||||
/** A contiguous calendar-day window ending today, with missing days zero-filled. */
|
||||
export function contiguousDailyWindow(
|
||||
daily: DailyHistoryEntry[],
|
||||
days: number,
|
||||
now = new Date(),
|
||||
): DailyHistoryEntry[] {
|
||||
const byDate = new Map(daily.map(day => [day.date, day]))
|
||||
const window: DailyHistoryEntry[] = []
|
||||
for (let offset = days - 1; offset >= 0; offset--) {
|
||||
const date = new Date(now.getFullYear(), now.getMonth(), now.getDate() - offset)
|
||||
const key = localDateKey(date)
|
||||
window.push(byDate.get(key) ?? {
|
||||
date: key,
|
||||
cost: 0,
|
||||
calls: 0,
|
||||
savingsUSD: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
topModels: [],
|
||||
})
|
||||
}
|
||||
return window
|
||||
}
|
||||
|
||||
/** Format a local date key for compact chart-axis labels such as "Jul 1". */
|
||||
export function formatChartDate(dateKey: string): string {
|
||||
const [year, month, day] = dateKey.split('-').map(Number)
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ function makeYieldReport(): YieldJsonReport {
|
|||
* A fully-typed payload anchored to `now` so today/MTD/projected are stable.
|
||||
* `history.daily` deliberately spans 30 backfill days (the real CLI emits up to
|
||||
* 365 regardless of period) so tests can prove period aggregation while the
|
||||
* trend chart keeps its contiguous 30-day window.
|
||||
* trend chart keeps the real last 30 entries.
|
||||
*/
|
||||
function makePayload(now: Date): MenubarPayload {
|
||||
const DAYS = 30
|
||||
|
|
@ -276,17 +276,21 @@ describe('Overview', () => {
|
|||
expect(screen.getByText('$0.00')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('always shows at least a 30-day window regardless of the selected period', async () => {
|
||||
it('shows the available real history entries without zero-filling a 30-day window', async () => {
|
||||
const now = new Date()
|
||||
getOverview.mockResolvedValue(makePayload(now))
|
||||
const payload = makePayload(now)
|
||||
payload.history.daily = payload.history.daily.slice(-5)
|
||||
getOverview.mockResolvedValue(payload)
|
||||
|
||||
// The daily chart is a trend, not scoped to the period selector: even for a
|
||||
// short period like "week" it renders a contiguous >= 30-day window,
|
||||
// backfilling days with no activity as zero-height bars.
|
||||
// short period like "week" it uses the real history entries and does not
|
||||
// synthesize zero-height bars to reach 30 days.
|
||||
const { container } = render(<Overview period="week" provider="all" />)
|
||||
|
||||
expect(await screen.findByText('parser-service')).toBeInTheDocument()
|
||||
expect(container.querySelectorAll('.chart .col')).toHaveLength(30)
|
||||
const bars = container.querySelectorAll('.chart .col')
|
||||
expect(bars).toHaveLength(5)
|
||||
expect([...bars].map(bar => bar.getAttribute('data-cost'))).toEqual(['5', '5', '5', '5', '6.2'])
|
||||
})
|
||||
|
||||
it('computes month-to-date, projection, and previous-month pace', async () => {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { Panel } from '../components/Panel'
|
|||
import { type Polled, usePolled } from '../hooks/usePolled'
|
||||
import { formatUsd } from '../lib/format'
|
||||
import { codeburn } from '../lib/ipc'
|
||||
import { contiguousDailyWindow, formatChartDate, localDateKey, sliceDailyToPeriod } from '../lib/period'
|
||||
import { formatChartDate, localDateKey, sliceDailyToPeriod } from '../lib/period'
|
||||
import type {
|
||||
ActReportJson,
|
||||
DailyHistoryEntry,
|
||||
|
|
@ -472,7 +472,7 @@ export function OverviewContent({
|
|||
const now = new Date()
|
||||
const stats = deriveStats(data, now)
|
||||
const periodDaily = sliceDailyToPeriod(data.history.daily, period, now)
|
||||
const chartDaily = contiguousDailyWindow(data.history.daily, Math.max(30, periodDaily.length), now)
|
||||
const chartDaily = data.history.daily.slice(-Math.max(30, periodDaily.length))
|
||||
const models = aggregateModels(periodDaily)
|
||||
const recent14 = data.history.daily.slice(-14)
|
||||
const weekNow = mean(recent14.slice(-7).map(day => day.cost))
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ describe('Spend', () => {
|
|||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('renders a contiguous 15-day spend window, date axis, projects, and Sankey ribbons', async () => {
|
||||
it('renders the real last-15 spend entries, date axis, projects, and Sankey ribbons', async () => {
|
||||
getOverview.mockResolvedValue(makePayload(new Date()))
|
||||
getSpendFlow.mockResolvedValue(makeFlow())
|
||||
|
||||
|
|
@ -147,27 +147,17 @@ describe('Spend', () => {
|
|||
expect(screen.getByText('top 2')).toBeInTheDocument()
|
||||
|
||||
const barColumns = container.querySelectorAll('.sbars .c')
|
||||
expect(barColumns).toHaveLength(15)
|
||||
expect(barColumns).toHaveLength(5)
|
||||
expect([...barColumns].map(col => col.getAttribute('data-date'))).toEqual([
|
||||
'2026-06-26',
|
||||
'2026-06-27',
|
||||
'2026-06-28',
|
||||
'2026-06-29',
|
||||
'2026-06-30',
|
||||
'2026-07-01',
|
||||
'2026-07-02',
|
||||
'2026-07-03',
|
||||
'2026-07-04',
|
||||
'2026-07-05',
|
||||
'2026-07-06',
|
||||
'2026-07-07',
|
||||
'2026-07-08',
|
||||
'2026-07-09',
|
||||
'2026-07-10',
|
||||
])
|
||||
const ticks = container.querySelectorAll('.sbars-wrap > .ov-xax span')
|
||||
expect(ticks).toHaveLength(5)
|
||||
expect([...ticks].map(tick => tick.textContent)).toEqual(['Jun 26', 'Jun 30', 'Jul 4', 'Jul 8', 'Jul 10'])
|
||||
expect(ticks).toHaveLength(2)
|
||||
expect([...ticks].map(tick => tick.textContent)).toEqual(['Jun 30', 'Jul 10'])
|
||||
|
||||
expect(container.querySelectorAll('[data-testid="sankey-ribbon"]')).toHaveLength(makeFlow().links.length)
|
||||
})
|
||||
|
|
@ -253,7 +243,7 @@ describe('Spend', () => {
|
|||
expect(container.querySelector('.spend-breakdowns')?.children).toHaveLength(4)
|
||||
})
|
||||
|
||||
it('renders an empty 15-day chart window and empty flow state', async () => {
|
||||
it('renders empty chart and flow states when no daily spend exists', async () => {
|
||||
const payload = makePayload(new Date())
|
||||
payload.history.daily = []
|
||||
getOverview.mockResolvedValue(payload)
|
||||
|
|
@ -261,9 +251,8 @@ describe('Spend', () => {
|
|||
|
||||
const { container } = render(<Spend period="week" provider="all" />)
|
||||
|
||||
expect(await screen.findByLabelText('Daily spend by model')).toBeInTheDocument()
|
||||
expect(container.querySelectorAll('.sbars .c')).toHaveLength(15)
|
||||
expect(container.querySelectorAll('.sbars .s')).toHaveLength(0)
|
||||
expect(await screen.findByText('No model spend in this range yet.')).toBeInTheDocument()
|
||||
expect(container.querySelector('.sbars')).not.toBeInTheDocument()
|
||||
expect(await screen.findByText('No model-project flow in this range yet.')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { StackedBars } from '../components/StackedBars'
|
|||
import { type Polled, usePolled } from '../hooks/usePolled'
|
||||
import { formatUsd } from '../lib/format'
|
||||
import { codeburn } from '../lib/ipc'
|
||||
import { contiguousDailyWindow } from '../lib/period'
|
||||
import type { DateRange, MenubarPayload, Period, SpendFlow } from '../lib/types'
|
||||
|
||||
function EmptyNote({ children }: { children: React.ReactNode }) {
|
||||
|
|
@ -58,7 +57,10 @@ function SpendPage({
|
|||
data: MenubarPayload
|
||||
flow: ReturnType<typeof usePolled<SpendFlow>>
|
||||
}) {
|
||||
const chartDaily = contiguousDailyWindow(data.history.daily, 15)
|
||||
// Use the real last-15 backfilled days directly. Rebuilding a calendar window
|
||||
// client-side (contiguousDailyWindow) mismatched the CLI's date keys and
|
||||
// zero-filled every day, leaving the chart empty.
|
||||
const chartDaily = data.history.daily.slice(-15)
|
||||
const projects = data.current.topProjects
|
||||
const breakdowns = [
|
||||
{
|
||||
|
|
@ -111,7 +113,7 @@ function SpendPage({
|
|||
<>
|
||||
<div className="spend-top-row">
|
||||
<Panel title="Daily spend by model" className="spend-chart-panel">
|
||||
<StackedBars daily={chartDaily} />
|
||||
{chartDaily.length ? <StackedBars daily={chartDaily} /> : <EmptyNote>No model spend in this range yet.</EmptyNote>}
|
||||
</Panel>
|
||||
<Panel title="By project" right={projects.length ? `top ${projects.length}` : undefined} className="spend-scroll">
|
||||
{projects.length ? (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue