Gunnar

nordisk/holger-egui

Cargo.toml

[package]
name = "holger-server-ui"
version = "0.1.4"
edition = "2021"
description = "Operator/admin UI for holger over the HolgerObject core API — egui via facett, embedded (LocalHolger, direct core calls) or remote (RemoteHolger gRPC)."
license = "MIT OR Apache-2.0"
repository = "https://codeberg.org/nordisk/holger"

# The data/view-model layer (default build) is transport-agnostic and depends
# only on `traits` — it calls the core `HolgerObject` API and exposes every view
# as `state_json()` for headless robot tests. No egui in the default build, so
# `cargo test -p holger-server-ui` is fast and needs no display/server.
#
# The egui + facett presentation, plus the `holger-server-ui` binary, live behind the
# `gui` feature (pulls eframe/egui/facett and `server-lib` for the concrete
# Local/Remote handles).
[dependencies]
traits = { package = "holger-traits", version = "0.6.7", path = "../holger/traits" }

# nornir-appshell — the shared, egui-FREE app-shell MODEL (edda leaf). holger's About
# surface CONSUMES the `Identity` (LAW #5) instead of twinning a const-blob: `about.rs`
# builds one `Identity` and dumps its `state_json` oracle, so the lean, display-free
# default build asserts holger's identity with no egui. Path+version (a sibling edda
# checkout, three levels up like facett-helix); the `gui` build feeds this Identity to
# the shared `facett_about::AboutCard`.
nornir-appshell = { version = "0.1", path = "../edda/crates/nornir-appshell" }

# jera — the SHARED job manager (edda). The Tools "Demo appliance" entry declares a
# `jera::container::ContainerSpec` and records its bring-up in the shared `jera::jobs`
# durable ledger (the same seam holger-server routes promote/GC through) — never a
# bespoke thread::spawn / docker call (L5). `default-features = false` keeps the lean
# build daemon-free (spec + ledger always compile); the LIVE OCI boot is opt-in behind
# holger-server-ui's `containers` feature (→ `jera/container-oci` → `draupnir/backend-oci`).
jera = { version = "0.1", path = "../edda/crates/jera", default-features = false }

tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"

# LAW #4 — WebGPU everywhere, no OpenGL/WebGL: `wgpu`, never `glow`. eframe resolves
# `Renderer::default()` to `Glow` whenever its `glow` feature is on, so this one word
# decided holger's shipped renderer with nothing in main.rs saying so. main.rs now names
# `Renderer::Wgpu` explicitly too. Enforced by server/ui/tests/law4_no_opengl_backend.rs.
eframe = { version = "0.35", optional = true, default-features = false, features = ["default_fonts", "wgpu", "wayland", "x11"] }
egui = { version = "0.35", optional = true }
facett = { version = "0.1.2", optional = true }
# facett-about — the shared About-card widget: the ONE robot-testable egui `Window` that
# renders a `nornir_appshell::Identity`. holger DELETED its hand-rolled About body and
# consumes this (LAW #5 — ADOPTION). Sibling facett checkout, three levels up like
# facett-helix; pulled only under `gui` (the operator card renders it).
facett-about = { version = "0.1", path = "../facett/facett-about", optional = true }
# facett-app `scene` — the SHARED declarative pane/scene host (LAW #5 — ADOPTION). holger's
# tabbed shell is now a `facett_app::scene::Scene` (Tabs root, one `Slot` per tab) rendered
# through facett's OWN TabView + pane engine by `scene::render`, and a thin `SceneHost` fills
# each slot by driving holger's existing `*_tab` bodies. This DELETED holger's hand-rolled tab
# strip + central `match self.tab` pane-host. `Device::from_width` drives the responsive
# canvas-first fold (the control chrome folds behind a ⚙ drawer on a phone-class viewport).
# Sibling facett checkout, three levels up like facett-about/facett-helix; pulled only under `gui`.
facett-app = { path = "../facett/facett-app", version = "0.1", optional = true }
# The facett-helix TIME-HELIX DAG view (sibling facett checkout — holger and facett
# are siblings under the same parent, so from server/ui it is three levels up). Pulled
# in under `gui` exactly like `facett`/`egui`/`eframe`; the Helix tab renders it.
facett-helix = { path = "../facett/facett-helix", version = "0.1", optional = true }
# facett-auth-ui — the SHARED in-GUI device-code SIGN-IN pane (LAW #5 — ADOPTION): the ONE
# robot-testable egui `Facet` that renders an RFC 8628 device-code login, so holger-server-ui, korp
# and dwarves-Profile mount ONE pane, not per-app twins. Presentational + host-driven: holger
# implements the `LoginSeam` (`login_seam::HolgerLoginSeam`) over the shared
# `nornir-auth-client` grant; the pane only renders the seam's states. Sibling facett checkout,
# three levels up like facett-about/facett-helix; pulled ONLY under `gui` (the operator Sign-in
# tab renders it) so the lean/server tiers gain no auth/egui edge.
facett-auth-ui = { path = "../facett/facett-auth-ui", version = "0.1", optional = true }
# nornir-auth-client — the SHARED client-side login (edda leaf): the RFC 8628 device-code grant
# + access-token acquire/silent-refresh + the object-safe `TokenStore` seam. holger's
# `HolgerLoginSeam` CONSUMES this so sign-in works identically to korp/dwarves (L5, never a
# device-code twin); holger-server-ui has no vault, so it uses the client's DEFAULT 0600 file store
# (`~/.holger/auth-token.json`). `online` links the networked device-code request + poll.
# gui-ONLY optional (it pulls the auth/reqwest stack) so the lean/server build stays lean.
nornir-auth-client = { version = "0.1", path = "../edda/crates/nornir-auth-client", optional = true, default-features = false }
# JWT subject decode for the signed-in identity DISPLAY (the server verifies the signature on
# presentation). gui-ONLY optional, pulled by the Sign-in pane alongside `nornir-auth-client`.
base64 = { version = "0.22", optional = true }
# Decodes the embedded holger+znippy splash logo PNG (compiled in via
# `include_bytes!`, no runtime file dependency — airgap). PNG-only to stay light.
image = { version = "0.25", optional = true, default-features = false, features = ["png"] }
# holger's own core, in-process: the `LocalHolger` / `RemoteHolger` handles, the
# `security` scanner types and the `audit` feed the Lineage tab taps.
#
# `default-features = false` — and it MATTERS: server-lib's `default` is
# `["mini", "online", "auth-bearer", "auth-oidc", "auth-oauth2", "auth-mtls"]`,
# and `online = ["dep:reqwest", "dep:ureq", …]`. Taking the defaults linked a live
# outbound HTTP client into every UI tier, including the one built to drive an
# airgapped server. The UI is a CLIENT — it never runs holger's auth providers —
# so it needs none of them; the gRPC/`Local` handles compile with zero features.
server-lib = { package = "holger-server-lib", version = "0.6.7", path = "../holger/server/lib", optional = true, default-features = false }
# Tools#3 warehouse-backed advisory scan (gui only): read modgunn's cve_advisories
# from the shared Iceberg warehouse when HOLGER_WAREHOUSE points at a catalog root.
# Pulls skade/Iceberg into the gui build — accepted for the operator UI; the lean
# server build never enables it.
modgunn = { version = "0.0", path = "../modgunn", default-features = false, features = ["scan", "warehouse"], optional = true }
# Tool #5 Airgap populate interface (always available — pure Rust planning, tiny
# leaf dep): the UI Tools#5 pane + view-model build a typed PopulateArgs and show
# the plan; execution delegates to the documented driver.
holger-airgap = { version = "0.1.0", path = "../holger/airgap" }

# Tool #1 holger-ds: the served-over-HTTP artifact-delivery benchmark driver
# (synthetic corpus → real Holger HTTP gateway → gatling GET pool). Pulled only
# under `gui` (the operator pane runs it); the `served` feature keeps it light
# (no JVM-competitor testcontainers infra).
bench-scenarios = { package = "holger-bench-scenarios", path = "../holger/bench-scenarios", features = ["served"], optional = true , version = "0.1.0" }
# Tool #2 migrate/suck: the holger agent connectors (Nexus / Artifactory → znippy)
# the operator pane invokes. Pulled only under `gui`.
agent-lib = { package = "holger-agent-lib", version = "0.6.6", path = "../holger/agent/lib", optional = true }

# 🎬 Demo mode (gui only): pull REAL packages from crates.io via the existing
# agent/world connector, and write them into a dev znippy store via the rust
# repository backend. Both are gui-gated leaf deps the demo command needs.
world-connector = { package = "holger-world-connector", version = "0.6.6", path = "../holger/agent/connector/world", optional = true }
znippy-rust-repo = { package = "holger-rust-znippy-repository", version = "0.6.7", path = "../holger/server/repository/znippy-rust", optional = true }

# Optional + feature-gated: pulled ONLY under `--features testmatrix`. A release
# build never compiles it, so the headless-render / view-model self-test emit
# calls are stripped. Local path resolved via the workspace [patch.crates-io].
nornir-testmatrix = { version = "0.2", optional = true }

# nornir-robotui — the SHARED robot-UI framework (edda leaf, resolved via the workspace
# `[patch.crates-io]`). Pulled ONLY under `robot-remote` with its lean serde-only `wire`
# feature (no eframe/windowing): it mounts a `cmd_json::CmdJsonServer` over holger's egui
# shell so edda's `fleet` drives holger WIRE-REMOTE over a real Unix socket — the SAME
# robotui vocabulary the co-located kittest robot tests use (REUSE, no second drive path).
# OFF by default so the shipped holger-server-ui binary never links the wire/kittest stack. The
# robot TESTS keep their own no-`wire` dev-dependency below; cargo unifies them.
nornir-robotui = { version = "0.3", optional = true, default-features = false }

[features]
default = []

# ══════════════════════════════════════════════════════════════════════════════
# THE UI TIER LADDER — three tiers, each STRICTLY leaner than the one above,
# in BYTES and in crate count, not just in tab count.
# ══════════════════════════════════════════════════════════════════════════════
#
# This used to be `mini = ["gui"]`. **Cargo features are purely additive — a
# feature cannot subtract a dependency**, so that line made the "mini" tier link
# the IDENTICAL closure as the full one and then add tab-filtering code on top.
# Measured 2026-07-22 (debug), under the bin names of the day: `holger-ui`
# 1 048 139 472 B vs `holger-ui-mikro`
# 1 050 137 592 B — the mini artifact was 2 MB BIGGER. The structure below is the
# inversion: every heavy dependency is optional and hangs off the ONE pane that
# needs it, and each tier selects only the panes it can actually drive.
#
# The ladder is guarded by `tests/tier_ladder.rs`, which BUILDS all three and
# fails if a tier is not strictly leaner than the tier above it. A declaration is
# not evidence.
#
#   tier        bin               surface                       panes
#   ─────────── ───────────────── ───────────────────────────── ───────────────
#   gui         holger-server-ui        11 tabs, read + write    all
#   gui-mini    holger-server-ui-mini    6 read tabs             about-card
#   gui-mikro   holger-server-ui-mikro   3 read tabs             —

# `gui-core` — the egui presentation ITSELF, and the only thing every tier links:
# eframe/egui + facett + facett-app (the scene/pane host) + the Local/Remote
# handles from `server-lib`.
#
# `server-lib` is declared `default-features = false` (see the dep above): its
# DEFAULT feature set turns on `online` → **reqwest + ureq**, and a UI client has
# no business linking an outbound HTTP client just to hold a gRPC handle. The
# full tier re-enables what its Tools panes need via `tools`.
gui-core = ["dep:eframe", "dep:egui", "dep:facett", "dep:facett-app", "dep:server-lib"]

# ── the removable PANES: each owns the leaf deps it, and only it, needs ───────
# `about-card` — the shared `facett_about::AboutCard` + the compiled-in splash
# logo (`image`, PNG decode). Chrome, not surface: the mikro tier drops both and
# renders the text identity instead.
about-card = ["dep:facett-about", "dep:image"]
# `signin` — the Sign-in tab. `nornir-auth-client/online` is the ONE thing that
# links an outbound HTTP client (reqwest) into a CLIENT tier, which is why the
# airgap-facing mini/mikro tiers do not have it.
signin = ["dep:facett-auth-ui", "dep:nornir-auth-client", "nornir-auth-client/online", "dep:base64"]
# `helix` — the Helix tab and the Lineage tab's time-helix companion view.
helix = ["dep:facett-helix"]
# `tools` — the Tools tab over the WIRE: migrate/suck (#2 → agent-lib → reqwest,
# which talks to a REMOTE Nexus/Artifactory), deploy (#4), airgap (#5) and the
# jera demo appliance (#6). Every one of these drives something ACROSS a network
# or plans a command; none opens holger's own state in this process.
tools = ["dep:agent-lib"]
# `bench` — Tool #1 `holger-ds`, the artifact-delivery benchmark. Measured, not
# assumed: this driver BOOTS A REAL HOLGER HTTP GATEWAY in-process (`server-lib`'s
# serve path) and bombards it with a gatling GET pool. `nm` finds
# `tonic::transport::server` and 18 `FastRoutes` symbols in a binary that links it.
# A benchmark harness that stands up the server it benchmarks is by definition an
# embedded workload, so it belongs to `gui-embedded`, not to the client a human runs.
bench = ["dep:bench-scenarios"]
# `warehouse-scan` — Tools #3's warehouse-backed advisory corpus: `modgunn` with
# `warehouse`, i.e. **skade**, i.e. an Iceberg catalog OPENED IN THIS PROCESS from
# `$HOLGER_WAREHOUSE`. It is the single feature that drags skade into a holger
# artifact.
#
# That is the second-writer hazard the suite-wide THIN-CLIENT rule exists to stop:
# a UI that can open the warehouse itself is a second process on the warehouse
# lock, showing its own view of the data instead of the server's, with nothing on
# screen saying which. So it is NOT in the default `gui` tier — the Tools #3 pane
# still works there and falls back to the built-in demo advisory corpus
# (`scanner_for` in `data.rs` already has that branch). `gui-embedded` is its home.
warehouse-scan = ["dep:modgunn"]
# `demo` — `holger-server-ui --demo`: pull REAL crates from crates.io through the agent
# world-connector (→ reqwest) into a dev znippy store and robot-drive the tour.
demo = ["dep:world-connector", "dep:znippy-rust-repo"]

# ── the SURFACE markers. These carry NO dependency: they only say which tabs
# exist, and every `#[cfg(feature = "mini")]` in `app.rs` / `app/scene.rs` /
# `app/command.rs` keys off `mini`. Keeping the marker separate from the tier is
# what lets `mikro` be a SUBSET of `mini` (it implies the marker) while still
# dropping `about-card`, which a `mikro = ["gui-mini"]` chain could never do.
# `mini` — the read-only surface for the mini SERVER (`holger-mikro`:
# server-lib/mini, no federation, no `online`): Status / Repos / Browse /
# Archive / Artifact / Search, the six read ops that server's gRPC surface backs.
mini = []
# `mikro` — the MINIMAL surface, a strict subset of `mini`: Status / Repos /
# Artifact. It keeps exactly the three ops that answer "is the registry up, what
# does it carry, and is THIS exact coordinate on it" — `AdminService.Health`,
# `RepositoryService.ListRepositories`, `RepositoryService.GetArtifact`. It drops
# Browse (ListArtifacts), Archive (ListArchiveFiles + ArchiveInfo) and Search
# (SearchService.Search) — the three paging/scanning surfaces — plus the About
# card and the splash-logo PNG decoder.
mikro = ["mini"]

# ── the TIERS a `[[bin]]` selects ────────────────────────────────────────────
# `gui` — the operator UI a human runs (bin `holger-server-ui`), and **THIN**: every pane
# it carries works over the gRPC channel to a RUNNING holger. It deliberately does
# NOT enable `demo` (an in-process `LocalHolger` over a real znippy store it fills
# itself) or `warehouse-scan` (an in-process Iceberg catalog), because a client
# that embeds its own server shows its own world instead of the server's and
# becomes a second writer against the warehouse lock.
#
# Verified by symbol probe on the RELEASE BINARY, not by this comment — see
# `tests/thin_client.rs`.
gui = ["gui-core", "about-card", "signin", "helix", "tools"]
# `gui-embedded` — the deliberate exception (bin `holger-server-ui-embedded`): `gui`
# plus the three panes that open state or stand up a server IN THIS PROCESS —
# `holger-server-ui --demo` (a hands-free tour over a znippy store it fills itself), the
# Tools #3 warehouse-backed advisory corpus (an Iceberg catalog), and the Tool #1
# holger-ds bench (a real HTTP gateway). Opt-in BY NAME, exactly like dwarves'
# `--embedded`: the zero-config path is thin, and the fat one is something you ask
# for.
gui-embedded = ["gui", "warehouse-scan", "bench", "demo"]
# `gui-mini` — the read-only UI for the mini SERVER (bin `holger-server-ui-mini`).
# No `signin`/`tools`/`demo` ⇒ no reqwest, no ureq, no skade.
gui-mini = ["gui-core", "about-card", "mini"]
# `gui-mikro` — the leanest client (bin `holger-server-ui-mikro`). `gui-mini` minus the
# About card, the splash PNG decoder, and the three paging surfaces.
gui-mikro = ["gui-core", "mikro"]
# LIVE demo container: turn on jera's OCI engine (routed THROUGH draupnir) so the
# Tools "Demo appliance" entry actually boots the demo registry. Off by default — the
# lean build only DECLARES the spec + records the jera dispatch job (daemon-free).
containers = ["jera/container-oci"]
# Passthrough: turns on the emitter so the view-model + look-&-feel self-tests
# (which read each view's observable `state_json()`) append real functional rows.
testmatrix = ["dep:nornir-testmatrix", "nornir-testmatrix/testmatrix"]
# `robot-remote` — MOUNT a `nornir_robotui::cmd_json::CmdJsonServer` over holger's egui
# operator shell on a Unix socket, so edda's `fleet` can drive holger WIRE-REMOTE (the fat
# leg) exactly like a co-located `RobotSession` — the SAME robotui vocabulary over a byte
# stream (REUSE, no second drive path). Implies `gui` (the egui app + server-lib handles it
# drives) and pulls `nornir-robotui` with its lean serde-only `wire` feature (no eframe/
# windowing). OFF by default: the SHIPPED holger-server-ui binary never mounts an always-on socket
# — the mount is reached only via the `--robot-remote <socket>` CLI flag under this feature.
robot-remote = ["gui", "dep:nornir-robotui", "nornir-robotui/wire"]

# ══════════════════════════════════════════════════════════════════════════════
# THE PACKAGE AND ITS BINS SAY WHICH COMPONENT THEY DRIVE (renamed 2026-08-01)
# ══════════════════════════════════════════════════════════════════════════════
#
# `holger-ui*` → `holger-server-ui*`. The suite convention is that the CLI/UI
# driving a component is `<product>-<component>-cli` / `<product>-<component>-ui`:
# `server/cli` is already package `holger-server-cli` and produces the SERVER
# daemons, so the UI that drives those daemons is the `server` UI. Plain
# `holger-ui` did not say WHAT it was a UI of, and holger ships more than one
# drivable thing (server, agent).
#
# The BINS were renamed first (they are the build-matrix surface and cost nothing
# outward). The PACKAGE followed on the same day, on Rickard's explicit call:
# `holger-ui` is PUBLISHED on crates.io (0.1.4, three versions, not yanked —
# re-checked 2026-08-01 against the API; the published description is
# byte-identical to this manifest's, so it is ours) and a package rename is an
# irreversible crates.io act — the old name can never be reclaimed or removed and
# the new one has to be published fresh. Reverse dependencies are `"total":0`, so
# nothing downstream breaks; `holger-server-ui` was unclaimed at the time of the
# rename. The old `holger-ui` releases stay up, unyanked, as a dead end.
[[bin]]
name = "holger-server-ui"
path = "src/main.rs"
required-features = ["gui"]

# The embedded exception — same source, `--demo` and the warehouse scan compiled
# in. Named so that "this build opens state in the UI process" is a thing you ask
# for and never a thing you get by default.
[[bin]]
name = "holger-server-ui-embedded"
path = "src/main.rs"
required-features = ["gui-embedded"]

# The mini-tier UI artifact — same source, read-only surface, and now a genuinely
# leaner LINK (no signin/tools/demo/helix ⇒ no reqwest, no ureq, no skade).
#
# **Renamed twice**: `holger-ui-mikro` → `holger-ui-mini` (2026-07-23, when this
# became the MIDDLE rung and a new, leaner bottom rung took the `mikro` name) →
# `holger-server-ui-mini` (2026-08-01, the `<product>-<component>-ui` convention).
# (The server-side collision is unchanged and is NOT this binary:
# `holger-server-cli`'s `mini` tier builds a SERVER called `holger-mikro`.)
[[bin]]
name = "holger-server-ui-mini"
path = "src/main.rs"
required-features = ["gui-mini"]

# The mikro-tier UI artifact — the bottom rung: Status / Repos / Artifact, no
# About card, no splash PNG decoder. Mirrors server-cli's one-source /
# three-`required-features`-binaries pattern.
[[bin]]
name = "holger-server-ui-mikro"
path = "src/main.rs"
required-features = ["gui-mikro"]

[dev-dependencies]
async-trait = "0.1"
# nornir-build-thing — the SHARED `LinkageAudit` + `THIN_FORBIDDEN_SYMBOLS` that
# `tests/thin_client.rs` holds every UI tier to (LAW #5 — ADOPTION). This used to be a
# hand-rolled re-implementation of `dwarves::build_thing::LinkageAudit`, because dwarves
# is the whole CI/CD forge (~127 deps) and dev-depending on it would have made `cargo
# test -p holger-server-ui` build the forge to reach one struct. The audit was promoted into
# this dep-light edda leaf instead, so the twin is gone and the cost is ~25 crates
# holger already links (serde/serde_json/anyhow/sha2/chrono/toml/cargo_metadata) and
# ZERO edges on dwarves — `cargo tree -p holger-server-ui --all-features | grep -c dwarves` is
# 0, which is the whole point. Default features only: no wasmtime (`plugin-host`), no
# object parser (`artifact-section`), no bollard — the symbol read shells out to `nm`,
# so the audit itself adds no dependency at all. Same sibling-checkout path as
# nornir-appshell.
# path-only dev-dep: the version beside it fail-stops the upload over a crate no
# consumer of the published package ever sees.
nornir-build-thing = { path = "../edda/crates/nornir-build-thing" }
# The infra (Tools demo) dispatch test opens a real jera redb ledger in a temp dir.
tempfile = "3"
# Build a display-only JWT access_token in the Sign-in robot test's stub IdP so the
# `HolgerLoginSeam` subject decoder recovers the identity (base64url, no signature).
base64 = "0.22"
# Integration tests drive the view-model against *real* holger core: LocalHolger
# (direct) and an in-process gRPC server reached via RemoteHolger (remote).
server-lib = { package = "holger-server-lib", path = "../holger/server/lib" }
# The Tool #1 / Tool #2 run methods are `#[cfg(any(feature = "gui", test))]`, so
# the default test build needs the bench driver + agent connectors too.
bench-scenarios = { package = "holger-bench-scenarios", path = "../holger/bench-scenarios", features = ["served"] }
agent-lib = { package = "holger-agent-lib", path = "../holger/agent/lib" }

tonic = { version = "0.14", features = ["tls-ring"] }
tokio-stream = { version = "0.1", features = ["net"] }

# Robot-UI: reuse nornir's egui_kittest + nornir-robotui framework so the REAL
# app is driven by AccessKit clicks and each (action → expected) step becomes a
# nornir test-matrix row — nornir can SEE holger's UI tests. egui_kittest with
# default-features off (no wgpu): clicks + state_json assertions need only the
# headless harness.
nornir-robotui = { version = "0.3" }
egui_kittest = { version = "0.35", default-features = false }
egui = "0.35"

# holger-egui is its own workspace. It was holger's `server/ui` until 2026-09-15 and
# still links holger's crates by path (`../holger/...`), the sibling layout every
# nordisk repo uses.
[workspace]
# `holger-egui-build-thing` is this repo's OWN build thing: the four `ui-*`
# variants and the thin-client rules that came with `server/ui` from holger on
# 2026-09-16. holger keeps no rule naming a variant it no longer declares — such
# a rule fails OPEN, which is the defect those rules exist to prevent.
members = [".", "holger-egui-build-thing"]
resolver = "2"

# Same as holger's: local facett is ahead of the registry.
[patch.crates-io]
facett = { path = "../facett/facett" }