Gunnar

nordisk/holger

.nornir/common-framework-design.md

<!-- nornir:class: design -->
# holger on the shared common framework — About + a jera-backed Tools demo

> **Class:** `design`. Companion guides: [`common-framework-guide.md`](common-framework-guide.md)
> (how to use it) and [`common-framework-for-idiots.md`](common-framework-for-idiots.md)
> (plain language). Extends the suite-wide mandate (`workspace_nordisk/prompt` line 65):
> every app shares — not duplicates — lifecycle management, an **About**, and a **Tools
> menu** whose demos start containers through the shared **jera** job manager, "a bit
> like JetBrains has a common framework".

![holger consumes the shared toolbox](assets/common-framework.svg)

This is **additive parity** work — no working path removed, no behaviour changed for an
unconfigured caller. It brings holger's operator GUI onto the same shared surfaces the
other suite apps present, **consuming** shared crates rather than twinning them (Law 5).

---

## What holger already consumed (verified, not re-done)

The survey found holger already on the shared spine — these are named here so the
reuse story is one place:

- **Service lifecycle → `skidbladnir::service`.** `server/cli` declares a
  backend-neutral `ServiceSpec` (`holger_spec`) + a `SystemdServiceSpec`
  (`holger_service_spec`) and hands them to `skidbladnir::service::Lifecycle`
  (install / start / stop / status / health). System scope creates + chowns holger's
  **own dedicated `holger` system user** via the engine's `ensure_system_user` — the
  same code nornir/dwarves run (`nornir`→`nornir`, `holger`→`holger`).
- **Serve loop → `edda/nornir-service`.** `server/lib` serves the tonic router through
  `nornir_service::serve_with_tls` — the ONE canonical serve loop, not a hand-rolled
  `Server::builder()`.
- **Job ledger → `edda/jera`.** Promotions and retention-GC already route through
  `jera::jobs::JobStore` (a durable redb ledger a reader can list) — not ad-hoc spawns.
- **UI → `facett`.** The GUI is built from facett components (look/theme, `menu_bar` +
  `CommandPalette`, `Table`, `graph3d`, `facett-helix`).

## What this slice added (the gaps)

### 1. The About identity surface (`about.rs`) — ADOPTED the shared widget

holger's About now **consumes** the shared `facett_about` card (LAW #5 — ADOPTION):
the hand-rolled egui `draw_about_window` twin was **DELETED** and the ONE render body
lives in `facett-about`, which renders a `nornir_appshell::Identity`. `about.rs` keeps
only holger's **data** — the embedded logo, an `about::identity()` builder, and the
headless oracle — all egui-free:

- **Identity oracle** `about::state_json(open)` = `identity().state_json(open)` —
  always compiled (no egui, sourced from the egui-free `nornir_appshell::Identity`), so
  the lean, display-free build asserts it: `wordmark` (`holger`), a real `version`,
  `tagline`, `logo_alt`, `has_logo`, and a **`framework`** list naming the shared
  components holger is built on (skidbladnir · nornir-service · **jera** · facett).
- **egui render** — the shared `facett_about::AboutCard` (behind `gui`), built lazily
  on first open with `.with_max_width(360.0)` and fed `about::identity()` each frame.
  The logo decode is graceful (LFS-tolerant): an un-pulled git-lfs blob shows the text
  identity, never a panic. holger no longer owns any About egui body.
- Deps added: `nornir-appshell` (always — the egui-free `Identity` model) +
  `facett-about` (gui — the shared card widget).
- Wired into the app: an `ℹ About` header button, a `Help · About holger` menu /
  palette command (`about:open`), and `state_json.about`.
- **The SCENE migration is now DONE** — see §4 below: the hand-rolled tab strip +
  central `match self.tab` shell was folded onto the shared `facett_app::scene`.

### 2. Tools · Demo appliance — start a container THROUGH jera (`infra.rs`)

`server/ui/src/infra.rs` mirrors **korp's `infra.rs`**: the demo is a plain
`jera::container::ContainerSpec` (which maps onto draupnir's ONE OCI engine via
`ContainerSpec::to_draupnir`), and its bring-up is **recorded as a durable
`jera::jobs` job** — never a `thread::spawn` or a bespoke docker call.

- `demo_registry_spec()` — a **pinned** demo artifact registry
  (`docker.io/library/registry:2.8.3`, env-overridable via `HOLGER_DEMO_IMAGE`,
  port `5000`); the natural demo for a registry (holger can then proxy / migrate from
  it). Pinned, never `:latest` (reproducibility).
- `dispatch_demo(jobs_root)` — opens the shared `jera::jobs::JobStore`, starts a
  `kind::BOOT_CONTAINER` job carrying the spec (returning the **job id**), then boots
  it live through jera→draupnir under the new **`containers`** feature
  (→ `jera/container-oci`), else records a terminal `dispatched` job (daemon-free
  default). Either way the ledger holds a row a reader (`nornir jobs`) can enumerate.
- Wired into the GUI as **Tools #6 · demo appliance** (button + pane), a `tool:demo`
  command, and `state_json.demo_appliance` (job id + spec + status).

### 3. Tools-pane render-time introspection (`app.rs`)

The server-side gRPC surfaces and RBAC gates already self-report a
`functional_status` row on every real check (GREEN on success, RED on the failure
path). This slice closes the remaining §B gap: the **five operator Tools panes**
(#1 holger-ds · #2 migrate · #3 security · #4 deploy · #5 airgap) now self-report
the moment they render, mirroring that server-side pattern and **reusing holger's
own emit bridge** (`app::functional_status` → `nornir_testmatrix::functional_status`,
no twin).

- `HolgerUiApp::tools_pane_report()` — the single oracle for the *currently
  selected* pane: `(slug, ok, detail)`. `ok=true` when the pane draws a usable
  operator surface; **`ok=false` ("RED on empty")** when a write-tool (#2–#5) is
  blanked to a disabled notice under static read-only mode. #1 holger-ds always
  draws its config, so it renders GREEN.
- `tools_tab` emits `holger-server-ui/tools/<pane>` · `pane_rendered` each frame the pane
  is shown (gated whole behind `#[cfg(feature = "testmatrix")]` so a plain `gui`
  build allocates nothing), so `nornir test --features testmatrix` SEES all five
  surfaces fire without a human at the GUI.
- The same report is surfaced as `state_json.tools_pane` (`pane` / `ok` /
  `rendered` / `detail`) — the robot's oracle, asserted as DATA (never pixels).

### 4. The tabbed shell — ADOPTED the shared pane/scene host (`app.rs`)

holger's operator shell now **consumes** the shared `facett_app::scene` layer (LAW #5 —
ADOPTION), the same declarative pane/scene host dwarves' tabbed shell adopted. The
hand-rolled tab host was **DELETED**:

- **Twin removed:** the `egui::Panel::top("tabs")` strip of ten `selectable_value`
  buttons **and** the central `CentralPanel::default().show_inside(ui, |ui| match self.tab
  { … })` dispatch (~85 LOC of bespoke pane-hosting) are gone.
- **Adopted:** `build_scene()` describes the shell as a `Scene` — a `Tabs` root pane
  (`SHELL_PANE = "holger-shell"`) with one foldable `Slot` per tab, in `TAB_ORDER`.
  `scene::render(&scene, &mut nav, ui, &mut host)` draws the tab strip + active content
  through facett's OWN `TabView` + pane engine. A thin `AppHost: SceneHost` fills each
  slot by driving holger's existing `*_tab(ui)` body — no tab render logic was rewritten.
- **`self.tab` stays authoritative:** it is seeded into the persistent `NavState` before
  each render (so palette / menu / demo dispatch still move the tab) and read back after
  (so a TabView click moves `self.tab` + `state_json.tab`). The `Scene` is cloned and the
  `NavState` taken out as locals per frame, so there is no aliasing with the `&mut self`
  the slot dispatch holds.
- **Responsive, canvas-first (LAW #36.3):** `Device::from_width(available)` classifies the
  viewport; on a **phone-class** width (< 600 pt) the shell chrome — the `⚓ static` toggle
  and the look switcher — folds behind ONE `⚙ Controls` drawer button so the tab canvas
  gets the screen (the same mobile fold facett-demo uses). `state_json.controls_folded`
  mirrors the drawer; `state_json.scene` carries the last render's `device`/`width` + the
  per-pane map.
- **Consumed as-is:** holger adopts `facett-app` from the sibling facett checkout
  (`../../../facett/facett-app`, gui-only) with **no edits to facett** (LAW #36 guardrail).
  Note: the richer draggable-splits + rail-`drawer(bp)` scene API has since **merged to
  facett `main`**; holger deliberately adopts the **Tabs-root** scene shape (Tabs root +
  `.phone()`/`Device` fold), the natural fit for holger's tab-based shell — the draggable
  master/detail splits + activity-rail drawer are there to take if holger's shell ever wants
  them.

### 5. The sign-in tab — ADOPTED the shared `facett-auth-ui` LoginPane (`app.rs`)

holger's operator shell mounts the shared **`facett_auth_ui::LoginPane`** as a first-class
`Tab::SignIn` (slot `"signin"`, header `Sign in`, in `TAB_ORDER`; the command palette maps
`"signin" → Tab::SignIn`) — the SAME reusable device-code pane korp mounts (LAW #5 —
ADOPTION, no twin):

- **Host seam:** `server/ui/src/login_seam.rs` `HolgerLoginSeam` implements
  `facett_auth_ui::LoginSeam` over edda's **`nornir-auth-client`** RFC 8628 **device-code**
  grant. It exposes the signed-in **subject** and credential **NAMES** only — never a token
  or secret value (`credential_names_are_store_driven_names_only`).
- **Mount:** `login_pane: LoginPane` on the app, built once as
  `LoginPane::new(Box::new(HolgerLoginSeam::from_env()))`; the Sign-in tab body renders it.
  Config is read from the environment (`login_config_reads_env_then_placeholders`).
- **Consumed as-is:** dep `facett-auth-ui` + `nornir-auth-client` (gui-only), no edits to
  facett. The pane only renders the seam's states (Sign in → user-code + verification URL →
  poll → signed-in subject → Sign out); it is presentational.

## Testing (Law 4 — observe from outside, ≥1 RED-when-broken)

- `robot_tools_panes_self_report_render_status` (`tests/robot_ui_app.rs`) — a real
  egui_kittest robot scenario that opens Tools, selects each of the five panes and
  asserts `state_json.tools_pane.{pane,ok}` (GREEN on render), then engages static
  silent-running and asserts the selected write-tool flips to `ok=false`
  (**RED-when-broken**: drop the static gate and the RED step goes green + fails).
  13 steps → 13 nornir-testmatrix rows.
- `infra::demo_dispatch_records_a_jera_boot_container_job` — dispatches, re-opens the
  ledger, and finds the SAME job id in the `BOOT_CONTAINER` list. **RED-when-broken**
  (verified: neutralizing the job-id capture fails it end to end).
- `about::about_oracle_carries_identity_and_framework` — asserts wordmark/version and
  that the framework list names jera. **RED-when-broken** (verified: removing the jera
  framework line fails it).
- Real **egui_kittest** robot scenario `robot_opens_about_and_dispatches_the_jera_demo`
  (`tests/robot_ui_app.rs`) — AccessKit clicks: `ℹ About` → open, `Close` → dismiss,
  Tools → `6 · demo appliance` → `Start demo appliance…` → a non-empty jera job id.
  Each step is a nornir-testmatrix row. Plus two app-level `state_json` robot cells in
  `app::look_tests`.
- `robot_drives_the_adopted_facett_scene` (`tests/robot_ui_app.rs`) — proves the adopted
  scene is live: asserts `state_json.scene.scene == "holger"`, the root pane is a facett
  `tabs` layout, a wide viewport is `device == desktop` with `controls_folded == false`,
  and clicking a TabView header (through the scene) moves `/tab`. 7 steps → 7 matrix rows.
  **RED-when-broken**: resurrect the bespoke strip / drop `scene::render` and the
  `/scene/*` facts vanish.
- `robot_scene_folds_controls_on_narrow_viewport` — drives the real app at 460 pt and
  asserts `device == phone` + `controls_folded == true` while the canvas still draws.
  **RED-when-broken**: break the `Device::from_width` fold and the flags lie. 4 rows.
- `robot_login_pane.rs` (`holger_seam_drives_the_shared_login_pane_no_leak`,
  `login_pane_renders_a_signin_button`) — drives the mounted `facett_auth_ui::LoginPane`
  through `HolgerLoginSeam` and asserts the pane renders a Sign-in button and leaks no token
  value. **RED-when-broken**: the no-leak assertion fails if a secret ever reaches the pane.

## Alternatives (why not) — see `reasoning`

Extracting the About window into a shared `facett` component was once deferred as a
"move candidate" — that extraction has since **landed**: the About render body now lives
ONCE in the shared `facett-about` crate and holger (like korp) **consumes** it (§1); the
local `about.rs` egui twin was deleted, keeping only holger's data + headless oracle. The
same ADOPTION applies to the pane/scene host, now the shared `facett_app::scene` (§4). So
this slice is no longer a one-repo hold-out — holger is a REFERENCE consumer of the shared
facett UI widgets, not a carrier of twins.