viz operation buttons + the nornir root pane
The write-side of nornir viz (nornir-ui): every data pane that reads the warehouse now also carries the operation that produces that data, plus a brand-new 🧬 nornir root pane that holds the server / workspace-lifecycle operations. The decision (made by the user): operations are spread out to the pane they belong to, NOT collected into one central "operations" menu.
Source code: src/viz/nornir_tab.rs (the root pane NornirRootView), src/viz/ops_tabs.rs (📈 Bench "Run bencher"), src/viz/test_tab.rs (🧪 Test "Run matrix"), src/viz/release_tab.rs (🚀 Release "Release gate"), src/viz/remote.rs (the thin-client RPC wrappers), proto/nornir.proto (the Ops service), src/bin/nornir-server.rs (OpsSvc handlers), src/bin/nornir.rs (the CLI *_remote parity paths). Tests: tests/viz_ops_buttons.rs. Companions: cli-command-map.md, architecture-wiring.md, creating-a-workspace.md.
1. Where each operation lives
Every operation sits in the pane that owns its data. The HEAVY/release-grade ops (full matrix, full bench, release gate) fire behind a confirm + "heavy / long-running" warning before they run.
| Pane | Operation (button) | gRPC RPC | Heavy? | CLI twin |
| 🧬 nornir (root) | Server status | Health.Ping |
no | — |
| 🧬 nornir (root) | Add workspace (form: name + descriptor + mode + poll) | Workspaces.Register |
no | nornir workspace add |
| 🧬 nornir (root) | Kill workspace (confirm-gated) | Workspaces.Remove |
no (confirm) | nornir workspace rm |
| 🧬 nornir (root) | Populate | Workspaces.Fetch{background} |
no | nornir workspace populate |
| 🧬 nornir (root) | Refresh / poll-now | Workspaces.Fetch{force} |
no | nornir workspace fetch |
| 🧪 Test | Run full matrix | Ops.RunTestMatrix (repo="") |
yes (confirm) | nornir test all (thin) |
| 🧪 Test | Run repo | Ops.RunTestMatrix (repo=…) |
no | nornir test run <repo> (thin) |
| 📈 Bench | Run bencher | Ops.RunBench |
yes (confirm) | nornir bench run <repo> (thin) |
| 🚀 Release | Release (heavy gate) | Ops.RunRelease |
yes (confirm) | nornir release gate all (thin) |
The root pane is named nornir (the user notes it could equally have been called "server"): it is where workspace lifecycle lives — every other pane is a view onto one workspace's data, while this one is where a workspace is born (Add/Populate) and where it dies (Kill). It renders the first/home tab; 🧵 Timeline follows it.
2. Why a new Ops gRPC service (and what was reused)
Tests, benches and the release gate run where the source checkout lives. In server mode the server owns the monitored git clones AND the warehouse lock, and already does heavy work server-side in a spawn_blocking (Workspaces.Fetch / republish, Release.GateAll). So the run-side verbs follow that exact shape: a button → one NAMED Ops.Verb handler → a reusable core fn → persist into the warehouse the server owns. This obeys the layer-boundary-discoverability law (named Service.Verb, not a dispatched ident) and the persist-to-warehouse law.
service Ops {
rpc RunTestMatrix (RunTestMatrixRequest) returns (RunOpResult); // run_aspect → test_results
rpc RunBench (RunBenchRequest) returns (RunOpResult); // run_bench_example → bench_runs
rpc RunRelease (RunReleaseRequest) returns (RunOpResult); // release::gate → release_events
}
| RPC | Status | Server handler | Reused core |
Ops.RunTestMatrix |
NEW | OpsSvc::run_test_matrix |
test_matrix::{detect_runner,list_tests,run_aspect,outcome_to_rows} + append_test_results |
Ops.RunBench |
NEW | OpsSvc::run_bench |
release::pipeline::run_bench_example + append_bench_run_async |
Ops.RunRelease |
NEW | OpsSvc::run_release |
release::gate::no_path_patches + append_release_events |
Workspaces.Register |
reused | WorkspacesSvc::register |
registry::Workspace + monitor::fetch_workspace/republish |
Workspaces.Remove |
reused | WorkspacesSvc::remove |
registry::Registry::remove |
Workspaces.Fetch |
reused | WorkspacesSvc::fetch |
monitor::fetch_workspace/republish |
Health.Ping |
reused | HealthSvc::ping |
— |
This closed the reported gap: there was no remote RPC to run the test matrix or run benches, so the thin nornir test run / nornir bench were broken in client mode. Ops.* supplies them, and the CLI now routes test run/test all/bench run/release gate all to *_remote paths when NORNIR_SERVER is set (CLI parity — no GUI-only capability).
RunOpResult carries { ok, summary, targets[], run_id }: an overall verdict, the same one-line summary the CLI prints, the per-repo outcomes, and the warehouse run id the rows landed under (so the viz can deep-link to the rows it just produced).
3. The wiring, per operation (button ↔ RPC ↔ CLI)
🧬 nornir / Add workspace ─┐
viz: NornirRootView::draw → remote::register_workspace
rpc: Workspaces.Register → OpsSvc-sibling WorkspacesSvc::register (eager populate)
cli: nornir workspace add --monitored → run_workspace_remote
🧪 Test / Run (full) matrix ┐
viz: TestTabState::draw_run_ops → remote::run_test_matrix
rpc: Ops.RunTestMatrix → OpsSvc::run_test_matrix (run_aspect, persist test_results)
cli: nornir test all / test run <repo> (thin) → run_test_matrix_remote
📈 Bench / Run bencher ──────┐
viz: BenchState::draw → remote::run_bench
rpc: Ops.RunBench → OpsSvc::run_bench (run_bench_example, persist bench_runs)
cli: nornir bench run <repo> (thin) → run_bench_remote
🚀 Release / Release (heavy gate) ┐
viz: ReleaseTabState::draw_release_op → remote::run_release
rpc: Ops.RunRelease → OpsSvc::run_release → run_gate_suite (persist release_events)
cli: none — release orchestration moved to `dwarves release` (2026-07-18)
The Release-pane gate persists release_events, so pressing it lights up the very same 🚀 Release / 📡 Live panes (the op DAG renders the run it just made).
One gate suite, two buttons. Ops.RunRelease (🚀 Release) and Release.GateAll (🚦 Release-gates) both run the repo's CONFIGURED [repo.<name>.gates] suite through the single server_grpc::run_gate_suite driver, whose gate selection is release::gate::selected_gates. They previously disagreed: gate_all honoured the config while run_release loaded it, threw it away, and ran one hardcoded no_path_patches — so the 🚀 button could report "N/N repo(s) ready" having skipped every gate the repo declared (and having run one it had explicitly disabled). Each repo's OpTarget.message now names the gates that produced the verdict.
An all-default [gates] table (nothing declared) selects the no_path_patches floor rather than running nothing — a suite that checks nothing must not report readiness. NOTE: a server-monitored workspace builds its repos from config::Repo::default() (open_monitored_workspace), so gates are all-false there and the floor is what runs; declared gates only take effect for a workspace whose config actually carries them.
4. Components expose readable data (LAW #6)
Each new pane/button emits what it renders as readable data in state_json, so "see what the user sees" is available as data, not pixels, and the headless test matrix can mechanically discover + cover every operation:
state_json["nornir"]—buttons[](id → rpc + heavy/confirm),add_form(the typed field values),kill(target + armed),resultsper op,server(ping identity).state_json["test"]["ops"]["buttons"]/["run_result"],state_json["bench"]["ops"]/["run_result"],state_json["release"]["ops"]/["run_result"]— same shape per pane.
The 🧬 nornir pane is enumerated in Tab::ALL → state_json["all_tabs"], so the existing matrix enumerator (viz_matrix.rs / viz_buttons.rs / viz_surface.rs) discovers it automatically; the sentinel-ban over the live surface guarantees no TODO/not wired stub ships green.
5. Tests (inject real input, assert real output)
nornir_root_pane_discovers_every_root_operation_and_form_roundtrips— the pane lists every root op button wired to its RPC; kill is confirm-gated; the pane is inall_tabs.add_workspace_form_fields_roundtrip_into_state_json— the form's values are readable data; no sentinel in the pane's rendered state.per_pane_operation_buttons_are_wired_with_heavy_confirm_flags— Test / Bench / Release each expose their op with the right RPC +heavy/confirmflags.ops_run_test_matrix_rpc_runs_server_side_and_returns_real_targets— against a LIVE ephemeral server monitoring THIS repo,Ops.RunTestMatrixruns the fastfmtaspect server-side and returns one real per-repo target + a warehouse run id (a true verdict, not "didn't panic").register_then_remove_workspace_changes_the_served_set— Add then Kill via the buttons' RPCs; the servedWorkspaces.Listgrows then shrinks.