Commit graph

17 commits

Author SHA1 Message Date
Resham Joshi
dbd93fee51
app,dash: render pre-history days as no data, not a currency zero (#765)
* fix(app,dash): render pre-history days as no data, not a currency zero

Days before the first recorded day in history.daily were painted as a real
0.00/0 calls in the daily activity heatmap and the daily spend charts. That
zero is unknown, not measured, so those cells and bars now read "No data
recorded" instead of a currency zero. Genuinely idle days within recorded
history stay as real zeros.

- app: heatmap cells, the daily spend bars, and the daily-by-model columns
  get a distinct no-data style plus a "No data recorded" hover for days before
  the first recorded day.
- dash: the granular line chart drops buckets before the first recorded day so
  no flat zero line is drawn before any history exists.
- data-start is derived in the UI layer from history.daily; payload shape is
  unchanged.

Verified: app vitest 418 pass (7 new for the no-data behavior); app, dash, and
root typecheck clean; dash and app renderer vite builds succeed.

* no-data: cap guard, timezone-free dash trim, StackedBars aria

Three hardening fixes from adversarial review of the no-data rendering:

dataStartKey returns null at the payload's 365-entry history cap, where
the oldest retained entry is no longer the true data start; classifying
past it would label real aged-out history as no data on long custom
ranges. Documented that the install-to-first-use gap reading as no data
is literally accurate: nothing was recorded then either.

The dash granular chart now trims leading zero-only buckets by value
instead of comparing bucket timestamps against date keys, so producer
and viewer timezone skew can never drop a real first-day bucket, and an
all-zero series lands in the established empty state.

StackedBars no-data columns expose their state via aria-label, not only
a title on a non-focusable div.
2026-07-20 08:03:24 -07:00
iamtoruk
300e949951 feat(app): refresh cadence, instant switching, cache coherence, telemetry v1
Performance and coherence:
- Settings > General 'Refresh every' (Manual/30s/1m/3m/5m/10m), live via
  RefreshCadenceContext; Manual polls only on demand
- usePolled memoKey LRU: provider/period switches paint the last-good
  result instantly with a switching hairline while refreshing quietly
- quota TTL 5min + honest rate-limited copy on 429 backoff
- version-suffixed cache files (session-cache.v5.json, daily-cache.v12,
  auto-minted on future bumps); legacy files never written or deleted,
  adopted once when versions match: old and new binaries coexist
  without clobbering (field-observed menubar-vs-desktop ping-pong)
- advisory hydration lock: concurrent cold starts share one scan
  (wait-then-read-warm), stale/dead locks self-heal, never a
  correctness gate

Telemetry v1 + onboarding (desktop only, per owner decisions):
- first-launch onboarding (3 feature screens + consent); region-split
  default (EU/EEA/UK/CH off, elsewhere on, unknown off); nothing sends
  before consent completion or while off; dev builds never send
- anonymous install UUID, rotated on opt-out; day-granularity events,
  cost buckets only; whitelisted names; 200-event queue, 5min flush
- Settings > Privacy live toggle replaces the static claim

Zero computed-number changes. App 316/316, root 1805.

Wire contract targets api.codeburn.app/v1/telemetry (Worker follows).
2026-07-16 11:47:05 -07:00
iamtoruk
55bc53e6be fix(app): cold start survives, first-run shows per-provider progress
Root cause of the field-reported 45s timeout + perpetual slowness: on
a cold cache all six sections spawned different CLI subcommands at
once, each running its own full-history parse (~11-31s, ~3GB RSS),
contending past the 45s kill so the rebuilt cache never persisted and
every poll restarted from zero.

- the first overview fetch runs as a warmup: 10-minute timeout,
  re-arms until it succeeds, reverts to 45s after; section polls gate
  on that first resolution (usePolled gains enabled), so cold
  hydration happens exactly once
- warmup streams the CLI's progress protocol; the splash shows 'First
  run: indexing your usage history' with a per-provider ingest list
  (logo, live counts, check on done), static under reduced motion,
  generic fallback without events; warm launches unchanged
- overview spawns pass --no-timeline (desktop never renders it)

Measured on real data: cold 31s once (was: killed at 45s forever),
warm 2.6s. 295/295 app, 1797 root.
2026-07-16 10:51:18 -07:00
iamtoruk
daf944e714 feat(app): motion layer (gsap) — count-ups, chart grow-in, section fade, skeletons, toasts
- one gate (motionEnabled): off under reduced-motion, missing matchMedia,
  and vitest; every path checks it first
- mount/filter-change only: count-up and bar grow-in key off the
  period|provider|range key, so 30s poll refreshes snap values silently
  instead of re-animating
- first-load skeleton shimmer replaces bare scanning text (kept sr-only
  for screen readers); slide-in toast host for Settings/export feedback
  (validation errors stay inline); CSS hover-lift + press micro-
  interactions, all with a reduced-motion escape hatch
- gsap 3.15.0 + @gsap/react 2.1.2 (+74KB raw JS; G4 flame work shares it)

244/244, typecheck + build green.
2026-07-16 04:39:09 -07:00
iamtoruk
0eb0fd60d7 polish(app): spacing scale, unified insets, shared banner classes, scrollbar cues
- --sp-1..7 scale; card headers unified on 10x14, bodies on 12x14;
  orphan gaps snapped to rungs
- CapsuleChart's orphaned CSS deleted
- StaleBanner/budget/empty/range notes moved from inline styles to
  plain.css classes; Panel bodyStyle prop retired (scroll-x class)
- thin always-visible scrollbars on scrolling panels and overflow-x
  tables; Compare pickers flex below 760px instead of overflowing
- one capitalization fix (loading quota…)

235/235, build green.
2026-07-16 04:02:21 -07:00
iamtoruk
f8b2894b4e feat(app): Sessions provider quick-filter + Spend project inline expand
- Sessions: All + per-provider logo buttons (detected, cost > 0) that
  drive the same app-level provider state as the top-bar dropdown, so
  the two controls can never disagree; aria-pressed group semantics
- Spend By project: rows expand inline (single-open, aria-expanded,
  chevron rotation) to the project's sessions: date, top model, calls,
  currency-aware cost; date-only strings parsed at local noon so days
  never roll across time zones

194/194, typecheck + build green.
2026-07-16 02:49:17 -07:00
iamtoruk
015a1a069a polish(app): shared empty states, type ladder, AA contrast, honest affordances, a11y semantics
D1 one shared EmptyNote (7 copies removed); CliErrorPanel on canonical
   tokens; dead code deleted (CapsuleChart, stale SERIES_HEX palette,
   Stat tone API, unused rail/doc-chrome CSS)
D2 type ladder tokens (--fs-*/--fw-*); panel titles unified 13/600/ink;
   high-visibility sizes/weights snapped to rungs
D3 contrast: --mut2 5.00:1 light / 5.74:1 dark (was 2.37/3.11); new
   --accent-text 5.07:1 + persistent underline on text links; last
   prose em-dash removed
D4 'See all' and 'Most expensive sessions' navigate to Sessions; 'add
   alias' jumps to Settings/Aliases; chevrons only on clickable rows;
   inline confirm replaces window.confirm; default-period setting
   honored at boot; Sankey caption no longer promises a click; Optimize
   'improving' trend badges; cursor + Space-key fixes
D5 aria-current on nav, SegTabs as real tablist, aria-pressed toggles,
   ProviderPop rebuilt on Dropdown (one tab stop, arrow keys, focus ring)

191/191, typecheck + build green. Contrast ratios independently
recomputed. Net -93 lines.
2026-07-16 02:27:41 -07:00
iamtoruk
cbddd3a9d7 fix(app): accuracy batch A — provider scoping, honest chart axes, range threading, stale-error banner
- Provider filter: Models-this-period sources current.topModels (history
  days carry empty topModels per provider); StackedBars falls back to a
  cost-proportional single segment like the Swift menubar
- Chart windows align to the CLI (week -7, 30days -30, all = 6 months)
  and zero-fill a contiguous calendar window; history.daily is sparse and
  both sides key local YYYY-MM-DD, so lookups are safe (verified on real
  CLI output)
- Custom range threads into Overview panels (chart, models table) and
  suppresses MTD/projected and vs-last-week comparisons; Compare states
  that custom dates fall back to the period
- usePolled clears errors per attempt and sections show a StaleBanner
  instead of silently rendering last-good data after a failed refresh
- Cache-hit denominator matches the CLI; hero savings relabeled 'Saved by
  applied fixes' + new 'Saved via local models' line; formatCompact
  everywhere; Optimize 'Fixes' count matches the rendered list

App suite 163/163 (was 147), root 1613/1613.
2026-07-16 01:14:41 -07:00
iamtoruk
a8351bfd1a 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.
2026-07-12 17:06:46 -07:00
iamtoruk
82f0402abd feat(app): Spend chart — fixed 15-day window + date axis, no gridlines
- extract contiguousDailyWindow + formatChartDate to lib/period (shared)
- Spend "Daily spend by model": fixed contiguous 15-day window (zero-filled)
  with an .ov-xax date axis, matching the Overview chart
- remove the horizontal gridlines from both charts (they read like data lines)

typecheck clean; 129/129 tests pass.
2026-07-12 16:21:41 -07:00
iamtoruk
185f25f8d1 feat(app): fixed-height + scrollbar on Spend list panels
By-project and the Activity/Tools/MCP/Subagents breakdown panels cap at 300px
with a slim internal scrollbar, so they stay a consistent compact size instead
of growing unbounded with the data.

typecheck clean; 126/126 tests pass.
2026-07-12 15:56:07 -07:00
iamtoruk
bd32624afb feat(app): flatten Spend to one page (no tabs)
Drop the Projects/Activity/Tools/MCP/Subagents lens tabs. Everything shows on
one scrolling page: chart + By-project, Cost-flow Sankey, then the breakdown
sections in a responsive grid. Empty sections are hidden. All reflects the
selected date range.

typecheck clean; 128/128 tests pass.
2026-07-12 15:30:00 -07:00
iamtoruk
1efc0ceb44 fix(app): daily-spend chart fills its card, bars bottom-aligned
The StackedBars chart lived in a fixed 96px band, so when the grid
stretched its card to match the taller "By project" panel the bars
floated at the top over a large empty gap. Make the panel/body flex so
the chart fills the card height (min 120px) with bars bottom-anchored;
round stacked-segment corners only at the top/bottom of each column.
2026-07-11 17:06:00 -07:00
iamtoruk
d0ce20e49c feat(app): working periods, detected-provider dropdown, drag-select date range
- Period seg now uses the 5 real CLI periods (Today/7D/30D/Month/6M);
  6M maps to 'all' (matches the menubar).
- Provider control is a real dropdown listing only DETECTED providers
  (accumulated from current.providers so it never shrinks) + All.
- New RangeCalendar: month grid with click-and-drag range selection
  (+ two-click fallback, future-date disabling) behind a calendar button;
  commits a custom {from,to} threaded to Overview/Spend/Models/Optimize
  via new --from/--to CLI args (Plans/Settings stay period-only).
- typecheck clean, 87 vitest pass.
2026-07-11 09:14:54 -07:00
iamtoruk
9e6ea306fe feat(app): T8 integration + cleanup — single refresh, ⌘-nav, shared error/format/period/series, README
- One 30s refresh + manual ⌘R; footer shows real last-fetch time; getOverview fetched once (no double-poll).
- Keyboard nav: ⌘1–⌘5/⌘, switch sections, ⌘R refresh.
- Verified period/provider re-polls all sections.
- Shared CliErrorPanel: amber permission ("Full Disk Access") vs red error, used by every section.
- DRY: shared lib/format.ts (USD), lib/period.ts (Overview migrated to it), lib/modelSeries.ts (unified model→series).
- Settings review-minor tests (!paired filter, empty/not-found states) + App.test.tsx.
- app/README.md incl. the approved M2 delivery model (self-contained, bundles engine, Electron auto-updater).
- 76 tests, typecheck clean. Live GUI smoke (npm --prefix app run dev) still pending a display.

Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
2026-07-10 19:35:41 -07:00
iamtoruk
5cd5c783a2 test(app): Spend coverage (lenses, states, colors, Sankey, period windows) + proportional/labeled Sankey
Addresses T3 review: full test coverage for all lenses + empty/error states, segment
color mapping, Sankey ribbon properties, and lib/period.ts across all windows; Sankey
ribbon widths now truly proportional (removed the 28px cap) and labels are pretty/basename
+ truncated so real ids don't clip; legend reflects present models; honest "top N" project count.

Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
2026-07-10 18:30:17 -07:00
iamtoruk
d7f3ccd1f0 feat(app): Spend section — daily-by-model stacked chart, by-project, model→project Sankey
Period-sliced StackedBars (history.daily[].topModels within the selected window via
a new shared lib/period.ts), the By-project list, and a Sankey rendered dynamically
from getSpendFlow (per-model hue ribbons). Lens tabs; series palette shared with the
legend. Adds a neutral .s-other class for the rollup segment.

Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
2026-07-10 16:43:27 -07:00