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

nordisk/holger-ui

src/admin.rs

//! **The admin view's decisions** — pure, host-tested; `pages/admin.rs`
//! renders them.
//!
//! Two rules, and the first one is not this module's to enforce:
//!
//! * **The server decides who is an admin.** `/api/admin/snapshot` is refused
//!   by the console's server to every session below admin (the role floor in
//!   `holger-ui-server`'s middleware, and the handler again) and holger refuses
//!   the RPC below its admin rung. Hiding the section here protects nothing —
//!   a browser that received the snapshot and did not draw it has still shown
//!   it to devtools. [`admin_visible`] only keeps a normal user from being
//!   offered a door that will refuse them; the role it reads is the SERVER's
//!   answer (`/auth/status`), never a guess.
//! * **Sizes in human units, the raw numbers beside them.** A disk that reads
//!   "1.2 TB" in a card is read in a second; the byte counts in the detail row
//!   are what an operator compares with `df -B1` or pastes into a ticket.

#![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::session::Shown;
use crate::util::fmt_bytes;

/// Where the admin view lives, in the console and on its server.
pub const PAGE: &str = "/admin";

/// The panes, in order: `(slug, label)`.
pub const PANES: [(&str, &str); 11] = [
    ("disks", "Disks"),
    ("twin", "Twin"),
    // Twins and paired downstreams: lag, state, last sync (`crate::sync`).
    ("sync", "Sync"),
    ("growth", "Growth"),
    ("clock", "Clock"),
    ("plugins", "Plugins / Monetize"),
    ("logs", "Logs"),
    // holger admit-queue: the job queue and the door's admission, each read
    // from its own admin-only route (`crate::admin_queue`).
    ("queue", "Queue"),
    ("admission", "Admission"),
    // Single-use invite links: mint, list, revoke (`crate::invites`).
    ("invites", "Invites"),
    // This front's certificates, read from the front itself (`crate::front_certs`).
    ("certs", "Certificates"),
];

/// Pure: may the Admin section be offered? Only to a session the SERVER says
/// is an admin — or to the open console, which the server itself serves as
/// admin (no passkey and no token: loopback only). Never before the server
/// answered, never to a viewer or an operator, never to a pasted token.
pub fn admin_visible(shown: &Shown) -> bool {
    match shown {
        Shown::Session { role, .. } => role == "admin",
        Shown::Unknown | Shown::LoggedOut { .. } | Shown::TokenOnly { .. } => false,
    }
}

/// Pure: a size in human units (`1.2 GB`).
pub fn human(bytes: u64) -> String {
    fmt_bytes(i64::try_from(bytes).unwrap_or(i64::MAX))
}

/// Pure: per mille as a percentage with one decimal (`612` → `61.2 %`).
pub fn percent(permille: u32) -> String {
    format!("{}.{} %", permille / 10, permille % 10)
}

/// Pure: the fill tone of a disk — `bad` at 90 %, `warn` at 75 %.
pub fn fill_tone(permille: u32) -> &'static str {
    match permille {
        900.. => "bad",
        750.. => "warn",
        _ => "good",
    }
}

/// Pure: one disk's headline — used / total, free, floor, servable — in human units.
pub fn disk_headline(total: u64, used: u64, available: u64, floor: u64, servable: u64) -> String {
    format!(
        "{} of {} used · {} free · floor {} · {} servable",
        human(used),
        human(total),
        human(available),
        human(floor),
        human(servable)
    )
}

/// Pure: the same disk as raw numbers, for the detail row.
pub fn disk_raw(total: u64, used: u64, free: u64, available: u64, reserved: u64, floor: u64, servable: u64, block: u64) -> String {
    format!(
        "total {total} B · used {used} B · free {free} B · available {available} B · root-reserved {reserved} B · floor {floor} B · servable {servable} B · block {block} B"
    )
}

/// Pure: a twin's lag in words (`-1` = never whole; a primary follows nobody).
pub fn lag_words(role: &str, lag_seconds: i64) -> String {
    if role != "twin" {
        return "— (a primary follows nobody)".into();
    }
    match lag_seconds {
        s if s < 0 => "never whole".into(),
        s if s < 120 => format!("{s} s"),
        s if s < 7_200 => format!("{} min", s / 60),
        s if s < 172_800 => format!("{} h", s / 3_600),
        s => format!("{} days", s / 86_400),
    }
}

/// Pure: a clock offset in words (`+1234` → `+1.234 s`).
pub fn offset_words(ms: i64) -> String {
    let sign = if ms < 0 { "−" } else { "+" };
    let a = ms.unsigned_abs();
    if a < 1_000 {
        format!("{sign}{a} ms")
    } else {
        format!("{sign}{}.{:03} s", a / 1_000, a % 1_000)
    }
}

/// Pure: a credential's remaining life in words.
pub fn days_left_words(installed: bool, days_left: i64) -> String {
    match (installed, days_left) {
        (false, _) => "not installed".into(),
        (true, d) if d < 0 => format!("EXPIRED {} days ago", -d),
        (true, d) => format!("{d} days left"),
    }
}

/// Pure: **where the "Order your own Holger" button sends the person**: this site's OWN
/// sign-up page for a plan, `/signup?plan=<plan>` (A12, owner 2026-10-23: the buyer stays on
/// the site and never sees monetize), as holger answered it (`RoomService.PrivateEstate`).
/// The link is the path, or the same path on `site`, the console's own origin
/// (`location.origin`). `Ok(None)`: holger sells nothing here (its words are shown instead).
/// `Err`: any other link (an outside https page, the old ONE shop's `/shop/private`, another
/// path), refused by name. A metered console's only (A28 M10).
#[cfg(feature = "metered")]
pub fn private_estate_link<'a>(shop_url: &'a str, site: &str) -> Result<Option<&'a str>, String> {
    let u = shop_url.trim();
    if u.is_empty() {
        return Ok(None);
    }
    let site = site.trim().trim_end_matches('/');
    let path = match u.strip_prefix(site) {
        Some(rest) if !site.is_empty() => rest,
        _ => u,
    };
    let plan = path.strip_prefix(SIGNUP_PLAN).unwrap_or_default();
    let a_plan = !plan.is_empty() && plan.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_');
    if a_plan {
        Ok(Some(u))
    } else {
        Err(format!("this console follows only this site's own sign-up page ({SIGNUP_PLAN}…), not {u:?}"))
    }
}

/// The site's own sign-up page for a plan: the only page the order button follows.
#[cfg(feature = "metered")]
pub const SIGNUP_PLAN: &str = "/signup?plan=";

/// The Disks pane's paths that sit on no mount, as holger sent them, split in two (lane
/// HOL-UI-FIX): the faults (`label (path) sits on no mount: why`) and the repositories'
/// storage folders holger has not made yet (`label (path)`; holger says why once, in its own
/// words, `made_at_first_upload`). The split is holger's flag; nothing is guessed here.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Unplaced {
    pub faults: Vec<String>,
    pub later: Vec<String>,
    /// holger's words for the folders not made yet (the first one's `why`).
    pub later_why: String,
}

/// Pure: [`Unplaced`] from holger's `AdminDisks.unresolved`.
pub fn unplaced(rows: &[pb::AdminUnresolved]) -> Unplaced {
    let mut u = Unplaced::default();
    for r in rows {
        if r.made_at_first_upload {
            if u.later_why.is_empty() {
                u.later_why = r.why.clone();
            }
            u.later.push(format!("{} ({})", r.label, r.path));
        } else {
            u.faults.push(format!("{} ({}) sits on no mount: {}", r.label, r.path, r.why));
        }
    }
    u
}

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

    /// ★ **The section is offered to an admin's session and to nobody else's.**
    /// (The data is refused by the server regardless — see the module note and
    /// `holger-ui-server`'s `api::admin_view` tests.)
    #[test]
    fn the_admin_section_is_offered_only_to_an_admin() {
        let s = |role: &str| Shown::Session { user: "u".into(), role: role.into() };
        assert!(admin_visible(&s("admin")));
        assert!(!admin_visible(&s("operator")));
        assert!(!admin_visible(&s("viewer")));
        assert!(!admin_visible(&s("")), "a session with no role is not an admin");
        assert!(!admin_visible(&s("Admin ")), "the server's token is exact");
        assert!(!admin_visible(&Shown::Unknown), "never before the server answered");
        assert!(!admin_visible(&Shown::LoggedOut { login: "/login" }));
        assert!(!admin_visible(&Shown::TokenOnly { has_token: true }), "a pasted token is not a session role");
    }

    /// The section is offered from the SERVER's status body, end to end.
    #[test]
    fn the_role_comes_from_the_servers_status() {
        let from = |body: Option<&str>| crate::session::shown(Some(&crate::session::status_of(true, body)), false);
        assert!(admin_visible(&from(Some(r#"{"display":"ada","role":"admin"}"#))));
        assert!(!admin_visible(&from(Some(r#"{"display":"vera","role":"reader"}"#))));
        assert!(!admin_visible(&from(None)));
        assert!(!admin_visible(&from(Some("not json"))));
    }

    #[test]
    fn the_panes_the_owner_named_in_order() {
        let labels: Vec<_> = PANES.iter().map(|(_, l)| *l).collect();
        assert_eq!(labels, ["Disks", "Twin", "Sync", "Growth", "Clock", "Plugins / Monetize", "Logs", "Queue", "Admission", "Invites", "Certificates"]);
        assert_eq!(PAGE, "/admin");
    }

    #[test]
    fn sizes_are_human_and_the_raw_row_keeps_every_byte() {
        assert_eq!(human(107_374_182_400), "107.4 GB");
        assert_eq!(human(512), "512 B");
        assert_eq!(human(u64::MAX), human(i64::MAX as u64), "no overflow into a negative");
        assert_eq!(disk_headline(100_000_000_000, 61_000_000_000, 39_000_000_000, 5_000_000_000, 34_000_000_000), "61.0 GB of 100.0 GB used · 39.0 GB free · floor 5.0 GB · 34.0 GB servable");
        let raw = disk_raw(100, 61, 40, 39, 1, 5, 34, 4096);
        assert_eq!(raw, "total 100 B · used 61 B · free 40 B · available 39 B · root-reserved 1 B · floor 5 B · servable 34 B · block 4096 B");
    }

    #[test]
    fn percent_and_fill_tone() {
        assert_eq!(percent(612), "61.2 %");
        assert_eq!(percent(0), "0.0 %");
        assert_eq!((fill_tone(100), fill_tone(750), fill_tone(900)), ("good", "warn", "bad"));
    }

    #[test]
    fn lag_offset_and_credential_life_in_words() {
        assert_eq!(lag_words("primary", 0), "— (a primary follows nobody)");
        assert_eq!(lag_words("twin", -1), "never whole");
        assert_eq!(lag_words("twin", 42), "42 s");
        assert_eq!(lag_words("twin", 600), "10 min");
        assert_eq!(lag_words("twin", 7_200), "2 h");
        assert_eq!(lag_words("twin", 259_200), "3 days");
        assert_eq!(offset_words(1_234), "+1.234 s");
        assert_eq!(offset_words(-80), "−80 ms");
        assert_eq!(days_left_words(false, 0), "not installed");
        assert_eq!(days_left_words(true, 364), "364 days left");
        assert_eq!(days_left_words(true, -2), "EXPIRED 2 days ago");
    }

    /// Lane P1, then A12: the order button follows the site's own private choice, and an
    /// empty link means holger sells nothing here.
    #[cfg(feature = "metered")]
    #[test]
    fn the_order_button_follows_the_sites_own_signup() {
        let site = "https://holger.example.org";
        assert_eq!(private_estate_link(" /signup?plan=private ", site), Ok(Some("/signup?plan=private")), "A12: the site's own private choice");
        assert_eq!(private_estate_link("https://holger.example.org/signup?plan=private", site), Ok(Some("https://holger.example.org/signup?plan=private")), "the same page on the console's own origin");
        assert_eq!(private_estate_link("https://holger.example.org/signup?plan=grow", "https://holger.example.org/"), Ok(Some("https://holger.example.org/signup?plan=grow")));
        assert_eq!(private_estate_link("", site), Ok(None), "holger sells nothing here");
        assert_eq!(private_estate_link("  ", site), Ok(None));
    }

    /// A28 M10: since 10-23 the buyer never leaves the site, so the order button follows only
    /// the site's OWN `/signup?plan=…`. An outside https page (the old ONE shop's
    /// `/shop/private`) is refused, and the refusal names the link.
    #[cfg(feature = "metered")]
    #[test]
    fn the_order_button_refuses_an_outside_https_page() {
        let site = "https://holger.example.org";
        let refused = |u: &str| match private_estate_link(u, site) {
            Err(why) => assert!(why.contains(&format!("{:?}", u.trim())) && why.contains("/signup?plan="), "{why}"),
            Ok(l) => panic!("{u:?} was followed: {l:?}"),
        };
        refused("https://monetize.example.org/shop/private?product=holger&ticket=ab");
        refused("https://evil.example/signup?plan=private");
        refused("https://holger.example.org.evil.example/signup?plan=private");
        refused("http://holger.example.org/signup?plan=private");
        refused("//evil.example/signup?plan=private");
        refused("javascript:alert(1)");
        refused("/shop/private");
        refused("/kop?product=holger");
        refused("/signup");
        refused("/signup?plan=");
        refused("/signupx?plan=private");
        refused("/signup?plan=private&return=https://evil.example");
        refused("/signup?plan=private#x");
        // With no origin known, only the path is followed.
        assert_eq!(private_estate_link("/signup?plan=private", ""), Ok(Some("/signup?plan=private")));
        assert!(private_estate_link("https://holger.example.org/signup?plan=private", "").is_err());
    }

    /// ★ HOL-UI-FIX: a storage folder holger makes at the first upload is no fault; holger's
    /// flag splits them, and its words say why.
    #[test]
    fn a_folder_made_at_the_first_upload_is_no_fault() {
        let row = |label: &str, path: &str, why: &str, later: bool| pb::AdminUnresolved { label: label.into(), path: path.into(), why: why.into(), made_at_first_upload: later };
        let u = unplaced(&[
            row("storage:incoming", "/d/store/incoming", "created at the first upload", true),
            row("data", "/gone", "/gone: No such file", false),
            row("storage:npm", "/d/store/npm", "created at the first upload", true),
        ]);
        assert_eq!(u.faults, ["data (/gone) sits on no mount: /gone: No such file"]);
        assert_eq!(u.later, ["storage:incoming (/d/store/incoming)", "storage:npm (/d/store/npm)"]);
        assert_eq!(u.later_why, "created at the first upload");
        assert_eq!(unplaced(&[]), Unplaced::default());
    }
}