src/pages/dashboard.rs
//! **The Overview** (`/overview`, lane HOL-UI-FIX, owner 2026-10-31): the estate's state at a
//! glance. What needs a person, the health of each box (main, twin, front, downstreams), the
//! last sync and lag per downstream, the intake queue, the latest refusals, the storage, the
//! sources, and this server. No picture: the flow is the Flow page's alone, and the Overview
//! links to it. Every card is a live read of holger's (`Topology`,
//! `SecuritySummary`, `ListIntakeArtifacts`, the sync board, `Health`, `ServerProfile`); each
//! card says the server's refusal in its own words when holger refuses it.
use leptos::prelude::*;
use crate::components::feedback::{Badge, EmptyState, Loading, Tone};
use crate::grpc;
use crate::overview::{self as ov, BoxCard};
use crate::security::{self as sec, GrpcDoor, IntakeDoor as _, RefusalView};
use crate::util::uptime_words;
/// A card of the Overview: a title, an optional link to where it is handled, its body.
#[component]
fn Card(#[prop(into)] title: String, #[prop(into)] key: String, #[prop(optional, into)] more: Option<(String, String)>, #[prop(optional, into)] wide: bool, children: Children) -> impl IntoView {
let class = if wide { "card ov-card ov-wide" } else { "card ov-card" };
view! {
<div class=class data-overview=key>
<div class="card-head">
<h3>{title}</h3>
{more.map(|(href, words)| view! { <a class="card-more" href=href>{words}</a> })}
</div>
{children()}
</div>
}
}
fn refused(context: &str, r: RefusalView) -> AnyView {
view! { <p class="err" role="alert"><span class="badge badge-bad">{r.code}</span>{format!("{context}: {}", r.text)}</p> }.into_any()
}
/// One box of the estate.
fn box_card(c: BoxCard) -> impl IntoView {
let class = if c.this_box { "estate-box this-box" } else { "estate-box" };
view! {
<div class=class data-box=c.name.clone() data-role=c.role.clone()>
<div class="estate-box-head">
<strong class="estate-box-name">{c.name.clone()}</strong>
<span class="chip">{c.role.clone()}</span>
<Badge tone=c.tone label=c.state.clone()/>
</div>
<p class="estate-box-line">{c.headline.clone()}</p>
<dl class="mini-facts">
<dt>"Seen"</dt><dd>{c.seen.clone()}</dd>
{c.lag.clone().map(|l| view! { <dt>"Lag"</dt><dd>{l}</dd> })}
{(!c.via.is_empty()).then(|| view! { <dt>"Via"</dt><dd>{c.via.clone()}</dd> })}
{(!c.address.is_empty()).then(|| view! { <dt>"Address"</dt><dd class="mono">{c.address.clone()}</dd> })}
{c.disk.clone().map(|d| view! {
<dt>"Disk"</dt>
<dd>
<span class="meter" role="img" aria-label=format!("{} of {} used", d.used, d.total)><span class="meter-fill" style=format!("width: {}%", d.percent)></span></span>
<span class="count">{format!("{} of {} ({}%)", d.used, d.total, d.percent)}</span>
</dd>
})}
</dl>
{(!c.why.is_empty()).then(|| view! { <p class="count estate-box-why">{c.why.clone()}</p> })}
</div>
}
}
/// **The Overview** (`/overview`, and the console's home `/`).
#[component]
pub fn Dashboard() -> impl IntoView {
crate::webio::set_title("Overview");
let health = LocalResource::new(|| async { grpc::answer(grpc::admin().health(grpc::pb::HealthRequest {}).await) });
let profile = LocalResource::new(|| async { grpc::answer(grpc::admin().server_profile(grpc::pb::ServerProfileRequest {}).await) });
let repos = LocalResource::new(|| async { grpc::answer(grpc::admin().list_repositories(grpc::pb::ListRepositoriesRequest {}).await).map(|r| r.repositories) });
let topology = LocalResource::new(|| async { GrpcDoor.topology().await.map_err(|r| sec::refusal(&r)) });
let security = LocalResource::new(|| async { sec::load_security(&GrpcDoor).await });
let queue = LocalResource::new(|| async { sec::load_dashboard(&GrpcDoor, "").await });
let sync = LocalResource::new(|| async {
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))
});
let section = move |key: &'static str| security.get().and_then(|r| r.ok()).and_then(|v| v.sections.into_iter().find(|s| s.key == key));
view! {
<section class="page overview">
<header class="page-head">
<div>
<h2>"Overview"</h2>
<p class="lede" data-overview="summary">
{move || match topology.get() {
Some(Ok(t)) => {
let estate = if t.estate.is_empty() { String::new() } else { format!("Estate {} · ", t.estate) };
format!("{estate}{}", t.summary)
}
_ => "The estate's state at a glance.".to_string(),
}}
</p>
</div>
<a class="btn" href=ov::FLOW data-overview="flow-link">"Open the flow"</a>
</header>
// Degraded-mode warnings sit above everything: an unreachable server or a read-only
// posture is the first thing an operator must see.
{move || match health.get() {
Some(Err(e)) => Some(view! { <p class="banner banner-bad" role="alert">{format!("Server unreachable — every card below will fail until this clears: {e}")}</p> }.into_any()),
_ => None,
}}
{move || match profile.get() {
Some(Ok(p)) if p.read_only => Some(view! {
<p class="banner banner-warn" role="alert">{format!("Read-only posture: the server accepts no writes on any repository ({}).", p.label)}</p>
}.into_any()),
_ => None,
}}
<div class="ov-grid">
<Card title="Needs action" key="needs-action" wide=true>
{move || {
let cards = topology.get().and_then(|r| r.ok()).map(|t| ov::boxes(&t).0).unwrap_or_default();
let alarm = sync.get().and_then(|r| r.ok()).and_then(|v| v.alarm);
let waiting = section("approvals").map(|s| s.rows).unwrap_or_default();
if topology.get().is_none() || security.get().is_none() {
return view! { <Loading what="what needs action"/> }.into_any();
}
let todo = ov::needs_action(alarm.as_deref(), &cards, &waiting);
if todo.is_empty() {
return view! { <p class="all-clear"><Badge tone=Tone::Good label="all clear"/>"Nothing waits for a person."</p> }.into_any();
}
let (shown, more) = ov::first(&todo);
view! {
<ul class="todo">
{shown.into_iter().map(|a| view! {
<li><Badge tone=a.tone label=if a.tone == Tone::Bad { "now" } else { "soon" }/><a href=a.href>{a.what}</a>{(!a.detail.is_empty()).then(|| view! { <span class="count">{a.detail}</span> })}</li>
}).collect_view()}
</ul>
{(more > 0).then(|| view! { <p class="count">{format!("and {more} more")}</p> })}
}.into_any()
}}
</Card>
<Card title="Boxes" key="boxes" wide=true more=(ov::MACHINES.to_string(), "See them in the flow".to_string())>
{move || match topology.get() {
None => view! { <Loading what="the estate's boxes"/> }.into_any(),
Some(Err(r)) => refused("holger refused the estate's boxes", r),
Some(Ok(t)) => {
let (cards, _) = ov::boxes(&t);
if cards.is_empty() {
return view! { <EmptyState message="holger names no box"/> }.into_any();
}
view! { <div class="estate-boxes">{cards.into_iter().map(box_card).collect_view()}</div> }.into_any()
}
}}
</Card>
<Card title="Downstreams" key="downstreams" wide=true more=("/admin".to_string(), "Manage in Admin".to_string())>
{move || match sync.get() {
None => view! { <Loading what="the downstreams"/> }.into_any(),
Some(Err(e)) => view! { <EmptyState message=e/> }.into_any(),
Some(Ok(v)) if v.rows.is_empty() => view! { <EmptyState message="No downstream is enrolled."/> }.into_any(),
Some(Ok(v)) => view! {
{v.alarm.map(|a| view! { <p class="err" role="alert">{a}</p> })}
<div class="tablewrap">
<table>
<thead><tr><th>"Downstream"</th><th>"State"</th><th>"Last sync"</th><th>"Lag"</th><th>"Last ack"</th><th>"Via"</th></tr></thead>
<tbody>
{v.rows.into_iter().map(|r| { let key = r.name.clone(); view! {
<tr data-downstream=key>
<td class="mono">{r.name}</td>
<td><Badge tone=r.tone label=r.state/></td>
<td>{r.last_sync}</td><td>{r.lag}</td><td>{r.last_ack}</td><td>{r.via}</td>
</tr>
}}).collect_view()}
</tbody>
</table>
</div>
<p class="count mono">{v.header}</p>
}.into_any(),
}}
</Card>
<Card title="Intake queue" key="queue" more=(sec::DASHBOARD.to_string(), "Open Security".to_string())>
{move || match queue.get() {
None => view! { <Loading what="the intake queue"/> }.into_any(),
Some(Err(r)) => refused("holger refused the intake queue", r),
Some(Ok(v)) => view! {
<ul class="queue-list">
{v.tiles.into_iter().map(|t| view! {
<li><a href=t.href>{t.title}</a><span class="queue-count">{t.count.to_string()}</span></li>
}).collect_view()}
</ul>
}.into_any(),
}}
</Card>
<Card title="Latest refusals" key="refusals" more=(sec::DASHBOARD.to_string(), "All on Security".to_string())>
{move || match security.get() {
None => view! { <Loading what="the refusals"/> }.into_any(),
Some(Err(r)) => refused("holger refused the security summary", r),
Some(Ok(_)) => match section("held") {
None => view! { <EmptyState message="holger sent no refusals section"/> }.into_any(),
Some(s) if s.rows.is_empty() => view! { <EmptyState message=s.empty/> }.into_any(),
Some(s) => {
let (shown, more) = ov::first(&s.rows);
view! {
<ul class="row-list">
{shown.into_iter().map(|r| view! {
<li>
<Badge tone=r.tone label=r.state/>
{match r.href { Some(h) => view! { <a href=h>{r.name}</a> }.into_any(), None => view! { <strong>{r.name}</strong> }.into_any() }}
{(!r.detail.is_empty()).then(|| view! { <span class="count">{r.detail}</span> })}
</li>
}).collect_view()}
</ul>
{(more > 0).then(|| view! { <p class="count">{format!("and {more} more")}</p> })}
}.into_any()
}
},
}}
</Card>
<Card title="Storage" key="storage" more=("/admin".to_string(), "Disks in Admin".to_string())>
{move || match topology.get() {
None => view! { <Loading what="the storage"/> }.into_any(),
Some(Err(r)) => refused("holger refused the estate's disks", r),
Some(Ok(t)) => {
let disks = ov::storage(&ov::boxes(&t).0);
if disks.is_empty() {
return view! { <EmptyState message="holger measured no data volume"/> }.into_any();
}
view! {
<ul class="disk-list">
{disks.into_iter().map(|(name, d)| view! {
<li>
<div class="disk-line"><strong>{name}</strong><span class="count">{format!("{}% full", d.percent)}</span></div>
<span class="meter" role="img" aria-label=format!("{}% full", d.percent)><span class="meter-fill" style=format!("width: {}%", d.percent)></span></span>
<span class="count">{format!("{} used of {}", d.used, d.total)}</span>
</li>
}).collect_view()}
</ul>
}.into_any()
}
}}
</Card>
<Card title="Sources" key="sources">
{move || match topology.get() {
None => view! { <Loading what="the sources"/> }.into_any(),
Some(Err(r)) => refused("holger refused the sources", r),
Some(Ok(t)) => {
let (_, sources) = ov::boxes(&t);
if sources.is_empty() {
return view! { <EmptyState message="No upstream source: packages come in by upload only."/> }.into_any();
}
view! {
<ul class="row-list">
{sources.into_iter().map(|s| { let key = s.name.clone(); view! {
<li data-source=key>
<Badge tone=s.tone label=s.state/>
<strong>{s.name}</strong>
<span class="chip">{s.kind}</span>
<span class="count">{format!("{} · feeds {} · {}", s.headline, s.feeds, s.fetched)}</span>
</li>
}}).collect_view()}
</ul>
}.into_any()
}
}}
</Card>
<Card title="This server" key="server" more=("/repositories".to_string(), "Repositories".to_string())>
{move || match health.get() {
None => view! { <Loading/> }.into_any(),
Some(Err(e)) => view! { <Badge tone=Tone::Bad label="unreachable"/><p class="count">{e}</p> }.into_any(),
Some(Ok(h)) => view! {
<p><Badge tone=Tone::Good label=h.status.clone()/><strong>{h.version.clone()}</strong></p>
<dl class="mini-facts">
<dt>"Up"</dt><dd>{format!("up {}", uptime_words(h.uptime_seconds))}</dd>
{move || profile.get().and_then(|p| p.ok()).map(|p| view! { <dt>"Mode"</dt><dd>{format!("{} · {}", p.profile, p.label)}</dd> })}
{move || repos.get().and_then(|r| r.ok()).map(|list| {
let writable = list.iter().filter(|r| r.writable).count();
view! { <dt>"Repositories"</dt><dd><a href="/repositories">{format!("{} configured, {writable} writable", list.len())}</a></dd> }
})}
</dl>
}.into_any(),
}}
</Card>
</div>
</section>
}
}