Commit graph

23 commits

Author SHA1 Message Date
iamtoruk
f26bf69f88 perf(app): cap CLI spawn concurrency, background-priority prefetch, default to today 2026-07-18 16:28:30 -07:00
ozymandiashh
db39157ea6 fix(telemetry): cap cli_error per kind per day, flush app_close on quit
Two robustness fixes from the #741 telemetry audit:

- cli_error had no rate limit: one install emitted 804 timeout events in a
  single day (56% of all events ever received). track() now caps cli_error
  at 20 per kind per calendar day. The day and per-kind counts persist in
  the telemetry state file (defensively loaded), so app relaunches within
  the same day cannot reset the budget; other event names are unaffected.

- app_close almost never fired (1 close vs 48 opens): the quit path raced
  the outbound POST. before-quit now defers quit once, tracks the close and
  flushes with a bounded wait (1500ms race), then quits. Every step of the
  handler is independently guarded so a synchronous failure can neither
  skip the flush nor wedge quitting, and a full queue evicts its oldest
  event rather than dropping app_close. Disabled or not-onboarded telemetry
  quits instantly.
2026-07-18 23:06:30 +03:00
iamtoruk
707d621d7a fix(app): resolve bundled CLI on Windows + enrich cli_error telemetry
path.startsWith('/') POSIX guard rejected the Windows bundled-CLI path
(C:\...\launch.js), so 100% of Windows installs got not-found
(confirmed in telemetry: 84 events, all win32/x64). Use path.isAbsolute
everywhere. cli_error now carries {cmd, kind, detail?} for
self-diagnosing errors, never paths/args/stderr.
2026-07-17 13:51:35 -07:00
iamtoruk
8fc9e303b9 feat(app): update-available notifications
Checks the GitHub releases for newer desktop-v tags once per launch
and every 24h (15s timeout, no identifying headers, offline = silent
retry). A dismissible banner and a Settings About row surface the
newer version and link the release page; dismissal persists per
version so each release nags at most once. No auto-install: unsigned
builds cannot, by platform constraint; the checker only notifies.

App 364/364 (+24), typechecks + build green.
2026-07-17 04:58:13 -07:00
iamtoruk
16e53c2e9d fix(app): one cold_start per launch, attempt-scoped
Coalesced overview polls joined to one stuck cold child each emitted
their own cold_start on the shared settle, measured from their own
start times — producing laddered pseudo-durations in telemetry.
Launch-scoped latch emits at most once, on the first cold attempt's
settle, with that attempt's duration; timedOut wiring and local-day
stamps verified already correct and locked with regression tests.

App 324/324.
2026-07-16 14:26:30 -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
edaa284c0f feat(app): Audit lens on Models + Pricing pane in Settings
- Models gains an Audit lens: raw provider token fields vs displayed
  totals and cost derivation per model, est badge where pricing is
  missing or recomputed cost diverges from attributed
- Settings Pricing pane: list/add/remove price overrides (USD per 1M),
  inline confirm on remove, only provided rates sent; rates validated
  finite and strictly positive on both sides of the bridge
- four new IPC channels with the established validation pattern

Root 1617/1617, app 235/235, build green.
2026-07-16 03:45:36 -07:00
iamtoruk
55fdbfd761 feat(app): Claude config switcher (multi-account overview scope)
- TopBar picker (shown only when >1 Claude config dir exists) threads
  --claude-config-source through getOverview; synthetic 'All Claude
  configs' entry returns to the aggregate default
- config id validation accepts the real <kind>:<16hex> id shape while
  still rejecting flag-shaped input (leading char anchored)
- both incompatibility directions handled: selecting a config resets a
  non-Claude provider filter, and picking a non-Claude provider clears
  the config scope, so the CLI rejection is unreachable
- choice persisted (localStorage); active config shown in the scope
  line and read-only in Settings/General

Verified live against 2 real config dirs. 205/205, build green.
2026-07-16 03:06:05 -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
880f84d593 feat(app): live quota fetch module + getQuota bridge (Phase 4 backend)
Port the menu bar's OAuth quota fetch to the Electron main:
- app/electron/quota/{claude,codex,index,security}.ts reads the local Claude/
  Codex credentials, hits the usage APIs, normalizes to QuotaProvider (5h/weekly
  windows, auto-detected tier, resets-at) with 429 backoff + single-flight
- getQuota IPC bridge (main + preload + renderer types); needs a full relaunch

Security: tokens are never returned or logged (redact Bearer/sk-ant/sk-/JWT,
strip NUL, truncate 240); credential reads refuse symlinks (O_NOFOLLOW/lstat) and
insecure modes; Anthropic refresh is never POSTed (single-use, shared with CLI).

typecheck clean; 147/147 tests pass; Electron build passes.
2026-07-12 16:43:53 -07:00
iamtoruk
905d94253a feat(app): actionable Optimize — wire the CLI's rich findings + copy-paste fixes
- new getOptimizeReport bridge (main + preload + renderer types) running
  `codeburn optimize --format json` (needs a full relaunch)
- restore the Waste / Reverts / Abandoned / Fixes tabs
- Waste findings expand inline to show the explanation (why you're losing
  tokens) + a copy-paste fix block with a Copy button, like the TUI
- summary header: findings count / potential savings / health score

typecheck clean; 133/133 tests pass.
2026-07-12 16:28:47 -07:00
iamtoruk
6f4b2bef38 feat(app): About panel + working social links (+ LinkedIn)
- add a shell.openExternal bridge (main ipc handler + preload + renderer type)
  so links open in the default browser (requires a full relaunch)
- wire the sidebar social icons to real URLs and add LinkedIn
- AboutModal: flame + wordmark, version, tagline, the five social links, and a
  "Check for updates" link to the GitHub releases page

typecheck clean; 128/128 tests pass.
2026-07-12 13:07:38 -07:00
iamtoruk
1de907ac8b feat(app): Settings part 2 — Plans, Export, Devices actions, theme on boot
- Plans tab: add/remove budget presets via `plan set`/`plan reset`, lists
  configured plans, links to the full Plans screen
- Export tab: format + provider + native folder picker (electron dialog)
  → runs `codeburn export`, reports the result
- Devices: real Remove (`devices rm`) + Refresh; visibility is read-only
  from `share status`; pairing shows an honest "pair from terminal" note
  instead of a dead Approve button; combine row is read-only status
- Apply the persisted theme at app startup (not only when General mounts)
- Settings gets onNavigate for the "Open Plans" link

Only one-shot, non-interactive CLI commands are wired as buttons; the
interactive share daemon and pairing flow stay honestly read-only.
2026-07-11 17:31:32 -07:00
iamtoruk
55e9fa51f6 feat(app): functional Settings — rail nav + General/Providers/Aliases/Privacy
Settings was a dead shell (rail didn't switch, all buttons disabled).
Now the rail navigates and four tabs are wired to the real CLI:
- action bridge: spawnCliAction + runAction (text output, not JSON) with
  getAliases/getProxyPaths reads and setCurrency/resetCurrency/addAlias/
  removeAlias actions
- General: theme switcher (System/Light/Dark, persisted) + live currency
  (reads status, changes via `currency`) + default-period preference
- Providers: real detected-tools list with spend
- Model aliases: full add/remove against real config
- Privacy: honest local-only info

Devices content preserved as-is; Plans/Export are placeholders for Part 2.
2026-07-11 17:24:04 -07:00
iamtoruk
6023067bdc feat(app): bridge for sessions/compare + dev-mode CLI fallback
Adds getSessions / getCompareModels / getCompare IPC channels (argv for
the new CLI JSON emitters), mirrors SessionRow + compare-stats types into
the renderer, and — in Vite dev — resolves the repo's own dist/cli.js so
newly-added commands work without setting CODEBURN_BIN.
2026-07-11 12:37:04 -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
95cf876999 feat(app): plain theme + Overview reskin wired to real data
New plain.css token layer (light+dark) repaints the whole app shell;
Overview.tsx rebuilt to the locked mockup: hero burn with count-up +
streak, budget fuel ring (getPlans), coach insight, Saved-to-date
(act report), area sparklines, gridline + hover-tooltip daily chart,
sessions. Adds getActReport bridge plumbing. All data real/derived;
per-day tooltip shows calls (no per-day sessions field). typecheck
clean, 82/82 vitest pass.

Note: Saved-to-date needs a codeburn build with 'act report --json'
(source-only today; installed 0.9.15 lacks it) — falls back to $0.
2026-07-11 08:32:11 -07:00
iamtoruk
f362195946 fix(app): final-review nits — app menu (⌘R refresh), honest model labels, yield retain, hint cleanup
Install an app menu without the CmdOrCtrl+R reload accelerator so ⌘R reaches the renderer for
in-place refresh (keeps Edit/Window roles + dev DevTools). Model labels are now family-level
("Opus"/"Sonnet"/"Haiku"/"GPT / Codex") and the Sankey shows the real model id instead of the
wireframe's sample version names. Optimize retains last-good yield across 30s revalidation (no
flicker to "—"). Removed hints for unimplemented ⌘K/⌘E/esc; fixed stale TopBar comment.

Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
2026-07-10 19:54:09 -07:00
iamtoruk
dc52b3226b feat(app): Settings/Devices section + getDevicesScan bridge
Settings rail (Devices active) with This device (getIdentity), Discovered nearby
(new getDevicesScan bridge → `codeburn devices scan --format json`), and Paired
(getDevices perDevice). Pairing/approve/pull/visibility/combine are M1 visual
affordances (mutations = M2); share status is process-local so not shown as
authoritative. Settings uses a title-only bar (no period/provider).

Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
2026-07-10 19:17:14 -07:00
iamtoruk
b36253d2e2 fix(app): resolve usePolled stale-response race + review nits
Review of T0 (Electron scaffold) approved with one Important finding + cheap
Minors. All fixes scoped to app/.

- usePolled: replace per-call cancel closure with a generation/epoch counter so
  an orphaned in-flight fetch (from refresh() or an interval tick, discarded
  cancel handle) can't resolve after a newer fetch and clobber fresh data. New
  TDD race test (slow deps-A resolves after fast deps-B; keeps B).
- cli: nvm resolution now scans version dirs descending and takes the first
  whose bin actually contains codeburn (was lexicographic max, unverified),
  matching CodeburnCLI.swift. Export nodeManagerDirs + hermetic nvm test.
- index.html: tighten CSP connect-src ws: -> ws://localhost:5173.
- preload: import type { Envelope } from main instead of redeclaring it.
- main.test: table-driven channel->argv assertion over all 9 codeburn:* channels
  plus a keys check and a non-spawning cliStatus case.
2026-07-10 15:40:11 -07:00
iamtoruk
e22e5511ba feat(app): electron shell, 30s timer, and typed IPC bridge
- main.ts: 1200x820 BrowserWindow (contextIsolation:true,
  nodeIntegration:false, sandbox:true), loads the Vite dev URL or the
  built index.html, registers one ipcMain.handle per CodeburnBridge
  channel, and starts a 30s tick that emits `codeburn:refresh`.
  createBridgeHandlers() is a pure, injectable channel→argv map so the
  spawn→IPC wiring is unit-testable without a GUI (main.test.ts).
- preload.ts: exposes window.codeburn (CodeburnBridge, exactly) and
  window.codeburnEvents.onRefresh via contextBridge. Handlers return an
  {ok,value|error} envelope so the structured CliError kind survives the
  world boundary.
- types.ts: mirrors MenubarPayload/DailyHistoryEntry/DailyModelBreakdown
  and the other CLI payload types verbatim, plus the exact CodeburnBridge
  interface and Period union.
- ipc.ts: typed 1:1 wrapper + normalizeCliError.
2026-07-10 15:17:03 -07:00