Belay.Dashboard (Belay v2.0.0)

Copy Markdown View Source

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 (0 picks an ephemeral port; see port/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 browser EventSource can authenticate)
  • :authorizer — gates the mutating endpoints exactly like the MCP server's authorizer: authorize(tool, args) -> :ok | {:error, msg}
  • :allowed_hosts — the Host values this dashboard will answer to. Defaults to the loopback names when bound to loopback (the default), and to :any otherwise, since a proxied deployment is addressed by the proxy's name. Set it explicitly (["belay.internal"]) when you bind beyond loopback; :any disables the check.

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.

Host checking

Comparing Origin to Host cannot tell a genuine localhost request from a DNS-rebinding one, because an attacker-controlled name pointed at 127.0.0.1 satisfies both sides. :allowed_hosts is what actually pins the dashboard to the names you expect.

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

The port the dashboard is listening on (useful with port: 0).

Functions

port(pid)

@spec port(GenServer.server()) :: :inet.port_number()

The port the dashboard is listening on (useful with port: 0).