dwarves — GetRunReport RPC + the dwarves-embed live-forge tab
The last leg of the live-report arc (deferred in
7092669/fb0e064): the demo Pipeline tab renders an in-process run because thedwarf.v1wire only carried the parentRunRowsummary — no per-step data crossed the network. This work puts the full per-step report on the wire (GetRunReport) and adds a thin embed client (dwarves-embed) that renders a LIVEdwarves-serverrun on the SAME reused viewer. Additive (the in-process demo path stays), nonornircrate dep (that edge is a cycle: nornir → dwarves already), and it reuses the existingForgePipelineAdapter.
(1) RunReport on the wire — GetRunReport(run_id)
RunStatus(run_id) returns only the parent RunRow (status/counts/duration). To render a build DAG a client needs each step's status, duration, and — crucially — its needs edges. So dwarf.proto (package dwarf.v1) gains:
message BuildRow— the wire twin ofbrokkr::history::BuildRow(run_id, step, stage, backend, matrix, status, summary, artifact_id, job_ref, duration_ms, finished_at) plusrepeated string needs = 12. Theneedsedges live on the run DAG, not the ledger row, so they ride here too — enough for a client to reconstruct BOTH theRunReportrows AND the buildDagfrom a single reply, with no descriptor on hand.message RunReport—RunRow run = 1; repeated BuildRow steps = 2;(the wire twin ofbrokkr::engine::RunReport).rpc GetRunReport (RunStatusRequest) returns (RunReport)— reuses the existingRunStatusRequest { run_id }(DRY), so no new request type.
Server handler (DwarfSvc::get_run_report)
- Find the parent
PipelineRunRowby id (store::read_runs) — absent ⇒NotFound(parity withRunStatus). - Read the run's step rows with the new
store::read_steps(warehouse, run_id)(the per-step twin ofread_runs;steps.jsonlwas already persisted byappend_history, only a reader was missing — no new persistence). - Recover the
needsedges by rebuilding theDagfrom the still-registered pipeline (registry→Dag::build), mappingstep → needs. If the pipeline is no longer registered (or the DAG won't build),needsdefault empty — the per-step rows still cross and the viewer tolerates missing edges (a clean degrade, never an error). - Emit
RunReport { run, steps }, eachBuildRowcarrying itsneeds.
Bearer-gated by the SAME shared nornir_service::auth_interceptor as every other RPC.
(2) dwarves-embed — the live-forge Panel (backend (2))
A NEW leaf crate, the reusable <guest>-embed shape (nornir-funnel-embed is the template):
- Thin tonic gRPC client. Compiles ONLY a trimmed
proto/dwarf.proto(packagedwarf.v1,History+GetRunReport, field numbers matching the server) withbuild_client(true)/build_server(false)viaprotox+tonic-build(pure-Rust, zero-shell). It talks to a livedwarves-serverWITHOUT linking dwarves'serverfeature (which would build the whole tonic router + skidbladnir lifecycle). - No
nornirdep, no cycle. It depends on thedwarvescrate pulled lean (default-features = false, features = ["facett-adapter"]) purely for the domain types (Dag,DagNode,RunReport,BuildRow,PipelineRunRow,RunStatus) and theForgePipelineAdapter.dwarves-embed → dwarvesis a clean one-way edge. report_from_wire(pure, unit-testable). Reconstructs the(Dag, RunReport)pair from apb::RunReport: each wireBuildRow→ a domainBuildRow(status label parsed leniently; unknown ⇒Skipped, never a false green) and aDagNode(backend string → the&'statictag,needscopied verbatim). The DAG is rebuilt from the per-stepneedsalone.- Render via the REUSED viewer.
ForgePipelineAdapter::from_report(&dag, &report)→PipelineView::from_adapter— the identical path the in-process Pipeline tab uses (one viewer, many adapters; a live run is not a new viewer). - The seam.
fn panel(endpoint) -> Box<dyn Panel>(token from$DWARVES_SERVER_TOKEN, optional pinned run from$DWARVES_RUN_ID; empty ⇒ newest run viaHistory(limit=1)). Auto-poll (1.5s) re-fetches so a run in flight fills in its steps live. An unreachable server (or a forge with no runs) yields a clean "forge unavailable" state — never a panic.panel_with(endpoint, token, run_id)is the explicit-creds variant.
dwarves-gui mounts it as an additional native tab — Forge (live) — beside the in-process Pipeline tab (additive; wasm keeps the single Forge panel).
Why this shape
- Reuse, never twin (L5). No new viewer, no new adapter, no hand-rolled auth; the report is reconstructed onto the existing
ForgePipelineAdapter+facett-pipeline. - No cycle. The thin-proto trick (funnel-embed's) keeps the embed off the
nornircrate and off dwarves' heavyserverfeature. - Honest. Per-step status/duration/edges are the SERVER's real historized rows; unknown statuses degrade to
Skipped, missing edges to none — never invented green.
Deferred
- The server recovers
needsfrom the live registry; a run whose pipeline was unregistered renders nodes without edges. Persisting per-runneeds(or the whole descriptor) alongside the step rows would make the DAG exact regardless — a follow-up. - A run-picker in the pane (list runs, choose one) instead of "newest or
$DWARVES_RUN_ID".