The dashboard Cache read/write cards and the `codeburn devices` summarizer
summed the full 365-day history.daily backfill instead of the selected period,
so shorter windows over-counted (today ~197x on real data). Every other metric
already reads the period-scoped `current` block.
buildMenubarPayload now emits period-scoped cacheReadTokens/cacheWriteTokens on
`current` (from PeriodData, alongside inputTokens/outputTokens); the web cards
(single + combined views) and summarizeOneDevice read those instead of reducing
history.daily. The trend chart still uses history.daily. Older peers that omit
the fields fall back to 0 (web) or the windowed daily sum (CLI).
Adds regression tests for both surfaces.
The web server is long-lived, so cache what the per-invocation CLI cannot:
- Cache the parsed local payload in-memory (single-flight, 180s TTL matched to
the parser session cache, expired entries pruned on write). /api/usage and the
local half of /api/devices now return from a Map hit after the first parse.
- Prewarm today at startup and inline that payload into index.html as a
bootstrap, so the SPA paints today's numbers with no round-trip. Only the
local device is embedded; '<' is escaped so a name cannot break the script
tag; served no-store; the seeded view refetches at once so live peers appear.
- Default the web command and dashboard to today.
- Cap the peer connect phase at 3s. req.setTimeout does not abort a stalled TCP
connect, so an offline paired device hung ~75s on the OS timeout; it now
degrades to an unreachable row in ~3s. The cap clears on TCP connect, so the
TLS handshake and the 65s pairing-approval wait are unaffected.
Measured: /api/usage 0.0007s warm (was ~0.22s), /api/devices ~3s with an offline
peer (was ~75s), first paint instant.
* feat(menubar): expose combined multi-device usage in menubar-json (#566)
The macOS menubar reads `codeburn status --format menubar-json`, which only
ever reflected the local machine. Multi-device "Combined" usage existed only in
the terminal `devices` command. This exposes combined + per-device usage through
the menubar-json contract so the menubar can mirror the combined dashboard.
- Extract the per-device summary + combined reduce that was inline in
renderDevices into a reusable pure `summarizeDeviceUsage(results, window?)`.
Error/unreachable devices are excluded from the combined sums (kept in
perDevice); deviceCount counts all, reachableCount counts the reachable ones.
renderDevices now formats from it with byte-identical output.
- Add an optional `combined?: CombinedUsage` block to MenubarPayload (perDevice
list + combined totals incl. calls/sessions). Absent by default.
- Add `--scope local|combined` to `status` (default local). `combined` builds the
local payload, pulls paired devices (pullDevices isolates per-peer failures),
and attaches the summary.
- Correctness guards: reject `--scope combined` with `--days` (non-contiguous,
not representable over the sharing query) and with `--provider`/`--project`/
`--exclude` (the sharing query carries no filters, so peers would report
unfiltered usage). Window-scope the cache-token sum to the selected period
(cache lives in 365-day daily history; current carries no cache counts).
TS/CLI only. The menubar Local/Combined toggle + render is a follow-up.
* fix(menubar): never let combined enrichment break the base local payload
The status --format menubar-json --scope combined path pulls paired devices.
Wrap that best-effort enrichment in a guard so an unexpected failure (corrupt
remotes store, peer I/O) can never take down the base local menubar payload —
on error the local payload is still emitted with combined omitted. Add a test
that a corrupt remote-devices.json still yields a valid combined (local-only) payload.
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
* fix(web): reject invalid dashboard periods without exiting
* test(web): assert invalid periods return 400 without exiting; drop redundant /api/devices re-parse
- Add tests/web-dashboard.test.ts: boots the dashboard on an ephemeral port and asserts
/api/usage and /api/devices answer 400 (not process.exit) for a bad period, and that
the server keeps serving afterward. runWebDashboard now returns the http.Server so it
can be exercised in-process; callers that ignore the return value are unaffected.
- /api/devices: resolve periodInfo once instead of validating then re-parsing it inside
localGetUsage (pullDevices invokes localGetUsage with the same already-validated query).
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
* feat(web): device discovery + pairing endpoints for the dashboard
Add /api/identity, /api/devices/scan (mDNS browse with confirm code and
paired status), and /api/devices/pair (approve-style pairing via
linkRemote). Backs the Search local devices button so pairing can happen
from the browser instead of the CLI.
* feat(web): two-panel dashboard with eywa-inspired warm theme
Redesign the dashboard into a left sidebar (device switcher + Search
local devices) and a right content panel, restyled to a warm-paper,
forest-green archival theme (serif display numbers, ink text, hairline
borders) in place of the dark midnight theme. The Search local devices
modal discovers nearby devices over mDNS and pairs in one click (approve
on the other device), backed by /api/devices/scan and /api/devices/pair.
* feat(web): use the CodeBurn flame logo in the dashboard header
Replace the placeholder triangle with the flame mark (the repo's
codeburn-symbolic vector), tinted the theme's forest green, and set the
same flame as the favicon.
* feat(web): color the All-devices chart by device, add task-level combined views
In the All devices view the daily chart now stacks by device (one color
per device) instead of by model, and the combined panels add a By task
breakdown plus in/out token detail. Devices that cannot be reached are
hidden entirely rather than shown as error rows.
* feat(web): use the flame image as the dashboard logo and favicon
Replace the inline vector flame with the brand flame PNG
(public/codeburn-flame.png) in the header and as the favicon.
* fix(web): trim flame logo padding and tighten brand spacing
Crop the transparent border off the flame PNG (it was 184px of padding
each side) and reduce the header gap so the mark sits close to the
wordmark.
* feat(web): cost/tokens toggle, cache read/write, full feature panels
Add a Cost/Tokens unit toggle that switches the hero number and the chart
between dollars and tokens. Surface cache write/read token totals as
metric cards. Add per-device panels for subagents, skills, MCP servers,
and a savings / retry-tax / routing-waste summary. The combined view
gains cache totals and the unit toggle too.
* feat(web): use CodeBurn website logos and add community links
Use the website's navbar flame (logo.png) with the Code+Burn wordmark in
the header, and the three-flame app icon (icon.png) as the favicon.
Add Website, Discord, and X links to the sidebar footer.
* fix(web): use the single-flame logo for the favicon too
The three-flame icon was wrong for the tab; use the same single flame as
the navbar everywhere and drop the unused three-flame asset.
* chore(web): set dashboard title to CodeBurn - Local Dashboard
* harden sharing + dashboard for public launch
Security:
- pairing: cap PIN attempts (close window after 5 wrong guesses) so a
6-digit PIN cannot be brute-forced within the TTL on a 0.0.0.0 listener.
- web dashboard: reject non-loopback Host (defeats DNS rebinding that
could read unsanitized local usage) and cross-origin requests (CSRF);
require application/json on the pair endpoint.
- store tokens with 0600 perms (was world-readable), 0700 dir.
Robustness:
- client: per-request timeout so a hung/asleep peer cannot hang a pull;
pullDevices fetches remotes concurrently and isolates failures.
- dashboard: normalize peer payloads at the boundary and add an error
boundary so a peer on a different version cannot white-screen the SPA;
finite-guard fmtNum/compactUsd.
Tests: PIN attempt-cap test added (1269 pass).
* feat(web): share this device from the dashboard, with browser approval
Add a Share this device toggle to the dashboard sidebar. It runs the
secure share server in-process (mTLS + mDNS advertise) so no terminal is
needed, with a Keep sharing always option (persisted; otherwise it stops
after idle). Incoming approve-style pairings are queued and surfaced in
the browser as an Approve/Deny prompt with the matching code, instead of
a terminal prompt. The shared payload is sanitized; start degrades
gracefully if the port is already held by a CLI share.
* fix(sharing): keep a paired device from dropping on a transient pull
- client: 8s -> 15s timeout and a fresh socket per request (agent:false) so
the pinned-fingerprint check always reads this connection's cert.
- share server: wrap request handling so a getUsage error returns a fast
500 instead of hanging the caller (which times out and drops the device).
- dashboard: re-pull paired devices every 20s so a brief drop self-heals
instead of staying gone until you change tabs.
* fix(sharing): re-sanitize remote payloads on receipt
A peer might run an older build that does not strip its own project
names/sessions. Sanitize every remote payload when we receive it too, so
project names never cross into our dashboard regardless of the sender's
version. Aggregate numbers (cost, tokens, models, tools, daily) are kept.
* harden dashboard sharing: version-skew, lifecycle, server errors
- normalize remote daily-history entries so a peer on an older build (daily
rows missing topModels) can no longer crash the chart / brick the page.
- ShareController: commit always/sharing state only after listen() binds, so
a port-in-use start no longer reports sharing when it is not.
- attach durable error/tlsClientError handlers to both HTTPS servers so a
malformed peer connection cannot crash the process.
- reset view/provider selection when the viewed device disappears or lacks
the selected provider (no more empty/no-selection state on sleep-wake).
- by-device chart: stable per-device color/key so bars do not reshuffle when
a device drops or returns between polls.
* polish: device identity, approval guard, queue cap, formatting
- give each device a stable unique id (cert fingerprint for remotes,
'local' for this device); key the sidebar, selection, and by-device
chart by id so two devices sharing a hostname no longer collide.
- approval prompt: drop a request from the UI the moment it is answered
so it cannot be double-clicked.
- share controller: cap concurrent pending approvals and allow only one
per device, so a LAN peer cannot flood the prompt.
- usd(): render negatives as -$5.00, consistent with compactUsd.
* feat(sharing): pairing, token, and device-identity core
First piece of local device sharing: self-cert fingerprint identity
(trust-on-first-use), a one-time expiring pairing PIN, fingerprint-bound
tokens, and a peer store that authorizes a pull only when both the token
and the TLS peer fingerprint match the same paired device. Pure logic,
fully unit-tested; the TLS share server and host pull build on this.
* feat(sharing): secure mutual-TLS transport + pairing handshake
Add device identity (self-signed cert, persisted; fingerprint = sha256 of
the cert DER), an HTTPS share server (mutual TLS: presents its cert, reads
the client's, and serves /api/usage only when the bearer token AND the
client fingerprint match the same paired peer), a one-time-PIN pairing
endpoint, and a fingerprint-pinning client. Verified end to end on
loopback: PIN pairing, pinned authed pull, and rejection of a wrong PIN,
a token replayed from another device, and a mismatched server
fingerprint. Adds the selfsigned dep (Node cannot generate certs natively).
* feat(sharing): share + devices CLI (pair, pull, combine)
Phase 3 terminal flow: codeburn share runs the secure server on-demand
(stops after 10 min idle; --always to persist, --pair to add a device),
and codeburn devices add <host> --pin <pin> pairs and pins a remote.
codeburn devices pulls this machine plus every paired device, keeps each
separate, and prints a per-device table with a simple summed Combined
row (no server-side merge). Persists identity and peers under the config
dir. Host pair-and-pull flow covered by a loopback integration test.
* feat(sharing): mDNS discovery + approve-style (no-PIN) pairing
Add bonjour-service discovery (advertise/browse over the LAN), a short
confirmation code derived from both cert fingerprints (Bluetooth-style
'do these match?' check), and an approve-style pairing endpoint that
prompts the owner instead of requiring a typed PIN. Loopback-tested:
approved device gets a working token with a matching code on both sides,
declined device is rejected.
* feat(sharing): AirDrop-style discover + approve UX
codeburn share now advertises on the LAN and approves incoming devices
interactively (confirm the matching code, no typed PIN). codeburn devices
add (no args) discovers nearby devices, lets you pick one, shows the
confirmation code, and waits for the owner to approve. Manual
add <host> --pin stays as a fallback for networks that block mDNS.
* feat(sharing): share only aggregates, never project names or paths
Sanitize each device's payload before it leaves the machine: drop
topProjects and topSessions (project names + session detail) and send
only aggregate numbers (cost, tokens, models, tools, activities, daily).
What you are working on stays local; only the totals travel.