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/headers.rs

//! **What a page header says**, on the host side of the wasm boundary.
//!
//! Owner, 2026-10-02: every page header is its TITLE and nothing explaining
//! it. "Storage — What the store holds on disk, repository by repository",
//! "Overview — The server, its contents, and what is happening right now" and
//! the other eighteen were sentences a reader skips on the second visit and
//! that crowd the one line they came for. So `ui::PageHeader` has no subtitle
//! slot any more; a second line under a title is DATA or nothing (its `line`
//! slot), and exactly two pages have one, Administration → Overview and a
//! person's Repositories: [`server_key_line`] — the
//! forge's SSH host key fingerprint, read live from gunnar, so a user can check
//! it against what their ssh client shows on first connect (the same value the
//! installer pins in `forge-host-keys.toml`).

/// **"Server key SHA256:…"** — Overview's second line. `None` unless what
/// arrived is an OpenSSH SHA-256 fingerprint: the line is a fact or it is not
/// drawn, never a placeholder that reads like a key.
pub fn server_key_line(fingerprint: &str) -> Option<String> {
    let fp = fingerprint.trim();
    let digest = fp.strip_prefix("SHA256:")?;
    (!digest.is_empty() && digest.chars().all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/'))
        .then(|| format!("Server key {fp}"))
}

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

    /// The code of every page this console compiles, by file name — its
    /// comments taken out ([`crate::code`]).
    fn pages() -> Vec<(String, String)> {
        let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/pages");
        let mut out: Vec<(String, String)> = std::fs::read_dir(&dir)
            .expect("src/pages")
            .filter_map(|e| e.ok())
            .map(|e| e.path())
            .filter(|p| p.extension().is_some_and(|x| x == "rs"))
            .map(|p| (p.file_name().unwrap().to_string_lossy().into_owned(), crate::code::of(&std::fs::read_to_string(&p).unwrap())))
            .collect();
        out.sort();
        out
    }

    /// ★ **No page header carries an explanatory subtitle**, and the shared
    /// header has no slot for one, so none can grow back.
    #[test]
    fn no_page_header_carries_an_explanatory_subtitle() {
        let ui = crate::code::of(include_str!("ui.rs"));
        let at = ui.find("pub fn PageHeader").expect("PageHeader");
        let header = &ui[at..at + ui[at..].find("\n}\n").unwrap()];
        assert!(!header.contains("subtitle"), "PageHeader still has a subtitle slot");
        let mut with = Vec::new();
        for (name, page) in pages() {
            if page.contains("subtitle=") {
                with.push(name);
            }
        }
        assert!(with.is_empty(), "page headers with a subtitle: {with:?}");
        // ` line=` INSIDE a `<PageHeader …>` element (up to the next element),
        // not `<Hint line=…>` elsewhere on the page.
        let header_has_line = |p: &str| {
            p.match_indices("<PageHeader").any(|(i, m)| {
                let rest = &p[i + m.len()..];
                rest[..rest.find('<').unwrap_or(rest.len())].contains(" line=")
            })
        };
        let lines: Vec<String> = pages().into_iter().filter(|(_, p)| header_has_line(p)).map(|(n, _)| n).collect();
        assert_eq!(lines, ["overview.rs", "repos.rs"], "only the Overview's and Repositories' headers have a second line, and it is data");
    }

    /// ★ **Overview's second line is the server's SSH host key fingerprint**,
    /// as gunnar reports it, and the line is not drawn without one.
    #[test]
    fn overviews_second_line_is_the_server_key_fingerprint() {
        let fp = "SHA256:e4I/CnsYwmK8tq0yJ3o5c3VtZS1rZXktZm9yLXRlc3Q";
        assert_eq!(server_key_line(fp).as_deref(), Some("Server key SHA256:e4I/CnsYwmK8tq0yJ3o5c3VtZS1rZXktZm9yLXRlc3Q"));
        assert_eq!(server_key_line("  ").as_deref(), None);
        assert_eq!(server_key_line("MD5:aa:bb").as_deref(), None, "only the fingerprint ssh shows by default");
        // On Administration → Overview, and at the top of a person's
        // Repositories page (owner, 2026-10-02: users need it to verify ssh,
        // and the Overview is an administrator's now).
        for (name, page) in [("overview", crate::code::of(include_str!("pages/overview.rs"))), ("repos", crate::code::of(include_str!("pages/repos.rs")))] {
            assert!(page.contains("crate::headers::server_key_line("), "{name} does not draw the server key line");
            assert!(page.contains("/api/server-key"), "{name} does not read the key from the server");
            assert!(!page.contains("SHA256:"), "a fingerprint is hard-coded in {name}");
        }
    }

    /// ★ **The console's own identity is not on the Overview** (owner,
    /// 2026-10-02). It is an administrator's fact and lives under
    /// Administration (Instruments).
    #[test]
    fn the_consoles_own_identity_is_on_administration_not_on_overview() {
        let overview = crate::code::of(include_str!("pages/overview.rs"));
        for gone in ["IdentityPanel", "The console's own identity", "console_fingerprint", "/api/bootstrap"] {
            assert!(!overview.contains(gone), "Overview still shows the console's identity: {gone:?}");
        }
        let admin = crate::code::of(include_str!("pages/admin.rs"));
        assert!(admin.contains("ConsoleIdentity"), "the console's identity is not on Administration");
        assert!(admin.contains("console_fingerprint"), "Administration does not show the console's key");
    }
}

#[cfg(test)]
mod banner_tests {
    /// ★ **The Administration strip is ONE line** (owner 2026-10-17: it read
    /// as three broken lines, "These act on the whole appliance. Yours alone
    /// is on / Account / ."): the sentence and its link are one inline run —
    /// one element, the link inside it, the period right after it — so the
    /// strip's flex layout cannot break it apart.
    #[test]
    fn the_admin_strip_is_one_sentence_with_the_link_inline() {
        let src = crate::code::of(include_str!("pages/admin.rs"));
        let at = src.find("<div class=\"admin-banner\"").expect("the strip");
        let strip = &src[at..at + src[at..].find("<details").expect("its why")];
        let line = r#"<span class="admin-banner-line">"Settings for the whole site. Your own are under " <a href="/account">"Account"</a> "."</span>"#;
        assert!(strip.contains(line), "the strip is not one line with the link inline:\n{strip}");
        assert!(!strip.contains("appliance") && !strip.contains("Yours alone"), "the old words:\n{strip}");
        assert_eq!(strip.matches("<span").count(), 2, "the tag and the one line, nothing loose:\n{strip}");
    }
}

#[cfg(test)]
mod plan_words_tests {
    /// Every string literal of the console's sources, comments stripped.
    fn literals() -> Vec<(String, String)> {
        fn walk(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
            for e in std::fs::read_dir(dir).unwrap().flatten() {
                let p = e.path();
                if p.is_dir() {
                    walk(&p, out);
                } else if p.extension().is_some_and(|x| x == "rs") {
                    out.push(p);
                }
            }
        }
        let mut files = Vec::new();
        walk(&std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"), &mut files);
        let mut out = Vec::new();
        for f in files {
            let src = crate::code::of(&std::fs::read_to_string(&f).unwrap());
            // Test modules hold the words they forbid; only what is drawn counts.
            let src = src.split("#[cfg(test)]").next().unwrap_or_default().to_owned();
            for (i, lit) in src.split('"').enumerate() {
                if i % 2 == 1 {
                    out.push((f.file_name().unwrap().to_string_lossy().into_owned(), lit.to_owned()));
                }
            }
        }
        out
    }

    /// ★ **"Plan", never "Subscription", never "None yet"** (owner
    /// 2026-10-17: "it must be stringent" — a term is paid once, it is no
    /// subscription). No text the console draws says either; the API's
    /// paths may keep their names.
    #[test]
    fn the_console_says_plan_and_never_subscription_or_none_yet() {
        let bad: Vec<_> = literals()
            .into_iter()
            .filter(|(_, l)| {
                let low = l.to_lowercase();
                (low.contains("subscription") && !l.starts_with("/api/")) || l.contains("None yet")
            })
            .collect();
        assert!(bad.is_empty(), "the console still says it: {bad:?}");
    }
}