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

nordisk/gunnar-ui

crates/gunnar-ui-web/src/nav.rs

//! **Where everything lives**: the top bar, a repository's tabs, and the old
//! addresses that now lead somewhere else — on the host side of the wasm
//! boundary, so a test can state the menu.
//!
//! The owner's final navigation (2026-10-02):
//!
//! ```text
//! [gunnar]  Repositories   (search…)        You ▾   Administration ▾
//! ```
//!
//! * **Repositories** is the only main entry and everybody's landing page.
//! * **You ▾**: Account; Activity (the events of the reader's OWN account,
//!   filtered on the server); Keys & passkeys; Log out. No Storage entry
//!   (owner 2026-10-17): buying is the ONE sign-up page, from Account's button.
//! * **Administration ▾**, an administrator's only: Overview (the whole
//!   server's events, and the Machine card: the estate's boxes and the few
//!   lines gunnar judges the disks by), Storage (the machine's: data set,
//!   engine, layout, disks, twin, /serialdat, performance — every disk detail,
//!   once), Identities, Mirrors, Instruments. No Machine page any more (owner,
//!   2026-10-04): it said again what Overview and Storage say.
//! * **Inside a repository**: Code · History · Pulls · Issues · Activity ·
//!   Settings.
//! * Every old address (`/`, `/overview`, `/events`, `/storage`,
//!   `/usage[/…]`, `/access`, `/machine`) leads to its new home ([`moved`]);
//!   `/machine` to the Overview, where its card is.

/// One menu entry.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Link {
    /// The menu it sits in.
    pub group: &'static str,
    pub label: &'static str,
    pub href: &'static str,
    /// Drawn as a sub-entry of the link before it.
    pub sub: bool,
}

/// Who is reading, as far as the top bar is concerned.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Reader {
    pub signed_in: bool,
    pub may_write: bool,
    pub is_admin: bool,
    /// **A member of an account** — gunnar's word (`/auth/status`
    /// `account_role: employee`, `Status::is_member`): an enterprise account's
    /// person, whom gunnar keeps out of every clan (round 5.3).
    pub member: bool,
}

/// gunnar's word (`/auth/status` `account_role`) for a member the account's admin gave Admin
/// over the account (owner 2026-10-08, round 5.3.1).
pub const APPOINTED_ADMIN: &str = "admin-appointed";

/// **A member of an account** — uses its space, buys nothing: an employee, and an employee
/// appointed its admin.
pub fn is_member_role(account_role: Option<&str>) -> bool {
    matches!(account_role, Some("employee" | APPOINTED_ADMIN))
}

/// **Is the account's Audit log offered?** gunnar admits the account's admins alone (and
/// the operator's own sessions, which name no account): never a plain member.
pub fn shows_audit_log(account_role: Option<&str>) -> bool {
    !is_member_role(account_role) || account_role == Some(APPOINTED_ADMIN)
}

/// **The top bar's menus.**
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Menu {
    /// Left: the main entries.
    pub main: Vec<Link>,
    /// "You ▾": the reader's own.
    pub you: Vec<Link>,
    /// "Administration ▾": an administrator's only.
    pub admin: Vec<Link>,
}

/// **The top bar** (owner's final navigation, 2026-10-02):
/// `[gunnar]  Repositories  (search…)        You ▾   Administration ▾`.
///
/// * Repositories — the only main entry, and every reader's;
/// * You ▾ — a signed-in reader's own: their Account, its Activity (the
///   events of their own account), their keys and passkeys — and Log out, which is the
///   menu's own button. Room is left for "Members" (the account model);
/// * Administration ▾ — an ADMINISTRATOR's only: the Overview (the whole
///   server's events and counts, and the Machine card), the machine's Storage,
///   Identities, Mirrors, and the Instruments page (passkeys, replication,
///   orphan stores, the console's own identity). Overview · Storage ·
///   Identities · Mirrors · Instruments (owner, 2026-10-04); since 2026-10-05
///   the Identity provider (company SSO) after Identities — an account admin
///   who is no box admin reaches it from their account's page — and (the
///   owner's enterprise list) Accounts — the accounts' totals — after
///   Overview, and Audit log before Instruments.
pub fn menu(r: Reader) -> Menu {
    let link = |group, label, href| Link { group, label, href, sub: false };
    let mut m = Menu { main: vec![link("Main", "Repositories", "/repos")], ..Menu::default() };
    if !r.signed_in {
        return m;
    }
    m.you = vec![
        link("You", "Account", "/account"),
        link("You", "Activity", "/account/activity"),
        // Clans (owner, 2026-10-05): the accounts the reader groups with.
        link("You", "Groups", "/account/clans"),
        link("You", "Keys & passkeys", "/account#keys"),
    ];
    // **Never a member's** (round 5.3; the members walkthrough of 2026-10-07):
    // gunnar keeps an enterprise account's people out of every clan ("ben is
    // an employee of acme: an enterprise account's people may not make a clan
    // — their account admin's rules win"), so the entry led a member to a
    // form gunnar refuses. gunnar's word on who is a member decides; nothing
    // is decided here but not drawing a door it closed.
    if r.member {
        m.you.retain(|l| l.href != "/account/clans");
    }
    if r.is_admin {
        m.admin = vec![
            link("Administration", "Overview", "/admin/overview"),
            // The accounts' TOTALS (owner's enterprise list, 2026-10-05): the
            // operator sees no account's names or events.
            link("Administration", "Accounts", "/admin/accounts"),
            link("Administration", "Storage", "/admin/storage"),
            link("Administration", "Identities", "/access"),
            link("Administration", "Identity provider", "/admin/identity-provider"),
            link("Administration", "Mirrors", "/mirrors"),
            // The account admin's audit log — of the administrator's OWN
            // account (another account's admin reaches theirs from Account).
            link("Administration", "Audit log", "/admin/audit"),
            link("Administration", "Instruments", "/admin"),
        ];
    }
    m
}

/// **The role badge on the Account page**, or none: an operator's and an
/// administrator's role is shown; a viewer — every ordinary person — sees no
/// badge (owner order 2026-10-18).
pub fn role_badge(role: Option<&str>) -> Option<String> {
    role.filter(|r| matches!(*r, "operator" | "admin")).map(str::to_owned)
}

/// The Provenance tab (the signed chain of a line): hidden from the tabs
/// since 2026-10-18 (owner, wide audience). The page itself still answers.
pub const SHOW_PROVENANCE: bool = false;

/// **A repository's tabs**: Code · History · Pulls · Issues · Activity ·
/// Settings. Pulls and Activity need a session (their reads are
/// behind one), Issues are a visitor's too (GUN-GUEST); Settings is for whoever may change the repository (an
/// operator or an administrator — the server refuses anybody else whatever
/// is drawn).
/// **What a guest on a repository's Settings sees** (GUN-FIELD-TEST, 2026-11-01): only this,
/// and a Log in link; no settings, no Danger zone.
pub const SETTINGS_SIGN_IN: &str = "Sign in to see this repository's settings.";

/// **Which tab is the current one** (GUN-FIELD-TEST, 2026-11-01): the tab whose address
/// is the longest prefix of `here`; the README's (the repository's own address) only on
/// that address exactly. A page without a tab of its own (a guest on Pulls or Settings)
/// marks none: it marked Readme, the prefix of everything.
pub fn current_tab(tabs: &[(&'static str, String)], here: &str) -> Option<&'static str> {
    let home = tabs.first().map(|(_, h)| h.as_str()).unwrap_or("");
    tabs.iter()
        .filter(|(_, href)| here == *href || (href.as_str() != home && here.starts_with(&format!("{href}/"))))
        .max_by_key(|(_, href)| href.len())
        .map(|(label, _)| *label)
}

pub fn repo_tabs(namespace: &str, name: &str, signed_in: bool, may_write: bool) -> Vec<(&'static str, String)> {
    // GUN-GUEST (owner 2026-10-30): the one address scheme, `/{ns}/{name}/…` (`crate::addr`).
    let base = crate::addr::repo(namespace, name);
    // One row for the whole repository (owner, 2026-10-02): what used to be
    // the Code tab's header buttons are tabs too, and the README is the home.
    let mut tabs = vec![
        ("Readme", base.clone()),
        ("Code", format!("{base}/tree")),
        ("History", format!("{base}/commits")),
        ("Compare", format!("{base}/diff")),
        ("Search", format!("{base}/search")),
        ("Blame", format!("{base}/blame")),
    ];
    // HIDDEN (owner order 2026-10-18, wide audience): the Provenance tab.
    // Its page stays at its address, behind this flag.
    if SHOW_PROVENANCE {
        tabs.push(("Provenance", format!("{base}/chain")));
    }
    if signed_in {
        tabs.push(("Pulls", format!("{base}/pulls")));
    }
    // A visitor reads a public repository's issues too (GUN-GUEST); filing and
    // commenting ask them to sign in (`crate::guest`).
    tabs.push(("Issues", format!("{base}/issues")));
    if signed_in {
        tabs.push(("Activity", format!("{base}/activity")));
    }
    if may_write {
        tabs.push(("Settings", format!("{base}/settings")));
    }
    tabs
}

/// **Where an old address leads now**, or `None` to draw it where it is.
///
/// Not a permission: the server refuses every one of these reads to whoever
/// may not make them. This only stops a person landing on a page that would
/// draw refusals, and keeps every old link — a bookmark, a buy link —
/// working.
pub fn moved(path: &str, is_admin: bool) -> Option<&'static str> {
    let path = path.trim_end_matches('/');
    let path = if path.is_empty() { "/" } else { path };
    let usage = path == "/usage" || path.starts_with("/usage/");
    match (path, is_admin) {
        // Repositories is the default, for everybody.
        ("/", _) => Some("/repos"),
        // The Machine page is gone (owner, 2026-10-04): its card is on the
        // Overview, its disk details on Administration → Storage.
        ("/overview", true) | ("/events", true) | ("/machine", true) => Some("/admin/overview"),
        ("/overview", false) | ("/admin/overview", false) | ("/machine", false) => Some("/repos"),
        ("/events", false) => Some("/account/activity"),
        ("/storage", true) => Some("/admin/storage"),
        ("/storage", false) | ("/admin/storage", false) => Some("/account"),
        ("/access", false) => Some("/account"),
        _ if usage && !is_admin => Some("/account"),
        _ => None,
    }
}

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

    fn labels(links: &[Link]) -> Vec<&'static str> {
        links.iter().map(|l| l.label).collect()
    }

    const VIEWER: Reader = Reader { signed_in: true, may_write: false, is_admin: false, member: false };
    const ADMIN: Reader = Reader { signed_in: true, may_write: true, is_admin: true, member: false };
    const OPERATOR: Reader = Reader { signed_in: true, may_write: true, is_admin: false, member: false };
    /// An account's member, as gunnar names one (`account_role: employee`).
    const MEMBER: Reader = Reader { signed_in: true, may_write: false, is_admin: false, member: true };

    /// ★ **No "New instance" in the top bar** (owner 2026-10-27, Gunnar is the template; reverts
    /// ALICA-1's top-bar door, ce76319): a private instance is ordered where it always was,
    /// Account → "Buy a plan", whose page has the two tabs "New instance | Buy a plan".
    #[test]
    fn the_top_bar_has_no_new_instance() {
        for r in [VIEWER, ADMIN, OPERATOR, MEMBER, Reader::default()] {
            let m = menu(r);
            for l in m.main.iter().chain(&m.you).chain(&m.admin) {
                assert!(!l.label.to_lowercase().contains("instance"), "{r:?}: the top bar offers {}", l.label);
            }
        }
        let app = include_str!("app.rs");
        assert!(!app.contains("new-instance") && !app.contains("New instance") && !app.contains("new_instance"), "the top bar draws a New instance");
    }

    /// ★ **An appointed admin is a member who sees the audit log** (round 5.3.1): he buys
    /// nothing like every member, and gunnar admits him to the account's audit log.
    #[test]
    fn an_appointed_admin_is_a_member_who_sees_the_audit_log() {
        assert!(is_member_role(Some("employee")) && is_member_role(Some(APPOINTED_ADMIN)));
        for role in [Some("admin"), Some("individual"), None] {
            assert!(!is_member_role(role), "{role:?}");
            assert!(shows_audit_log(role), "{role:?}");
        }
        assert!(shows_audit_log(Some(APPOINTED_ADMIN)), "the appointed admin reads the audit log");
        assert!(!shows_audit_log(Some("employee")), "a member is not shown the door");
    }

    /// ★ **A member's You ▾ has no Clans** (round 5.3; the members walkthrough
    /// of 2026-10-07: ben's You ▾ offered Clans and its "Make a clan" form,
    /// which gunnar refuses an enterprise account's people). Everything else
    /// in it is his own and stays; an account's admin and an individual keep
    /// Clans.
    #[test]
    fn a_members_you_menu_has_no_clans() {
        assert_eq!(labels(&menu(MEMBER).you), ["Account", "Activity", "Keys & passkeys"]);
        assert!(menu(MEMBER).admin.is_empty(), "a member is no administrator");
        for r in [VIEWER, OPERATOR, ADMIN] {
            assert!(labels(&menu(r).you).contains(&"Groups"), "not a member keeps Groups: {r:?}");
        }
    }

    /// ★ **The final navigation** (owner, 2026-10-02): a top bar —
    /// `[gunnar] Repositories (search…)   You ▾  Administration ▾` —
    /// Repositories the only main entry; You ▾ a person's own (Account, its
    /// Activity, keys and passkeys; log out is the menu's own
    /// button); Administration ▾ ONLY for an administrator. No top-level
    /// Storage, Usage, Overview or Events any more.
    #[test]
    fn the_top_bar_has_repositories_you_and_administration_only_for_an_admin() {
        for r in [Reader::default(), VIEWER, OPERATOR, ADMIN] {
            assert_eq!(labels(&menu(r).main), ["Repositories"], "{r:?}");
            assert_eq!(menu(r).main[0].href, "/repos");
        }
        assert!(menu(Reader::default()).you.is_empty(), "a visitor has no You menu (the top bar's Log in instead)");
        let you = ["Account", "Activity", "Groups", "Keys & passkeys"];
        for r in [VIEWER, OPERATOR, ADMIN] {
            assert_eq!(labels(&menu(r).you), you, "{r:?}");
        }
        for r in [Reader::default(), VIEWER, OPERATOR] {
            assert!(menu(r).admin.is_empty(), "Administration is shown to an administrator only: {r:?}");
        }
        assert_eq!(labels(&menu(ADMIN).admin), ["Overview", "Accounts", "Storage", "Identities", "Identity provider", "Mirrors", "Audit log", "Instruments"]);
        let href = |links: &[Link], label: &str| links.iter().find(|l| l.label == label).unwrap().href;
        assert_eq!(href(&menu(ADMIN).admin, "Identity provider"), "/admin/identity-provider");
        assert_eq!(href(&menu(VIEWER).you, "Activity"), "/account/activity");
        assert_eq!(href(&menu(VIEWER).you, "Groups"), "/account/clans", "a person's clans are theirs (You ▾)");
        assert_eq!(href(&menu(ADMIN).admin, "Overview"), "/admin/overview");
        assert_eq!(href(&menu(ADMIN).admin, "Storage"), "/admin/storage");
        assert_eq!(href(&menu(ADMIN).admin, "Accounts"), "/admin/accounts");
        assert_eq!(href(&menu(ADMIN).admin, "Audit log"), "/admin/audit");
        for r in [Reader::default(), VIEWER, OPERATOR, ADMIN] {
            let m = menu(r);
            let all: Vec<&str> = m.main.iter().chain(&m.you).chain(&m.admin).map(|l| l.href).collect();
            for gone in ["/storage", "/usage", "/events", "/", "/overview", "/machine"] {
                assert!(!all.contains(&gone), "{gone} is still a menu entry: {all:?}");
            }
        }
    }

    /// ★ **Repositories is the default, and every old address leads to its
    /// new home** — a person's or an administrator's — so no link breaks.
    #[test]
    fn every_old_address_leads_to_its_new_home() {
        for admin in [false, true] {
            assert_eq!(moved("/", admin), Some("/repos"), "everybody lands on Repositories");
        }
        assert_eq!(moved("/overview", false), Some("/repos"));
        assert_eq!(moved("/overview", true), Some("/admin/overview"));
        assert_eq!(moved("/admin/overview", false), Some("/repos"), "the Overview is not a person's");
        assert_eq!(moved("/admin/overview", true), None);
        assert_eq!(moved("/events", false), Some("/account/activity"), "a person's events are their account's");
        assert_eq!(moved("/events", true), Some("/admin/overview"), "the whole server's events are the Overview's");
        assert_eq!(moved("/storage", false), Some("/account"));
        assert_eq!(moved("/storage", true), Some("/admin/storage"));
        assert_eq!(moved("/admin/storage", false), Some("/account"));
        assert_eq!(moved("/usage", false), Some("/account"));
        assert_eq!(moved("/usage/anna", false), Some("/account"));
        assert_eq!(moved("/usage", true), None);
        assert_eq!(moved("/usage/anna", true), None);
        assert_eq!(moved("/access", false), Some("/account"), "a person's keys are under Account");
        assert_eq!(moved("/access", true), None);
        assert_eq!(moved("/machine", false), Some("/repos"));
        assert_eq!(moved("/machine", true), Some("/admin/overview"), "the Machine page is the Overview's card now");
        // The Identity provider (company SSO) and the Audit log (2026-10-05)
        // are the ACCOUNT admin's: they stay for a reader who is no box admin,
        // who reaches them from Account.
        for stays in ["/account", "/account/storage", "/account/activity", "/repos", "/a/b", "/mirrors", "/admin/identity-provider", "/admin/audit"] {
            assert_eq!(moved(stays, false), None, "{stays}");
        }
    }

    #[test]
    fn a_viewer_is_shown_no_role_badge() {
        assert_eq!(role_badge(Some("viewer")), None);
        assert_eq!(role_badge(None), None);
        assert_eq!(role_badge(Some("operator")).as_deref(), Some("operator"));
        assert_eq!(role_badge(Some("admin")).as_deref(), Some("admin"));
    }

    /// ★ **Inside a repository: Readme · Code · History · Compare · Search ·
    /// Blame · Provenance · Pulls · Issues · Activity · Settings** — the collaboration tabs for a signed-in session (their
    /// APIs are behind one), Settings for whoever may change the repository.
    #[test]
    fn a_repository_has_its_tabs_and_settings_only_for_who_may_change_it() {
        let tabs = |signed_in, may_write| repo_tabs("alice", "uitest", signed_in, may_write).into_iter().map(|(l, _)| l).collect::<Vec<_>>();
        // GUN-GUEST (owner 2026-10-30): a visitor reads a public repository's issues too.
        assert_eq!(tabs(false, false), ["Readme", "Code", "History", "Compare", "Search", "Blame", "Issues"]);
        assert_eq!(tabs(true, false), ["Readme", "Code", "History", "Compare", "Search", "Blame", "Pulls", "Issues", "Activity"]);
        assert_eq!(tabs(true, true), ["Readme", "Code", "History", "Compare", "Search", "Blame", "Pulls", "Issues", "Activity", "Settings"]);
        let hrefs: Vec<String> = repo_tabs("alice", "uitest", true, true).into_iter().map(|(_, h)| h).collect();
        assert_eq!(
            hrefs,
            ["/alice/uitest", "/alice/uitest/tree", "/alice/uitest/commits", "/alice/uitest/diff", "/alice/uitest/search", "/alice/uitest/blame", "/alice/uitest/pulls", "/alice/uitest/issues", "/alice/uitest/activity", "/alice/uitest/settings"]
        );
        assert_eq!(repo_tabs("a b", "c", false, false)[0].1, "/a%20b/c", "names are path segments");
    }

    /// ★ **Your mirrors live with the repositories, not in a menu** (owner,
    /// 2026-10-02, fitting tenant-import into the final navigation): an
    /// "Imported only" filter on Repositories with a link to the reader's
    /// mirrors, the same link under a repository's "Mirrored from" panel, the
    /// "Import repository" button beside "New repository" — and the box-wide
    /// Mirrors page under Administration only.
    #[test]
    fn your_mirrors_are_found_from_the_repositories_not_from_a_menu() {
        for r in [VIEWER, OPERATOR, ADMIN] {
            let m = menu(r);
            assert!(!m.you.iter().chain(&m.main).any(|l| l.href == "/mirrors"), "{r:?}: a top-level mirrors entry");
        }
        assert!(menu(ADMIN).admin.iter().any(|l| l.label == "Mirrors"));
        let repos = page("repos.rs");
        // "Imported only" is gunnar's `mirrored_only`, asked of the server
        // (thin layer, 2026-10-04) — not a set the page builds itself.
        assert!(repos.contains("Imported only") && repos.contains("imported=true") && repos.contains("href=\"/mirrors\""), "no Imported filter / Your mirrors link on Repositories");
        let (new, import) = (repos.find("\"New repository\"").unwrap(), repos.find("\"Import repository\"").unwrap());
        assert!(new < import, "Import repository sits beside New repository");
        let mirrors = page("mirrors.rs");
        let panel = &mirrors[mirrors.find("pub fn MirrorPanel").unwrap()..];
        assert!(panel[..panel.find("\n}\n").unwrap()].contains("href=\"/mirrors\""), "the repository's Mirrored from panel does not lead to Your mirrors");
    }

    fn page(name: &str) -> String {
        std::fs::read_to_string(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/pages").join(name)).unwrap()
    }

    /// The panels of a page source, in the order they are drawn.
    fn panels(src: &str) -> Vec<String> {
        src.match_indices("<Panel title=\"").map(|(i, m)| src[i + m.len()..].split('"').next().unwrap().to_owned()).collect()
    }

    /// ★ **`/account/storage` is no purchase page** (owner 2026-10-17): it
    /// goes on to the ONE sign-up page's door, or to Account — no offers, no
    /// panel of its own; and the room it reads stays the session's own, and
    /// nothing of the machine's.
    #[test]
    fn account_storage_goes_on_and_draws_no_offers() {
        let src = page("my_storage.rs");
        assert!(src.contains("crate::purchase::storage_goes_to(s.buy_url)"), "the route does not go on to the door");
        for gone in ["MoreRoom", "SignUpDoors", "signup-grow", "signup-private", "Buy a private Gunnar instance", "subscription::"] {
            assert!(!src.contains(gone), "{gone} is still on Storage");
        }
        // The read-only view of what is used stays, on Account (owner 2026-10-17).
        let own = src.find("pub fn OwnSpace").expect("the person's own room");
        assert!(src[own..].contains("<MyRoom") && src[own..].contains("<PerRepository"), "My room and Per repository");
        assert!(src.contains("/api/account/room"), "a member's room reads the session's own namespace");
        for machine in ["/api/disks", "/api/dataset", "/api/storage\"", "/api/machine", "DataSetPanel", "TwinPanel"] {
            assert!(!src.contains(machine), "the person's room shows the machine's {machine}");
        }
    }

    /// ★ **Administration → Storage**: the machine, in the owner's order, and
    /// the Data set ONCE in the whole console.
    #[test]
    fn administration_storage_has_the_machine_in_order_and_the_data_set_once() {
        let src = page("admin_storage.rs");
        let order = ["Data set", "Engine", "Disks", "Twin", "/serialdat", "Performance"];
        let at: Vec<usize> = order.iter().map(|p| src.find(&format!("<Panel title=\"{p}\"")).unwrap_or_else(|| panic!("no {p} panel"))).collect();
        assert!(at.windows(2).all(|w| w[0] < w[1]), "{order:?} out of order");
        assert!(src.contains("is_admin()"), "Administration → Storage is an administrator's");
        // The engine: its NAME, linked to its source, its description the
        // tooltip — never the prose line gunnar used to send.
        assert!(src.contains("href=engine.repo_url") && src.contains("title=engine.description"), "the engine is not name + link + tooltip");
        assert!(!src.contains("object_backend"), "the engine's prose line is still drawn");
        let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/pages");
        let mut data_sets = Vec::new();
        for e in std::fs::read_dir(dir).unwrap().flatten() {
            let s = std::fs::read_to_string(e.path()).unwrap();
            for p in panels(&s) {
                if p == "Data set" {
                    data_sets.push(e.file_name().to_string_lossy().into_owned());
                }
            }
            assert!(!s.contains("<h3>\"Data set\"</h3>"), "a second Data set in {:?}", e.file_name());
        }
        assert_eq!(data_sets, ["admin_storage.rs"], "the Data set is shown once");
    }

    /// ★ UW-19: **a grant is revoked from its row** — the Access page's grants carry an
    /// admin's Revoke, posted to `/api/grants/revoke` (the route had no caller).
    #[test]
    fn a_grant_is_revoked_from_its_row_on_the_access_page() {
        let access = page("access.rs");
        assert!(access.contains("\"/api/grants/revoke\""), "no Revoke on the grants list");
    }

    /// ★ **No Machine page** (owner, 2026-10-04): the Overview's Machine card
    /// carries the estate's picture and gunnar's few lines; every disk detail
    /// is on Administration → Storage, ONCE; and Instruments has no "Pages"
    /// panel repeating the Administration menu.
    #[test]
    fn the_machine_is_a_card_on_the_overview_and_its_disks_are_on_storage_once() {
        let app = std::fs::read_to_string(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/app.rs")).unwrap();
        assert!(!app.contains("pages::machine::Machine"), "the Machine page is still routed");
        let overview = page("overview.rs");
        assert!(overview.contains("<Panel title=\"Machine\""), "no Machine card on the Overview");
        assert!(overview.contains("\"/api/machine/topology\"") && overview.contains("inner_html"), "the card does not draw the estate from Admin.Topology, inline");
        assert!(overview.contains("glance_grade"), "the card does not draw gunnar's glance lines by gunnar's verdict");
        let storage = page("admin_storage.rs");
        for panel in ["machine::Layout", "machine::BoxAllocation"] {
            assert!(storage.contains(panel), "Storage lacks {panel}");
        }
        let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/pages");
        for e in std::fs::read_dir(dir).unwrap().flatten() {
            let name = e.file_name().to_string_lossy().into_owned();
            let s = std::fs::read_to_string(e.path()).unwrap();
            if name != "admin_storage.rs" {
                assert!(!s.contains("machine::Layout") && !s.contains("machine::BoxAllocation"), "{name} draws a disk detail Storage already draws");
            }
        }
        let admin = page("admin.rs");
        assert!(!admin.contains("<Panel title=\"Pages\"") && !admin.contains("admin-links"), "Instruments still repeats the menu");
    }

    /// ★ **No "N/A — not reported / not measured" rows**: a row is drawn only
    /// when gunnar reports a value (write amplification, priority transfer,
    /// the read cache's high/low water, its hit/miss counts, a cold tier an
    /// archive engine does not have).
    #[test]
    fn no_page_draws_a_row_gunnar_did_not_report() {
        let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/pages");
        for e in std::fs::read_dir(dir).unwrap().flatten() {
            let s = std::fs::read_to_string(e.path()).unwrap();
            let name = e.file_name().to_string_lossy().into_owned();
            for na in ["\"N/A\"", "na_line(", "hysteresis_line(", "Write amplification", "Priority transfer", "Hit and \\\n", "miss counts", "not reported by gunnar"] {
                assert!(!s.contains(na), "{name}: {na}");
            }
        }
    }
}

/// **A principal the box runs on, not a person** — the console's own key, the
/// guest, the twin and the front's certify key, the installer's seed, a
/// builder (`dwarves-oden`), a deploy key, a group. gunnar already knows: every
/// fed principal is `machine` (gunnar-server `fed_principals.rs`), a group is
/// `group`, a deploy key `deploy`. Only `user` is a person. Decided from that
/// KIND, never from a list of names.
pub fn is_system_kind(kind: &str) -> bool {
    kind != "user"
}

/// **Is this namespace one of the box's own?** gunnar's `owner_kind` on the
/// namespace (thin layer, owner 2026-10-04): the kind of the principal that
/// owns it. A namespace no principal owns (`unspecified`) is nobody's machine
/// and is shown. Until then the page joined the namespaces against the
/// administrator-only identity table, which a person may not read.
pub fn is_system_namespace(owner_kind: &str) -> bool {
    !matches!(owner_kind, "user" | "unspecified" | "") && is_system_kind(owner_kind)
}

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

    /// ★ **Internal principals and their namespaces are not in the lists**
    /// (owner, 2026-10-02: gunnar-guest, gunnar-twin, installer-seed,
    /// dwarves-oden…) — by their KIND, so a person named "gunnar-something"
    /// is still shown and a new machine principal is hidden without a code
    /// change. An administrator's toggle shows them.
    #[test]
    fn system_principals_are_known_by_kind_not_by_name() {
        for kind in ["machine", "deploy", "group"] {
            assert!(is_system_kind(kind), "{kind}");
        }
        assert!(!is_system_kind("user"));
        // A namespace is hidden by its OWNER's kind, as gunnar sends it.
        for kind in ["machine", "group", "deploy"] {
            assert!(is_system_namespace(kind), "{kind}");
        }
        for kind in ["user", "unspecified", ""] {
            assert!(!is_system_namespace(kind), "a person's or an ownerless namespace was hidden: {kind:?}");
        }
        let src = std::fs::read_to_string(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/nav.rs")).unwrap();
        let body = &src[src.find("pub fn is_system_kind").unwrap()..src.find("#[cfg(test)]\nmod system_tests").unwrap()];
        for name in ["\"gunnar-guest\"", "\"gunnar-twin\"", "\"installer-seed\"", "\"dwarves-oden\""] {
            assert!(!body.contains(name), "a hard-coded name list: {name}");
        }
        for (page, what) in [("access.rs", "Show system principals"), ("usage.rs", "Show system principals")] {
            let src = std::fs::read_to_string(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/pages").join(page)).unwrap();
            assert!(src.contains(what) && src.contains("crate::nav::"), "{page}: no system-principal filter with its toggle");
        }
    }
}

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

    /// ★ A guest's tabs: no tab on Pulls or Settings is marked; Readme only at home.
    #[test]
    fn a_page_without_its_own_tab_marks_none() {
        let guest = repo_tabs("alice", "field", false, false);
        assert_eq!(current_tab(&guest, "/alice/field"), Some("Readme"));
        assert_eq!(current_tab(&guest, "/alice/field/tree/docs"), Some("Code"));
        assert_eq!(current_tab(&guest, "/alice/field/issues/1"), Some("Issues"));
        assert_eq!(current_tab(&guest, "/alice/field/pulls/1"), None, "a guest has no Pulls tab");
        assert_eq!(current_tab(&guest, "/alice/field/settings"), None, "nor a Settings tab");
        let owner = repo_tabs("alice", "field", true, true);
        assert_eq!(current_tab(&owner, "/alice/field/pulls/1"), Some("Pulls"));
        assert_eq!(current_tab(&owner, "/alice/field/settings"), Some("Settings"));
    }

    /// ★ The tabs use it, and a guest's Settings draws only the sign-in line.
    #[test]
    fn the_tabs_and_a_guests_settings_page_follow_it() {
        let repo = crate::code::of(include_str!("pages/repo.rs"));
        assert!(repo.contains("crate::nav::current_tab("), "the tabs");
        assert!(repo.contains("crate::nav::SETTINGS_SIGN_IN"), "a guest's Settings");
    }
}