Commit graph

16 commits

Author SHA1 Message Date
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
8d1a10369f fix(app): quota connects via macOS keychain + Connect affordance
Root causes (verified live): the Claude keychain branch was never
enabled (allowClaudeKeychain never passed), security -w returns
hex-encoded JSON the parser rejected, and Codex had no keychain
discovery at all.

- shared readKeychainPassword: Apple-signed /usr/bin/security, 90s
  window so the user can answer the macOS dialog, hex decode,
  notFound vs accessDenied classification; secrets never logged
- Claude: keychain fallback active (gated to explicit Refresh so
  launch never ambushes with a dialog); connection retained across
  keychain-less background polls
- Codex sources in order: the menubar's own cached OAuth (read-only,
  401 = one re-read, never rotate), legacy ~/.codex/auth.json
  (writable), com.openai.codex plaintext if usable; Safe Storage
  never decrypted
- new accessDenied state (amber, 'locked') + ConnectAffordance on
  Plans cards and Settings rows: login command, keychain-Allow note,
  force Refresh

Live verification: Claude connected with its real tier and windows;
Codex correctly accessDenied until the keychain dialog is allowed.

273/273 (26 files), typecheck + build green.
2026-07-16 06:20:44 -07:00
iamtoruk
4b1426eab2 polish(app): consistency review nits
Loading quota label matches sibling skeleton labels (Sentence case);
matchMedia test stub deduped into lib/testMatchMedia.

253/253, typecheck + build green.
2026-07-16 05:11:10 -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
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
916aa6430f harden(app): Electron security + spawn efficiency batch
- will-navigate/window-open denied; openExternal restricted to http(s);
  production CSP drops 'unsafe-inline' scripts (dev-only Vite plugin
  re-adds it for the Fast Refresh preamble)
- single-instance lock; before-quit reaps in-flight CLI children; 16MB
  output cap rejects with CliError 'too-large'
- read-only CLI spawns coalesce: concurrent identical calls share one
  child, a 5s result cache absorbs same-cadence pollers (six sections
  polled getOverview independently); actions bypass and flush the cache
  on completion so post-action refetches are fresh
- every renderer-supplied argv string validated (period/provider/range/
  currency allowlists, no flag-shaped tokens, absolute export path) with
  new CliError kind 'bad-args'; envelope + action stderr routed through
  sanitizeError
- quota force-refresh: manual refresh (refreshToken) passes force which
  invalidates QuotaService's cache; steady polls do not
- POSIX credential mode check gated off on win32

App suite 190/190, build green (production index.html verified strict).
2026-07-16 01:57:44 -07:00
iamtoruk
8a3fa69fe9 fix(app): real currency conversion, provider ids in picker, yield provider scoping
- formatUsd applies the payload currency {code,symbol,rate} once at
  display; formatConverted (symbol only) for CLI-preconverted plan
  values so nothing converts twice
- provider picker built from providerDetails: label shown, internal id
  sent as --provider (fixes filters for providers whose display name
  differs, e.g. Grok Build); falls back to map keys on older CLIs
- getYield threads the active provider through preload/main/sections

App suite 170/170, root 1615/1615.
2026-07-16 01:36:59 -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
a216b841ae feat(app): rebuild Plans on live quota (real 5h/weekly windows + auto-detected tier)
Replace the $-budget model as the primary view: per-provider cards (Claude/Codex)
from getQuota show the real 5-hour/weekly windows (% used, resets-in, severity
colors), the auto-detected subscription tier, connection states, and the credit
footer. Manual budget plans demoted to a secondary section.

typecheck clean; 148/148 tests pass. Needs a full relaunch for live quota.
2026-07-12 16:48:58 -07:00
iamtoruk
144d2b3363 fix(app): strip prose em-dashes from UI copy
Replace em-dashes used as sentence punctuation (Plans, Settings, Compare,
Overview, CliErrorPanel) with cleaner punctuation; keep the standalone "—"
empty-value placeholders (a no-data convention). Update affected copy assertions.

typecheck clean; 128/128 tests pass.
2026-07-12 13:13:31 -07:00
iamtoruk
b0299c57ac feat(app): design reskin — token unification, a11y, and chart color + shading
Design-consistency pass (from the audit):
- unify the indigo→plain grey/border token families (alias --t1/2/3, --hair/2 onto --ink/mut/mut2, --line/2); fix a leftover hardcoded chevron color
- accent :focus-visible rings on SegTabs, chart bars, calendar, buttons, Settings toggles; ov-link hover
- theme-aware Spend legend swatches + Sankey (fixes a light-mode contrast failure)
- wire Models "Compare…" → Compare and Plans "Add plan…" → Settings pane; drop the fake-affordance Plans label; Settings loading captions
- Compare card radius/title, set-input, alias CTA color, outer-border tokens, session mono numbers, frame spacing to match the mockup
- scope a date-flaky Overview test assertion to its card

Reskin batch 1 — color + shade (current tone):
- validated CVD-safe model-series palette via dedicated --s-* tokens (light+dark), applied to the daily bars, Sankey, and series dots
- subtle soft card elevation via --card-shadow on the main card containers

typecheck clean; 128/128 tests pass.
2026-07-12 12:36:50 -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
73fb86dec6 fix(app): Plans — inclusive local cycle end (periodEnd-1d), correct day count, honest near copy
Review fix: periodEnd is exclusive (next reset), so the cycle end now renders periodEnd-1 day
with totalDays=diff (was Jul 15/31, now Jul 14/30); dates format in local time (not UTC);
test mock re-derived from the real emitter. Minor: en/em dashes, CLI provider order, non-
contradictory near-status copy, +near-status and plan-singular tests.

Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
2026-07-10 19:12:42 -07:00
iamtoruk
e1bb78399c feat(app): Plans section — per-plan budget tracks, overage, pace text from getPlans
One track per plan from getPlans() (StatusJson/JsonPlanSummary): fill = percentUsed, red
`.over` past 100% with overage shown, amber `.pace.hot` when over/near pace vs mint `.ok`.
Cycle caption from the plan period; pay-as-you-go rows use `.mut`. Add plan… is M2 (visual).
Honest empty state when no plan is configured.

Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
2026-07-10 19:01:23 -07:00