Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

.nornir/viz-operation-buttons.md

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:

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)

tests/viz_ops_buttons.rs: