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-server/src/anonymous/public_site.rs

//! **gunnar.rs's public face, end to end through the console** (TASKS-12, the
//! owner's SLIM RULE): the default build serves none of the site's routes, and
//! a `public-site` build serves them to a visitor, built from the grantless
//! guest key's answers, naming no private repository, and switched off at run
//! time by an administrator.

use super::{anon, console, FakeGunnar, NS, PUBLIC};

/// The paths the public site serves (TASKS-12), for the default build's half.
fn site_paths() -> Vec<String> {
    vec![
        "/llms.txt".into(),
        "/llms-full.txt".into(),
        "/robots.txt".into(),
        "/sitemap.xml".into(),
        format!("/{NS}/{PUBLIC}/readme.md"),
        format!("/{NS}/{PUBLIC}/tree.txt"),
        // GUN-GUEST: `/{ns}/{name}[/…]` is the console's address, no site page
        // (`anonymous::console_address`).
        format!("/{NS}/{PUBLIC}/issues.md"),
    ]
}

/// One issue in `repo`, open, with one comment in its thread (PUB-ISSUES).
#[cfg(feature = "public-site")]
fn seed_issue(fake: &FakeGunnar, repo: &str, title: &str, comment: &str) {
    let mut issues = fake.issues.lock().unwrap();
    let number = issues.iter().filter(|i| i.repo == repo).count() as u64 + 1;
    issues.push(crate::pb::IssueState {
        repo: repo.to_owned(),
        number,
        title: title.to_owned(),
        body: format!("{title}, in words."),
        author: "the-filers-principal-id".to_owned(),
        open: true,
        comments: 1,
        opened_at_unix_ms: 1_761_264_000_000,
    });
    let comment = crate::pb::ForgeEvent {
        repo: repo.to_owned(),
        pull: number,
        actor: "the-commenters-principal-id".to_owned(),
        at_unix_ms: 1_761_300_000_000,
        kind: Some(crate::pb::forge_event::Kind::IssueCommented(crate::pb::IssueCommented { body: comment.to_owned() })),
    };
    fake.issue_threads.lock().unwrap().insert((repo.to_owned(), number), vec![comment]);
}

#[cfg(feature = "public-site")]
const PUBLIC_ISSUE: &str = "The boot is dark";
#[cfg(feature = "public-site")]
const PUBLIC_COMMENT: &str = "It happens on Mondays only.";
#[cfg(feature = "public-site")]
const PRIVATE_ISSUE: &str = "payroll-issue-nobody-may-read";

/// ★ **The default build contains none of it**: every site path answers as a
/// path the console has never heard of does, and nothing on it says Gunnar.
#[cfg(not(feature = "public-site"))]
#[tokio::test]
async fn the_default_build_serves_no_public_site_route() {
    assert_eq!(crate::PUBLIC_SITE, cfg!(feature = "public-site"));
    let (app, _passkeys, _feed) = console(FakeGunnar::new(), true).await;
    for path in site_paths() {
        let (status, body) = anon(&app, &path).await;
        assert_ne!(status, http::StatusCode::OK, "{path} was served by the default build: {body}");
        assert!(!body.contains("European git server"), "{path}: {body}");
    }
    let (status, body) = super::send(&app, http::Method::GET, "/api/public-site", Some("break-glass")).await;
    assert!(!body.contains("\"on\""), "the switch exists in the default build: {status} {body}");
}

/// ★ **GUI-PUBSW: the console tells an administrator which switches this build
/// carries** (`switches` in `/auth/status`), so `/admin` draws the Public site
/// switch on every `public-site` build, whatever its bundle and whether or not
/// the switch's own read answers; nobody else is told, and the default build
/// names none.
#[tokio::test]
async fn the_status_names_the_switches_this_build_carries_to_an_administrator() {
    let (app, passkeys, _feed) = console(FakeGunnar::new(), true).await;
    let switches = |body: &str| -> Vec<String> {
        let v: serde_json::Value = serde_json::from_str(body).expect("the status is JSON");
        v.get("switches").and_then(|s| s.as_array()).map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_owned)).collect()).unwrap_or_default()
    };
    let (_, admin) = super::signed_in(&app, &passkeys, "root", crate::role::Role::Admin, "/auth/status").await;
    let mut want: Vec<String> = Vec::new();
    if cfg!(feature = "public-site") {
        want.push("public-site".into());
    }
    if cfg!(feature = "mcp") {
        want.push("mcp".into());
    }
    assert_eq!(switches(&admin), want, "an administrator is told this build's switches: {admin}");
    let (_, operator) = super::signed_in(&app, &passkeys, "otto", crate::role::Role::Operator, "/auth/status").await;
    assert!(switches(&operator).is_empty(), "only an administrator flips a switch, so only one is told: {operator}");
    let (_, visitor) = anon(&app, "/auth/status").await;
    assert!(switches(&visitor).is_empty(), "a visitor is told no switch: {visitor}");
}

#[cfg(feature = "public-site")]
mod served {
    use super::*;
    use crate::anonymous::{signed_in, signer, GUEST_SEED, PRIVATE};

    /// The site's pages are built by a refresh on the timer, started with the
    /// router; wait for the first one to land. Slowly: every poll is a request
    /// from the same client, and the site's rate limit counts it.
    async fn built(app: &axum::Router, passkeys: &std::sync::Arc<crate::auth::Webauthn>) -> String {
        switch_on(app, passkeys).await;
        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
        for _ in 0..40 {
            let (status, body) = anon(app, "/llms.txt").await;
            if status == http::StatusCode::OK && body.contains(&format!("{NS}/{PUBLIC}")) {
                return body;
            }
            tokio::time::sleep(std::time::Duration::from_millis(250)).await;
        }
        panic!("the public site never built its pages");
    }

    /// The site starts OFF (the owner, 2026-10-25); an administrator turns it on.
    async fn switch_on(app: &axum::Router, passkeys: &std::sync::Arc<crate::auth::Webauthn>) {
        use tower::ServiceExt;
        let admin = passkeys.make_session("root", crate::role::Role::Admin, true);
        let put = http::Request::put("/api/public-site")
            .header(http::header::COOKIE, format!("{}={admin}", crate::auth::SESSION_COOKIE))
            .header(http::header::CONTENT_TYPE, "application/json")
            .header(http::header::ORIGIN, passkeys.origin())
            .body(axum::body::Body::from("{\"on\":true}"))
            .unwrap();
        assert_eq!(app.clone().oneshot(put).await.unwrap().status(), http::StatusCode::OK);
    }

    /// ★ **Off unless told `on`** (owner 2026-10-25: a sold private estate is the
    /// airgap default and is never indexed): with `GUNNAR_UI_PUBLIC_SITE` unset, a
    /// `public-site` build answers as the default build, robots.txt disallows
    /// everything, and the guest key is never used.
    #[tokio::test]
    async fn unset_the_site_is_off_and_robots_txt_disallows_everything() {
        assert!(std::env::var(gunnar_public_site::ENV_SWITCH).is_err(), "the test environment sets the switch");
        let fake = FakeGunnar::new();
        let (app, _passkeys, _feed) = console(fake.clone(), true).await;
        let (status, robots) = anon(&app, "/robots.txt").await;
        assert_eq!((status, robots.as_str()), (http::StatusCode::OK, "User-agent: *\nDisallow: /\n"));
        for path in site_paths().into_iter().filter(|p| p != "/robots.txt") {
            let (status, body) = anon(&app, &path).await;
            assert_ne!(status, http::StatusCode::OK, "{path} is served with the switch unset: {body}");
        }
        let (status, _) = anon(&app, "/").await;
        assert_ne!(status, http::StatusCode::OK, "the public home is served with the switch unset");
        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
        assert!(fake.fingerprints().is_empty(), "the switched-off site asked gunnar: {:?}", fake.fingerprints());
    }

    #[tokio::test]
    async fn a_visitor_reads_llms_txt_and_the_public_readme_through_the_guest_key() {
        let fake = FakeGunnar::new();
        let (app, passkeys, _feed) = console(fake.clone(), true).await;
        let llms = built(&app, &passkeys).await;
        assert!(llms.starts_with("# Gunnar"), "{llms}");
        assert!(!llms.contains(PRIVATE), "llms.txt names a private repository: {llms}");
        let (status, readme) = anon(&app, &format!("/{NS}/{PUBLIC}/readme.md")).await;
        assert_eq!(status, http::StatusCode::OK, "{readme}");
        assert!(readme.contains(crate::anonymous::README_HEADING), "{readme}");
        let (status, full) = anon(&app, "/llms-full.txt").await;
        assert_eq!(status, http::StatusCode::OK);
        assert!(!full.contains(PRIVATE), "{full}");
        let guest = signer(GUEST_SEED).fingerprint().to_owned();
        let seen = fake.fingerprints();
        assert!(!seen.is_empty() && seen.iter().all(|f| *f == guest), "every call the site made must be the GUEST's: {seen:?}");
    }

    /// ★ PUB-HOME (owner 2026-10-29): with the switch ON, a visitor's `/` is the
    /// console's own landing page, exactly as before the site was on (the
    /// picture, the look), with only the site's search tags and JSON-LD put in
    /// its `<head>`; a signed-in person still gets the console there.
    #[tokio::test]
    async fn a_visitor_gets_the_consoles_landing_with_the_search_tags_and_a_signed_in_person_the_console() {
        let (app, passkeys, _feed) = console(FakeGunnar::new(), true).await;
        built(&app, &passkeys).await;
        let (status, home) = anon(&app, "/").await;
        assert_eq!(status, http::StatusCode::OK, "{home}");
        let landing = crate::auth::routes::login_html();
        let at = landing.find("</head>").expect("the landing has a head");
        assert!(home.ends_with(&landing[at..]), "everything a visitor sees is the landing's, byte for byte");
        assert!(home.contains("url(\"/gunnar_silver.webp\")"), "the picture");
        let head = &home[..home.find("</head>").expect("a head")];
        for tag in [
            "<title>Gunnar — a European git server in Rust</title>",
            "<meta name=\"description\" content=\"Gunnar is a European git server in Rust: self-hosted or SaaS, with SLSA Level 3 releases.\">",
            "<link rel=\"canonical\" href=\"",
            "<meta property=\"og:title\"",
            "<meta name=\"twitter:card\"",
            "<script type=\"application/ld+json\">",
        ] {
            assert_eq!(head.matches(tag).count(), 1, "{tag} once, in the head");
        }
        assert!(!home.contains("<h2>What it does</h2>") && !home.contains("<ul class=\"cards"), "the old text home is drawn");
        assert!(!home.contains(PRIVATE), "{home}");
        let (_, console_root) = signed_in(&app, &passkeys, "root", crate::role::Role::Admin, "/").await;
        assert!(!console_root.contains("application/ld+json"), "a signed-in person was handed the public home: {console_root}");
    }

    /// S-2 through the console: robots.txt is served to a visitor, before the
    /// login redirect, and names no repository.
    #[tokio::test]
    async fn a_crawler_reads_robots_txt_without_a_login() {
        let (app, passkeys, _feed) = console(FakeGunnar::new(), true).await;
        built(&app, &passkeys).await;
        let (status, robots) = anon(&app, "/robots.txt").await;
        assert_eq!(status, http::StatusCode::OK, "{robots}");
        for line in ["User-agent: GPTBot", "User-agent: ClaudeBot", "Disallow: /login", "Disallow: /auth/", "Crawl-delay: 10", "Sitemap: "] {
            assert!(robots.contains(line), "{line}: {robots}");
        }
        assert!(!robots.contains(PRIVATE) && !robots.contains(&format!("{NS}/")), "{robots}");
    }

    /// S-3 through the console: the sitemap is served to a visitor and names
    /// the public repository, never the private one.
    #[tokio::test]
    async fn a_crawler_reads_the_sitemap_without_a_login() {
        let (app, passkeys, _feed) = console(FakeGunnar::new(), true).await;
        built(&app, &passkeys).await;
        let (status, map) = anon(&app, "/sitemap.xml").await;
        assert_eq!(status, http::StatusCode::OK, "{map}");
        assert!(map.contains("<urlset") && map.contains(&format!("/{NS}/{PUBLIC}</loc>")), "{map}");
        assert!(!map.contains(PRIVATE), "{map}");
    }

    /// ★ **PUB-ISSUES through the console**: a visitor reads, searches and
    /// fetches the archive of a PUBLIC repository's issues, built from the
    /// grantless guest's `Forge.ListIssues`/`GetIssue`; a private repository's
    /// issues answer exactly as a repository that does not exist, are named on
    /// no page, and no page names who filed or answered.
    #[tokio::test]
    async fn a_visitor_reads_and_searches_the_public_issues_through_the_guest_key() {
        let fake = FakeGunnar::new();
        seed_issue(&fake, &format!("{NS}/{PUBLIC}"), PUBLIC_ISSUE, PUBLIC_COMMENT);
        seed_issue(&fake, &format!("{NS}/{PRIVATE}"), PRIVATE_ISSUE, "a private comment");
        let (app, passkeys, _feed) = console(fake.clone(), true).await;
        built(&app, &passkeys).await;
        // GUN-GUEST: the list, an issue and the search are the console's pages
        // (`anonymous::console_address`); the archive is the site's file.
        let mut archive = (http::StatusCode::NOT_FOUND, String::new());
        for _ in 0..40 {
            archive = anon(&app, &format!("/{NS}/{PUBLIC}/issues.md")).await;
            if archive.1.contains(PUBLIC_COMMENT) {
                break;
            }
            tokio::time::sleep(std::time::Duration::from_millis(250)).await;
        }
        let (status, archive) = archive;
        assert_eq!(status, http::StatusCode::OK);
        assert!(archive.contains(PUBLIC_ISSUE) && archive.contains(PUBLIC_COMMENT), "{archive}");
        // The private repository: as one that does not exist.
        for leaf in ["issues.md"] {
            let private = anon(&app, &format!("/{NS}/{PRIVATE}/{leaf}")).await;
            let nobody = anon(&app, &format!("/{NS}/no-such-repository/{leaf}")).await;
            assert_eq!(private, nobody, "{leaf}: a private repository's issues must answer as none");
            assert_ne!(private.0, http::StatusCode::OK);
        }
        let pages = ["/llms.txt".to_owned(), "/llms-full.txt".to_owned(), "/sitemap.xml".to_owned(), "/".to_owned(), format!("/{NS}/{PUBLIC}/issues"), format!("/{NS}/{PUBLIC}/issues/1"), format!("/{NS}/{PUBLIC}/issues?q=x"), format!("/{NS}/{PUBLIC}/issues.md")];
        for page in pages {
            let (_, body) = anon(&app, &page).await;
            assert!(!body.contains(PRIVATE_ISSUE) && !body.contains("a private comment"), "{page} names the private issue: {body}");
            assert!(!body.contains("principal-id"), "{page} names who filed or answered: {body}");
        }
        let guest = signer(GUEST_SEED).fingerprint().to_owned();
        let seen = fake.fingerprints();
        assert!(seen.iter().all(|f| *f == guest), "every call the site made must be the GUEST's: {seen:?}");
        // GUN-GUEST: a guest reads the public issues through the console's own read, as the guest.
        assert_eq!(anon(&app, &format!("/api/repos/{NS}/{PUBLIC}/issues")).await.0, http::StatusCode::OK);
        // Filing one is the console's too.
        let (status, _) = crate::anonymous::send(&app, http::Method::POST, &format!("/{NS}/{PUBLIC}/issues"), None).await;
        assert_ne!(status, http::StatusCode::OK, "a visitor's POST was taken");
    }

    /// The gunnar README's own first line (gunnar.rs, 2026-10-29), its picture
    /// beside it in `.nornir/assets/`.
    const GUNNAR_README: &str = "<p align=\"center\"><img src=\".nornir/assets/gunnar.webp\" alt=\"gunnar\" width=\"320\"></p>\n\n# The Public Repository\n";
    /// A WEBP's first bytes: RIFF, a length, WEBP, a VP8 chunk.
    const WEBP: &[u8] = b"RIFF\x1a\x00\x00\x00WEBPVP8 \x0e\x00\x00\x00\x30\x01\x00\x9d\x01\x2a\x01\x00\x01\x00\x02\x00";

    /// One tree as gunnar hands it back: `{mode} {name}\0` and 32 raw bytes.
    fn tree(entries: &[(&str, &str, u8)]) -> Vec<u8> {
        let mut out = Vec::new();
        for (mode, name, tag) in entries {
            out.extend_from_slice(format!("{mode} {name}\0").as_bytes());
            let mut raw = [0u8; 32];
            raw[0] = *tag;
            raw[31] = *tag;
            out.extend_from_slice(&raw);
        }
        out
    }

    fn oid(tag: u8) -> String {
        format!("{tag:02x}{}{tag:02x}", "0".repeat(60))
    }

    /// Give `repo` gunnar's README shape: `README.md` with the picture's
    /// `<img>`, and `.nornir/assets/gunnar.webp`.
    fn with_the_gunnar_readme(fake: &FakeGunnar, repo: &str) {
        let mut repos = fake.repos.lock().unwrap();
        let held = repos.get_mut(&format!("{NS}/{repo}")).expect("a repository this fake holds");
        held.objects.insert("tree-oid".to_owned(), tree(&[("40000", ".nornir", 0x41), ("100644", "README.md", 0x42)]));
        held.objects.insert(oid(0x41), tree(&[("40000", "assets", 0x43)]));
        held.objects.insert(oid(0x43), tree(&[("100644", "gunnar.webp", 0x44)]));
        held.objects.insert(oid(0x42), GUNNAR_README.as_bytes().to_vec());
        held.objects.insert(oid(0x44), WEBP.to_vec());
    }

    /// GET with the response's headers and bytes.
    async fn fetch(app: &axum::Router, uri: &str) -> (http::StatusCode, http::HeaderMap, Vec<u8>) {
        use tower::ServiceExt;
        let r = app.clone().oneshot(http::Request::get(uri).body(axum::body::Body::empty()).unwrap()).await.unwrap();
        let (status, headers) = (r.status(), r.headers().clone());
        (status, headers, axum::body::to_bytes(r.into_body(), 1 << 20).await.unwrap().to_vec())
    }

    /// ★ **PUB-IMG (2026-10-29): the README's picture is an anonymous 200
    /// image.** gunnar.rs/nordisk/gunnar drew `<img src=".../blob/.nornir/assets/
    /// gunnar.webp">`, and `/blob/` is an HTML file page for a text file only, so
    /// the URL fell through to the console's 401. The picture now points at the
    /// console's own anonymous raw route, read as the guest: a public
    /// repository's picture is image/webp under nosniff and a sandbox CSP; a
    /// private one is refused; and the switch changes nothing about that route.
    #[tokio::test]
    async fn the_gunnar_readmes_picture_is_an_anonymous_200_image() {
        let fake = FakeGunnar::new();
        with_the_gunnar_readme(&fake, PUBLIC);
        with_the_gunnar_readme(&fake, PRIVATE);
        let (app, passkeys, _feed) = console(fake.clone(), true).await;
        // Off first: the console's raw route answers as it always has.
        let raw = format!("/api/repos/{NS}/{PUBLIC}/raw?path=.nornir/assets/gunnar.webp");
        let (off_status, _, off_bytes) = fetch(&app, &raw).await;
        built(&app, &passkeys).await;
        let (status, page) = anon(&app, &format!("/{NS}/{PUBLIC}")).await;
        assert_eq!(status, http::StatusCode::OK, "{page}");
        let src = page
            .split("<img ")
            .filter_map(|tag| tag.split("src=\"").nth(1).and_then(|v| v.split('"').next()))
            .find(|v| v.contains("gunnar.webp"))
            .unwrap_or_else(|| panic!("the README's picture is on the page: {page}"));
        assert!(!src.contains("/blob/"), "a picture must not point at a file page: {src}");
        let path = &src[src.find("/api/repos/").expect("the console's raw route")..];
        assert_eq!(path, raw, "{src}");
        let (status, headers, bytes) = fetch(&app, path).await;
        assert_eq!(status, http::StatusCode::OK, "{}", String::from_utf8_lossy(&bytes));
        assert_eq!(headers[http::header::CONTENT_TYPE], "image/webp");
        assert_eq!(headers[http::header::X_CONTENT_TYPE_OPTIONS], "nosniff");
        assert!(headers[http::header::CONTENT_SECURITY_POLICY].to_str().unwrap().contains("sandbox"), "{headers:?}");
        assert_eq!(bytes, WEBP);
        assert_eq!((off_status, off_bytes), (status, bytes), "the switch changes nothing on the raw route");
        // A private repository's picture: refused to a visitor, on or off.
        let (status, _, body) = fetch(&app, &format!("/api/repos/{NS}/{PRIVATE}/raw?path=.nornir/assets/gunnar.webp")).await;
        assert_ne!(status, http::StatusCode::OK, "a private picture was served: {}", String::from_utf8_lossy(&body));
        assert_ne!(body, WEBP);
    }

    #[tokio::test]
    async fn a_private_repository_answers_as_it_does_today() {
        let (app, passkeys, _feed) = console(FakeGunnar::new(), true).await;
        built(&app, &passkeys).await;
        let private = anon(&app, &format!("/{NS}/{PRIVATE}")).await;
        let nobody = anon(&app, &format!("/{NS}/no-such-repository")).await;
        assert_eq!(private, nobody, "a private repository's page must answer as one that does not exist");
        assert_ne!(private.0, http::StatusCode::OK);
        for leaf in ["readme.md", "tree.txt", "tree", "commits"] {
            let private = anon(&app, &format!("/{NS}/{PRIVATE}/{leaf}")).await;
            let nobody = anon(&app, &format!("/{NS}/no-such-repository/{leaf}")).await;
            assert_eq!(private, nobody, "{leaf}: a private repository must answer as one that does not exist");
            assert_ne!(private.0, http::StatusCode::OK);
        }
    }

    #[tokio::test]
    async fn a_second_request_does_not_reach_gunnar() {
        let fake = FakeGunnar::new();
        let (app, passkeys, _feed) = console(fake.clone(), true).await;
        built(&app, &passkeys).await;
        let before = fake.fingerprints().len();
        for _ in 0..3 {
            assert_eq!(anon(&app, &format!("/{NS}/{PUBLIC}/tree.txt")).await.0, http::StatusCode::OK);
            assert_eq!(anon(&app, "/llms.txt").await.0, http::StatusCode::OK);
        }
        assert_eq!(fake.fingerprints().len(), before, "a visitor's request reached gunnar");
    }

    #[tokio::test]
    async fn without_a_guest_key_no_repository_is_published() {
        let (app, passkeys, _feed) = console(FakeGunnar::new(), false).await;
        switch_on(&app, &passkeys).await;
        let (status, body) = anon(&app, "/llms.txt").await;
        assert_eq!(status, http::StatusCode::OK);
        assert!(!body.contains(&format!("[{NS}/{PUBLIC}]")), "{body}");
        assert!(!body.contains(PRIVATE));
    }

    /// ★ **The runtime off switch**: an administrator flips it on the running
    /// console, and every site path answers as the default build's does.
    #[tokio::test]
    async fn an_administrator_switches_the_site_off_and_on_at_run_time() {
        use tower::ServiceExt;
        let fake = FakeGunnar::new();
        let (app, passkeys, _feed) = console(fake.clone(), true).await;
        built(&app, &passkeys).await;
        // ★ Owner 2026-10-26: the switch is gunnar's generic setting `console.public-site`.
        assert_eq!(fake.setting("console.public-site").as_deref(), Some(&b"on"[..]), "the flip on is stored at gunnar");
        let put = |cookie: String, on: bool| {
            http::Request::put("/api/public-site")
                .header(http::header::COOKIE, format!("{}={cookie}", crate::auth::SESSION_COOKIE))
                .header(http::header::CONTENT_TYPE, "application/json")
                .header(http::header::ORIGIN, passkeys.origin())
                .body(axum::body::Body::from(format!("{{\"on\":{on}}}")))
                .unwrap()
        };
        let viewer = passkeys.make_session("vera", crate::role::Role::Viewer, true);
        let refused = app.clone().oneshot(put(viewer, false)).await.unwrap();
        assert_eq!(refused.status(), http::StatusCode::FORBIDDEN);
        assert_eq!(anon(&app, "/llms.txt").await.0, http::StatusCode::OK, "a viewer switched it off");
        assert_eq!(fake.setting("console.public-site").as_deref(), Some(&b"on"[..]), "a refused flip stores nothing");

        let admin = passkeys.make_session("root", crate::role::Role::Admin, true);
        let done = app.clone().oneshot(put(admin.clone(), false)).await.unwrap();
        assert_eq!(done.status(), http::StatusCode::OK);
        assert_eq!(fake.setting("console.public-site").as_deref(), Some(&b"off"[..]));
        let (_, state) = signed_in(&app, &passkeys, "root", crate::role::Role::Admin, "/api/public-site").await;
        assert!(state.contains("\"on\":false"), "{state}");
        for path in site_paths().into_iter().filter(|p| p != "/robots.txt") {
            let off = anon(&app, &path).await;
            assert_ne!(off.0, http::StatusCode::OK, "{path} is still served with the switch off: {}", off.1);
        }
        let back = app.clone().oneshot(put(admin, true)).await.unwrap();
        assert_eq!(back.status(), http::StatusCode::OK);
        assert_eq!(anon(&app, "/llms.txt").await.0, http::StatusCode::OK);
    }

    /// ★ **The stored position wins over the environment's** (owner 2026-10-26): with
    /// `GUNNAR_UI_PUBLIC_SITE` unset the site starts off, but a console started over a gunnar
    /// that holds `console.public-site = on` takes it at start, with no flip.
    #[tokio::test]
    async fn a_console_takes_the_stored_switch_at_start() {
        let fake = FakeGunnar::new();
        fake.settings.lock().unwrap().insert("console.public-site".into(), b"on".to_vec());
        let (app, passkeys, _feed) = console(fake, true).await;
        for _ in 0..80 {
            let (_, state) = signed_in(&app, &passkeys, "root", crate::role::Role::Admin, "/api/public-site").await;
            if state.contains("\"on\":true") {
                return;
            }
            tokio::time::sleep(std::time::Duration::from_millis(25)).await;
        }
        panic!("the console never took the stored switch");
    }

    /// ★ A32-16: **IndexNow's key is gunnar's generic setting** `console.public-site.indexnow`
    /// (owner decision 2026-10-26, item 5), set by an administrator only: stored first, then
    /// applied to the running site (its key file is served), and cleared the same way. A viewer
    /// and a key that is not one are refused and store nothing.
    #[tokio::test]
    async fn an_administrator_sets_and_clears_the_indexnow_key_as_a_stored_setting() {
        use tower::ServiceExt;
        const KEY: &str = "0123456789abcdef0123456789abcdef";
        let fake = FakeGunnar::new();
        let (app, passkeys, _feed) = console(fake.clone(), true).await;
        built(&app, &passkeys).await;
        let put = |cookie: String, body: &str| {
            http::Request::put("/api/public-site/indexnow")
                .header(http::header::COOKIE, format!("{}={cookie}", crate::auth::SESSION_COOKIE))
                .header(http::header::CONTENT_TYPE, "application/json")
                .header(http::header::ORIGIN, passkeys.origin())
                .body(axum::body::Body::from(body.to_owned()))
                .unwrap()
        };
        let (_, state) = signed_in(&app, &passkeys, "root", crate::role::Role::Admin, "/api/public-site").await;
        assert!(state.contains("\"indexnow\":false"), "{state}");
        let viewer = passkeys.make_session("vera", crate::role::Role::Viewer, true);
        assert_eq!(app.clone().oneshot(put(viewer, &format!("{{\"key\":\"{KEY}\"}}"))).await.unwrap().status(), http::StatusCode::FORBIDDEN);
        assert_eq!(fake.setting("console.public-site.indexnow"), None, "a refused set stores nothing");
        let admin = passkeys.make_session("root", crate::role::Role::Admin, true);
        assert_eq!(app.clone().oneshot(put(admin.clone(), "{\"key\":\"not a key\"}")).await.unwrap().status(), http::StatusCode::BAD_REQUEST);
        assert_eq!(fake.setting("console.public-site.indexnow"), None, "a key that is not one stores nothing");
        assert_eq!(app.clone().oneshot(put(admin.clone(), &format!("{{\"key\":\"{KEY}\"}}"))).await.unwrap().status(), http::StatusCode::OK);
        assert_eq!(fake.setting("console.public-site.indexnow").as_deref(), Some(KEY.as_bytes()), "the key is stored at gunnar");
        let (status, body) = anon(&app, &format!("/{KEY}.txt")).await;
        assert_eq!((status, body.as_str()), (http::StatusCode::OK, KEY), "the key file is served once the key is set");
        let (_, state) = signed_in(&app, &passkeys, "root", crate::role::Role::Admin, "/api/public-site").await;
        assert!(state.contains("\"indexnow\":true"), "{state}");
        assert_eq!(app.clone().oneshot(put(admin, "{\"key\":\"\"}")).await.unwrap().status(), http::StatusCode::OK);
        assert_eq!(fake.setting("console.public-site.indexnow").as_deref(), Some(&b""[..]), "cleared is stored as an empty key");
        assert_ne!(anon(&app, &format!("/{KEY}.txt")).await.1, KEY, "the key file is served after the key was cleared");
    }

    /// ★ A32-16: a console started over a gunnar that stores an IndexNow key takes it, with no
    /// environment key and no set.
    #[tokio::test]
    async fn a_console_takes_the_stored_indexnow_key_at_start() {
        const KEY: &str = "fedcba9876543210fedcba9876543210";
        assert!(std::env::var(gunnar_public_site::ENV_INDEXNOW_KEY).is_err(), "the test environment sets an IndexNow key");
        let fake = FakeGunnar::new();
        fake.settings.lock().unwrap().insert("console.public-site.indexnow".into(), KEY.as_bytes().to_vec());
        let (app, passkeys, _feed) = console(fake, true).await;
        for _ in 0..80 {
            let (_, state) = signed_in(&app, &passkeys, "root", crate::role::Role::Admin, "/api/public-site").await;
            if state.contains("\"indexnow\":true") {
                return;
            }
            tokio::time::sleep(std::time::Duration::from_millis(25)).await;
        }
        panic!("the console never took the stored IndexNow key");
    }

    /// ★ RA2-1: **a flip made elsewhere is seen.** Another console (or a direct
    /// `Admin.SetSetting`) stores `on` at gunnar after this console started: this console
    /// reads it again and follows, with no restart.
    #[tokio::test]
    async fn a_flip_made_elsewhere_is_seen_by_a_running_console() {
        crate::console_setting::recheck_every(std::time::Duration::from_millis(100));
        let fake = FakeGunnar::new();
        let (app, passkeys, _feed) = console(fake.clone(), true).await;
        tokio::time::sleep(std::time::Duration::from_millis(300)).await;
        let (_, state) = signed_in(&app, &passkeys, "root", crate::role::Role::Admin, "/api/public-site").await;
        assert!(state.contains("\"on\":false"), "{state}");
        fake.settings.lock().unwrap().insert("console.public-site".into(), b"on".to_vec());
        for _ in 0..120 {
            let (_, state) = signed_in(&app, &passkeys, "root", crate::role::Role::Admin, "/api/public-site").await;
            if state.contains("\"on\":true") {
                return;
            }
            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
        }
        panic!("a flip made at gunnar was never seen by the running console");
    }

    /// ★ **GUN-LANDING (owner 2026-10-30): gunnar.rs's front page, in a real browser.**
    /// "gunnar.rs just shows a long button Log in now — where is the sign up | login?" and
    /// "it stands flashing" (on gunnar.rs, the host holding the session cookie; www, without
    /// it, stood still). A selling console with the site on, in headless Chromium:
    ///
    /// * a visitor's `/` is the landing with **Sign up and Log in side by side**, one row,
    ///   the same width, the picture behind them;
    /// * **signed in, the front page settles**: what the sign-in does next (`location.href =
    ///   '/'` on the landing) lands on the console once, and the landing is never drawn
    ///   again; within 5 s at most one more document loads, and so it does for `/repos`.
    #[cfg(feature = "metered")]
    mod in_the_browser {
        use std::time::Duration;

        use serde_json::{json, Value};

        use super::*;
        use crate::passkeys_at_gunnar::first_touch::{chrome, Chromium, ONE_BROWSER};

        /// Every document this tab loads, kept in its `sessionStorage`: its path, and whether
        /// it was the landing (the Log in button) or the console's page.
        const LOADS: &str = r#"(() => {
          addEventListener('DOMContentLoaded', () => {
            let all = [];
            try { all = JSON.parse(sessionStorage.getItem('__loads') || '[]'); } catch (e) {}
            all.push({ path: location.pathname, landing: !!document.getElementById('login'), console: !!document.getElementById('console'), at: Date.now() });
            try { sessionStorage.setItem('__loads', JSON.stringify(all)); } catch (e) {}
          });
        })();"#;

        /// The console's bundle, as small as it gets: one page that names itself.
        const CONSOLE_INDEX: &str = "<!doctype html><html><head><meta charset=\"utf-8\"><title>Gunnar</title></head><body><h1 id=\"console\">Repositories</h1></body></html>\n";

        fn loads(c: &Chromium) -> Vec<Value> {
            c.js("JSON.parse(sessionStorage.getItem('__loads') || '[]')").as_array().cloned().unwrap_or_default()
        }

        #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
        async fn the_front_page_offers_sign_up_beside_log_in_and_settles_once_signed_in() {
            let Some(chrome) = chrome() else { return };
            let ui = tempfile::tempdir().expect("a bundle dir");
            std::fs::write(ui.path().join("index.html"), CONSOLE_INDEX).expect("the bundle's page");
            // A selling console: monetize is behind it (a lazy channel nobody answers here).
            let (app, passkeys, _feed) = crate::anonymous::console_waiting(FakeGunnar::new(), Some(GUEST_SEED), Some("http://127.0.0.1:9".into()), None, None, None, Some(ui.path().to_owned())).await;
            built(&app, &passkeys).await;
            let session = passkeys.make_session("vera", crate::role::Role::Viewer, true);
            let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("a loopback port");
            let base = format!("http://localhost:{}", listener.local_addr().expect("its address").port());
            let served = app.clone();
            tokio::spawn(async move { axum::serve(listener, served.into_make_service_with_connect_info::<std::net::SocketAddr>()).await });
            tokio::task::spawn_blocking(move || {
                let _one = ONE_BROWSER.lock().unwrap_or_else(|p| p.into_inner());
                let c = Chromium::launch(&chrome);
                c.on_page("Page.addScriptToEvaluateOnNewDocument", json!({ "source": LOADS }));

                // ── a visitor's front page: Sign up | Log in, one row, the same width ──
                c.open(&format!("{base}/"));
                c.until("the landing's Log in", "(() => { const e = document.getElementById('login'); return !!e && e.offsetParent !== null; })()");
                let row = c.js("(() => { const s = document.getElementById('signup'), l = document.getElementById('login'); if (!s || !l || s.offsetParent === null) return null; const a = s.getBoundingClientRect(), b = l.getBoundingClientRect(); return { signup: s.textContent.trim(), href: s.getAttribute('href'), login: l.textContent.trim(), same_row: Math.abs(a.top - b.top) < 1, same_width: Math.abs(a.width - b.width) < 1, signup_first: a.left < b.left, title: document.title, picture: getComputedStyle(document.body).backgroundImage + getComputedStyle(document.documentElement).backgroundImage }; })()");
                // Every finding is said at the end, so a RED names all of them.
                let mut red: Vec<String> = Vec::new();
                let way_in = (row["signup"].clone(), row["href"].clone(), row["login"].clone(), row["same_row"].clone(), row["same_width"].clone(), row["signup_first"].clone());
                if way_in != (json!("Sign up"), json!("/signup"), json!("Log in"), json!(true), json!(true), json!(true)) {
                    red.push(format!("the front page's way in is not Sign up | Log in side by side: {row}"));
                }
                if row["title"] != "Gunnar — a European git server in Rust" {
                    red.push(format!("the title: {row}"));
                }

                // ── signed in (the cookie a passkey sign-in leaves), the landing goes on to `/` ──
                c.on_page("Network.setCookie", json!({ "name": crate::auth::SESSION_COOKIE, "value": session, "url": format!("{base}/"), "path": "/", "secure": true, "httpOnly": true, "sameSite": "Strict" }));
                let before = loads(&c).len();
                c.js("(() => { location.href = '/'; return true; })()");
                std::thread::sleep(Duration::from_secs(5));
                let after: Vec<Value> = loads(&c).into_iter().skip(before).collect();
                if after.len() > 1 {
                    red.push(format!("the signed-in front page did not settle: {} documents in 5 s (at most 1), the first: {:?}", after.len(), after.iter().take(3).collect::<Vec<_>>()));
                }
                if after.iter().any(|l| l["landing"] == true) {
                    red.push(format!("the landing was drawn again for a signed-in person ({} times)", after.iter().filter(|l| l["landing"] == true).count()));
                }
                if c.js("!!document.getElementById('console')") != json!(true) {
                    red.push(format!("the signed-in front page is not the console: {}", c.js("location.href")));
                }

                // ── and the console's own page, opened signed in ──
                c.open(&format!("{base}/repos"));
                c.until("a document at /repos", "location.pathname === '/repos' && document.readyState === 'complete'");
                let before = loads(&c).len();
                std::thread::sleep(Duration::from_secs(5));
                let more: Vec<Value> = loads(&c).into_iter().skip(before).collect();
                if !more.is_empty() || c.js("!!document.getElementById('console') && location.pathname === '/repos'") != json!(true) {
                    red.push(format!("/repos did not settle on the console: {} more documents, at {}", more.len(), c.js("location.href")));
                }
                assert!(red.is_empty(), "GUN-LANDING:\n- {}\n--- the page says ---\n{}", red.join("\n- "), c.text());
            })
            .await
            .expect("the browser half");
        }
    }
}