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

nordisk/holger

.nornir/scene-design.md

holger-server-ui on the declarative facett-scene layer — design

Class: design · Function axis: f0 (the shell as data) · f1 describe · f2 host · f3 render + the tab bridge · f4 observe · f5 what holger does NOT use. Companion: facett's own ../../facett/.nornir/scene-design.md (the layer itself). This file states only the shape holger runs; it lives beside the code in server/ui/src/app/scene.rs.

f0 — the holger shell as DATA

holger-server-ui's top-level shell — a grouped menubar over a tabbed workspace of twelve views (Status · Repos · Browse · Archive · Artifact · Graph · Helix · Lineage · Tools · Upload · Sign in · ⚙ Settings) — is expressed as ONE facett_app::scene::Scene value and rendered through facett-app's OWN primitives (Router / TabView / facett_menu). Nothing is forked: the scene DRIVES the existing <tab>_tab(ui) renderers via a SceneHost and folds each view-model's state_json() into the observable state. The old hand-wired Panel::top("menubar") + Panel::top("tabs") strip + match self.tab { … } central panel are gone; the surrounding chrome (wordmark, ⚓ static toggle, ⌘K, ℹ About, look switcher, frost badge) stays in a Panel::top("chrome") above the scene.

Scene { name: "holger", menubar: [6 groups], root: holger-shell, routes: [] }

Pane tree (server/ui/src/app/scene.rs:holger_scene)

holger-shell ── Tabs(active 0) .phone(Tabs, 600)   // the whole shell (the ROOT)
├─ status_pane   (Foldable 🩺) slot:status          status_tab   → data.status.state_json()
├─ repos_pane    (Foldable 🗄) slot:repos           repos_tab    → data.repos.state_json()
├─ browse_pane   (Foldable 📂) slot:browse          browse_tab   → data.browse.state_json()
├─ archive_pane  (Foldable 🧊) slot:archive         archive_tab  → data.archive.state_json()
├─ artifact_pane (Foldable 📦) slot:artifact        artifact_tab → data.artifact.state_json()
├─ graph_pane    (Foldable 🕸) slot:graph           graph_tab    → graph_state_json()
├─ helix_pane    (Foldable 🌀) slot:helix           helix_tab    → {built}
├─ lineage_pane  (Foldable ◉) slot:lineage          lineage_tab  → data.lineage.state_json()
├─ tools_pane    (Foldable 🛠) slot:tools           tools_tab    → {tool}
├─ upload_pane   (Foldable ⬆) slot:upload           upload_tab   → {writable, notice}
└─ signin_pane   (Foldable 🔑) slot:signin          signin_tab   → login_pane.state_json()

The tab ORDER, slot ids and header labels are NOT restated in the scene module — they come from the app's TAB_ORDER + Tab::slot_id() / Tab::label(), the single source of truth, so a tab added there automatically gains a scene pane (only the fold icon is scene-local). The root pane id is the app's SHELL_PANE.

Every pane name (holger-shell, *_pane) and every menu name is unique — the addressability invariant Scene::validate() enforces (app/scene.rs holger_scene_is_valid, debug-asserted in HolgerUiApp::new).

f1 — describe (holger_scene(repos) → Scene, server/ui/src/app/scene.rs:80)

The Rust builder produces the tree above plus a six-group menubar. The pane tree is constant (one leaf per TAB_ORDER entry) so a persisted NavState stays valid frame-to-frame; only the Repositories submenu is data-driven (one item per reported repo), so the scene is rebuilt each frame from self.data.repos.repos. Every menu leaf carries an Action::Command(<existing id>) — goto:* / tool:* / repo:* / action:* / look:* / about:open — the exact ids the Ctrl-K palette already uses, so the menubar and the palette share the ONE dispatch_command interpreter (no second command engine):

Group items routes to
View Status … Sign in (Ctrl+1..0 hints) goto:<tab>
Operate holger-ds / migrate / security / deploy / airgap / demo tool:<id>
Repositories one per reported repo repo:<name>
Action Refresh (F5) / Scan selected / Toggle static action:*
Look dark / light / device look:<name>
Help About holger about:open

f2 — host (SceneHostImpl, server/ui/src/app/scene.rs)

SceneHost::slot(id, ui) switches on the slot id, calls the matching existing <tab>_tab(ui) method on &mut HolgerUiApp (the child module reaches the private tab renderers), and returns that view-model's state_json() (folded into render.slots). SceneHost::command(id) delegates straight to HolgerUiApp::dispatch_command(id) — the menu reuses the palette's dispatcher.

f3 — render + the self.tab bridge (HolgerUiApp::render_scene, app/scene.rs)

render() calls render_scene(ui) once (after the chrome panel). It builds the scene, clones the persistent nav, and calls facett_app::scene::render with a host that borrows &mut self (scene + nav are owned locals, so the borrow is disjoint). self.tab stays the authoritative tab so every command / demo / palette / robot hook keeps driving it:

  1. before render: nav.set_active_tab("holger-shell", tab_index(self.tab));
  2. render draws the menubar + the TabView strip + the active tab's slot;
  3. after render: if a tab-header click moved active_tab this frame, adopt it (self.tab = tab_from_index(post)); a menu command instead changed self.tab directly and left active_tab at pre, so it is not clobbered;
  4. the static-mode Upload guard bounces Upload → Status; then nav is persisted.

f4 — observe (HolgerUiApp::scene_state_json, folded into state_json.scene)

state_json["scene"] is the last scene::render value ({scene, device, width, route, panes{rect,layout,folded}, slots, nav}) with the declarative shell's own structure folded in beside it: { name, pane_names, foldable, menus, active_tab, folded, render }. Both surfaces live in ONE object, so a robot asserts the menubar, the tabs, the fold state AND the responsive device class without a screen. Note panes is the render map (keyed by pane name) while pane_names is the flat roster; render repeats the raw render value for callers that want it unmerged. state_json.controls_folded (top level, beside scene) mirrors the chrome's ⚙ drawer.

f5 — what holger deliberately does NOT use

holger's shell is a flat, single-window tabbed workspace: there is no master/detail and no peripheral rail. So it uses tabs (the root layout), folds (every tab body), a routed menubar, and one .phone(Tabs, 600) breakpoint — the last only so scene::render CLASSIFIES and reports the device class the chrome folds its controls on (a Tabs shell is already canvas-first, so the phone layout is the same Tabs). It does NOT use draggable Splits or .drawer() rails, because there is no honest surface to hang them on. Fabricating a split just to tick a box would violate the REUSE law; the scene DRIVES the real widgets as they are.

Tests

Pure scene tests — server/ui/src/app/scene.rs (--features gui):

Robot-UI — server/ui/tests/robot_ui_app.rs (egui_kittest + nornir-robotui):

NOTE (resolved): robot_opens_about_and_dispatches_the_jera_demo used to fail in a checkout where the git-LFS hero-logo blob is materialised — the ~1536px-tall logo made the shared facett_about::AboutCard window exceed the old 720pt test viewport, pushing its "Close" button off-screen so the positional kittest click missed. The test now builds its harness at a 2400pt height so the real "Close" click always lands whether the logo is a full PNG or an LFS pointer; every assertion is unchanged. The underlying ergonomic (the card rendering the logo at full height) lives in the shared facett crate, not holger.