Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger-ui

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>
    }
}