Files
routstrd/docs/plans/tui-history-charts.md

5.1 KiB
Raw Permalink Blame History

TUI "History" tab with adaptive bar charts

Status: implemented. Branch feat/tui-history-charts (worktree .worktrees/tui-history-charts, based on main @ 5069b2d). The dual-axis grouped-bar revision lives on main (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), TimelinePoint and Timeline types; WindowedStats gains a required timeline: Timeline.
  • buildTimeline(driver, baseFilter, window, now, tzOffsetMinutes):
    • today (24h) → aggregate({ groupBy: "hour" }) over the last 24 local-aligned hours. The SDK's hour key is HH with 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 the hour key 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 no after, folded into ISO weeks (Monday start). The SDK has no week grouping, hence the fold.
    • Every series is zero-filled so the x-axis is evenly spaced.
  • buildTimeline is called once per summary build (after buildAggregates) 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 to History; tabSupportsWindow now excludes only Overview and Recent.
  • render.ts — renderToday replaced by renderHistory + 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 fallback windowFromAllTime supplies 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 timeline still 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 an HH label.
  • 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 for 7d/30d. Before this, the 24h totals were calendar-day (since local midnight) while the chart drew a rolling 24h, and 7d/30d totals were exact rolling windows while the chart snapped to whole days.