Dashboard HTTP API

The dashboard server (dashboard.py, also started by orch.py dashboard) is a stdlib ThreadingHTTPServer bound to 127.0.0.1:8471 by default (--port, --host). It serves GET requests only, sends Cache-Control: no-store, and never kills itself on a bad request (errors return 404/500 plain text).

Endpoints

Path 200 response 404 / error

/, /index.html, /dashboard.html

the dashboard page (text/html): a self-contained dark UI that polls /api/run every 2 s

—

/api/run

the run snapshot as JSON (application/json) — schema below

—

/run_report.html

the latest out/run_report.html, when it exists

run report not found (no report generated yet)

/docs/<file>

static files from the target repo’s docs/ directory (e.g. the orchestra banner); path traversal outside docs/ is rejected

not found

anything else

—

not found

curl -s http://127.0.0.1:8471/api/run | python3 -m json.tool | head

The snapshot document

html_report.build_snapshot(tickets, state, cfg, ralph_dir) assembles the single JSON document the dashboard and the report render. Tickets come from tickets.yaml plus .ralph/dynamic.json (change-request follow-ups and their added dependencies); state from .ralph/state.json (absent state yields the "NO_RUN" empty snapshot).

Top level

{
  "run":      { ... },   // run-level facts
  "stats":    { "total": 9, "merged": 4, "running": 1, "pending": 3, "blocked": 1, "failed": 0 },
  "tasks":    [ { ... } ],   // one entry per ticket (definition + live state)
  "edges":    [ {"source": "T1", "target": "T2"} ],   // real dependencies only
  "stages":   [ {"index": 0, "ids": ["T1"]}, {"index": 1, "ids": ["T2","T3"]}, ... ],
  "agents":   [ {"model": "tef-qwen3-8/qwen3.8-27b", "provider": "tef-qwen3-8",
                 "tasks": ["T2","T3"], "active": true} ],
  "plugins":  [ {"name": "ponytail", "source": "project", "active": true}, ... ],
  "watch":    { "active": false, "note": "" },
  "run_events": [ { ... } ],   // last 500 global events
  "graph":    { "node_w": 232, "node_h": 88, "width": 1240, "height": 716 }   // layout constants + computed canvas size
}

graph.width / graph.height are the canvas dimensions the server-side layout (_layout) computed from the current stages; node_w/node_h are the fixed node size.

run

Field Meaning

started, heartbeat

epoch seconds; heartbeat is refreshed on every state save

finished_at

epoch of the last run event, when the run has a reason

pid

orchestrator process id

reason

` (running) or `DONE / BLOCKED / STUCK / BUDGET / API_DOWN / INTERRUPTED

finished

true when a reason is set

status

derived: FINISHED (reason set), else NO_RUN (no heartbeat), else RUNNING (heartbeat ≤ 30 s), else STALE

agent_runs, peak_agents

total agent invocations; maximum simultaneous workers

elapsed_s

seconds since started

completion_pct

100 × merged / total (0.0 when there are no tickets)

generated_at

ISO timestamp of the snapshot

tasks[]

Field Meaning

id, title, description

from tickets.yaml

complexity

S / M / L

accept

the acceptance command (verbatim)

scope, criteria

scope globs; acceptance criteria list

depends_on, dependents

real edges both directions (unknown ids filtered out)

status

pending / running / merged / blocked / failed

attempts, max_attempts

attempts used; the ticket’s or the configured budget

model, provider

the model of the recorded attempt; provider = the part before /

branch

ralph/<id>

feedback

the reason of the last failure (acceptance output, panel report, scope reversion, …​)

started_at, finished_at, last_attempt_at, updated_at

epoch seconds; started_at set on the first running, finished_at on a terminal status

events

the ticket’s last 50 recorded events (see events)

stage, x, y

0-based dependency wave and the ticket’s canvas position (pixels) from the server-side layout

plugins[] and watch

plugins lists what the run’s opencode run agents actually load, read in OpenCode’s load order (project .opencode/opencode.json / opencode.json, global ~/.config/opencode/opencode.json[c], then project/global local plugin directories); expected-but-missing entries carry "active": false and a hint. watch is {"active": true, "note": …​} while the watch agent’s pid in .ralph/watch.json is alive and its heartbeat is < 120 s.

Event shapes

run_events and each task’s events use the same records:

{"ts": 1761012345.6, "ticket": "T4", "type": "status", "from": "pending", "to": "running"}
{"ts": 1761012399.1, "ticket": "T4", "type": "attempt", "attempt": 2, "model": "tef-qwen3-8/qwen3.8-27b"}
{"ts": 1761012410.2, "ticket": "T4", "type": "reply", "from": "me@example.com", "note": "operator reply: T4: the key is ..."}
{"ts": 1761012500.0, "ticket": null, "type": "run", "reason": "DONE"}

Types: status (with from/to, optional note), attempt, reply, run. Both lists are capped at 500 entries (state.py: MAX_EVENTS).