holger-server-ui on the declarative facett-scene layer — design
Class:
design· Function axis:f0(the shell as data) ·f1describe ·f2host ·f3render + the tab bridge ·f4observe ·f5what 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 inserver/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:
- before render:
nav.set_active_tab("holger-shell", tab_index(self.tab)); - render draws the menubar + the
TabViewstrip + the active tab's slot; - after render: if a tab-header click moved
active_tabthis frame, adopt it (self.tab = tab_from_index(post)); a menu command instead changedself.tabdirectly and leftactive_tabatpre, so it is not clobbered; - the static-mode Upload guard bounces
Upload → Status; thennavis 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):
holger_scene_is_valid—validate()ok; root + one leaf per tab. RED-when-broken.holger_scene_ron_round_trips_byte_stable—to_ron ∘ from_ronis a byte-stable fixed point AND equals the builder scene. RED-when-broken.every_tab_leaf_is_foldable— every tab body isFoldable. RED-when-broken.root_is_tabs_and_index_bridge_is_total— root isTabs;tab_index⇄tab_from_index.menubar_groups_route_existing_commands— the six groups, each leaf routes anAction::Command(<existing id>)the shared dispatcher understands. RED-when-broken.
Robot-UI — server/ui/tests/robot_ui_app.rs (egui_kittest + nornir-robotui):
robot_drives_scene_shell_tabs_and_folds(new) — asserts/scene(pane tree, all one foldable body per tab, six menus,render.scene == "holger"); clicks the scene'sTabViewheaders (Browse/Graph) and asserts the tab +active_tabmoved; folds a tab body and asserts/scene/folded/graph_pane. RED-when-broken.- Every pre-existing robot cell (
robot_drives_tabs_tools_and_static_mode,…_lineage_tab,…_tools_1_3_5_panes,…_repo_row_selection_propagates,…_static_mode_*,…_upload_*,…_artifact/browse/archive/security/deploy, …) now drives its tabs through the scene'sTabViewstrip and stays green — proof the migration is behaviour-preserving.
NOTE (resolved):
robot_opens_about_and_dispatches_the_jera_demoused to fail in a checkout where the git-LFS hero-logo blob is materialised — the ~1536px-tall logo made the sharedfacett_about::AboutCardwindow 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.