An embedded web dashboard with zero dependencies — no Phoenix, no Plug, no JS build. One child spec:
children = [
{Belay, name: MyApp.Belay, ...},
{Belay.Dashboard, belay: MyApp.Belay, port: 4004}
]Then open http://localhost:4004 — live queue tiles, a filterable job list, a job drawer with the full journal (steps with costs, events, errors, children), a rendered workflow DAG, and operator actions: retry, cancel, signal, and steer.
Options:
:belay(required) — the instance name:port— default 4004 (0picks an ephemeral port; seeport/1):bind— default{127, 0, 0, 1}; bind{0, 0, 0, 0}only behind a proxy you trust:token— when set, every request must carry it (Authorization: Bearer <token>;?token=is accepted only for GET requests so browserEventSourcecan authenticate):authorizer— gates the mutating endpoints exactly like the MCP server's authorizer:authorize(tool, args) -> :ok | {:error, msg}
Mutations are disabled by default. Configure either :token or
:authorizer to enable them. Mutating requests must use JSON and, when an
Origin header is present, it must match the request's Host header.
The server speaks plain HTTP/1.1 over gen_tcp with Erlang's built-in
request parsing; live updates stream over SSE. It is an operator tool:
no TLS (front it with your proxy for remote access) and one connection
per request.
Summary
Functions
@spec port(GenServer.server()) :: :inet.port_number()
The port the dashboard is listening on (useful with port: 0).