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

nordisk/holger-ui

src/overview.rs

//! **The Overview's lines** (lane HOL-UI-FIX, owner 2026-10-31: "Overview = the estate's state at
//! a glance"): the boxes of the estate and their health, the sources, the storage and what needs
//! a person, every word from holger's own answers (`AdminService.Topology`,
//! `SecuritySummary`, the sync board). Nothing here decides a state: the server's state word is
//! shown as it came, and only given a tone. Pure, host-tested; the page is `pages::dashboard`.

#![cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]

#[cfg(not(target_arch = "wasm32"))]
use crate::admin_queue::pb;
#[cfg(target_arch = "wasm32")]
use crate::grpc::pb;
use crate::util::{fmt_bytes, Tone};

/// The Flow page, where the picture is (the Overview links to it and draws none): main
/// unfolded, Henrik's level 2 (HOL-FLOW-SMART).
pub const FLOW: &str = "/flow";
/// The Flow page with nothing open: the machines, level 1 (where the boxes are drawn).
pub const MACHINES: &str = "/flow?open=";
/// How many rows of a long server list the Overview shows before its "all on …" link.
pub const FIRST_ROWS: usize = 5;

/// One box of the estate (front, main, twin, a downstream), as holger knows it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BoxCard {
    pub name: String,
    /// The role in a person's word: `main` for holger's `primary`.
    pub role: String,
    /// holger's state word (`up`, `behind`, `silent`, `unknown`), or `held`.
    pub state: String,
    pub tone: Tone,
    /// holger's few words (`answering`, `behind 120 entries`, `silent for 4 min`).
    pub headline: String,
    /// holger's sentence: how it knows.
    pub why: String,
    /// When it was last heard from, in words.
    pub seen: String,
    pub this_box: bool,
    pub address: String,
    /// `front` or `wireguard`, a downstream's road here; empty otherwise.
    pub via: String,
    /// A twin's or a downstream's lag, in log entries; `None` for any other box.
    pub lag: Option<String>,
    pub disk: Option<Disk>,
}

/// A measured data volume.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Disk {
    pub used: String,
    pub total: String,
    /// 0..=100, the bar's width.
    pub percent: u32,
}

/// One upstream source (a registry this box proxies, a holger it mirrors, the upload door).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SourceLine {
    pub name: String,
    /// `proxy`, `mirror` or `upload`, holger's word.
    pub kind: String,
    pub state: String,
    pub tone: Tone,
    pub headline: String,
    /// The local repositories it feeds.
    pub feeds: String,
    /// Packages taken from it since this boot, in words.
    pub fetched: String,
}

/// Pure: the tone of holger's box state word.
pub fn state_tone(state: &str) -> Tone {
    match state {
        "up" => Tone::Good,
        "behind" | "held" => Tone::Warn,
        "silent" => Tone::Bad,
        _ => Tone::Neutral,
    }
}

/// Pure: holger's role word in a person's word.
pub fn role_word(role: &str) -> &str {
    match role {
        "primary" => "main",
        r => r,
    }
}

/// Pure: milliseconds since the newest evidence, in words; `never` when holger has none.
pub fn seen_words(last_seen_unix_ms: i64, seen_ago_ms: u64) -> String {
    if last_seen_unix_ms <= 0 {
        return "never heard".into();
    }
    match seen_ago_ms / 1000 {
        s if s < 5 => "just now".into(),
        s => format!("{} ago", crate::sync_board::span(s as i64)),
    }
}

/// Pure: a measured volume, or `None` when holger did not measure it (then its numbers are 0
/// and must not be read).
pub fn disk(measured: bool, total: u64, used: u64) -> Option<Disk> {
    if !measured || total == 0 {
        return None;
    }
    let percent = ((used.min(total) as u128 * 100) / total as u128) as u32;
    Some(Disk { used: fmt_bytes(used as i64), total: fmt_bytes(total as i64), percent })
}

/// Pure: **the estate's boxes and its sources**, in holger's order: every row but the
/// upstreams is a box; the upstreams are the sources.
pub fn boxes(t: &pb::TopologyReply) -> (Vec<BoxCard>, Vec<SourceLine>) {
    let mut cards = Vec::new();
    let mut sources = Vec::new();
    for b in &t.boxes {
        let state = if b.held { "held".to_string() } else { b.state.clone() };
        if b.role == "upstream" {
            sources.push(SourceLine {
                name: b.name.clone(),
                kind: if b.upstream_kind.is_empty() { "upload".into() } else { b.upstream_kind.clone() },
                tone: state_tone(&state),
                state,
                headline: b.headline.clone(),
                feeds: if b.feeds.is_empty() { "—".into() } else { b.feeds.join(", ") },
                fetched: format!("{} since this boot", b.fetched_total),
            });
            continue;
        }
        let lag = matches!(b.role.as_str(), "twin" | "downstream").then(|| match b.lag_entries {
            0 => "no lag".to_string(),
            1 => "1 entry behind".to_string(),
            n => format!("{n} entries behind"),
        });
        cards.push(BoxCard {
            name: b.name.clone(),
            role: role_word(&b.role).to_string(),
            tone: state_tone(&state),
            state,
            headline: b.headline.clone(),
            why: b.why.clone(),
            seen: if b.this_box { "this box".into() } else { seen_words(b.last_seen_unix_ms, b.seen_ago_ms) },
            this_box: b.this_box,
            address: b.address.clone(),
            via: b.via.clone(),
            lag,
            disk: disk(b.disk_measured, b.disk_total_bytes, b.disk_used_bytes),
        });
    }
    (cards, sources)
}

/// Pure: **the estate's storage**: each box whose data volume holger measured, with it.
pub fn storage(cards: &[BoxCard]) -> Vec<(String, Disk)> {
    cards.iter().filter_map(|c| c.disk.clone().map(|d| (format!("{} ({})", c.name, c.role), d))).collect()
}

/// One thing a person should look at, and where it is handled.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Attention {
    pub what: String,
    pub detail: String,
    pub tone: Tone,
    /// The page that handles it.
    pub href: String,
}

/// Pure: **what needs action**, from what holger said elsewhere on the page: the sync alarm
/// it raised, each box it calls silent, behind or held, and each package that waits for its
/// approvals (the Security summary's `approvals` section). Empty: nothing waits for a person.
pub fn needs_action(alarm: Option<&str>, cards: &[BoxCard], approvals: &[crate::security::SectionRow]) -> Vec<Attention> {
    let mut out = Vec::new();
    if let Some(a) = alarm {
        out.push(Attention { what: "Sync alarm".into(), detail: a.to_string(), tone: Tone::Bad, href: "/admin".into() });
    }
    for c in cards.iter().filter(|c| matches!(c.tone, Tone::Bad | Tone::Warn)) {
        out.push(Attention { what: format!("{} ({}) is {}", c.name, c.role, c.state), detail: c.headline.clone(), tone: c.tone, href: MACHINES.into() });
    }
    for r in approvals {
        out.push(Attention {
            what: format!("{} waits for approval", r.name),
            detail: r.detail.clone(),
            tone: Tone::Warn,
            href: r.href.clone().unwrap_or_else(|| crate::security::DASHBOARD.into()),
        });
    }
    out
}

/// Pure: the first [`FIRST_ROWS`] of a list and how many more there are.
pub fn first<T: Clone>(rows: &[T]) -> (Vec<T>, usize) {
    (rows.iter().take(FIRST_ROWS).cloned().collect(), rows.len().saturating_sub(FIRST_ROWS))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn b(name: &str, role: &str, state: &str) -> pb::TopologyBox {
        pb::TopologyBox { name: name.into(), role: role.into(), state: state.into(), headline: format!("{name} says"), ..Default::default() }
    }

    #[test]
    fn the_boxes_are_every_row_but_the_upstreams_and_the_sources_are_those() {
        let mut main = b("main", "primary", "up");
        main.this_box = true;
        main.disk_measured = true;
        main.disk_total_bytes = 1000;
        main.disk_used_bytes = 250;
        let mut lab = b("lab", "downstream", "behind");
        lab.lag_entries = 3;
        lab.via = "wireguard".into();
        lab.last_seen_unix_ms = 1;
        lab.seen_ago_ms = 125_000;
        let mut cloud = b("cloud", "upstream", "silent");
        cloud.upstream_kind = "mirror".into();
        cloud.feeds = vec!["crates".into()];
        let team = b("team", "upstream", "unknown");
        let t = pb::TopologyReply { boxes: vec![main, b("front", "front", "unknown"), lab, cloud, team], ..Default::default() };
        let (cards, sources) = boxes(&t);
        assert_eq!(cards.iter().map(|c| c.name.as_str()).collect::<Vec<_>>(), ["main", "front", "lab"], "holger's order, no upstream among the boxes");
        assert_eq!(cards[0].role, "main", "primary is main to a person");
        assert_eq!(cards[0].seen, "this box");
        assert_eq!(cards[0].disk, Some(Disk { used: "250 B".into(), total: "1.0 kB".into(), percent: 25 }));
        assert_eq!(cards[0].lag, None, "a main has no lag");
        assert_eq!(cards[1].seen, "never heard");
        assert_eq!(cards[1].disk, None, "an unmeasured disk is no disk");
        assert_eq!((cards[2].tone, cards[2].lag.as_deref(), cards[2].via.as_str(), cards[2].seen.as_str()), (Tone::Warn, Some("3 entries behind"), "wireguard", "2 min ago"));
        assert_eq!(sources.iter().map(|s| (s.name.as_str(), s.kind.as_str(), s.tone)).collect::<Vec<_>>(), [("cloud", "mirror", Tone::Bad), ("team", "upload", Tone::Neutral)]);
        assert_eq!(sources[0].feeds, "crates");
    }

    #[test]
    fn a_held_box_says_held() {
        let mut main = b("main", "primary", "silent");
        main.held = true;
        let (cards, _) = boxes(&pb::TopologyReply { boxes: vec![main], ..Default::default() });
        assert_eq!((cards[0].state.as_str(), cards[0].tone), ("held", Tone::Warn));
    }

    #[test]
    fn storage_is_every_measured_box() {
        let mut main = b("main", "primary", "up");
        main.disk_measured = true;
        main.disk_total_bytes = 200_000_000_000;
        main.disk_used_bytes = 50_000_000_000;
        let (cards, _) = boxes(&pb::TopologyReply { boxes: vec![main, b("twin", "twin", "unknown")], ..Default::default() });
        assert_eq!(storage(&cards), vec![("main (main)".to_string(), Disk { used: "50.0 GB".into(), total: "200.0 GB".into(), percent: 25 })], "an unmeasured twin is not a 0 B disk");
    }

    #[test]
    fn needs_action_is_the_alarm_the_unwell_boxes_and_the_waiting_packages() {
        let t = pb::TopologyReply { boxes: vec![b("main", "primary", "up"), b("twin", "twin", "silent"), b("lab", "downstream", "unknown")], ..Default::default() };
        let (cards, _) = boxes(&t);
        let waiting = vec![crate::security::SectionRow { name: "demo.whl".into(), href: Some("/security/artifact?ns=&name=demo.whl&version=".into()), state: "waiting".into(), tone: Tone::Warn, detail: "0 of 2".into() }];
        let a = needs_action(Some("ALARM: lab"), &cards, &waiting);
        assert_eq!(a.iter().map(|a| a.what.as_str()).collect::<Vec<_>>(), ["Sync alarm", "twin (twin) is silent", "demo.whl waits for approval"]);
        assert_eq!(a[2].href, "/security/artifact?ns=&name=demo.whl&version=");
        assert_eq!(a[1].href, MACHINES, "an unwell box leads to the machines, level 1");
        assert!(needs_action(None, &cards[..1], &[]).is_empty(), "a well estate needs nothing");
    }

    #[test]
    fn first_shows_five_and_counts_the_rest() {
        let (shown, more) = first(&[1, 2, 3, 4, 5, 6, 7]);
        assert_eq!((shown.len(), more), (5, 2));
        assert_eq!(first::<u8>(&[]), (vec![], 0));
    }
}