5.1 KiB
TUI "History" tab with adaptive bar charts
Status: implemented. Branch
feat/tui-history-charts(worktree.worktrees/tui-history-charts, based onmain@5069b2d). The dual-axis grouped-bar revision lives onmain(uncommitted at the time of writing).
Goal
Replace the old Today tab (a totals box + a text "Recent Days" list + a
horizontal hourly bar list) with a History tab that draws usage over time
as bar charts, with the bucketing driven by the existing [W] time-window
selector.
[W] window |
Bucket | Bars |
|---|---|---|
24h |
hour | 24 |
7d |
6h | 7 × 4 |
30d |
day | 30 |
All |
week (ISO, Monday) | one per week since first usage |
Each bucket draws two grouped bars — orange spend (left axis, sats) and cyan tokens (right axis, tokens) — flush inside the pair with a gap between buckets. A headroom row holds the peak ticks above the tallest bar so blocks never appear to overshoot the axis; both axes round up to a readable ceiling (1/2/5 × 10ⁿ) rather than the exact peak. The summary is a single compact line and the chart grows to fill the terminal height.
Backend — src/daemon/http/usage-summary.ts
- New
TimelineBucket(hour|day|week),TimelinePointandTimelinetypes;WindowedStatsgains a requiredtimeline: Timeline. buildTimeline(driver, baseFilter, window, now, tzOffsetMinutes):today(24h) →aggregate({ groupBy: "hour" })over the last 24 local-aligned hours. The SDK'shourkey isHHwith no date, so the window is aligned to whole hours (each hour-of-day appears at most once).7d→ 6-hour buckets (7 × 4), built from per-day hourly aggregates and then folded. Because thehourkey has no date, each local day is aggregated over its own[start, end)range.30d→aggregate({ groupBy: "day" })over the last 30 days.all→aggregate({ groupBy: "day" })with noafter, folded into ISO weeks (Monday start). The SDK has noweekgrouping, hence the fold.- Every series is zero-filled so the x-axis is evenly spaced.
buildTimelineis called once per summary build (afterbuildAggregates) and attached to both the all-time and windowed blocks. The zero-data short-circuit returns an empty timeline.
Historical timezone handling
The monitor sends its IANA timezone (for example America/New_York) alongside
tz on /usage/summary. Invalid timezone identifiers return HTTP 400. When
timezone is absent, the daemon retains fixed-offset behavior for older clients.
IANA-aware calendar arithmetic uses historical offsets, not today's offset:
7d/30d start at midnight 6/29 local dates ago. The 7d series retains 28
wall-clock six-hour buckets; a DST transition can make a bucket cover five or
seven elapsed hours. The 24h view retains 24 elapsed-hour buckets and counts
both occurrences of a repeated hour. Weekly buckets begin on local Mondays.
Overview day/hour grouping also uses historical local dates.
The SDK supports only fixed-offset grouping, so IANA chart/overview grouping is performed locally over driver entries (bounded for selected windows; unbounded for all-time). Non-calendar totals still use SDK aggregates. Timezone identity is included in the summary cache key.
TUI
types.ts—TabId"today"→"history"; re-export the timeline types.constants.ts— tab renamed toHistory;tabSupportsWindownow excludes only Overview and Recent.render.ts—renderTodayreplaced byrenderHistory+renderTimelineChart(dual-axis grouped bars, y-axis gutters, x labels, legend). Buckets are merged when they no longer fit; the chart height is derived from the viewport so the tab fills the terminal.app.ts— passes the content height into the tab renderer and pads the content so the footer stays pinned to the bottom.data.ts— the old-daemon fallbackwindowFromAllTimesupplies an empty timeline and marks selectable windows unsupported. The selection/cache is reset to all-time; window cycling is disabled with an upgrade notice.
Tests
src/daemon/http/usage-summary.test.ts— hourly / 7-day-hourly / 30-day / week-folded timelines, including zero-fill and window totals.src/tui/usage/render.test.ts— dual-axis grouped bars, labels, legend, empty state, height-filling, and full-width alignment of every line.
Notes / limits
- Daemons too old to return
timelinestill render: the History tab falls back to an empty chart rather than erroring. - The 24h window is hour-aligned rather than a rolling
now - 24h, so each elapsed-hour bucket is separate; repeated DST hours may share anHHlabel. - The windowed aggregates use the same bucket-aligned boundary as the chart
(fixed-offset or IANA-aware), so a tab's totals always equal the sum of its bars:
24 whole hours for
24h, whole local days for7d/30d. Before this, the24htotals were calendar-day (since local midnight) while the chart drew a rolling 24h, and7d/30dtotals were exact rolling windows while the chart snapped to whole days.