Full-screen dashboard that shows your Claude Code Pro/Max subscription usage in real time on a TV browser.
Local machine (Claude Code installed) Remote server TV browser
┌────────────────────────────────┐ ┌────────────┐ ┌──────────┐
│ agent — polls ccusage every │─POST/10s→│ server │─WebSocket│ web │
│ 10s, pushes to server │ │ (relay) │────────▶ │ dashboard│
└────────────────────────────────┘ └────────────┘ └──────────┘
| Package | Purpose |
|---|---|
shared/ |
Shared TypeScript types (UsageSnapshot) |
agent/ |
Runs on the Claude Code machine — polls ccusage, pushes to server |
server/ |
Remote-deployable Node HTTP + WebSocket server |
web/ |
Vite + React TV dashboard |
cp .env.example .env
# Edit .env — set AGENT_TOKEN to any random secret
pnpm install
pnpm devOpens:
- Server:
http://localhost:8787 - Web dev server:
http://localhost:5173
There are two equivalent ways to deploy the server. Same code, same build output — just packaged differently.
- Direct (below) — best when you already have Node ≥ 20 on the server, want to integrate with an existing nginx/Caddy/systemd setup, or prefer not to depend on Docker. Smallest moving-parts footprint.
- Docker + Traefik — best for a fresh remote VPS where you want HTTPS in one command. The host needs only Docker; the build happens inside the image, so no Node/pnpm on the host. Trades a Docker dependency for skipping nginx + certbot + Node setup.
In both modes, the agent still runs directly on your Claude Code machine (see Run the agent) — it needs the local macOS Keychain or ~/.claude/.credentials.json, which a remote container can't reach.
pnpm buildCopy server/dist/ and server/node_modules/ to the remote host. Set env vars:
PORT=8787
AGENT_TOKEN=<same-secret-as-agent>Run: node server/dist/index.js
Copy agent/dist/ and agent/node_modules/ to the machine where Claude Code is installed. Set env vars:
SERVER_URL=https://your-server.com
AGENT_TOKEN=<same-secret-as-server>
POLL_MS=10000 # optional, default 10s
AGENT_HOST_LABEL= # optional, defaults to hostnameRun: node agent/dist/index.js
To run as a background service on macOS, create a launchd plist in ~/Library/LaunchAgents/.
Navigate to https://your-server.com in a fullscreen/kiosk browser.
A drop-in alternative to the direct deployment that gives you automatic HTTPS via Let's Encrypt with no extra reverse-proxy setup and no Node/pnpm on the host — the build happens inside Docker.
The Compose stack uses two stock Docker Hub Alpine images:
traefik:v3.3— terminates:80/:443, redirects HTTP→HTTPS, fetches and renews Let's Encrypt certificates via the HTTP-01 challenge.node:22-alpine— base for the server image. The includedDockerfileis a small multi-stage build that installs the workspace, runspnpm build, then ships only the compileddist/and production deps in the runtime stage. The container runs as the non-rootnodeuser with a read-only filesystem (the server is stateless).
- A host with Docker Engine + Compose plugin installed.
- A domain whose A/AAAA record points at the host.
- Ports 80 and 443 reachable from the public internet (Let's Encrypt HTTP-01 needs port 80).
cp .env.docker.example .env.docker
# edit .env.docker — set DOMAIN, LETSENCRYPT_EMAIL, AGENT_TOKEN
docker compose --env-file .env.docker up -d --buildThat's it. Traefik will fetch a cert on first request to https://$DOMAIN and the
dashboard becomes available there.
To check progress:
docker compose --env-file .env.docker logs -f traefik # ACME / cert issuance
docker compose --env-file .env.docker logs -f server # agent ingest hitsThe agent is not containerised — it runs directly on your Claude Code machine exactly as in the direct deployment. Just point it at the Docker host:
SERVER_URL=https://your-domain.com
AGENT_TOKEN=<same-as-.env.docker>Pick Docker + Traefik if any of these apply:
- You're starting from a fresh VPS and don't want to install Node, pnpm, nginx, and certbot just to run this.
- You want HTTPS with auto-renewal handled for you.
- You're comfortable with Docker as a dependency on the server.
Stick with the direct deploy if:
- You already have a reverse proxy (nginx/Caddy/Cloudflare Tunnel) handling TLS.
- You'd rather run a plain
nodeprocess under systemd. - You don't want Docker on the host.
| Var | Default | Where |
|---|---|---|
AGENT_TOKEN |
required | both agent & server |
PORT |
8787 |
server |
STALE_MS |
60000 |
server — ms before snapshot is flagged stale |
STALE_CHECK_MS |
15000 |
server — interval to broadcast stale status |
SERVER_URL |
http://localhost:8787 |
agent |
POLL_MS |
10000 |
agent |
AGENT_HOST_LABEL |
hostname() |
agent |
The dashboard ships with 5 themes, selectable from the menu in the upper right corner.
Layout, card positions and data are identical across themes — only colors, typography
and accents change. Your choice persists in localStorage (cc-usage-theme).
| Theme | Mode | Style | Font |
|---|---|---|---|
| Midnight | Dark | Default — calm navy with blue/teal accents | JetBrains Mono |
| Solar | Light | Clean modern UI, indigo accents, soft shadows | Inter |
| Terminal | Dark | CRT phosphor green, square corners, glow text | VT323 |
| Paper | Light | Warm cream background, brown/olive serif | Lora |
| Synthwave | Dark | Retro neon — deep purple with pink/cyan glow | Orbitron |
Because the dashboard is designed to be left on a TV for hours or days,
it ships with a built-in screen-saver layer that mitigates OLED burn-in
and LED image retention. It runs unconditionally — no toggle, no JS,
pure CSS — and it disables itself automatically when the user has set
prefers-reduced-motion: reduce.
Two complementary techniques are combined:
- Pixel orbiter — the entire dashboard is translated by 1 px on a 4-direction cycle (top-left → top-right → bottom-right → bottom-left), one step per 60 s, full cycle every 4 minutes. This is the exact same principle TV manufacturers ship under the names Pixel Shift (Samsung / Sony) and Screen Shift (LG): static labels and numbers never sit on the same physical sub-pixel for long, so wear is spread out.
- Brief brightness pulse — once per minute the canvas dips to
filter: brightness(0.85)for ≈80 ms (≈5 frames at 60 Hz). Below the threshold of conscious perception but enough to force every LED off its steady-state drive level, breaking the always-on luminance pattern that drives burn-in.
The shift amount is small enough that nothing visibly moves and the 1 px translation never exposes an edge (the page background fills behind the dashboard). Together the two techniques cover both the spatial (pixel position) and temporal (luminance) sources of burn-in described in OLED manufacturer documentation.
If you'd rather opt out (e.g. on a non-OLED display), set
prefers-reduced-motion: reduce in your OS or browser settings.
- Node ≥ 20 on both agent machine and server
npxavailable on the agent machine (used to runccusage@latest)- Claude Code installed on the agent machine (
~/.claude/projects/must exist) - pnpm ≥ 10 for development




