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:?}");
}
}