No description
  • JavaScript 63.2%
  • CSS 25.2%
  • Dockerfile 5%
  • HTML 3.6%
  • Just 3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-09 09:21:05 +02:00
.woodpecker feat: containerize for incus deployment 2026-09-08 21:32:21 +02:00
.gitignore refactor: untrack config.js behind config.example.js 2026-09-08 17:47:34 +02:00
app.js feat: error sprite, and stop fading live sessions as stale 2026-09-08 17:47:52 +02:00
avatar.js feat: error sprite, and stop fading live sessions as stale 2026-09-08 17:47:52 +02:00
config.example.js refactor: untrack config.js behind config.example.js 2026-09-08 17:47:34 +02:00
Containerfile feat: containerize for incus deployment 2026-09-08 21:32:21 +02:00
index.html feat: claudia-tama session dashboard 2026-09-08 14:32:43 +02:00
justfile feat: containerize for incus deployment 2026-09-08 21:32:21 +02:00
mock.js feat: error sprite, and stop fading live sessions as stale 2026-09-08 17:47:52 +02:00
mqtt.js feat: claudia-tama session dashboard 2026-09-08 14:32:43 +02:00
README.md feat: containerize for incus deployment 2026-09-08 21:32:21 +02:00
style.css feat: make dark mode the default theme 2026-09-09 09:21:05 +02:00

claudia-tama

A live dashboard for claudia sessions: one avatar per active session, rendered from MQTT. Inspired by xteink-tamagotchi — but instead of one mascot for one assistant, it shows your whole fleet, one sprite per running session, each reacting as Claude works or waits.

It is three static files and no build step. The publishing side is claudia itself; this is the receiver.

How it fits together

Each claudia session reports its own status directly — no daemon:

   claudia session (its supervisor process)              this page
     publishes while it lives, and registers    ──▶    ws subscribe
     a Last-Will tombstone for when it dies            one avatar / session

Each session is a retained topic claudia/<host>/session/<pid> carrying:

{ "pid": 40321, "profile": "default", "title": "wire up the bridge",
  "cwd": "/home/you/dev/app", "state": "working",
  "host": "laptop", "started": "…", "ts": 1757308800 }

state is working (Claude is processing), idle (waiting for you), or error (its most recent command failed — cleared by the next one). A session that ends publishes an empty retained message (a tombstone) which despawns its avatar — so opening the page at any time shows exactly the live fleet. The tombstone is sent on a clean exit and, on a crash or kill, by the broker's Last-Will, so avatars don't get stuck. The dashboard turns a tombstone into a brief death animation on its way out.

Older setups used a per-machine claudia mqtt bridge poller. That still exists as a fallback for hosts without process isolation, but you don't run it when sessions self-report — see claudia mqtt.

Setup

1. Broker: add a WebSockets listener

Sessions publish over plain TCP (1883). Browsers can only speak MQTT over WebSockets, so the broker needs a second listener. In mosquitto.conf:

listener 1883
listener 9001
protocol websockets

Reload mosquitto. (1883 is for the sessions; 9001 is for this page.)

2. Publisher: enable MQTT on each machine

On every machine running claudia, outside the sandbox:

claudia config set mqtt.enabled true
claudia config set mqtt.broker  tcp://<broker-ip>:1883

That's it — every session from then on self-reports. No process to leave running. Watch what a session emits with claudia mqtt tap; wipe accumulated topics with claudia mqtt clear.

3. Dashboard: point it at the broker

Copy the template and edit it (config.js is gitignored, so your broker address stays out of version control):

cp config.example.js config.js
window.CLAUDIA_TAMA = {
  brokerUrl: "ws://<broker-ip>:9001",
  topicPrefix: "claudia",
  // deathFadeMs: 900,   // how long a dying session lingers on screen
};

Then serve the folder (any static server — the page uses ES modules, so file:// won't work):

python -m http.server 8080
# open http://localhost:8080

Develop without a broker

Open the page with ?mock (or leave brokerUrl empty) to render fake sessions that flip state, spawn, and despawn:

http://localhost:8080/?mock

Files

file what it is
index.html page shell
app.js subscribes, reconciles tiles, renders the fleet
mqtt.js minimal MQTT 3.1.1-over-WebSockets client (no deps)
avatar.js inline-SVG mood sprites (swap for pixel-art here)
style.css layout + sprite animations, light/dark
config.js broker URL + topic prefix (the only file you edit)
mock.js dev-only fake data for ?mock

Deploy

The dashboard ships as a tiny nginx image (Containerfile, ~12MB) and runs on the Incus cluster via ~/dev/meatbag/infra (the web module, service claudia-tama), fronted by Caddy at https://tama.meatbag.se behind SSO.

Caddy serves the page over HTTPS and reverse-proxies /mqtt to the broker's WebSocket listener, so the browser connects same-origin (wss://tama.meatbag.se/mqtt) with no mixed-content block. That URL is baked into config.js at image-build time (--build-arg BROKER_URL=… to change it).

Build and push the image from this repo:

just container-release        # podman build + push to the meatbag registry

or let CI do it (.woodpecker/image.yml pushes on every push). Then, in infra:

just update-image claudia_tama   # taint + pull the new image
just apply