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

100 lines
5.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.