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 |
|---|---|---|
|
the dashboard page ( |
— |
|
the run snapshot as JSON ( |
— |
|
the latest |
|
|
static files from the target repo’s |
|
anything else |
— |
|
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 |
|---|---|
|
epoch seconds; |
|
epoch of the last |
|
orchestrator process id |
|
|
|
true when a reason is set |
|
derived: |
|
total agent invocations; maximum simultaneous workers |
|
seconds since |
|
100 × merged / total (0.0 when there are no tickets) |
|
ISO timestamp of the snapshot |
tasks[]
| Field | Meaning |
|---|---|
|
from |
|
|
|
the acceptance command (verbatim) |
|
scope globs; acceptance criteria list |
|
real edges both directions (unknown ids filtered out) |
|
|
|
attempts used; the ticket’s or the configured budget |
|
the model of the recorded attempt; provider = the part before |
|
|
|
the reason of the last failure (acceptance output, panel report, scope reversion, …) |
|
epoch seconds; |
|
the ticket’s last 50 recorded events (see events) |
|
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).