Architecture

How the pieces fit.

Top-level#

text
Browser ──HTTPS/WS─→ Caddy ──→ Go API ──→ Postgres
                              └──→ Next.js standalone (dashboard)

Your machine ──WS─→ cu-daemon ──┬──→ pty.Broker (sessions WE spawn)
                                └──→ discover.Manager (sessions we OBSERVE)
                                       └─ fsnotify-tails .jsonl transcripts

Daemon — pty.Broker#

When dashboard or cu attach joins a daemon-spawned session, the broker fan-outs stdout to every subscriber and routes stdin from the current controller back to the PTY master. 64 KB ring scrollback per session; controller handoff is a one-shot message.

Daemon — discover.Manager#

Polls the OS every 5s for claude / codex processes owned by the daemon user. For each new process resolves the on-disk transcript (~/.claude/projects/… or ~/.codex/sessions/…), opens an fsnotify watcher + 2s safety-net poll, and forwards every JSONL line to backend as a session_event. Offsets persisted in ~/.controlum/discover-offsets.json so daemon restarts resume cleanly.

Backend#

Go API; routes split into REST (/api/v1/*) + WebSocket (/ws/*). Sessions table is the single source of truth — both source=cu and source=external rows live here. Migration 0002_sessions_source added source + log_file_path columns and a partial unique index to prevent duplicate external rows.

Frontend#

Next.js App Router, standalone build. Dashboard uses shadcn/ui + Tailwind v4. Sessions Interactive tab uses @xterm/xterm wired to /ws/sessions/<id> (read + write for source=cu, read-only for external).