src/pages/admin.rs
//! Admin — operational detail about the machine, for an ADMIN's session only.
//!
//! The panes read ONE answer, `GET /api/admin/snapshot` (holger's
//! `AdminService.AdminSnapshot`, relayed by this console's server). The server
//! decides who may read it: a session below admin is refused there with 403 and
//! holger is never asked (see `crate::admin` for why the nav link is only a
//! courtesy). This page renders what came back and says the refusal when one
//! did.
//!
//! Every size is shown in human units with its raw bytes in a detail row.
use leptos::prelude::*;
use crate::admin::{
days_left_words, disk_headline, disk_raw, fill_tone, human, lag_words, offset_words, percent, PANES,
};
use crate::components::copy_button::CopyButton;
use crate::components::feedback::{Badge, ErrorText, Loading, Tone};
use crate::pages::admin_invites::InvitesPane;
use crate::pages::admin_queue::{AdmissionPane, QueuePane};
use crate::pages::front_certs::CertsPane;
use crate::grpc::pb;
fn tone(word: &str) -> Tone {
match word {
"bad" => Tone::Bad,
"warn" => Tone::Warn,
_ => Tone::Good,
}
}
fn yes(b: bool) -> &'static str {
if b {
"yes"
} else {
"no"
}
}
fn when_ms(ms: i64) -> String {
if ms <= 0 {
"never".into()
} else {
format!("{ms} (Unix ms)")
}
}
#[component]
pub fn Admin() -> impl IntoView {
crate::webio::set_title("Admin");
let generation = RwSignal::new(0u32);
let pane = RwSignal::new(PANES[0].0);
// What holger answered the sync board's last Revoke. Kept here, not in the pane: the
// refresh that follows the revocation draws the pane anew.
let revoked = RwSignal::new(None::<Result<String, String>>);
let snap = LocalResource::new(move || {
let _ = generation.get();
async move { crate::grpc::answer(crate::grpc::admin().admin_snapshot(pb::AdminSnapshotRequest { log_lines: 0 }).await) }
});
view! {
<section>
<h2>"Admin"</h2>
<p class="muted">
"Operational detail about this machine, measured on every load. Shown to an \
administrator's session only — holger refuses everyone else."
</p>
<p>
<button on:click=move |_| generation.update(|g| *g += 1)>"Refresh"</button>
{move || crate::webio::now_hms()}
</p>
<OrderOwnHolger/>
<PublicSiteSwitch/>
// The estate's picture is the Flow page's alone (HOL-UI-FIX); Admin links to it.
<p class="count" data-board-link="estate">"The picture of the estate (sources, downstreams, counts, lag, consumers) is on the "<a href="/flow">"Flow"</a>" page; its state at a glance is the "<a href="/overview">"Overview"</a>"."</p>
<nav class="tabs" aria-label="Admin panes">
{PANES.iter().map(|(slug, label)| {
let slug: &'static str = slug;
view! {
<button
class=move || if pane.get() == slug { "tab active" } else { "tab" }
aria-pressed=move || (pane.get() == slug).to_string()
on:click=move |_| pane.set(slug)
>{*label}</button>
}
}).collect_view()}
</nav>
{move || match pane.get() {
// The two admit-queue panes read their own admin-only routes,
// not the snapshot.
"queue" => view! { <QueuePane generation=generation/> }.into_any(),
"admission" => view! { <AdmissionPane generation=generation/> }.into_any(),
"invites" => view! { <InvitesPane generation=generation/> }.into_any(),
// The front's own certificates, read from the front (not the snapshot).
"certs" => view! { <CertsPane generation=generation/> }.into_any(),
_ => (move || match snap.get() {
None => view! { <Loading what="the admin view"/> }.into_any(),
Some(Err(e)) => view! { <ErrorText context="admin view" message=e/> }.into_any(),
Some(Ok(s)) => {
let header = format!("{} · {} · {} · up {} · taken {}", s.host, s.role, s.version, crate::util::uptime_words(s.uptime_seconds), s.taken_unix_ms);
let body = match pane.get_untracked() {
"twin" => twin_pane(s.twin.unwrap_or_default()),
"sync" => view! { {sync_pane(&s)} <SyncBoardPane generation=generation revoked=revoked/> <FirstImportForm/> }.into_any(),
"growth" => growth_pane(s.growth.unwrap_or_default()),
"clock" => clock_pane(s.clock.unwrap_or_default()),
"plugins" => plugins_pane(s.plugins.unwrap_or_default(), s.payments.unwrap_or_default()),
"logs" => logs_pane(s.logs.unwrap_or_default(), s.audit.unwrap_or_default()),
_ => disks_pane(s.disks.unwrap_or_default()),
};
view! { <p class="count mono">{header}</p> {body} }.into_any()
}
})
.into_any(),
}}
</section>
}
}
/// The open bundle's twin: no switch at all.
#[cfg(not(feature = "metered"))]
#[component]
fn PublicSiteSwitch() -> impl IntoView {}
/// **The public site's switch** (owner 2026-10-26, `crate::site_switch`): drawn only where this
/// console's build carries it (a `public-site` build); holger keeps the position and decides
/// who may flip it.
#[cfg(feature = "metered")]
#[component]
fn PublicSiteSwitch() -> impl IntoView {
use crate::site_switch::{button, flip_body, said, shown, ROUTE};
let generation = RwSignal::new(0u32);
let words = RwSignal::new(None::<String>);
let position = LocalResource::new(move || {
let _ = generation.get();
async move {
let answer = gloo_net::http::Request::get(ROUTE).send().await.ok()?;
let status = answer.status();
shown(status, &answer.text().await.unwrap_or_default())
}
});
let press = move |on: bool| {
leptos::task::spawn_local(async move {
let req = gloo_net::http::Request::put(ROUTE).header("content-type", "application/json");
let answer = match req.body(flip_body(on)) {
Ok(r) => r.send().await,
Err(e) => {
words.set(Some(e.to_string()));
return;
}
};
match answer {
Ok(a) => {
let status = a.status();
words.set(said(status, &a.text().await.unwrap_or_default()));
}
Err(e) => words.set(Some(format!("the console did not answer: {e}"))),
}
generation.update(|g| *g += 1);
});
};
move || {
position.get().flatten().map(|on| {
view! {
<section class="card">
<h3>"Public site"</h3>
<p class="muted">"The public home, robots.txt, the sitemap and llms.txt, for visitors and search engines."</p>
<p>{if on { "It is on." } else { "It is off." }}</p>
<button on:click=move |_| press(!on)>{button(on)}</button>
{move || words.get().map(|w| view! { <p class="bad" role="alert">{w}</p> })}
</section>
}
})
}
}
/// **Order your own Holger** (lane P1): holger mints the signed-in person's `estate` ticket at the click
/// (`RoomService.PrivateEstate`: ten minutes, one use) and answers this site's own sign-up page
/// (`/signup?plan=private`, A12), which the console opens; any other link is refused (A28 M10). The console prices and orders nothing; a holger that sells nothing
/// says so in its own words.
#[cfg(feature = "metered")]
#[component]
fn OrderOwnHolger() -> impl IntoView {
let said = RwSignal::new(String::new());
let busy = RwSignal::new(false);
let order = move |_| {
busy.set(true);
leptos::task::spawn_local(async move {
match crate::grpc::answer_or_none(crate::grpc::room().private_estate(crate::grpc::pb_room::PrivateEstateRequest {}).await) {
Ok(Some(r)) => match crate::admin::private_estate_link(&r.shop_url, &site_origin()) {
Ok(Some(url)) => {
said.set(if r.words.is_empty() { "Opening the order page …".into() } else { r.words.clone() });
if let Some(w) = web_sys::window() {
let _ = w.location().assign(url);
}
}
Ok(None) => said.set(if r.words.is_empty() { "this Holger sells no private Holger".into() } else { r.words }),
Err(why) => said.set(why),
},
Ok(None) => said.set(crate::util::not_in_build("Ordering a private Holger")),
Err(e) => said.set(e),
}
busy.set(false);
});
};
view! {
<p>
<button class="btn primary" disabled=move || busy.get() on:click=order>"Order your own Holger"</button>
" "<span class="count" role="status">{move || said.get()}</span>
</p>
}
}
/// The console's own origin (`location.origin`): the only site the order button follows.
#[cfg(feature = "metered")]
fn site_origin() -> String {
web_sys::window().and_then(|w| w.location().origin().ok()).unwrap_or_default()
}
/// An open console orders nothing: no button, no words (A28 M13). A `#[cfg]` on a component
/// node inside `view!` is not honoured, so the twin is a component that renders nothing.
#[cfg(not(feature = "metered"))]
#[component]
fn OrderOwnHolger() -> impl IntoView {}
fn disks_pane(d: pb::AdminDisks) -> AnyView {
view! {
<div>
<p class="count">{format!("Floor: {}", d.floor_rule)}</p>
{d.problems.into_iter().map(|p| view! { <p class="banner banner-warn" role="alert">{p}</p> }).collect_view()}
// Paths holger could not place on a mount, split by holger's own flag (HOL-UI-FIX):
// a fault is a warning; a repository's storage folder not made yet is a plain,
// folded line in holger's words ("created at the first upload").
{
let u = crate::admin::unplaced(&d.unresolved);
let (faults, later, why) = (u.faults, u.later, u.later_why);
view! {
{(!faults.is_empty()).then(|| {
let n = faults.len();
view! {
<details class="foldbox" data-disks="faults" open>
<summary><Badge tone=Tone::Warn label="not placed"/>{format!("{n} path(s) sit on no mount")}</summary>
<ul class="pathlist">{faults.into_iter().map(|f| view! { <li>{f}</li> }).collect_view()}</ul>
</details>
}
})}
{(!later.is_empty()).then(|| {
let n = later.len();
view! {
<details class="foldbox" data-disks="later">
<summary><Badge tone=Tone::Neutral label="not made yet"/>{format!("{n} storage folder(s): {why}")}</summary>
<ul class="pathlist">{later.into_iter().map(|f| view! { <li>{f}</li> }).collect_view()}</ul>
</details>
}
})}
}
}
<div class="cards">
{d.mounts.into_iter().map(|m| {
let labels = if m.labels.is_empty() { String::new() } else { m.labels.join(", ") };
let title = format!("{} — {} ({}{})", m.mount_point, m.source, m.fstype, if m.read_only { ", read-only" } else { "" });
if !m.error.is_empty() {
return view! {
<div class="card"><h3>{title}</h3><Badge tone=Tone::Bad label="unmeasured"/><p class="count">{m.error}</p></div>
}.into_any();
}
let headline = disk_headline(m.total_bytes, m.used_bytes, m.available_bytes, m.floor_bytes, m.servable_bytes);
let raw = disk_raw(m.total_bytes, m.used_bytes, m.free_bytes, m.available_bytes, m.reserved_bytes, m.floor_bytes, m.servable_bytes, m.block_size);
let inodes = format!("inodes {} of {} free", m.inodes_free, m.inodes_total);
view! {
<div class="card">
<h3>{title}</h3>
<Badge tone=tone(fill_tone(m.used_permille)) label=percent(m.used_permille)/>
{(!labels.is_empty()).then(|| view! { <p class="chips"><span class="chip">{labels}</span></p> })}
<p class="cardline">{headline}</p>
<details><summary>"raw"</summary><p class="count mono">{raw}</p><p class="count mono">{inodes}</p></details>
</div>
}.into_any()
}).collect_view()}
</div>
<details class="foldbox">
<summary>{format!("Block devices ({})", d.block_devices.len())}</summary>
<div class="tablewrap">
<table>
<thead><tr><th>"device"</th><th>"size"</th><th>"kind"</th><th>"partitions"</th><th>"held by"</th><th>"members"</th><th>"raw bytes"</th></tr></thead>
<tbody>
{d.block_devices.into_iter().map(|b| {
let kind = format!("{}{}{}{}", if b.raid_level.is_empty() { String::new() } else { format!("{} ", b.raid_level) }, if b.rotational { "rotational" } else { "solid-state" }, if b.removable { ", removable" } else { "" }, if b.read_only { ", read-only" } else { "" });
let parts = b.partitions.iter().map(|p| format!("{} {}", p.name, human(p.size_bytes))).collect::<Vec<_>>().join(", ");
view! {
<tr>
<td class="mono">{b.name}</td><td>{human(b.size_bytes)}</td><td>{kind}</td><td>{parts}</td>
<td>{b.holders.join(", ")}</td><td>{b.members.join(", ")}</td><td class="mono">{b.size_bytes.to_string()}</td>
</tr>
}
}).collect_view()}
</tbody>
</table>
</div>
</details>
</div>
}
.into_any()
}
fn twin_pane(t: pb::AdminTwin) -> AnyView {
let alive = if t.role == "twin" {
if t.complete { (Tone::Good, "whole") } else { (Tone::Warn, "not whole") }
} else {
(Tone::Neutral, "primary")
};
view! {
<div>
<div class="cards">
<div class="card">
<h3>"Role"</h3>
<Badge tone=alive.0 label=alive.1/>
<p class="cardline">{format!("{}{}", t.role, if t.primary.is_empty() { String::new() } else { format!(" of {}", t.primary) })}</p>
<p class="count">{format!("serving: {} · accepts writes: {}", yes(t.serving), yes(t.accepts_writes))}</p>
{(!t.awaiting_recharge_from.is_empty()).then(|| view! { <p class="banner banner-warn">{format!("awaiting its recharge from {}", t.awaiting_recharge_from)}</p> })}
</div>
<div class="card">
<h3>"Follow"</h3>
<p class="cardline">{format!("lag: {}", lag_words(&t.role, t.lag_seconds))}</p>
<p class="count">{format!("last whole: {} · how: {}", t.last_whole_unix, if t.follow.is_empty() { "—".to_string() } else { t.follow.clone() })}</p>
{(!t.why.is_empty()).then(|| view! { <p class="count">{t.why.clone()}</p> })}
{(!t.store_warning.is_empty()).then(|| view! { <p class="banner banner-warn">{t.store_warning.clone()}</p> })}
</div>
<div class="card">
<h3>"Replication door"</h3>
<p class="cardline">{format!("refused {} · admitted {}", t.door_refused, t.door_admitted.iter().map(|(k, v)| format!("{k}×{v}")).collect::<Vec<_>>().join(", "))}</p>
{(!t.door_last_refusal.is_empty()).then(|| view! { <p class="count mono">{t.door_last_refusal.clone()}</p> })}
</div>
</div>
<h3>"mTLS credentials"</h3>
<table>
<thead><tr><th>"purpose"</th><th>"state"</th><th>"principal"</th><th>"issuer"</th><th>"doors"</th><th>"not after (Unix s)"</th></tr></thead>
<tbody>
{t.credentials.into_iter().map(|c| view! {
<tr>
<td>{c.purpose}</td>
<td>{if c.error.is_empty() { days_left_words(c.installed, c.days_left) } else { c.error }}</td>
<td class="mono">{c.principal}</td><td class="mono">{c.issuer}</td>
<td class="mono">{c.doors.join(", ")}</td><td class="mono">{c.not_after_unix.to_string()}</td>
</tr>
}).collect_view()}
</tbody>
</table>
<h3>"Twins enrolled here"</h3>
{(!t.twins_error.is_empty()).then(|| view! { <p class="count">{t.twins_error.clone()}</p> })}
<table>
<thead><tr><th>"twin"</th><th>"state"</th><th>"lag (entries)"</th><th>"last seen (Unix ms)"</th><th>"disk"</th><th>"raw bytes"</th></tr></thead>
<tbody>
{t.twins.into_iter().map(|r| view! {
<tr>
<td class="mono">{format!("{} {}", r.principal, r.address)}</td><td>{r.state}</td><td>{r.lag_entries.to_string()}</td>
<td class="mono">{r.last_seen_unix_ms.to_string()}</td>
<td>{format!("{} of {} ({})", human(r.disk_used_bytes), human(r.disk_total_bytes), percent(r.fill_permille))}</td>
<td class="mono">{format!("{} / {}", r.disk_used_bytes, r.disk_total_bytes)}</td>
</tr>
}).collect_view()}
</tbody>
</table>
</div>
}
.into_any()
}
/// Twins and paired downstreams, one line per repository: the server's state
/// word and numbers, said by `crate::sync`.
fn sync_pane(s: &pb::AdminSnapshotReply) -> AnyView {
let rows = crate::sync::rows(s);
if rows.is_empty() {
return view! { <p class="count">"No twin is enrolled here and no downstream is paired."</p> }.into_any();
}
view! {
<table>
<thead><tr><th>"kind"</th><th>"name"</th><th>"repository"</th><th>"lag"</th><th>"state"</th><th>"last synced"</th><th>"pulls today"</th></tr></thead>
<tbody>
{rows.into_iter().map(|r| view! {
<tr>
<td>{r.kind}</td><td class="mono">{r.name}</td><td class="mono">{r.repository}</td><td>{r.lag}</td>
<td><Badge tone=r.tone label=r.state/></td><td>{r.last_synced}</td><td>{r.pulls_today}</td>
</tr>
}).collect_view()}
</tbody>
</table>
}
.into_any()
}
/// **The first import** (lanes HOL-CDS, HOL-NCSC): with no anti-rollback floor, holger refuses
/// every bundle until an admin names the one that sets it. The form asks holger's door
/// (`crate::first_import`) and says its answer; holger checks, audits and keeps the grant.
#[component]
fn FirstImportForm() -> impl IntoView {
let said: RwSignal<Option<Result<String, String>>> = RwSignal::new(None);
let (vendor, product, sequence) = (RwSignal::new(String::new()), RwSignal::new(String::new()), RwSignal::new(String::new()));
let ask = move |ev: leptos::ev::SubmitEvent| {
ev.prevent_default();
let path = match crate::first_import::path(&vendor.get_untracked(), &product.get_untracked(), &sequence.get_untracked()) {
Ok(p) => p,
Err(e) => {
said.set(Some(Err(e)));
return;
}
};
leptos::task::spawn_local(async move {
let answer = match crate::session::door(&path, Some(&serde_json::json!({}))).await {
Ok((status, body)) => crate::first_import::granted(status, &body),
Err(e) => Err(e),
};
said.set(Some(answer));
});
};
view! {
<h3>"First import"</h3>
<p class="muted">"With no anti-rollback floor yet, this Holger refuses every bundle until an admin names the one that sets it (used once, audited)."</p>
<form class="toolbar first-import" on:submit=ask>
<input type="text" placeholder="vendor (the bundle's supplier)" aria-label="The bundle's vendor" required
prop:value=move || vendor.get() on:input=move |ev| vendor.set(event_target_value(&ev))/>
<input type="text" placeholder="product" aria-label="The bundle's product" required
prop:value=move || product.get() on:input=move |ev| product.set(event_target_value(&ev))/>
<input type="text" inputmode="numeric" placeholder="sequence" aria-label="The sequence that sets the floor" required
prop:value=move || sequence.get() on:input=move |ev| sequence.set(event_target_value(&ev))/>
<button type="submit">"Authorize the first import"</button>
</form>
{move || said.get().map(|s| match s {
Ok(w) => view! { <p class="count first-import-said" role="status">{w}</p> }.into_any(),
Err(w) => view! { <p class="err first-import-said" role="alert">{w}</p> }.into_any(),
})}
}
}
/// **The sync board** (Holger v2.0 H6): the upstream's `GET /-/sync/board`, per downstream
/// the last sync applied, lag, invalidation reach and the ALARM, in the server's words
/// (`crate::sync_board`). A Holger without a `sync:` block says so.
#[component]
fn SyncBoardPane(generation: RwSignal<u32>, revoked: RwSignal<Option<Result<String, String>>>) -> impl IntoView {
let board = LocalResource::new(move || {
let _ = generation.get();
async move {
let (status, body) = crate::session::door(crate::sync_board::BOARD_PATH, None).await?;
crate::sync_board::read(status, &body).map(|b| crate::sync_board::view(&b))
}
});
// Holger lane OX-WGDS2: the console enrols and invites through holger's doors, which check,
// audit and sign; it only says the answer.
let said: RwSignal<Option<Result<String, String>>> = RwSignal::new(None);
let invite: RwSignal<Option<crate::sync_board::Invited>> = RwSignal::new(None);
let (name, key, via, wg) = (RwSignal::new(String::new()), RwSignal::new(String::new()), RwSignal::new("front".to_string()), RwSignal::new(String::new()));
let enrol = move |ev: leptos::ev::SubmitEvent| {
ev.prevent_default();
let path = crate::sync_board::enrol_path(&name.get_untracked(), &key.get_untracked(), &via.get_untracked(), &wg.get_untracked());
leptos::task::spawn_local(async move {
let answer = match crate::session::door(&path, Some(&serde_json::json!({}))).await {
Ok((status, body)) => crate::sync_board::enrolled(status, &body),
Err(e) => Err(e),
};
if answer.is_ok() {
name.set(String::new());
key.set(String::new());
wg.set(String::new());
}
said.set(Some(answer));
generation.update(|g| *g += 1);
});
};
view! {
<h3>"Sync board"</h3>
{move || revoked.get().map(|s| match s {
Ok(w) => view! { <p class="count sync-revoke-said" role="status">{w}</p> }.into_any(),
Err(w) => view! { <p class="err sync-revoke-said" role="alert">{w}</p> }.into_any(),
})}
{move || match board.get() {
None => view! { <Loading what="the sync board"/> }.into_any(),
Some(Err(e)) => view! { <p class="count sync-board-none">{e}</p> }.into_any(),
Some(Ok(v)) => view! {
<div class="sync-board">
{v.alarm.map(|a| view! { <p class="err sync-alarm" role="alert">{a}</p> })}
<p class="count mono">{v.header}</p>
<table>
<thead><tr><th>"downstream"</th><th>"via"</th><th>"state"</th><th>"last sync"</th><th>"last sync applied"</th><th>"lag"</th><th>"last ack"</th><th>"invalidations"</th><th>"not acknowledged"</th><th></th></tr></thead>
<tbody>
{v.rows.into_iter().map(|r| {
let (name, class) = (r.name.clone(), if r.alarm { "alarm" } else { "" });
let revoke = crate::sync_board::revoke_path(&r.name);
let (invitable, invite_path, who) = (r.invitable, crate::sync_board::invite_path(&r.name), r.name.clone());
let gone = r.name.clone();
view! {
<tr class=class data-downstream=name>
<td class="mono">{r.name}</td>
<td>{r.via}</td>
<td><Badge tone=r.tone label=r.state/>" "{r.headline}</td>
<td>{r.last_sync}</td><td>{r.last_applied}</td><td>{r.lag}</td><td>{r.last_ack}</td><td>{r.invalidations}</td>
<td>{r.pending.join("; ")}</td>
<td>
{invitable.then(|| view! { <button on:click=move |_| {
let (path, who) = (invite_path.clone(), who.clone());
leptos::task::spawn_local(async move {
match crate::session::door(&path, Some(&serde_json::json!({}))).await.and_then(|(status, body)| crate::sync_board::invited(&who, status, &body)) {
Ok(i) => {
said.set(None);
invite.set(Some(i));
}
Err(e) => said.set(Some(Err(e))),
}
});
}>"Invite"</button>" " })}
<button class="danger" on:click=move |_| {
let (path, gone) = (revoke.clone(), gone.clone());
leptos::task::spawn_local(async move {
let answer = crate::session::door(&path, Some(&serde_json::json!({}))).await;
revoked.set(Some(answer.and_then(|(status, body)| crate::sync_board::revoke_said(&gone, status, &body))));
generation.update(|g| *g += 1);
});
}>"Revoke"</button>
</td>
</tr>
}}).collect_view()}
</tbody>
</table>
<table>
<thead><tr><th>"invalidated"</th><th>"why"</th><th>"reached"</th><th>"waiting"</th></tr></thead>
<tbody>
{v.invalidations.into_iter().map(|i| view! {
<tr><td class="mono">{i.coordinate}</td><td>{i.why}</td><td>{i.reached}</td><td>{i.waiting}</td></tr>
}).collect_view()}
</tbody>
</table>
<form class="toolbar sync-enrol" on:submit=enrol>
<strong>"Enrol a downstream "</strong>
<input type="text" placeholder="name" aria-label="The downstream's name" required
prop:value=move || name.get() on:input=move |ev| name.set(event_target_value(&ev))/>
<input type="text" placeholder="ack key (holger sync key on it)" aria-label="The downstream's ack key" required
prop:value=move || key.get() on:input=move |ev| key.set(event_target_value(&ev))/>
<select aria-label="How it reaches this Holger" prop:value=move || via.get() on:change=move |ev| via.set(event_target_value(&ev))>
<option value="front">"via the front"</option>
<option value="wireguard">"via WireGuard"</option>
</select>
{move || (via.get() == "wireguard").then(|| view! {
<input type="text" placeholder="WireGuard key (holger sync wg-key on it)" aria-label="The downstream's WireGuard key" required
prop:value=move || wg.get() on:input=move |ev| wg.set(event_target_value(&ev))/>
})}
<button type="submit">"Enrol"</button>
</form>
{move || said.get().map(|r| match r {
Ok(words) => view! { <p class="count sync-said"><Badge tone=Tone::Good label="enrolled"/>" "{words}</p> }.into_any(),
Err(words) => view! { <p class="err sync-said" role="alert">{words}</p> }.into_any(),
})}
{move || invite.get().map(|i| {
let text = i.invite.clone();
view! {
<div class="minted sync-invite">
<p><Badge tone=Tone::Good label="invite signed"/>{i.said()}</p>
<pre class="mono">{i.invite.clone()}</pre>
{(!i.upstream.is_empty()).then(|| view! { <p class="count mono">"its RON: sync.upstreams: [ "{i.upstream_line()}" ]"</p> })}
<CopyButton label="invite" value=Signal::derive(move || text.clone())/>
</div>
}
})}
</div>
}.into_any(),
}}
}
}
fn growth_pane(g: pb::AdminGrowth) -> AnyView {
view! {
<div>
<div class="cards">
<div class="card">
<h3>"Now"</h3>
{if g.present { view! { <Badge tone=Tone::Neutral label=g.phase.clone()/> }.into_any() } else { view! { <Badge tone=Tone::Warn label="no growth plane"/> }.into_any() }}
<p class="cardline">{g.why.clone()}</p>
<p class="count">{format!("epoch {} · serving {} · in flight {} · since {}", g.epoch, yes(g.serving), g.in_flight, when_ms(g.since_unix_ms))}</p>
<p class="count">{format!("set {} · {} ({} B)", if g.set_uuid.is_empty() { "—" } else { &g.set_uuid }, human(g.total_bytes), g.total_bytes)}</p>
{(!g.detail.is_empty()).then(|| view! { <p class="count">{g.detail.clone()}</p> })}
</div>
<div class="card">
<h3>"Can it grow?"</h3>
<p class="cardline">{g.capacity_growth_reason.clone()}</p>
<p class="count">{format!("data set: {} · engine: {} · PCI hot-plug: {}", if g.data_set.is_empty() { "—" } else { &g.data_set }, yes(g.engine_present), yes(g.hotplug))}</p>
</div>
</div>
<h3>"Members"</h3>
<p class="count">{g.members_verdict.clone()}</p>
<table>
<thead><tr><th>"#"</th><th>"device"</th><th>"disk"</th><th>"in the set"</th><th>"offered"</th><th>"raw bytes (disk / set / offered)"</th></tr></thead>
<tbody>
{g.members.into_iter().map(|m| view! {
<tr>
<td>{m.index.to_string()}</td><td class="mono">{format!("{} {}", m.device, m.partition)}</td>
<td>{human(m.disk_bytes)}</td><td>{human(m.set_bytes)}</td><td>{format!("{}{}", human(m.offered_bytes), if m.larger { " (larger)" } else { "" })}</td>
<td class="mono">{format!("{} / {} / {}", m.disk_bytes, m.set_bytes, m.offered_bytes)}</td>
</tr>
}).collect_view()}
</tbody>
</table>
<h3>"History (the grow journal)"</h3>
<p class="count mono">{format!("{} · {}", if g.journal_phase.is_empty() { "no journal" } else { &g.journal_phase }, g.journal_path)}</p>
{(!g.journal_detail.is_empty()).then(|| view! { <p class="count">{g.journal_detail.clone()}</p> })}
<ol>{g.journal_steps.into_iter().map(|s| view! { <li class="mono">{s}</li> }).collect_view()}</ol>
{(!g.journal_partial_members.is_empty()).then(|| view! {
<p class="banner banner-warn">{format!("a partial growth left members {:?} behind", g.journal_partial_members)}</p>
})}
</div>
}
.into_any()
}
fn sync_view(title: &'static str, has: bool, s: Option<pb::AdminClockSync>) -> AnyView {
let Some(s) = s.filter(|_| has) else {
return view! { <div class="card"><h3>{title}</h3><p class="count">"not synced yet"</p></div> }.into_any();
};
view! {
<div class="card">
<h3>{title}</h3>
<Badge tone=if s.vouches { Tone::Good } else { Tone::Warn } label=s.outcome.clone()/>
<p class="cardline">{format!("{} · offset {} · at {}", s.trigger, offset_words(s.offset_ms), s.at_unix_ms)}</p>
<p class="count">{s.detail.clone()}</p>
<ul>{s.sources.into_iter().map(|src| view! {
<li class="mono">{if src.answered { format!("{} {}", src.source, offset_words(src.offset_ms)) } else { format!("{} — {}", src.source, src.error) }}</li>
}).collect_view()}</ul>
</div>
}
.into_any()
}
fn clock_pane(c: pb::AdminClock) -> AnyView {
view! {
<div class="cards">
<div class="card">
<h3>"Estate clock"</h3>
<Badge tone=if c.trusted { Tone::Good } else { Tone::Warn } label=if c.trusted { "trusted" } else { "NOT trusted" }/>
<p class="cardline">{c.config.clone()}</p>
<p class="count">{format!("owed in-process: {} · estate {} ms · system {} ms", offset_words(c.owed_ms), c.estate_unix_ms, c.system_unix_ms)}</p>
</div>
{sync_view("Last sync", c.has_last, c.last)}
{sync_view("Boot sync", c.has_boot, c.boot)}
</div>
}
.into_any()
}
/// **The payments card** of the plugins pane: when the vendor's webhook last delivered, and
/// holger's note. A metered console's only (holger's own admin snapshot fills it only there).
#[cfg(feature = "metered")]
fn payments_card(pay: pb::AdminPayments) -> AnyView {
view! {
<div class="card">
<h3>"Stripe webhook"</h3>
<p class="cardline">{if pay.stripe_last_delivery_unix_ms > 0 { format!("last delivery {}", pay.stripe_last_delivery_unix_ms) } else { "last delivery: not measured here".to_string() }}</p>
<p class="count">{pay.note.clone()}</p>
</div>
}
.into_any()
}
/// An open console draws no payments card.
#[cfg(not(feature = "metered"))]
fn payments_card(_pay: pb::AdminPayments) -> AnyView {
().into_any()
}
fn plugins_pane(p: pb::AdminPlugins, pay: pb::AdminPayments) -> AnyView {
let reg = p.self_registration.clone();
view! {
<div>
<div class="cards">
<div class="card">
<h3>"Monetize"</h3>
<p class="cardline">{format!("metered build: {} · entitlement store: {} · meters: {}", yes(p.metered_build), if p.entitlements_configured { "configured" } else { "none" }, p.meters)}</p>
{match reg {
Some(r) if r.active => view! {
<Badge tone=if r.registered { Tone::Good } else if r.warning { Tone::Bad } else { Tone::Warn } label=if r.registered { "plugin registered" } else { "plugin not registered" }/>
<p class="count">{format!("{} (at {})", r.line, r.at_unix_ms)}</p>
}.into_any(),
_ => view! { <p class="count">"self-registration: not running in this process"</p> }.into_any(),
}}
</div>
{payments_card(pay)}
</div>
<h3>"Entitlements"</h3>
<table>
<thead><tr><th>"repository"</th><th>"state"</th><th>"caps"</th><th>"valid until (Unix ms)"</th><th>"source"</th></tr></thead>
<tbody>
{p.entitlements.into_iter().map(|e| view! {
<tr><td class="mono">{e.repository}</td><td>{e.state}</td><td class="mono">{e.caps.join(", ")}</td><td class="mono">{e.paid_until_unix_ms.to_string()}</td><td>{e.source}</td></tr>
}).collect_view()}
</tbody>
</table>
<h3>"Loaded modules"</h3>
<table>
<thead><tr><th>"name"</th><th>"kind"</th><th>"origin"</th><th>"digest"</th><th>"granted"</th></tr></thead>
<tbody>
{p.modules.into_iter().map(|m| view! {
<tr><td>{m.name}</td><td>{m.kind}</td><td class="mono">{m.origin}</td><td class="mono">{m.digest}</td><td class="mono">{m.granted.join(", ")}</td></tr>
}).collect_view()}
</tbody>
</table>
<p class="count">{format!("granted capabilities: {}", p.granted_capabilities.join(", "))}</p>
</div>
}
.into_any()
}
fn logs_pane(l: pb::AdminLogs, a: pb::AdminAudit) -> AnyView {
view! {
<div>
<div class="cards">
<div class="card">
<h3>"Installer log door"</h3>
{if l.present { view! { <Badge tone=Tone::Neutral label=l.door.clone()/> }.into_any() } else { view! { <Badge tone=Tone::Neutral label="none"/> }.into_any() }}
<p class="count">{if l.present { format!("{} lines logged, {} dropped from the ring", l.lines_total, l.lines_dropped) } else { l.door.clone() }}</p>
</div>
<div class="card">
<h3>"Audit log"</h3>
<Badge tone=if a.error.is_empty() { Tone::Neutral } else { Tone::Bad } label=a.mode.clone()/>
<p class="cardline">{format!("{} rows · {} segments · {}", a.total_rows, a.segments, human(a.total_bytes))}</p>
<p class="count mono">{format!("{} · {} B · first {} · last {} (ns)", a.source, a.total_bytes, a.first_ts_nanos, a.last_ts_nanos)}</p>
{(!a.error.is_empty()).then(|| view! { <p class="err">{a.error.clone()}</p> })}
</div>
</div>
<h3>"Newest log lines"</h3>
<pre class="mono logtail">{l.tail.join("\n")}</pre>
</div>
}
.into_any()
}