holger on the shared common framework — About + a jera-backed Tools demo
Class:
design. Companion guides:common-framework-guide.md(how to use it) andcommon-framework-for-idiots.md(plain language). Extends the suite-wide mandate (workspace_nordisk/promptline 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".
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/clideclares a backend-neutralServiceSpec(holger_spec) + aSystemdServiceSpec(holger_service_spec) and hands them toskidbladnir::service::Lifecycle(install / start / stop / status / health). System scope creates + chowns holger's own dedicatedholgersystem user via the engine'sensure_system_user— the same code nornir/dwarves run (nornir→nornir,holger→holger). - Serve loop →
edda/nornir-service.server/libserves the tonic router throughnornir_service::serve_with_tls— the ONE canonical serve loop, not a hand-rolledServer::builder(). - Job ledger →
edda/jera. Promotions and retention-GC already route throughjera::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-freenornir_appshell::Identity), so the lean, display-free build asserts it:wordmark(holger), a realversion,tagline,logo_alt,has_logo, and aframeworklist naming the shared components holger is built on (skidbladnir · nornir-service · jera · facett). - egui render — the shared
facett_about::AboutCard(behindgui), built lazily on first open with.with_max_width(360.0)and fedabout::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-freeIdentitymodel) +facett-about(gui — the shared card widget). - Wired into the app: an
ℹ Aboutheader button, aHelp · About holgermenu / palette command (about:open), andstate_json.about. - The SCENE migration is now DONE — see §4 below: the hand-rolled tab strip + central
match self.tabshell was folded onto the sharedfacett_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 viaHOLGER_DEMO_IMAGE, port5000); the natural demo for a registry (holger can then proxy / migrate from it). Pinned, never:latest(reproducibility).dispatch_demo(jobs_root)— opens the sharedjera::jobs::JobStore, starts akind::BOOT_CONTAINERjob carrying the spec (returning the job id), then boots it live through jera→draupnir under the newcontainersfeature (→jera/container-oci), else records a terminaldispatchedjob (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:democommand, andstate_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=truewhen 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_tabemitsholger-server-ui/tools/<pane>·pane_renderedeach frame the pane is shown (gated whole behind#[cfg(feature = "testmatrix")]so a plainguibuild allocates nothing), sonornir test --features testmatrixSEES 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 tenselectable_valuebuttons and the centralCentralPanel::default().show_inside(ui, |ui| match self.tab { … })dispatch (~85 LOC of bespoke pane-hosting) are gone. - Adopted:
build_scene()describes the shell as aScene— aTabsroot pane (SHELL_PANE = "holger-shell") with one foldableSlotper tab, inTAB_ORDER.scene::render(&scene, &mut nav, ui, &mut host)draws the tab strip + active content through facett's OWNTabView+ pane engine. A thinAppHost: SceneHostfills each slot by driving holger's existing*_tab(ui)body — no tab render logic was rewritten. self.tabstays authoritative: it is seeded into the persistentNavStatebefore each render (so palette / menu / demo dispatch still move the tab) and read back after (so a TabView click movesself.tab+state_json.tab). TheSceneis cloned and theNavStatetaken out as locals per frame, so there is no aliasing with the&mut selfthe 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⚓ statictoggle and the look switcher — folds behind ONE⚙ Controlsdrawer button so the tab canvas gets the screen (the same mobile fold facett-demo uses).state_json.controls_foldedmirrors the drawer;state_json.scenecarries the last render'sdevice/width+ the per-pane map. - Consumed as-is: holger adopts
facett-appfrom 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 facettmain; holger deliberately adopts the Tabs-root scene shape (Tabs root +.phone()/Devicefold), 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.rsHolgerLoginSeamimplementsfacett_auth_ui::LoginSeamover edda'snornir-auth-clientRFC 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: LoginPaneon the app, built once asLoginPane::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 assertsstate_json.tools_pane.{pane,ok}(GREEN on render), then engages static silent-running and asserts the selected write-tool flips took=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 theBOOT_CONTAINERlist. 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-levelstate_jsonrobot cells inapp::look_tests. robot_drives_the_adopted_facett_scene(tests/robot_ui_app.rs) — proves the adopted scene is live: assertsstate_json.scene.scene == "holger", the root pane is a facetttabslayout, a wide viewport isdevice == desktopwithcontrols_folded == false, and clicking a TabView header (through the scene) moves/tab. 7 steps → 7 matrix rows. RED-when-broken: resurrect the bespoke strip / dropscene::renderand the/scene/*facts vanish.robot_scene_folds_controls_on_narrow_viewport— drives the real app at 460 pt and assertsdevice == phone+controls_folded == truewhile the canvas still draws. RED-when-broken: break theDevice::from_widthfold 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 mountedfacett_auth_ui::LoginPanethroughHolgerLoginSeamand 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.