holger-ui — AI context (full API & source reference)
Read ai-context.md first. This is not normal prompt context — it is a lookup reference for exact public-API signatures, selected private APIs, complete rustdoc and
file:linesources. Do not load it for every task.
Snapshot
| Field | Value |
| Repository | holger-ui |
| Source revision | 11f89df5141d328237d43c8cbed2b8997715b639 |
| Working tree | Clean |
| Reproducible snapshot | Reproducible |
| Generation mode | development |
| Generator version | kvasir 0.2.1 |
| Maintained metadata | .nornir/ai-context.toml |
| Generated at | 2026-08-14T10:39:59+02:00 (source: commit) |
Navigation
Architecture, runtime wiring, security invariants, failure semantics, the trust lifecycle, the state inventory and the task map all live in ai-context.md:
- Runtime wiring map
- Non-negotiable invariants
- Failure semantics
- Task-to-file map
- Trust and lifecycle
- State and persistence
Crate and module map
| Category | Count | Included items |
| Cargo workspace packages | 1 | holger-ui-web |
| Detached Rust packages | 0 | — |
| Packages with indexed API surface | 1 | 1 workspace + 0 detached |
| Binary targets | 1 | [[bin]] + src/main.rs + src/bin/* |
| Library targets | 0 | packages with src/lib.rs |
The indexed count (1) is the 1 workspace members that expose a src/ tree plus 0 detached package(s) (own [workspace], indexed for their API but outside the dependency graph). Detached packages are in the API index; they are not in the workspace dependency graph.
| crate | description | target | modules | items |
holger-ui-web |
[web-console] The whole SPA: generated gRPC-web clients + routed Leptos views + OIDC PKCE. Single crate; the Vetra theme lives in index.html as plain CSS custom properties. | bin | 19 | 90 |
Workspace dependency graph
Garmr workspace crates only (external deps excluded); edges deduplicated, kind + feature-gate marked.
(no entries)
Central external dependency hotspots
Only architecturally meaningful dependencies. Used by is derived from Cargo metadata.
| Dependency | Used by | Architectural effects | Security/reliability implications | Feature/runtime impact |
tonic |
holger-ui-web |
gRPC transport for the Arrow-Flight ingest service. | The Flight path has no bearer check — collector id is self-declared. | Feature-gated (flight). |
leptos |
holger-ui-web |
Web console (WASM CSR SPA), served as static assets. | Separate binary; not linked into the daemon. | Separate binary. |
High-coupling crates
| Crate | Incoming workspace edges | Outgoing workspace edges | Why changes are high impact |
holger-ui-web |
0 | 0 | moderately central |
Dependency validation:
- duplicate workspace edges after dedup: 0 (must be 0)
- external dependency present in workspace graph: false (must be false)
- workspace crates: 1, workspace edges: 0, external deps: 15
- dependency kind retained per edge; feature-gated edges marked
API reference
Every exported item (plus security-relevant private items), grouped by change-relevance, then crate → module. Each entry: signature, file:line, and its rustdoc.
Core contracts
(no crates in this group)
Runtime and CLI
(no crates in this group)
Ingest
(no crates in this group)
Storage, search, query
(no crates in this group)
Detection, policy, catalog, baselines
(no crates in this group)
Agent, LLM, tools
(no crates in this group)
Governance, audit, actions
(no crates in this group)
Retention, backup, HA
(no crates in this group)
API and UI
(no crates in this group)
Development tooling
Crate holger-ui-web
[web-console] The whole SPA: generated gRPC-web clients + routed Leptos views + OIDC PKCE. Single crate; the Vetra theme lives in index.html as plain CSS custom properties.
Module holger_ui_web
fn main() · src/main.rs:19
fn main() · src/main.rs:25
Module holger_ui_web::app
The console shell: topbar (brand, nav, session, health) and the routed views. Every view is a URL you can share:
/ overview (dashboard) /repositories repository list /repo/:name one repository: browse · archive · upload /artifact?repo=&ns=&name=&version= artifact detail (query-addressed: raw artifact names contain slashes) /search?name=&…&repo=&prop=k=v search — axes in the query, auto-run /replicate replication (preview-first) /login token paste + OIDC/PKCE loginA directly-opened URL carries no view state, so each view fetches what it needs from its address alone. Serving note: any host must fall back to index.html for unknown paths (Trunk's dev server does; a reverse proxy needs the same rule).
fn App() -> impl IntoView · src/app.rs:32
fn HealthBadge() -> impl IntoView · src/app.rs:144
Module holger_ui_web::components::copy_button
Copy-to-clipboard with feedback: the button says what it copies (the accessible name carries it too) and confirms with a brief "copied".
fn CopyButton(# [prop(into)] label: String, # [prop(into)] value: Signal<String>,) -> impl IntoView · src/components/copy_button.rs:10
Module holger_ui_web::components::feedback
The console's shared feedback vocabulary: status badges, error/empty/loading states, and the confirmation dialog for irreversible actions. One rendering per state kind — views compose these instead of re-inventing them.
enum Tone { Good, Warn, Bad, Neutral } · src/components/feedback.rs:9
Semantic tone — never color alone: every badge carries its text.
fn Badge(tone: Tone, # [prop(into)] label: String) -> impl IntoView · src/components/feedback.rs:28
fn Loading(# [prop(optional, into)] what: String) -> impl IntoView · src/components/feedback.rs:33
fn ErrorText(# [prop(into)] context: String, # [prop(into)] message: String) -> impl IntoView · src/components/feedback.rs:44
An error is a sentence, not a color: the server's message rides verbatim.
fn EmptyState(# [prop(into)] message: String) -> impl IntoView · src/components/feedback.rs:51
An empty state says what "empty" MEANS here — a blank table never speaks for itself, and for some backends an empty listing is not "no data".
fn FailBox(# [prop(into)] context: String, # [prop(into)] fail: Signal<crate::grpc::Fail>, # [prop(optional)] retry: Option<Callback<()>>,) -> impl IntoView · src/components/feedback.rs:62
One failed call, rendered from the unified state model: a kind-specific summary FIRST (auth needed / permission denied / unsupported / backend unavailable are different situations demanding different next steps), then the server's verbatim message, the status-code name, the timestamp, and a retry button when the caller says retrying is safe (reads only — a mutation is never blind-retried from here).
fn ConfirmDialog(open: RwSignal<bool>, # [prop(into)] title: String, # [prop(into)] consequence: String, # [prop(into)] confirm_label: String, busy: RwSignal<bool>, on_confirm: Callback<()>, # [prop(optional, into)] confirm_phrase: Option<Signal<String>>, children: ChildrenFn,) -> impl IntoView · src/components/feedback.rs:119
Confirmation for irreversible actions. The consequence text is the point: it must say what cannot be undone, in the domain's own words. The confirm button disarms while the action runs (double-click safe) — and the caller keeps its own busy-guard, because THIS dialog is UI, not the safety.
confirm_phrase adds a type-to-confirm gate for the most destructive operations: the confirm button stays disabled until the operator types the exact target name. It defends against the wrong-target mistake (right intent, wrong repository) that a plain OK cannot catch. Reactive, because the target can change while the dialog is closed; the typed text is cleared on every open so a previous answer can never carry over.
A11y: focus moves into the dialog on open (the phrase input when there is one — it is inert until the name matches — otherwise Cancel, the safe default), Tab cycles between the dialog's focusable elements, Escape cancels (unless busy), and focus returns to the opener on close.
Module holger_ui_web::components::feedback::Tone
fn class(self) -> &'static str · src/components/feedback.rs:17
Module holger_ui_web::components::skeleton
Layout-preserving loading placeholders.
A skeleton is only honest when it has the SHAPE of what arrives: same table, same columns, so the page does not jump when data lands. Text-only
Loadingstays for small inline waits — skeletons are for the table-shaped ones, where a bare "loading…" collapses the layout twice.Screen readers get the sentence, not the shimmer: the boxes are
aria-hiddenand a single visually-hidden live region speaks instead.
fn TableSkeleton(# [doc = " How many placeholder rows to draw — pick the page's usual density, not"] # [doc = " a number that implies a count the server has not reported."] # [prop(default = 5)] rows: usize, # [prop(default = 4)] cols: usize, # [prop(into)] what: String,) -> impl IntoView · src/components/skeleton.rs:14
Module holger_ui_web::grpc
gRPC-web wiring — the one place that knows how the browser reaches holger, and the one place a credential is attached.
Generated stubs come from
proto/holger.proto(vendored from the holger main repo — that copy is the source of truth; refresh withcp ../holger/proto/holger.proto proto/). Transport istonic-web-wasm-clientover fetch; holger's server terminates gRPC-web with its tonic-web layer.
fn base_url() -> String · src/grpc.rs:31
Base URL of the holger gRPC-web door. Same-origin under /grpc by default: the dev proxy (Trunk) and any reverse proxy in front of holger mount the door there, so no CORS ceremony is needed. Override at build time with HOLGER_GRPC_WEB_URL (e.g. a direct server origin — that path needs CORS opened on the holger side instead).
fn storage() -> Option<web_sys::Storage> · src/grpc.rs:51
fn token() -> Option<String> · src/grpc.rs:55
fn set_token(token: &str) · src/grpc.rs:61
fn clear_token() · src/grpc.rs:67
struct AuthInterceptor · src/grpc.rs:77
Attaches authorization: Bearer <token> to every outgoing call when a token is set; without one, requests go out anonymous (reads may still be open server-side — that posture is the server's call, not the console's).
type Authed · src/grpc.rs:91
fn channel() -> Authed · src/grpc.rs:93
fn admin() -> AdminServiceClient<Authed> · src/grpc.rs:97
fn repositories() -> RepositoryServiceClient<Authed> · src/grpc.rs:101
fn search() -> SearchServiceClient<Authed> · src/grpc.rs:105
fn sboms() -> SbomServiceClient<Authed> · src/grpc.rs:109
fn properties() -> PropertyServiceClient<Authed> · src/grpc.rs:113
fn promotion() -> PromotionServiceClient<Authed> · src/grpc.rs:117
fn replication() -> ReplicationServiceClient<Authed> · src/grpc.rs:121
fn archives() -> ArchiveServiceClient<Authed> · src/grpc.rs:125
async fn who_am_i() -> Result<pb::WhoAmIResponse, tonic::Status> · src/grpc.rs:132
The server's answer to "who am I here?" — subject, method, effective global role. Discoverability only: every operation is still checked by its own gate server-side.
fn status_text(s: &tonic::Status) -> String · src/grpc.rs:142
The server's refusal, verbatim, with the status-code name as fallback when a transport-level failure carries no message — one rendering for every gRPC error in the console.
struct Fail { code: tonic::Code, message: String, at: String } · src/grpc.rs:156
One failed call, as the console's unified state model sees it: the code (classified for rendering), the server's verbatim message, and WHEN. Never carries the credential — the interceptor owns that and it does not appear in statuses.
fn fail(s: &tonic::Status) -> Fail · src/grpc.rs:162
Module holger_ui_web::grpc::AuthInterceptor
fn call(&mut self, mut req: tonic::Request<()>) -> Result<tonic::Request<()>, tonic::Status> · src/grpc.rs:80
Module holger_ui_web::oidc
OIDC authorization-code login with PKCE (RFC 7636, S256) — the browser-SPA shape: a PUBLIC client, no secret, the code exchange bound to a one-shot verifier so an intercepted redirect code is useless.
Flow:
begin_logindiscovers the issuer, stores verifier + state, and leaves the page for the IdP's authorization endpoint. The IdP redirects back to the console's own origin with?code&state;complete_login(run once at boot) validates the state echo, exchanges the code (with the verifier) at the token endpoint over fetch, hands the access token to the same store the pasted-token login uses, and scrubs the query string from the address bar. From there the tonic interceptor carries it — the two login forms are indistinguishable to the rest of the console.The IdP must list the console origin as an allowed redirect URI and allow it for CORS on the token endpoint — both standard SPA registration steps.
struct Discovery · src/oidc.rs:29
struct TokenResponse · src/oidc.rs:35
fn session() -> Option<web_sys::Storage> · src/oidc.rs:39
fn local() -> Option<web_sys::Storage> · src/oidc.rs:43
fn settings() -> (String, String) · src/oidc.rs:49
The issuer/client-id the operator saved (falling back to build-time defaults) — they are configuration, not secrets, so localStorage is right.
fn save_settings(issuer: &str, client_id: &str) · src/oidc.rs:63
fn b64url(bytes: &[u8]) -> String · src/oidc.rs:70
fn random_urlsafe(n_bytes: usize) -> Result<String, String> · src/oidc.rs:93
async fn sha256_b64url(input: &str) -> Result<String, String> · src/oidc.rs:105
fn redirect_uri() -> Result<String, String> · src/oidc.rs:123
async fn discover(issuer: &str) -> Result<Discovery, String> · src/oidc.rs:130
async fn begin_login(issuer: &str, client_id: &str) -> Result<(), String> · src/oidc.rs:145
Leave the page for the IdP. Only returns on failure-before-redirect.
async fn complete_login() -> Option<Result<(), String>> · src/oidc.rs:183
Run once at boot. None = this page load is not an OIDC callback. Some(Ok(())) = token exchanged and stored; Some(Err) = the callback was recognized but failed (state mismatch, exchange refused, …).
async fn exchange(code: String, echoed_state: Option<String>, verifier: String, expected_state: String, window: web_sys::Window,) -> Result<(), String> · src/oidc.rs:219
Module holger_ui_web::pages::artifact_detail
Query-addressed artifact detail. A shared URL carries only the coordinate, so every fact is looked up from the address alone. Sections: overview facts, actions, SBOM, properties, promotion — with promotion behind an explicit consequence-stating confirmation.
enum Facts { Found, Absent, Undetermined } · src/pages/artifact_detail.rs:22
What the exact-match page established about a coordinate's facts. Absent is the ONLY state that may render as "—": it is the only one where the server actually showed a complete page without the coordinate in it.
fn ArtifactDetail() -> impl IntoView · src/pages/artifact_detail.rs:29
fn set_property(props: RwSignal<Option<Result<Vec<grpc::pb::PropertyEntry>, String>>>, write: RwSignal<Option<Result<String, String>>>, busy: RwSignal<bool>, repo: String, id: grpc::pb::ArtifactId, key: String, values: Vec<String>,) · src/pages/artifact_detail.rs:300
Fire one SetProperty and land the server's returned full map in the shared signal — the response IS the new truth, no refetch.
The WRITE's outcome lives in its own signal: a refused write says nothing about what the last successful READ returned, so it must not overwrite the property list with an error (which would also mislabel the failure as "failed to fetch properties"). busy guards against double submits.
fn SbomAttach(repo: Memo<String>, id: Memo<grpc::pb::ArtifactId>, on_attached: Callback<()>,) -> impl IntoView · src/pages/artifact_detail.rs:345
Attach a CycloneDX document to the coordinate. The server owns validation and the write gate — its refusal is the UI text, verbatim.
fn PropertyEditor(repo: Memo<String>, id: Memo<grpc::pb::ArtifactId>, props: RwSignal<Option<Result<Vec<grpc::pb::PropertyEntry>, String>>>, write: RwSignal<Option<Result<String, String>>>, busy: RwSignal<bool>,) -> impl IntoView · src/pages/artifact_detail.rs:427
Set a key = comma-separated values on the coordinate. A re-set of an existing key overwrites its values; removal lives on each table row.
fn Promote(repo: Memo<String>, id: Memo<grpc::pb::ArtifactId>) -> impl IntoView · src/pages/artifact_detail.rs:493
Promote this coordinate into an immutable destination — behind an explicit confirmation that states the seal-once consequence. The server's ADMIN gate and the create-once seal are the safety; the dialog makes the irreversibility readable, and never softens it.
Module holger_ui_web::pages::dashboard
Översikt — the operational summary. Everything on this page is a live RPC (
Health,ServerProfile,ListRepositories); nothing is decorative.
fn Dashboard() -> impl IntoView · src/pages/dashboard.rs:11
Module holger_ui_web::pages::login
Both login forms, one credential store. The pasted token and the OIDC access token land in the same sessionStorage slot and ride the same interceptor — the rest of the console cannot tell them apart.
fn Login(logged_in: RwSignal<bool>) -> impl IntoView · src/pages/login.rs:17
Paste-a-token login. The token is whatever the server's configured auth methods accept — a static API token or an OIDC/OAuth2 access token issued by the operator's IdP; the console never sees a password and never mints anything itself. Stored in sessionStorage (dies with the tab, removed by Log out) and attached by the interceptor as authorization: Bearer.
fn SsoLogin() -> impl IntoView · src/pages/login.rs:92
OIDC authorization-code + PKCE. The console is a PUBLIC client: no secret, the code exchange is bound to a per-attempt verifier, and the resulting access token lands in the same store as a pasted one. Issuer + client id are operator configuration (persisted in localStorage, prefillable at build time via HOLGER_OIDC_ISSUER / HOLGER_OIDC_CLIENT_ID).
Module holger_ui_web::pages::replication
One-shot repository replication with the server's own two-step shape:
execute: falseis a PREVIEW and the default; the Execute button unlocks only after a preview of the same pair has been seen. An Ok RESPONSE is not success —failed/truncated/completerender as their own facts.
fn Replicate() -> impl IntoView · src/pages/replication.rs:14
Module holger_ui_web::pages::repositories
Repository list — scannable, with the writability/archive facts as badges (never color alone), every name a real link, and the filter/sort state in the URL so a filtered view is a shareable address.
fn Repositories() -> impl IntoView · src/pages/repositories.rs:14
Module holger_ui_web::pages::repository_detail
One repository's home: metadata header, browse (filter + offset-cursor pagination), the archive view when the backend carries a znippy archive, and upload when the repository accepts writes.
const PAGE_SIZE: i32 · src/pages/repository_detail.rs:17
One page of artifacts per fetch; the view accumulates pages until the server stops handing back a next_page_token.
fn RepositoryDetail() -> impl IntoView · src/pages/repository_detail.rs:20
fn Archive(repo: Memo<String>) -> impl IntoView · src/pages/repository_detail.rs:263
The znippy-archive view: totals from ArchiveInfo, paths (with prefix filter) from ListArchiveFiles. Read-gated like every other read.
fn Upload(repo: Signal<String>, on_uploaded: Callback<()>) -> impl IntoView · src/pages/repository_detail.rs:340
The write surface: one file becomes one PutArtifact. The name prefills from the picked file (for a raw repo the name IS the path — type a full dir/sub/file.ext to place it); the server's write gate decides whether the caller may, and its refusal is shown verbatim.
Module holger_ui_web::pages::search
Search — every axis the server's proto actually offers: name, namespace, version, path, checksum, a repository restriction, and custom-property filters (
PropertyMatch). The URL is the search state: filters prefill from it, a shared address auto-runs, and running a search IS navigating.
fn Search() -> impl IntoView · src/pages/search.rs:15
Module holger_ui_web::util
Pure helpers — no web APIs, so they compile and unit-test on the native target too (the rest of the crate is wasm-only; hence the allow — native builds see no callers).
fn display_name(namespace: &str, name: &str) -> String · src/util.rs:7
namespace/name when a namespace exists, else just the name.
fn enc(s: &str) -> String · src/util.rs:18
Percent-encode one URL query component (RFC 3986 unreserved kept) — raw artifact names carry /, so every coordinate that rides a query string goes through here. Pure Rust: testable natively, no js interop.
fn artifact_href(repo: &str, namespace: &str, name: &str, version: &str) -> String · src/util.rs:33
The shareable address of an artifact's detail view (query-addressed — names contain slashes).
fn fmt_bytes(n: i64) -> String · src/util.rs:44
1234567 → 1.2 MB — sizes are for scanning, not accounting.
fn parse_prop_param(raw: &str) -> (String, String) · src/util.rs:63
One property filter as it rides the search URL: key alone (presence) or key=value (exact value among the key's values) — the FIRST = splits, values may contain more.
fn prop_param(key: &str, value: &str) -> String · src/util.rs:70
fn confirm_phrase_matches(typed: &str, required: &str) -> bool · src/util.rs:84
Does the typed confirmation match the phrase the dialog demands?
Exact match after trimming the ends — a paste that picked up a trailing newline should not block a legitimate confirmation, but a DIFFERENT name must never pass, and an empty requirement can never be satisfied (a type-to-confirm gate with nothing to type would be no gate at all).
Module holger_ui_web::util::tests
fn display_name_joins_namespace_only_when_present() · src/util.rs:94
fn enc_keeps_unreserved_and_escapes_the_rest() · src/util.rs:100
fn artifact_href_is_query_addressed_and_escaped() · src/util.rs:106
fn fmt_bytes_scans_well() · src/util.rs:114
fn prop_param_round_trips_first_equals_split() · src/util.rs:123
fn confirm_phrase_demands_the_exact_name() · src/util.rs:132
fn an_empty_requirement_is_never_satisfied() · src/util.rs:145
Module holger_ui_web::webio
Browser-side I/O helpers (wasm-only): file reads and named downloads.
fn save_bytes(file_name: &str, data: &[u8], content_type: &str) -> Result<(), String> · src/webio.rs:8
Hand the bytes to the browser as a named download via a temporary object URL. The URL is revoked immediately after the click — the blob lives on until the download completes.
async fn read_file(file: &web_sys::File) -> Result<Vec<u8>, String> · src/webio.rs:35
Read the picked file's bytes off the main thread via File.arrayBuffer().
fn set_title(title: &str) · src/webio.rs:43
Set the browser-tab title — every routed view names itself.
fn now_hms() -> String · src/webio.rs:50
Local wall-clock HH:MM:SS — mutation results carry WHEN they happened.
async fn copy_text(text: &str) -> Result<(), String> · src/webio.rs:62
Put text on the clipboard (async Clipboard API; falls back to a rejected future outside secure contexts — the caller reports, never panics).
Benchmarks and test infrastructure
(no crates in this group)
Incomplete-work markers (full)
None found.
Tests inventory
7 unit test fns across 1 files; 3 integration test fns across 1 files.
| Crate | Unit test fns | Integration test fns |
src |
7 | 0 |
tests |
0 | 3 |
Size and generation metadata
Measured for the full pack (excludes this footer):
| Metric | Value |
| Bytes | 25349 |
| Lines | 586 |
| Words | 3401 |
| Approx tokens (chars/4) | 6338 |
| Token budget | 7% of 80000 |
| Duplicate-sentence ratio | 0.000 (max 0.000) |
| Duplicate-row ratio | 0.000 (max 0.000) |