diff --git a/app/renderer/lib/period.test.ts b/app/renderer/lib/period.test.ts
index e083dff5..ad331230 100644
--- a/app/renderer/lib/period.test.ts
+++ b/app/renderer/lib/period.test.ts
@@ -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')
diff --git a/app/renderer/lib/period.ts b/app/renderer/lib/period.ts
index 5d65169f..350d2aab 100644
--- a/app/renderer/lib/period.ts
+++ b/app/renderer/lib/period.ts
@@ -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)
diff --git a/app/renderer/sections/Overview.test.tsx b/app/renderer/sections/Overview.test.tsx
index 0ab73b4f..789d16a4 100644
--- a/app/renderer/sections/Overview.test.tsx
+++ b/app/renderer/sections/Overview.test.tsx
@@ -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()
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 () => {
diff --git a/app/renderer/sections/Overview.tsx b/app/renderer/sections/Overview.tsx
index bba6767f..3a823a42 100644
--- a/app/renderer/sections/Overview.tsx
+++ b/app/renderer/sections/Overview.tsx
@@ -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))
diff --git a/app/renderer/sections/Spend.test.tsx b/app/renderer/sections/Spend.test.tsx
index 7fd6d005..e90c1c0f 100644
--- a/app/renderer/sections/Spend.test.tsx
+++ b/app/renderer/sections/Spend.test.tsx
@@ -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()
- 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()
})
diff --git a/app/renderer/sections/Spend.tsx b/app/renderer/sections/Spend.tsx
index 10ec4ab5..17669e86 100644
--- a/app/renderer/sections/Spend.tsx
+++ b/app/renderer/sections/Spend.tsx
@@ -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>
}) {
- 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({
<>
-
+ {chartDaily.length ? : No model spend in this range yet.}
{projects.length ? (