src/app.rs
//! The console shell: topbar (brand, nav, session, health) and the routed
//! views. Every view is a URL you can share:
//!
//! ```text
//! / the overview again (the console's home)
//! /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
//! /security?queue= the intake queues (the security dashboard)
//! /security/artifact?ns=&name=&version= one artifact's state, evidence and buttons
//! /replicate replication (preview-first)
//! /migrate migrate a Nexus/Artifactory in (preview-first)
//! /admin the admin view (an admin's session only;
//! the server refuses the data to others)
//! /overview the estate at a glance: where a login lands
//! (`/` itself is holger's front page on a
//! served console)
//! /flow?open= the artifact flow, the console's only picture
//! /apps the watched applications (a Security tab)
//! /apps/:name one application's lamp, its reasons and call paths
//! /mine my repositories: list, make a private one,
//! client setup (cargo, pip, npm, maven)
//! /tokens my tokens: mint (shown once), list, revoke
//! /account who holger says this session is; token
//! paste for a holger without passkeys
//! /login NOT a route here: the static passkey page
//! (`assets/login.html`) the server serves
//! ```
//!
//! A metered console (feature `metered`, A28 M13) has one address more, reached through the
//! router's fallback ([`unmatched`]) and not as a route, so an open console has no such
//! address: `pages::plan::PATH`, the person's room, plan and orders.
//!
//! A 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).
use leptos::prelude::*;
use leptos::task::spawn_local;
use leptos_router::components::{Route, Router, Routes};
use leptos_router::path;
use crate::grpc;
use crate::pages::{
admin::Admin, artifact_detail::ArtifactDetail, dashboard::Dashboard, estate_board::Flow, login::Login,
migrate::Migrate, mine::{MyRepositories, MyTokens}, replication::Replicate,
repositories::Repositories,
repository_detail::RepositoryDetail, search::Search,
security::{SecurityArtifact, SecurityDashboard},
watched_app::WatchedApp, watched_apps::WatchedApps,
};
#[component]
pub fn App() -> impl IntoView {
// Flipped on login/out so the topbar asks holger again (the session itself is the console
// server's HttpOnly cookie, A34-38: no script here can read it).
let logged_in = RwSignal::new(false);
// holger's view of who is logged in — its passkey door's session answer
// for this tab's bearer — refetched whenever login state flips. ONE answer
// for the topbar and /account (`crate::session`).
let status = LocalResource::new(move || {
let _ = logged_in.get();
async move { crate::session::fetch_status().await }
});
let shown = move || {
let s = status.get().flatten();
crate::session::shown(s.as_ref(), s.as_ref().is_some_and(|s| s.token_held))
};
// The way to buy (A11 fix 5), a metered console's only: holger's sign-up door, asked with
// this tab's session.
let buy = buy_slot(logged_in);
// Which environment this console is pointed at — the gRPC door's host.
let environment = crate::util::environment_label(&grpc::base_url());
// Which security domain that Holger serves (its RON `domain:`, lane HOL-CDS), as holger
// says it on every answer (`X-Holger-Domain`).
let domain = LocalResource::new(|| async { grpc::domain().await });
view! {
<Router>
<a class="skip" href="#main">"Skip to content"</a>
<header class="topbar">
<a class="brand" href="/">
// holger's mark: the helmeted head, a 124×128 lossless webp
// that trunk copies into the bundle (`assets/holger-mark.webp`). Decorative:
// the word beside it names holger, once (HCON-3).
<img class="mark" src="/holger-mark.webp" width="124" height="128" alt="" aria-hidden="true"/>
<span class="brand-product">"holger"</span>
<span class="brand-sub">"console"</span>
</a>
// One entry per job (lane HOL-UI-FIX): the page's entry is marked wherever in it
// the person is (`util::menu_entry`). Admin is offered to an admin's session only;
// the role is the SERVER's, and holger refuses the data to everyone else whatever
// this link does (`crate::admin`).
<Menu admin=Signal::derive(move || crate::admin::admin_visible(&shown()))/>
// Search lives in the header: a plain GET of the Search page, which runs it.
<form class="topsearch" role="search" action="/search" method="get">
<label class="vh" for="topsearch">"Search artifacts by name"</label>
<input id="topsearch" type="search" name="name" placeholder="Search artifacts…" autocomplete="off"/>
</form>
<span class="envchip" title="the server this console talks to">{environment}</span>
{move || domain.get().flatten().map(|d| view! {
<span class="domainchip" title="the security domain this Holger serves (its domain: label)">{d}</span>
})}
<span class="authbox">
{move || {
use crate::session::Shown;
let out = move |ev: leptos::ev::MouseEvent| {
ev.prevent_default();
spawn_local(async move {
match crate::session::logout().await {
Ok(()) => {
logged_in.set(false);
// A passkey console's way back in is its login page.
if let Some(w) = web_sys::window() {
let _ = w.location().assign(crate::session::LOGIN_PAGE);
}
}
// holger did not end the session (RA5-11): said, and still signed in.
Err(why) => {
if let Some(w) = web_sys::window() {
let _ = w.alert_with_message(&format!("You are still signed in: holger did not end your session ({why}). Try again."));
}
}
}
});
};
match shown() {
Shown::Session { user, role } => view! {
{buy}
<a class="ident" href="/account" title="session details">
<span class="ok">{user}</span>
<span class="count">{format!(" · {role}")}</span>
</a>
<a href="#" on:click=out>"Log out"</a>
}.into_any(),
// ONE road in on a passkey console: the server's page.
Shown::LoggedOut { login } => view! { <a href=login rel="external">"Log in"</a> }.into_any(),
Shown::TokenOnly { has_token: true } => view! {
<a class="ident" href="/account" title="session details"><span class="ok">"token"</span></a>
<a href="#" on:click=move |ev| {
ev.prevent_default();
spawn_local(async move {
let _ = crate::session::hold_token("").await;
logged_in.set(false);
});
}>"Log out"</a>
}.into_any(),
Shown::TokenOnly { has_token: false } => view! { <a href="/account">"Log in"</a> }.into_any(),
// Not answered yet: say nothing rather than guess.
Shown::Unknown => view! { <span class="count">"…"</span> }.into_any(),
}
}}
</span>
<HealthBadge/>
</header>
<main id="main">
<Routes fallback=unmatched>
// The console's home is the Overview (HOL-UI-FIX: one page, not a second one
// with the same cards). On a box `/` is holger's front page anyway.
<Route path=path!("/") view=Dashboard/>
<Route path=path!("/overview") view=Dashboard/>
// The artifact flow: the console's ONLY picture (HOL-SVG-RON, HOL-FLOW-SMART).
<Route path=path!("/flow") view=Flow/>
// Where the flow's lamp on a watched application leads (H32-BOARD-LAMP).
<Route path=path!("/apps/:name") view=WatchedApp/>
// The watched applications: the list, the declaration, each OpenVEX (lane hui-audit).
<Route path=path!("/apps") view=WatchedApps/>
<Route path=path!("/repositories") view=Repositories/>
<Route path=path!("/repo/:name") view=RepositoryDetail/>
<Route path=path!("/artifact") view=ArtifactDetail/>
<Route path=path!("/search") view=Search/>
// The security workflow: holger's intake queues, and one
// artifact's evidence with the four buttons.
<Route path=path!("/security") view=SecurityDashboard/>
<Route path=path!("/security/artifact") view=SecurityArtifact/>
// The cross-repo verbs, replication and migration: tabs of the
// Repositories entry (HOL-UI-FIX), one address each (lane hui-audit).
<Route path=path!("/replicate") view=Replicate/>
// Migrate: the one-time registry pull. Preview-first like
// its neighbour, and it cannot create repositories -- see
// pages/migrate.rs for why that shapes the whole page.
<Route path=path!("/migrate") view=Migrate/>
// The admin view: its data is refused by the server to every
// session below admin; this route only renders the answer.
<Route path=path!("/admin") view=Admin/>
<Route path=path!("/account") view=move || view! { <Login logged_in=logged_in/> }/>
// The buyer's pages: the repositories holger says this person
// has, and their tokens for package clients. (A metered console's
// third is reached through the fallback: `unmatched`.)
<Route path=path!("/mine") view=MyRepositories/>
<Route path=path!("/tokens") view=MyTokens/>
</Routes>
</main>
</Router>
}
}
/// **An address no route names.** One exception, and only in a metered console: the
/// person's room, plan and orders (`pages::plan`, the money scanner's `METERED_MODULES`),
/// so an open console has no such address. gunnar-ui reaches its vendor's return page the
/// same way. Reactive on the path, so moving between two such addresses redraws.
fn unmatched() -> impl IntoView {
#[cfg(feature = "metered")]
let path = leptos_router::hooks::use_location().pathname;
move || {
#[cfg(feature = "metered")]
if let Some(page) = crate::pages::plan::at_its_address(&path.get()) {
return page;
}
view! { <p class="err">"no such page"</p> }.into_any()
}
}
/// **The way to buy in the topbar** (A11 fix 5, Gunnar's Account page "Buy a plan" / "Grow
/// your account"): holger's sign-up door, asked with this tab's session whenever the login
/// state flips. A metered console's only.
#[cfg(feature = "metered")]
fn buy_slot(logged_in: RwSignal<bool>) -> impl Fn() -> AnyView + Copy + Send + Sync + 'static {
let buy = LocalResource::new(move || {
let _ = logged_in.get();
async move { crate::session::fetch_buy_link().await }
});
move || match buy.get().flatten() {
Some((href, words)) => view! { <a class="btn primary" rel="external" href=href>{words}</a> }.into_any(),
None => ().into_any(),
}
}
/// An open console's topbar has no way to buy: nothing is asked and nothing is drawn.
#[cfg(not(feature = "metered"))]
fn buy_slot(_logged_in: RwSignal<bool>) -> impl Fn() -> AnyView + Copy + Send + Sync + 'static {
|| ().into_any()
}
/// **The menu** (lane HOL-UI-FIX): [`crate::util::MENU`], the entry of the page the person is
/// on marked (`aria-current`), Admin last for an admin's session.
#[component]
fn Menu(admin: Signal<bool>) -> impl IntoView {
let location = leptos_router::hooks::use_location();
let here = Memo::new(move |_| crate::util::menu_entry(&location.pathname.get()));
let entry = move |href: &'static str, label: &'static str| {
view! { <a href=href aria-current=move || (here.get() == Some(href)).then_some("page")>{label}</a> }
};
view! {
<nav class="nav" aria-label="Main">
{crate::util::MENU.iter().map(|(href, label)| entry(href, label)).collect_view()}
{move || admin.get().then(|| entry(crate::admin::PAGE, "Admin"))}
</nav>
}
}
#[component]
fn HealthBadge() -> impl IntoView {
let health = LocalResource::new(|| async {
grpc::admin()
.health(grpc::pb::HealthRequest {})
.await
.map(|r| r.into_inner())
.map_err(|s| grpc::status_text(&s))
});
view! {
<span class="health">
{move || match health.get() {
None => view! { <span class="pending">"connecting…"</span> }.into_any(),
// One word in the bar; the version and the uptime ride its title (the Overview's
// "This server" card says them in full).
Some(Ok(h)) => view! {
<span class="ok" title=format!("{} · {} · up {}", h.status, h.version, crate::util::uptime_words(h.uptime_seconds))>
<span class="dot" aria-hidden="true"></span>{h.status.clone()}
</span>
}.into_any(),
Some(Err(e)) => view! {
<span class="err" title=format!("unreachable: {e}")><span class="dot" aria-hidden="true"></span>"unreachable"</span>
}.into_any(),
}}
</span>
}
}