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

nordisk/monetize

crates/monetize-ui-server/tests/sites_browser.rs

//! ★ **The Sites page, clicked** (owner 2026-10-13: the operator's own sites, ordered at
//! this box). Until this file the page had a compile test and no press.
//!
//! The BUILT console (`trunk build`'s `dist`) is served by the REAL router of this crate,
//! over a fake monetize that answers `EstateOrder.ListSites / OrderSite / Terminate` on
//! the wire with the operator bearer — so the page, its cells, this server's routes and
//! the gRPC mapping are all the real ones. Headless Chrome drives it over the DevTools
//! Protocol (`uitest`'s own zero-dependency client) with REAL mouse clicks:
//!
//! 1. the list: each site's domain, Stripe, DNS, cloud and first admin read "already
//!    fed"; a site that runs is not orderable and says why, with no form; an orderable
//!    one draws the form;
//! 2. size Mini, the first admin's address, Order → the price (0.50 EUR), the checkout
//!    link, and then the order's state as it moves (waiting for payment → ordered →
//!    laying → installing → ready) with NO reload;
//! 3. Terminate on a terminable site is armed only by its name typed back, ends it in one
//!    press with no backup offered, and a protected site offers none;
//! 4. a refusal is the server's own sentence — never a gRPC code or an HTTP status.
//!
//! Needs `MONETIZE_UI_CHROME` (a Chrome binary) and `MONETIZE_UI_DIST` (the built
//! console); without both the test says NOT RUN and returns.

#[path = "../../../uitest/src/cdp.rs"]
mod cdp;
#[allow(dead_code, clippy::collapsible_if)]
#[path = "../../../uitest/src/http.rs"]
mod http;
#[allow(dead_code)]
#[path = "../../../uitest/src/json.rs"]
mod json;
#[allow(dead_code)]
#[path = "../../../uitest/src/ws.rs"]
mod ws;

use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::Duration;

use cdp::{Cdp, Chrome};
use json::{esc, Json};
use monetize_proto as pb;
use monetize_proto::estate_order_server::{EstateOrder, EstateOrderServer};
use monetize_proto::site_book_server::{SiteBook, SiteBookServer};
use monetize_ui_server::app::App;
use monetize_ui_server::grpc::Monetize;
use monetize_ui_server::serve;
use monetize_ui_server::sse::EventHub;
use tonic::{Request as TReq, Response, Status};

const SERVICE: &str = "the-service-bearer";
const OPERATOR: &str = "the-operator-bearer";
const CHECKOUT: &str = "https://checkout.example.invalid/c/pay/cs_test_site_1";
const REFUSED: &str = "broken.example cannot be ordered: this box cannot write its DNS names. Nothing was ordered.";

/// What the fake monetize holds: the sites as it would answer them, and every call taken.
#[derive(Default)]
struct Held {
    sites: Vec<pb::EstateSite>,
    orders: Vec<pb::EstateOrderSiteRequest>,
    terminates: Vec<pb::EstateOrderTerminateRequest>,
    settles: Vec<pb::EstateOrderSettleRequest>,
    updates: Vec<pb::EstateUpdateRequest>,
    bearers: Vec<String>,
    /// The site book's puts, as the console sent them (2026-10-14).
    puts: Vec<pb::SitePutRequest>,
    /// CODE RED as monetize would hold it (2026-10-18).
    code_red: Vec<pb::CodeRed>,
}

#[derive(Clone)]
struct Fake(Arc<Mutex<Held>>);

fn site(estate: &str, domain: &str, state: &str, words: &str) -> pb::EstateSite {
    pb::EstateSite {
        estate: estate.into(),
        domain: domain.into(),
        vendor: format!("stripe/{estate}"),
        first_admin: "ops".into(),
        dns_words: "this box writes its names".into(),
        dns_writes: true,
        cloud_words: "upcloud · se-sto1".into(),
        state: state.into(),
        words: words.into(),
        // A33-6: the server says what it costs.
        price_words: "0.50 EUR".into(),
        ..Default::default()
    }
}

fn sites() -> Vec<pb::EstateSite> {
    vec![
        // Runs already (the operator's kit laid it): not orderable, protected.
        pb::EstateSite { orderable: false, why_not: "it already runs.".into(), terminable: false, ..site("gunnar-rs", "gunnar.example", "stands", "Its machines report in.") },
        // Declared, not laid: the form.
        pb::EstateSite { orderable: true, ..site("new-rs", "new.example", "none", "Not ordered yet.") },
        // Declared, but monetize will refuse its order (the refusal row).
        pb::EstateSite { orderable: true, ..site("broken-rs", "broken.example", "none", "Not ordered yet.") },
        // Laid by this box and declared terminable.
        pb::EstateSite { orderable: false, why_not: "this box already set it up.".into(), terminable: true, ..site("old-rs", "old.example", "ready", "It is up.") },
        // Laid by this box and up to date (test2 live, 2026-10-17).
        pb::EstateSite { orderable: false, why_not: "it is already ordered.".into(), ..site("test2-rs", "test2.example", "ready", "The site is ready at https://test2.example. Its first sign-in link was mailed.") },
        // Its setup failed after payment: owed, settled here (retry or refund).
        pb::EstateSite { orderable: false, why_not: "it is already ordered.".into(), ..site("owed-rs", "owed.example", "owed", "The setup failed after payment; the operator can retry or refund.") },
    ]
}

impl Fake {
    fn took<T>(&self, req: &TReq<T>) {
        let bearer = req.metadata().get("authorization").and_then(|v| v.to_str().ok()).and_then(|s| s.strip_prefix("Bearer ")).unwrap_or("").to_owned();
        self.0.lock().unwrap().bearers.push(bearer);
    }
    /// The order moves on, as monetize's runner would move it.
    fn moves(&self, estate: &str, state: &str, words: &str) {
        let mut held = self.0.lock().unwrap();
        let s = held.sites.iter_mut().find(|s| s.estate == estate).expect("the site");
        (s.state, s.words) = (state.into(), words.into());
        if state != "waiting_for_payment" {
            s.checkout_url.clear();
        }
    }
}

/// What monetize answers about a site's update: release 02 sold, the boxes on 01.
fn update_of(estate: &str, state: &str) -> pb::EstateUpdateResponse {
    let line = |role: &str, st: &str, words: &str| pb::EstateUpdateBox { role: role.into(), state: st.into(), words: words.into() };
    if estate == "test2-rs" {
        // test2 live (owner 2026-10-17): every box runs the release sold now — nothing to offer.
        return pb::EstateUpdateResponse {
            host: "test2.example".into(),
            release: "27478f3a499ff8c5".into(),
            live_on: true,
            up_to_date: true,
            release_name: "r5.4 · 2026-10-17 · gunnar aa14357 · gunnar-ui f4b5d44".into(),
            boxes: vec![line("twin", "current", "current · release 27478f3a499f"), line("main", "current", "current · release 27478f3a499f"), line("front", "not-reported", "not reported yet — the front has no update taker yet; the update starts with the front")],
            words: "no update yet".into(),
            ..Default::default()
        };
    }
    pb::EstateUpdateResponse {
        host: format!("{estate}.example"),
        release: "02abcdef0123".into(),
        release_of: [("twin".to_string(), "01".to_string()), ("main".to_string(), "01".to_string()), ("front".to_string(), "01".to_string())].into(),
        live_on: true,
        state: state.into(),
        words: if state.is_empty() { "no update yet".into() } else { format!("update to 02abcdef0123: {state}") },
        offered: state.is_empty(),
        release_name: "r9 · 2026-10-18 · gunnar bb22222".into(),
        boxes: ["twin", "main", "front"].iter().map(|r| line(r, "behind", "runs release 01 · release 02abcdef0123 is published")).collect(),
        ..Default::default()
    }
}

fn not_here<T>() -> Result<Response<T>, Status> {
    Err(Status::unimplemented("not in this fake"))
}

#[tonic::async_trait]
impl EstateOrder for Fake {
    async fn product_order(&self, _: tonic::Request<pb::EstateProductOrderRequest>) -> Result<tonic::Response<pb::EstateProductOrderResponse>, tonic::Status> { Err(tonic::Status::unimplemented("product_order")) }
    async fn product_order_answer(&self, _: tonic::Request<pb::EstateProductOrderAnswerRequest>) -> Result<tonic::Response<pb::EstateProductOrderAnswerResponse>, tonic::Status> { Err(tonic::Status::unimplemented("product_order_answer")) }
    async fn product_order_kinds(&self, _: tonic::Request<pb::EstateProductOrderKindsRequest>) -> Result<tonic::Response<pb::EstateProductOrderKindsResponse>, tonic::Status> { Err(tonic::Status::unimplemented("product_order_kinds")) }
    async fn update_estate(&self, req: TReq<pb::EstateUpdateRequest>) -> Result<Response<pb::EstateUpdateResponse>, Status> {
        self.took(&req);
        let r = req.into_inner();
        self.0.lock().unwrap().updates.push(r.clone());
        Ok(Response::new(update_of(&r.estate, "running")))
    }
    async fn update_status(&self, req: TReq<pb::EstateUpdateStatusRequest>) -> Result<Response<pb::EstateUpdateResponse>, Status> {
        self.took(&req);
        let r = req.into_inner();
        let running = self.0.lock().unwrap().updates.iter().any(|u| u.estate == r.estate);
        Ok(Response::new(update_of(&r.estate, if running { "running" } else { "" })))
    }
    async fn cancel_update(&self, _: TReq<pb::EstateUpdateStatusRequest>) -> Result<Response<pb::EstateUpdateResponse>, Status> { Err(Status::unimplemented("cancel_update")) }
    async fn set_live_update(&self, req: TReq<pb::EstateSetLiveUpdateRequest>) -> Result<Response<pb::EstateUpdateResponse>, Status> {
        self.took(&req);
        Ok(Response::new(update_of(&req.into_inner().estate, "")))
    }
    async fn adopt_estate(&self, _: TReq<pb::EstateAdoptRequest>) -> Result<Response<pb::EstateAdoptResponse>, Status> {
        Err(Status::unimplemented("not in this fake"))
    }
    async fn list_code_red(&self, req: TReq<pb::CodeRedListRequest>) -> Result<Response<pb::CodeRedList>, Status> {
        self.took(&req);
        Ok(Response::new(pb::CodeRedList { items: self.0.lock().unwrap().code_red.clone() }))
    }
    async fn raise_code_red(&self, _: TReq<pb::CodeRed>) -> Result<Response<pb::CodeRedList>, Status> {
        Err(Status::unimplemented("not in this mock"))
    }
    async fn clear_code_red(&self, _: TReq<pb::CodeRedClearRequest>) -> Result<Response<pb::CodeRedList>, Status> {
        Err(Status::unimplemented("not in this mock"))
    }
    async fn narrate_log(&self, _: TReq<pb::EstateNarrateLogRequest>) -> Result<Response<pb::EstateNarrateLogResponse>, Status> {
        Err(Status::unimplemented("not in this mock"))
    }
    async fn install_log(&self, _: TReq<pb::EstateInstallLogRequest>) -> Result<Response<pb::EstateInstallLogResponse>, Status> {
        Err(Status::unimplemented("not in this mock"))
    }
    async fn ack_code_red(&self, req: TReq<pb::CodeRedAckRequest>) -> Result<Response<pb::CodeRedList>, Status> {
        self.took(&req);
        let r = req.into_inner();
        let mut h = self.0.lock().unwrap();
        let c = h.code_red.iter_mut().find(|c| c.key == r.key).ok_or_else(|| Status::not_found("no such code red"))?;
        c.acked_by = r.by;
        c.acked_at_unix_ms = 1;
        Ok(Response::new(pb::CodeRedList { items: h.code_red.clone() }))
    }
    async fn list_sites(&self, req: TReq<pb::EstateSitesRequest>) -> Result<Response<pb::EstateSitesResponse>, Status> {
        self.took(&req);
        Ok(Response::new(pb::EstateSitesResponse { sites: self.0.lock().unwrap().sites.clone(), sizes: vec!["Mini".into(), "Small".into(), "Medium".into()] }))
    }
    async fn order_site(&self, req: TReq<pb::EstateOrderSiteRequest>) -> Result<Response<pb::EstateOrderSiteResponse>, Status> {
        self.took(&req);
        let r = req.into_inner();
        let mut held = self.0.lock().unwrap();
        held.orders.push(r.clone());
        if r.estate == "broken-rs" {
            return Err(Status::failed_precondition(REFUSED));
        }
        let s = held.sites.iter_mut().find(|s| s.estate == r.estate).ok_or_else(|| Status::not_found("no such site"))?;
        (s.state, s.words, s.checkout_url, s.orderable, s.why_not) = ("waiting_for_payment".into(), "Waiting for your payment.".into(), CHECKOUT.into(), false, "it is already ordered.".into());
        Ok(Response::new(pb::EstateOrderSiteResponse {
            reference: "site/new-rs/1".into(),
            checkout_url: CHECKOUT.into(),
            price_words: "0.50 EUR".into(),
            words: "new.example is ordered. Pay to start setting it up.".into(),
            company: r.estate,
        }))
    }
    async fn first_link(&self, _: TReq<pb::EstateOrderFirstLinkRequest>) -> Result<Response<pb::EstateOrderFirstLinkResponse>, Status> {
        Err(Status::unimplemented("not in this mock"))
    }
    async fn refeed(&self, _: TReq<pb::EstateOrderRefeedRequest>) -> Result<Response<pb::EstateOrderRefeedResponse>, Status> {
        Err(Status::unimplemented("not in this mock"))
    }
    async fn console(&self, _: TReq<pb::EstateOrderConsoleRequest>) -> Result<Response<pb::EstateOrderConsoleResponse>, Status> {
        Err(Status::unimplemented("not in this mock"))
    }
    async fn terminate(&self, req: TReq<pb::EstateOrderTerminateRequest>) -> Result<Response<pb::EstateOrderTerminateResponse>, Status> {
        self.took(&req);
        let r = req.into_inner();
        let mut held = self.0.lock().unwrap();
        held.terminates.push(r.clone());
        held.sites.retain(|s| s.estate != r.company);
        Ok(Response::new(pb::EstateOrderTerminateResponse { words: "old.example is terminated.".into(), done: true }))
    }
    async fn rebuild_main(&self, _: TReq<pb::EstateRebuildMainRequest>) -> Result<Response<pb::EstateRebuildMainResponse>, Status> {
        not_here()
    }
    async fn start(&self, _: TReq<pb::EstateOrderStartRequest>) -> Result<Response<pb::EstateOrderStartResponse>, Status> {
        not_here()
    }
    async fn status(&self, _: TReq<pb::EstateOrderStatusRequest>) -> Result<Response<pb::EstateOrderStatusResponse>, Status> {
        not_here()
    }
    async fn renew(&self, _: TReq<pb::EstateOrderRenewRequest>) -> Result<Response<pb::EstateOrderRenewResponse>, Status> {
        not_here()
    }
    async fn send_sign_in_again(&self, _: TReq<pb::EstateOrderSignInAgainRequest>) -> Result<Response<pb::EstateOrderSignInAgainResponse>, Status> {
        not_here()
    }
    async fn change_plan(&self, _: TReq<pb::EstateOrderChangePlanRequest>) -> Result<Response<pb::EstateOrderChangePlanResponse>, Status> {
        not_here()
    }
    async fn ladder(&self, _: TReq<pb::EstateOrderLadderRequest>) -> Result<Response<pb::EstateOrderLadderResponse>, Status> {
        not_here()
    }
    async fn purchases(&self, _: TReq<pb::EstateOrderPurchasesRequest>) -> Result<Response<pb::EstateOrderPurchasesResponse>, Status> {
        not_here()
    }
    async fn notice(&self, _: TReq<pb::EstateNoticeRequest>) -> Result<Response<pb::EstateNoticeResponse>, Status> {
        not_here()
    }
    async fn sells(&self, _: TReq<pb::EstateOrderSellsRequest>) -> Result<Response<pb::EstateOrderSellsResponse>, Status> {
        not_here()
    }
    async fn cancel(&self, _: TReq<pb::EstateOrderCancelRequest>) -> Result<Response<pb::EstateOrderCancelResponse>, Status> {
        not_here()
    }
    async fn restore_front(&self, _: TReq<pb::EstateOrderRestoreFrontRequest>) -> Result<Response<pb::EstateOrderRestoreFrontResponse>, Status> {
        not_here()
    }
    async fn settle(&self, req: TReq<pb::EstateOrderSettleRequest>) -> Result<Response<pb::EstateOrderSettleResponse>, Status> {
        self.took(&req);
        let r = req.into_inner();
        let mut held = self.0.lock().unwrap();
        held.settles.push(r.clone());
        let s = held.sites.iter_mut().find(|s| s.estate == r.company).ok_or_else(|| Status::not_found("no order for that site"))?;
        (s.state, s.words) = ("queued".into(), "Paid. It is next.".into());
        Ok(Response::new(pb::EstateOrderSettleResponse { phase: "held".into(), words: "the operator sets it up again".into() }))
    }
}

const NEW_KEY: &str = "sk_test_51BROWSERSECRETKEYVALUE";
const NEW_HOOK: &str = "whsec_BROWSERHOOKVALUE";

/// The fake monetize's site book: a put of a NEW site adds it to the sites it lists (orderable,
/// as monetize would once its watcher took it); an edit moves its domain. Never a secret back.
#[tonic::async_trait]
impl SiteBook for Fake {
    async fn put_site(&self, req: TReq<pb::SitePutRequest>) -> Result<Response<pb::SitePutResponse>, Status> {
        self.took(&req);
        let r = req.into_inner();
        let mut held = self.0.lock().unwrap();
        held.puts.push(r.clone());
        if r.domain.ends_with(".invalid") {
            return Err(Status::failed_precondition("REFUSED [site-judged] the box's loader refused the feed with this change, and nothing was kept"));
        }
        if r.create {
            let vendor = if r.vendor.is_empty() { format!("stripe/{}", r.estate) } else { r.vendor.clone() };
            held.sites.push(pb::EstateSite { orderable: true, vendor, first_admin: r.first_admin.clone(), ..site(&r.estate, &r.domain, "none", "Not ordered yet.") });
            return Ok(Response::new(pb::SitePutResponse { words: format!("{} is added: the box takes it on its next look (two seconds), and it is ordered on the Sites page", r.estate), changed: vec![format!("domain: {}", r.domain), "Stripe secret key: set (sha256:0123abcd)".into()] }));
        }
        let s = held.sites.iter_mut().find(|s| s.estate == r.estate).ok_or_else(|| Status::not_found("no such site"))?;
        let was = std::mem::replace(&mut s.domain, r.domain.clone());
        Ok(Response::new(pb::SitePutResponse { words: format!("{} is edited: the box takes it on its next look (two seconds), and it is ordered on the Sites page", r.estate), changed: vec![format!("domain: {was} → {}", r.domain)] }))
    }
    async fn show_site(&self, req: TReq<pb::SiteShowRequest>) -> Result<Response<pb::SiteShowResponse>, Status> {
        self.took(&req);
        let estate = req.into_inner().estate;
        let held = self.0.lock().unwrap();
        let site = if estate.is_empty() {
            pb::SiteRecord { product: "gunnar".into(), operator_site: true, ..Default::default() }
        } else {
            let s = held.sites.iter().find(|s| s.estate == estate).ok_or_else(|| Status::not_found("no such site"))?;
            pb::SiteRecord { estate: s.estate.clone(), domain: s.domain.clone(), product: "gunnar".into(), first_admin: s.first_admin.clone(), operator_site: true, vendor: s.vendor.clone(), source: "fed".into(), secret_key_set: true, secret_key_fingerprint: "sha256:0123abcd".into(), webhook_secret_set: true, webhook_secret_fingerprint: "sha256:89abcdef".into(), terms: Some(pb::SiteTerms { term_days: 1, ..Default::default() }), ..Default::default() }
        };
        let vendors = held.sites.iter().map(|s| s.vendor.clone()).collect();
        Ok(Response::new(pb::SiteShowResponse { site: Some(site), products: vec!["gunnar".into()], vendors, media_sites: vec!["gunnar-rs".into()], currency: "EUR".into(), sizes: vec!["Mini".into(), "Small".into()], history: vec!["2026-10-14 12:00:00 UTC — console:op-1 — edited gunnar-rs: term days: 2 → 1".into()] }))
    }
}

/// The fake monetize and the real console over it, on loopback: the console's origin.
fn up(fake: Fake, dist: PathBuf) -> String {
    let (tx, rx) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        let rt = tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build().expect("a runtime");
        rt.block_on(async move {
            let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
            let at = l.local_addr().unwrap().to_string();
            tokio::spawn(tonic::transport::Server::builder().add_service(EstateOrderServer::new(fake.clone())).add_service(SiteBookServer::new(fake)).serve_with_incoming(tonic::transport::server::TcpIncoming::from(l)));
            let monetize = Monetize::connect_lazy(&at, SERVICE).unwrap();
            let operator = Some(Monetize::connect_lazy(&at, OPERATOR).unwrap());
            // The open posture (no passkey, no token, loopback): every request an admin's.
            let app = Arc::new(App { webauthn: None, admin_token: None, events: EventHub::spawn(monetize.clone(), Duration::from_secs(3600)), monetize, operator, ui_dir: Some(dist) });
            let web = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
            tx.send(format!("http://127.0.0.1:{}", web.local_addr().unwrap().port())).unwrap();
            axum::serve(web, serve::router(app)).await.unwrap();
        });
    });
    rx.recv_timeout(Duration::from_secs(20)).expect("the console came up")
}

/// A person's click: the mouse pressed and released at the middle of the element's own
/// box, scrolled into view first. A button something covers is not pressed.
fn click(c: &mut Cdp, sess: &str, selector: &str) {
    let at = c
        .eval(sess, &format!("(() => {{ const e = document.querySelector({}); e.scrollIntoView({{block: 'center'}}); const r = e.getBoundingClientRect(); return [r.x + r.width / 2, r.y + r.height / 2]; }})()", esc(selector)))
        .unwrap_or_else(|e| panic!("no {selector} to press: {e}"));
    let xy: Vec<f64> = at.arr().iter().filter_map(Json::as_f64).collect();
    for kind in ["mousePressed", "mouseReleased"] {
        c.call(Some(sess), "Input.dispatchMouseEvent", &format!(r#"{{"type":"{kind}","x":{},"y":{},"button":"left","clickCount":1}}"#, xy[0], xy[1])).expect("the mouse");
    }
}

/// Typed as a person types it: the field focused by a click, then the keys.
fn type_into(c: &mut Cdp, sess: &str, selector: &str, text: &str) {
    click(c, sess, selector);
    c.eval(sess, &format!("(() => {{ const e = document.querySelector({}); e.focus(); e.setSelectionRange && e.type !== 'email' && e.setSelectionRange(e.value.length, e.value.length); return true; }})()", esc(selector))).expect("the caret");
    c.call(Some(sess), "Input.insertText", &format!(r#"{{"text":{}}}"#, esc(text))).expect("the keys");
}

fn text(c: &mut Cdp, sess: &str, selector: &str) -> String {
    c.eval_str(sess, &format!("(document.querySelector({}) || {{innerText: ''}}).innerText.replace(/\\s+/g, ' ').trim()", esc(selector))).unwrap()
}

fn truthy(c: &mut Cdp, sess: &str, expr: &str) -> bool {
    c.eval(sess, &format!("!!({expr})")).unwrap_or_else(|e| panic!("{expr}: {e}")).as_bool().unwrap_or(false)
}

/// **Screenshots** (owner 2026-10-17: look at them): desktop and a 390 px phone, into
/// `MONETIZE_UI_SHOTS` when it names a directory.
fn shots(c: &mut Cdp, sess: &str, name: &str) {
    use base64::Engine as _;
    let Some(dir) = std::env::var_os("MONETIZE_UI_SHOTS").map(PathBuf::from) else { return };
    std::fs::create_dir_all(&dir).unwrap();
    for (tag, width, mobile) in [("desktop", 1440, false), ("390px", 390, true)] {
        c.call(Some(sess), "Emulation.setDeviceMetricsOverride", &format!(r#"{{"width":{width},"height":900,"deviceScaleFactor":1,"mobile":{mobile}}}"#)).expect("the viewport");
        std::thread::sleep(Duration::from_millis(400));
        // The whole page, not the first screen: the viewport as tall as the document.
        let tall = c.eval(sess, "Math.max(...[...document.querySelectorAll('*')].map(e => e.scrollHeight))").ok().and_then(|h| h.as_f64()).unwrap_or(900.0) as u64;
        c.call(Some(sess), "Emulation.setDeviceMetricsOverride", &format!(r#"{{"width":{width},"height":{tall},"deviceScaleFactor":1,"mobile":{mobile}}}"#)).expect("the viewport");
        std::thread::sleep(Duration::from_millis(400));
        let r = c.call(Some(sess), "Page.captureScreenshot", r#"{"format":"png"}"#).expect("a screenshot");
        let data = r.get("data").and_then(|d| d.as_str()).expect("png data").to_string();
        let png = base64::engine::general_purpose::STANDARD.decode(data).expect("base64");
        std::fs::write(dir.join(format!("{name}-{tag}.png")), png).unwrap();
    }
    c.call(Some(sess), "Emulation.clearDeviceMetricsOverride", "{}").expect("the viewport back");
}

fn card(estate: &str) -> String {
    format!("section.site[data-site=\"{estate}\"]")
}

/// Wait for the page to say `want` in `selector`, and say what it said when it never did.
fn reads(c: &mut Cdp, sess: &str, selector: &str, want: &str) {
    let expr = format!("(document.querySelector({}) || {{innerText: ''}}).innerText.includes({})", esc(selector), esc(want));
    if c.wait_for(sess, &expr, 15_000).is_err() {
        panic!("{selector} never read {want:?}; it reads {:?}", text(c, sess, selector));
    }
}

#[test]
fn the_sites_page_lists_orders_follows_and_terminates_with_real_clicks() {
    let (Some(bin), Some(dist)) = (std::env::var("MONETIZE_UI_CHROME").ok().filter(|v| !v.is_empty()), std::env::var("MONETIZE_UI_DIST").ok().filter(|v| !v.is_empty())) else {
        eprintln!("NOT RUN: set MONETIZE_UI_CHROME (a Chrome binary) and MONETIZE_UI_DIST (the console's built dist)");
        return;
    };
    assert!(std::path::Path::new(&dist).join("index.html").is_file(), "{dist} holds no built console");
    let fake = Fake(Arc::new(Mutex::new(Held { sites: sites(), ..Default::default() })));
    let origin = up(fake.clone(), PathBuf::from(dist));

    let debug = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port();
    let profile = std::env::temp_dir().join(format!("sites-browser-{}", std::process::id()));
    let _chrome = Chrome::launch(&bin, &profile, debug).expect("chrome starts");
    let mut c = Cdp::attach(debug).expect("cdp");
    let sess = c.page().expect("a page");
    let c = &mut c;
    c.navigate(&sess, &format!("{origin}/sites")).expect("the console loads");
    // The page marks its own document, so a reload would be seen.
    if c.wait_for(&sess, "!!document.querySelector('section.site')", 30_000).is_err() {
        panic!("/sites never drew: {}", text(c, &sess, "body"));
    }
    c.eval(&sess, "(() => { window.__never_reloaded = true; return true; })()").unwrap();

    // ── 1. The list ─────────────────────────────────────────────────────────
    let stands = text(c, &sess, &card("gunnar-rs"));
    // One line for what is configured (the details are on Edit); a running site says where it
    // runs; a kit-installed one says it is not managed here until adopted (owner 2026-10-17).
    assert!(stands.contains("Configured") && !stands.contains("already fed"), "{stands}");
    assert!(stands.contains("Running at https://gunnar.example") && stands.contains("Not managed by this box yet — adopt it to update it here"), "{stands}");
    assert!(!stands.contains("Cannot be ordered"), "a running site is not offered for ordering: {stands}");
    assert!(!truthy(c, &sess, &format!("document.querySelector('{} .site-order-send')", card("gunnar-rs"))), "a site that runs draws an order form");
    assert!(!truthy(c, &sess, &format!("document.querySelector('{} .site-terminate')", card("gunnar-rs"))), "a protected site offers Terminate");
    let new = card("new-rs");
    assert!(text(c, &sess, &new).contains("not ordered"), "{}", text(c, &sess, &new));
    assert_eq!(text(c, &sess, &format!("{new} .site-order-send")), "Order new.example");
    assert!(truthy(c, &sess, &format!("document.querySelector('{new} .site-order-send').disabled")), "Order is armed with no address typed");
    let page = text(c, &sess, "main");
    for internal in ["Market", ".toml", "waiting_for_payment", "monetize"] {
        assert!(!page.contains(internal), "the page says {internal:?}: {page}");
    }

    // ── 2. The order ────────────────────────────────────────────────────────
    assert_eq!(c.eval_str(&sess, &format!("document.querySelector('{new} .site-size').value")).unwrap(), "Mini", "the smallest size is chosen first");
    c.eval(&sess, &format!("(() => {{ const s = document.querySelector('{new} .site-size'); s.value = 'Small'; s.dispatchEvent(new Event('change', {{bubbles: true}})); s.value = 'Mini'; s.dispatchEvent(new Event('change', {{bubbles: true}})); return true; }})()")).unwrap();
    type_into(c, &sess, &format!("{new} .site-email"), "ops@example.invalid");
    c.wait_for(&sess, &format!("!document.querySelector('{new} .site-order-send').disabled"), 5_000).expect("the address arms Order");
    click(c, &sess, &format!("{new} .site-order-send"));
    reads(c, &sess, "#site-ordered", "new.example is ordered. Pay to start setting it up.");
    assert_eq!(text(c, &sess, "#site-pay"), "Pay 0.50 EUR", "the price");
    assert_eq!(c.eval_str(&sess, "document.querySelector('#site-pay').href").unwrap(), CHECKOUT, "the checkout link");
    {
        let held = fake.0.lock().unwrap();
        let o = held.orders.last().expect("an order was sent");
        assert_eq!((o.estate.as_str(), o.size.as_str(), o.email.as_str(), o.seed_key.as_str()), ("new-rs", "Mini", "ops@example.invalid", ""), "the order as typed");
        assert!(held.bearers.iter().all(|b| b == OPERATOR), "the Sites verbs go with the operator bearer: {:?}", held.bearers);
    }
    // Its state, as monetize moves it — read off the page with no reload.
    reads(c, &sess, &format!("{new} .site-state"), "waiting for payment");
    assert_eq!(c.eval_str(&sess, &format!("document.querySelector('{new} .site-pay').href")).unwrap(), CHECKOUT);
    assert!(!truthy(c, &sess, &format!("document.querySelector('{new} .site-order-send')")), "an ordered site still draws its form");
    for (state, words, shown) in [("queued", "Paid. It is next.", "ordered"), ("laying", "Its machines are being made.", "laying"), ("installing", "Gunnar is being installed.", "installing"), ("ready", "Running at https://new.example", "ready")] {
        fake.moves("new-rs", state, words);
        reads(c, &sess, &format!("{new} .site-state"), shown);
        reads(c, &sess, &format!("{new} .site-state"), words);
    }
    assert!(truthy(c, &sess, "window.__never_reloaded === true"), "the page reloaded to show the state");
    assert!(!truthy(c, &sess, &format!("document.querySelector('{new} .site-pay')")), "a paid order still offers the payment page");

    // ── 4. A refusal, in the server's own sentence ──────────────────────────
    let broken = card("broken-rs");
    type_into(c, &sess, &format!("{broken} .site-email"), "ops@example.invalid");
    c.wait_for(&sess, &format!("!document.querySelector('{broken} .site-order-send').disabled"), 5_000).expect("the address arms Order");
    click(c, &sess, &format!("{broken} .site-order-send"));
    reads(c, &sess, "#site-failed", REFUSED);
    let said = text(c, &sess, "#site-failed");
    assert_eq!(said, REFUSED, "the refusal is the sentence and nothing else");
    for raw in ["failedprecondition", "FailedPrecondition", "HTTP 4", "status:", "grpc"] {
        assert!(!text(c, &sess, "main").contains(raw), "the page shows {raw:?}");
    }
    assert_eq!(c.eval_str(&sess, &format!("document.querySelector('{broken} .site-email').value")).unwrap(), "ops@example.invalid", "a refusal emptied the form");

    // ── 3. Terminate ────────────────────────────────────────────────────────
    let old = card("old-rs");
    assert!(!text(c, &sess, &old).contains("Cannot be ordered"), "{}", text(c, &sess, &old));
    assert!(truthy(c, &sess, &format!("document.querySelector('{old} .site-terminate-send').disabled")), "Terminate is armed with nothing typed");
    click(c, &sess, &format!("{old} .site-terminate-send"));
    type_into(c, &sess, &format!("{old} .site-terminate-confirm"), "old.exampl");
    assert!(truthy(c, &sess, &format!("document.querySelector('{old} .site-terminate-send').disabled")), "a name not typed whole arms Terminate");
    click(c, &sess, &format!("{old} .site-terminate-send"));
    std::thread::sleep(Duration::from_millis(300));
    assert!(fake.0.lock().unwrap().terminates.is_empty(), "a disabled press was sent");
    type_into(c, &sess, &format!("{old} .site-terminate-confirm"), "e");
    c.wait_for(&sess, &format!("!document.querySelector('{old} .site-terminate-send').disabled"), 5_000).expect("the name arms Terminate");
    click(c, &sess, &format!("{old} .site-terminate-send"));
    // No backup is offered: one press ends it.
    reads(c, &sess, "#site-ended", "old.example is terminated.");
    c.wait_for(&sess, &format!("!document.querySelector('{old}')"), 10_000).expect("the terminated site leaves the list");
    let sent: Vec<(String, String)> = fake.0.lock().unwrap().terminates.iter().map(|t| (t.company.clone(), t.confirm.clone())).collect();
    assert_eq!(sent, [("old-rs".to_owned(), "old.example".to_owned())], "the name typed back (the wire carries no backup word, A32-27)");
    assert!(truthy(c, &sess, "window.__never_reloaded === true"), "the page reloaded");

    // ── 5. An owed site order: Set up again / Refund, and the buttons go after the press ──
    let owed = card("owed-rs");
    reads(c, &sess, &owed, "setup failed after payment");
    assert_eq!(text(c, &sess, &format!("{owed} .site-settle-send[data-action=retry]")), "Set up again", "the two settles");
    assert_eq!(text(c, &sess, &format!("{owed} .site-settle-send[data-action=refund]")), "Refund", "the two settles");
    assert!(!truthy(c, &sess, &format!("document.querySelector('{owed} .site-terminate')")), "nothing laid: no Terminate");
    click(c, &sess, &format!("{owed} .site-settle-send[data-action=retry]"));
    c.wait_for(&sess, &format!("!!document.querySelector('{owed} .site-settle-confirm[data-action=retry]')"), 5_000).expect("Set up again asks first");
    assert!(fake.0.lock().unwrap().settles.is_empty(), "nothing is sent before the confirm");
    click(c, &sess, &format!("{owed} .site-settle-confirm[data-action=retry]"));
    reads(c, &sess, &format!("{owed} .site-state"), "ordered");
    reads(c, &sess, "#site-ended", "the operator sets it up again");
    c.wait_for(&sess, &format!("!document.querySelector('{owed} .site-settle')"), 5_000).expect("the settle buttons go once it is set up again");
    {
        let held = fake.0.lock().unwrap();
        let sent: Vec<(String, String)> = held.settles.iter().map(|s| (s.company.clone(), s.action.clone())).collect();
        assert_eq!(sent, [("owed-rs".to_owned(), "retry".to_owned())], "the same EstateOrder.Settle the Accounts page uses");
        assert!(held.bearers.iter().all(|b| b == OPERATOR), "with the operator bearer: {:?}", held.bearers);
    }
    assert!(truthy(c, &sess, "window.__never_reloaded === true"), "the page reloaded");
}

fn value(c: &mut Cdp, sess: &str, selector: &str) -> String {
    c.eval_str(sess, &format!("(document.querySelector({}) || {{value: ''}}).value", esc(selector))).unwrap()
}

fn choose(c: &mut Cdp, sess: &str, selector: &str, v: &str) {
    c.eval(sess, &format!("(() => {{ const s = document.querySelector({}); s.value = {}; s.dispatchEvent(new Event('change', {{bubbles: true}})); return true; }})()", esc(selector), esc(v))).unwrap();
}

/// ★ **A site ADDED in the console, then ordered** (owner 2026-10-14: config in the
/// application). New site → the form (the box's choices; a new Stripe account of its own;
/// the secrets as password fields) → Add site → the box's answer, the new card in the list
/// with NO reload → its order form → ordered. Edit on a fed site: the form filled from the box,
/// each secret "set" with a fingerprint and its field EMPTY; a save sends no secret. A secret
/// typed is never shown back on the page; a refusal is the box's sentence.
#[test]
fn a_site_is_added_in_the_console_then_ordered_and_edited_with_real_clicks() {
    let (Some(bin), Some(dist)) = (std::env::var("MONETIZE_UI_CHROME").ok().filter(|v| !v.is_empty()), std::env::var("MONETIZE_UI_DIST").ok().filter(|v| !v.is_empty())) else {
        eprintln!("NOT RUN: set MONETIZE_UI_CHROME (a Chrome binary) and MONETIZE_UI_DIST (the console's built dist)");
        return;
    };
    assert!(std::path::Path::new(&dist).join("index.html").is_file(), "{dist} holds no built console");
    let fake = Fake(Arc::new(Mutex::new(Held { sites: sites(), ..Default::default() })));
    let origin = up(fake.clone(), PathBuf::from(dist));
    let debug = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port();
    let profile = std::env::temp_dir().join(format!("sites-book-browser-{}", std::process::id()));
    let _chrome = Chrome::launch(&bin, &profile, debug).expect("chrome starts");
    let mut c = Cdp::attach(debug).expect("cdp");
    let sess = c.page().expect("a page");
    let c = &mut c;
    c.navigate(&sess, &format!("{origin}/sites")).expect("the console loads");
    if c.wait_for(&sess, "!!document.querySelector('section.site')", 30_000).is_err() {
        panic!("/sites never drew: {}", text(c, &sess, "body"));
    }
    c.eval(&sess, "(() => { window.__never_reloaded = true; return true; })()").unwrap();

    // ── New site: the form ──────────────────────────────────────────────────
    click(c, &sess, "#site-new");
    c.wait_for(&sess, "!!document.querySelector('#site-form .sb-estate')", 10_000).expect("the New site form");
    let form = text(c, &sess, "#site-form");
    assert!(form.contains("Sold in EUR") && form.contains("Mini, Small"), "the shop's currency and sizes, read-only: {form}");
    assert_eq!(value(c, &sess, "#site-form .sb-vendor"), "", "a new site starts on a new Stripe account of its own");
    assert_eq!(value(c, &sess, "#site-form .sb-product"), "gunnar");
    for f in [".sb-key", ".sb-hook"] {
        assert_eq!(c.eval_str(&sess, &format!("document.querySelector('#site-form {f}').type")).unwrap(), "password", "{f} is a password field");
    }
    assert!(truthy(c, &sess, "document.querySelector('#site-form .sb-send').disabled"), "Add site is armed on an empty form");
    type_into(c, &sess, "#site-form .sb-estate", "test5-rs");
    type_into(c, &sess, "#site-form .sb-domain", "test5.example");
    type_into(c, &sess, "#site-form .sb-first-admin", "ops");
    // A number field has no caret to place: focused by a click, then the keys.
    click(c, &sess, "#site-form .sb-term-days");
    c.call(Some(&sess), "Input.insertText", r#"{"text":"1"}"#).expect("the keys");
    choose(c, &sess, "#site-form .sb-media", "gunnar-rs");
    click(c, &sess, "#site-form .sb-terminable");
    assert!(truthy(c, &sess, "document.querySelector('#site-form .sb-send').disabled"), "a new account is armed without its two secrets");
    type_into(c, &sess, "#site-form .sb-key", NEW_KEY);
    type_into(c, &sess, "#site-form .sb-hook", NEW_HOOK);
    c.wait_for(&sess, "!document.querySelector('#site-form .sb-send').disabled", 5_000).expect("the form arms Add site");
    assert_eq!(text(c, &sess, "#site-form .sb-send"), "Add site");
    click(c, &sess, "#site-form .sb-send");
    reads(c, &sess, "#site-put-done", "test5-rs is added");
    reads(c, &sess, "#site-put-done", "Stripe secret key: set (sha256:0123abcd)");
    assert!(!truthy(c, &sess, "document.querySelector('#site-form')"), "the form (and the secrets in it) left the page");
    {
        let held = fake.0.lock().unwrap();
        let p = held.puts.last().expect("a put was sent");
        assert_eq!((p.estate.as_str(), p.create, p.domain.as_str(), p.first_admin.as_str(), p.vendor.as_str(), p.media_from.as_str(), p.terminable, p.operator_site, p.live), ("test5-rs", true, "test5.example", "ops", "", "gunnar-rs", true, true, false), "the site as typed");
        assert_eq!((p.vendor_secret_key.as_str(), p.vendor_webhook_secret.as_str(), p.terms.as_ref().map(|t| t.term_days)), (NEW_KEY, NEW_HOOK, Some(1)), "the secrets go to monetize, and only there");
        assert!(p.actor.starts_with("console"), "who: {}", p.actor);
        assert!(held.bearers.iter().all(|b| b == OPERATOR), "the site book goes with the operator bearer: {:?}", held.bearers);
    }
    // ── The new site is in the list with no reload, and is ordered ──────────
    let t5 = card("test5-rs");
    c.wait_for(&sess, &format!("!!document.querySelector('{t5} .site-order-send')"), 15_000).expect("the added site's order form");
    assert_eq!(text(c, &sess, &format!("{t5} .site-order-send")), "Order test5.example");
    type_into(c, &sess, &format!("{t5} .site-email"), "ops@example.invalid");
    c.wait_for(&sess, &format!("!document.querySelector('{t5} .site-order-send').disabled"), 5_000).expect("the address arms Order");
    click(c, &sess, &format!("{t5} .site-order-send"));
    reads(c, &sess, "#site-ordered", "is ordered");
    reads(c, &sess, &format!("{t5} .site-state"), "waiting for payment");
    assert_eq!(fake.0.lock().unwrap().orders.last().map(|o| (o.estate.clone(), o.email.clone())), Some(("test5-rs".to_string(), "ops@example.invalid".to_string())), "the added site is the one ordered");
    let page = text(c, &sess, "body");
    assert!(!page.contains("BROWSERSECRETKEYVALUE") && !page.contains("BROWSERHOOKVALUE"), "a secret is shown back");

    // ── Edit a fed site: filled from the box, the secrets "set" and EMPTY ───
    click(c, &sess, &format!("{} .site-edit", card("gunnar-rs")));
    c.wait_for(&sess, "!!document.querySelector('#site-form .sb-domain')", 10_000).expect("the Edit form");
    assert_eq!(value(c, &sess, "#site-form .sb-domain"), "gunnar.example");
    assert_eq!(value(c, &sess, "#site-form .sb-term-days"), "1");
    assert!(!truthy(c, &sess, "document.querySelector('#site-form .sb-estate')"), "an edit does not rename the site");
    assert_eq!(text(c, &sess, "#site-form .sb-key-status"), "set (sha256:0123abcd)");
    assert_eq!(text(c, &sess, "#site-form .sb-hook-status"), "set (sha256:89abcdef)");
    assert_eq!((value(c, &sess, "#site-form .sb-key"), value(c, &sess, "#site-form .sb-hook")), (String::new(), String::new()), "a held secret is never put back in a field");
    // The history is folded (a closed <details>): its text is there, not drawn.
    assert!(c.eval_str(&sess, "document.querySelector('#site-form .sb-history').textContent").unwrap().contains("console:op-1"), "the history");
    // A refusal is the box's own sentence, and keeps the form.
    c.eval(&sess, "(() => { const e = document.querySelector('#site-form .sb-domain'); e.select(); return true; })()").unwrap();
    c.call(Some(&sess), "Input.insertText", r#"{"text":"gunnar.invalid"}"#).expect("the keys");
    click(c, &sess, "#site-form .sb-send");
    reads(c, &sess, "#site-failed", "REFUSED [site-judged]");
    assert!(truthy(c, &sess, "document.querySelector('#site-form')"), "a refusal closed the form");
    c.eval(&sess, "(() => { const e = document.querySelector('#site-form .sb-domain'); e.select(); return true; })()").unwrap();
    c.call(Some(&sess), "Input.insertText", r#"{"text":"gunnar2.example"}"#).expect("the keys");
    assert_eq!(text(c, &sess, "#site-form .sb-send"), "Save");
    click(c, &sess, "#site-form .sb-send");
    reads(c, &sess, "#site-put-done", "domain: gunnar.example → gunnar2.example");
    {
        let held = fake.0.lock().unwrap();
        let p = held.puts.last().unwrap();
        assert_eq!((p.estate.as_str(), p.create, p.domain.as_str(), p.vendor.as_str(), p.vendor_secret_key.as_str(), p.vendor_webhook_secret.as_str()), ("gunnar-rs", false, "gunnar2.example", "stripe/gunnar-rs", "", ""), "an edit sends no secret it was not typed");
    }
    reads(c, &sess, &card("gunnar-rs"), "gunnar2.example");
    assert!(truthy(c, &sess, "window.__never_reloaded === true"), "the page reloaded");
}

/// ★ **"Update to release X", clicked** (owner 2026-10-14: the ONE box updates the sites it
/// laid): a site this box laid shows what each box runs and the button naming the release
/// monetize sells now; a press sends THAT release with the operator bearer, and the page then
/// reads the update running with the button disarmed — no reload.
#[test]
fn the_update_button_names_the_release_and_sends_what_the_page_showed() {
    let (Some(bin), Some(dist)) = (std::env::var("MONETIZE_UI_CHROME").ok().filter(|v| !v.is_empty()), std::env::var("MONETIZE_UI_DIST").ok().filter(|v| !v.is_empty())) else {
        eprintln!("NOT RUN: set MONETIZE_UI_CHROME (a Chrome binary) and MONETIZE_UI_DIST (the console's built dist)");
        return;
    };
    let fake = Fake(Arc::new(Mutex::new(Held { sites: sites(), ..Default::default() })));
    let origin = up(fake.clone(), PathBuf::from(dist));
    let debug = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port();
    let profile = std::env::temp_dir().join(format!("sites-update-browser-{}", std::process::id()));
    let _chrome = Chrome::launch(&bin, &profile, debug).expect("chrome starts");
    let mut c = Cdp::attach(debug).expect("cdp");
    let sess = c.page().expect("a page");
    let c = &mut c;
    c.navigate(&sess, &format!("{origin}/sites")).expect("the console loads");
    let old = card("old-rs");
    if c.wait_for(&sess, &format!("!!document.querySelector('{old} .site-update-send')"), 30_000).is_err() {
        panic!("the laid site draws no update: {}", text(c, &sess, "main"));
    }
    c.eval(&sess, "(() => { window.__never_reloaded = true; return true; })()").unwrap();
    assert!(!truthy(c, &sess, &format!("document.querySelector('{} .site-update-send')", card("gunnar-rs"))), "a site this box did not lay offers an update");
    assert_eq!(text(c, &sess, &format!("{old} .site-update-send")), "Update to r9 · 2026-10-18 · gunnar bb22222", "a release named for a person");
    // The row names are drawn in capitals by the style sheet.
    let boxes = text(c, &sess, &format!("{old} .site-boxes")).to_lowercase();
    for role in ["twin", "main", "front"] {
        assert!(boxes.contains(&format!("{role} runs release 01")), "{boxes}");
    }
    let live = text(c, &sess, &format!("{old} .site-live"));
    assert!(live.contains("Live updates — on") && live.contains("Off = every update reinstalls the box from its keep medium, /data kept"), "{live}");
    // test2: up to date — said, named, no button; every box its line.
    let t2 = card("test2-rs");
    reads(c, &sess, &format!("{t2} .site-release"), "Up to date · r5.4 · 2026-10-17 · gunnar aa14357 · gunnar-ui f4b5d44");
    assert!(!truthy(c, &sess, &format!("document.querySelector('{t2} .site-update-send')")), "an update offered to the release it runs");
    assert!(text(c, &sess, &format!("{t2} .site-boxes")).contains("the update starts with the front"), "{}", text(c, &sess, &format!("{t2} .site-boxes")));
    assert!(text(c, &sess, &t2).contains("Running at https://test2.example") && !text(c, &sess, &t2).contains("first sign-in link"));
    shots(c, &sess, "sites");
    c.wait_for(&sess, &format!("!document.querySelector('{old} .site-update-send').disabled"), 5_000).expect("an admin may press it");
    click(c, &sess, &format!("{old} .site-update-send"));
    reads(c, &sess, &format!("{old} .site-update-words"), "update to 02abcdef0123: running");
    c.wait_for(&sess, &format!("document.querySelector('{old} .site-update-send').disabled"), 5_000).expect("disarmed while it runs");
    {
        let held = fake.0.lock().unwrap();
        let sent: Vec<(String, String)> = held.updates.iter().map(|u| (u.estate.clone(), u.release.clone())).collect();
        assert_eq!(sent, [("old-rs".to_string(), "02abcdef0123".to_string())], "the release the page showed");
        assert!(held.bearers.iter().all(|b| b == OPERATOR), "{:?}", held.bearers);
    }
    assert!(truthy(c, &sess, "window.__never_reloaded === true"), "the page reloaded");
}

/// ★ **CODE RED is on every page until an admin acknowledges it** (owner 2026-10-18): the
/// banner says site · box · what is down · the error · the next action, on the Overview and on
/// Sites alike; Ack sends the key and takes it off; nothing reloads.
#[test]
fn code_red_is_on_every_page_and_ack_takes_it_off() {
    let (Some(bin), Some(dist)) = (std::env::var("MONETIZE_UI_CHROME").ok().filter(|v| !v.is_empty()), std::env::var("MONETIZE_UI_DIST").ok().filter(|v| !v.is_empty())) else {
        eprintln!("NOT RUN: set MONETIZE_UI_CHROME (a Chrome binary) and MONETIZE_UI_DIST (the console's built dist)");
        return;
    };
    let red = pb::CodeRed { key: "update:gunnarrs:twin".into(), site: "gunnar.rs".into(), r#box: "twin".into(), what: "the update stopped at the twin".into(), error: "HTTP 404 STORAGE_NOT_FOUND".into(), next: "start the twin, then press Update again".into(), raised_at_unix_ms: 1, ..Default::default() };
    let fake = Fake(Arc::new(Mutex::new(Held { sites: sites(), code_red: vec![red], ..Default::default() })));
    let origin = up(fake.clone(), PathBuf::from(dist));
    let debug = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port();
    let profile = std::env::temp_dir().join(format!("code-red-browser-{}", std::process::id()));
    let _chrome = Chrome::launch(&bin, &profile, debug).expect("chrome starts");
    let mut c = Cdp::attach(debug).expect("cdp");
    let sess = c.page().expect("a page");
    let c = &mut c;
    for page in ["/", "/sites"] {
        c.navigate(&sess, &format!("{origin}{page}")).expect("the console loads");
        if c.wait_for(&sess, "!!document.querySelector('.code-red')", 30_000).is_err() {
            panic!("{page}: no CODE RED banner: {}", text(c, &sess, "body"));
        }
        let banner = text(c, &sess, ".code-red");
        assert!(banner.contains("CODE RED · gunnar.rs · twin — the update stopped at the twin: HTTP 404 STORAGE_NOT_FOUND") && banner.contains("Next: start the twin, then press Update again"), "{page}: {banner}");
    }
    shots(c, &sess, "code-red");
    click(c, &sess, ".code-red-ack");
    c.wait_for(&sess, "!document.querySelector('.code-red')", 10_000).expect("acknowledged: off the banner");
    assert!(fake.0.lock().unwrap().code_red.iter().all(|r| r.acked_at_unix_ms != 0), "the ack reached monetize");
}

/// The box's half for the browser test: the next release offered; once asked it does not answer
/// for one read (restarting), then runs it on trial, then confirmed.
struct FakeBox(Mutex<(u32, Vec<String>)>);

impl monetize_ui_server::self_box::SelfBox for FakeBox {
    fn look(&self) -> Result<monetize_ui_server::self_box::SelfLook, String> {
        use monetize_ui_server::self_box::SelfLook;
        let mut g = self.0.lock().unwrap();
        let base = SelfLook { app: "aaaaaaaaaaaaaaaa".into(), slot: "a".into(), base: "base1".into(), live_update: true, next_app: "bbbbbbbbbbbbbbbb".into(), next_build: "fd0ed14".into(), plan: "live".into(), ..Default::default() };
        let step = g.0;
        if step > 0 {
            g.0 += 1;
        }
        Ok(match step {
            0 => base,
            1 => SelfLook { asked: true, ..base },
            2 => return Err("the box is restarting".into()),
            3 | 4 => SelfLook { app: "bbbbbbbbbbbbbbbb".into(), slot: "b trial".into(), plan: "wait".into(), why: "[live-trial-open] slot b trial".into(), ..base },
            _ => SelfLook { app: "bbbbbbbbbbbbbbbb".into(), slot: "b".into(), plan: "nothing".into(), ..base },
        })
    }
    fn ask_update(&self, by: &str) -> Result<(), String> {
        let mut g = self.0.lock().unwrap();
        g.1.push(by.to_string());
        g.0 = 1;
        Ok(())
    }
}

/// ★ **"Update this box"** (owner 2026-10-18): the Sites page names the release the box would
/// take, the press asks the box (once), and the page follows it — asked, restarting, on trial —
/// to "Up to date" on the other slot, with real clicks.
#[test]
fn update_this_box_names_the_release_and_follows_it_to_confirmed() {
    let (Some(bin), Some(dist)) = (std::env::var("MONETIZE_UI_CHROME").ok().filter(|v| !v.is_empty()), std::env::var("MONETIZE_UI_DIST").ok().filter(|v| !v.is_empty())) else {
        eprintln!("NOT RUN: set MONETIZE_UI_CHROME (a Chrome binary) and MONETIZE_UI_DIST (the console's built dist)");
        return;
    };
    let the_box = Arc::new(FakeBox(Mutex::new((0, Vec::new()))));
    monetize_ui_server::self_box::install(the_box.clone());
    let fake = Fake(Arc::new(Mutex::new(Held { sites: sites(), ..Default::default() })));
    let origin = up(fake, PathBuf::from(dist));
    let debug = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port();
    let profile = std::env::temp_dir().join(format!("box-update-browser-{}", std::process::id()));
    let _chrome = Chrome::launch(&bin, &profile, debug).expect("chrome starts");
    let mut c = Cdp::attach(debug).expect("cdp");
    let sess = c.page().expect("a page");
    let c = &mut c;
    c.navigate(&sess, &format!("{origin}/sites")).expect("the console loads");
    if c.wait_for(&sess, "!!document.querySelector('#box-update-button') && !document.querySelector('#box-update-button').disabled", 30_000).is_err() {
        panic!("no Update this box button to press: {}", text(c, &sess, "body"));
    }
    assert_eq!(text(c, &sess, "#box-update-button"), "Update this box to fd0ed14");
    assert_eq!(text(c, &sess, ".box-words"), "Release fd0ed14 is ready for this box (it runs aaaaaaaaaaaa, slot a)");
    shots(c, &sess, "box-update");
    click(c, &sess, "#box-update-button");
    if c.wait_for(&sess, "(document.querySelector('.box-words')||{}).textContent==='Up to date — this box runs fd0ed14 (slot b)'", 60_000).is_err() {
        panic!("not followed to confirmed: {}", text(c, &sess, "#box-update"));
    }
    assert_eq!(the_box.0.lock().unwrap().1.len(), 1, "asked once: {:?}", the_box.0.lock().unwrap().1);
    assert!(truthy(c, &sess, "document.querySelector('#box-update-button').disabled"), "nothing more to take");
}