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-web/src/estate.rs

//! **The ESTATE is the top level** (owner 2026-10-02).
//!
//! Every estate this console administers is an equal, full estate — gunnar.rs,
//! a company's gunnar.acme.se and test2.gunnar.rs get the same menu. There
//! are no roles and no labels: the one difference between two estates is a
//! SETTING (which payment keys each holds, live or test), and it is shown as
//! a setting. The PRODUCT is only a group heading in the picker.
//!
//! Every decision the picker, the Overview and the estate shell take lives
//! here, compiled for the host and tested there; the components only lay
//! them out.

use std::collections::BTreeMap;

use serde::Deserialize;

use crate::fmt::{money, percent_encode, Tone};
use crate::model::EstateJson;

// ── Addresses ─────────────────────────────────────────────────────────

/// The left menu of EVERY estate, in order: `(path segment, label)`. The same
/// six for every estate — there is no estate that gets fewer.
pub const SECTIONS: &[(&str, &str)] = &[
    ("map", "Map"),
    ("customers", "Customers"),
    ("money", "Money"),
    ("offer", "Offer"),
    ("events", "Events"),
    ("settings", "Settings"),
];

/// `/e/<estate>/<section>` — every page of an estate carries the estate in its
/// address, so a reader (and a reload) always knows which estate it is.
pub fn estate_href(estate: &str, section: &str) -> String {
    format!("/e/{}/{section}", percent_encode(estate))
}

/// The estate and section in an address, `/e/<estate>/<section>` →
/// `(estate, section)`; `map` when the address names no section. `None`
/// outside an estate (the Overview, the passkeys).
pub fn estate_in(path: &str) -> Option<(String, String)> {
    let rest = path.strip_prefix("/e/")?;
    let (estate, section) = rest.split_once('/').unwrap_or((rest, ""));
    let section = section
        .split('/')
        .next()
        .filter(|s| !s.is_empty())
        .unwrap_or("map");
    (!estate.is_empty()).then(|| (estate.to_owned(), section.to_owned()))
}

/// The estate's system map as an image: ADDRESSED BY ESTATE
/// (`GET /api/estates/{estate}/map`), never by product.
pub fn map_src(estate: &str) -> String {
    format!("/api/estates/{}/map", percent_encode(estate))
}

/// One Overview row's own question to its estate.
pub fn summary_path(estate: &str) -> String {
    format!("/api/estates/{}/summary", percent_encode(estate))
}

// ── Wire shapes ───────────────────────────────────────────────────────

/// `/api/overview`: the estates that EXIST, as monetize classified them (OVW-CLEAN): the rows,
/// the quiet line of what ended or was never installed, and the New-estate slot's choices.
#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct OverviewJson {
    #[serde(default)]
    pub estates: Vec<EstateName>,
    /// Ended or never installed: [`quiet_line`].
    #[serde(default)]
    pub ended: Vec<EstateName>,
    #[serde(default)]
    pub slot: SlotJson,
    /// PROD-4: the box did not answer within its bound; this is its state this old.
    #[serde(default)]
    pub stale_secs: Option<u64>,
}

#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct EstateName {
    pub name: String,
    #[serde(default)]
    pub control: Option<String>,
    #[serde(default)]
    pub product: String,
    /// `installed` | `laying` | `ended` | `never_installed`, monetize's word.
    #[serde(default)]
    pub standing: String,
    /// Why it stands so, monetize's sentence.
    #[serde(default)]
    pub words: String,
    /// The fleet knows it: its row asks its own summary ([`asks_summary`]).
    #[serde(default)]
    pub in_fleet: bool,
    #[serde(default)]
    pub domain: String,
}

/// The New-estate slot's choices, as monetize offers them.
#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct SlotJson {
    /// The products a new estate can be made of.
    #[serde(default)]
    pub products: Vec<String>,
    /// The operator's declared sites with no row, started by their name.
    #[serde(default)]
    pub declared: Vec<String>,
}

// ── The Overview's rows, the quiet line and the slot (OVW-CLEAN) ─────

/// A row asks its own summary only when the fleet knows its estate; one that is being laid
/// before any box of it reports says monetize's words instead.
pub fn asks_summary(e: &EstateName) -> bool {
    e.in_fleet
}

/// **A row the fleet does not know yet**: its Health badge, monetize's words.
pub fn laying_cell(e: &EstateName) -> (Tone, String) {
    (
        Tone::Warn,
        if e.words.is_empty() {
            "being laid".into()
        } else {
            e.words.clone()
        },
    )
}

/// **The one quiet line** under the rows: `3 ended or never-installed`; `None` for none.
pub fn quiet_line(ended: &[EstateName]) -> Option<String> {
    (!ended.is_empty()).then(|| format!("{} ended or never-installed", ended.len()))
}

/// What the slot posts (`POST /api/estates/new`): what the operator typed, carried.
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize)]
pub struct NewEstateBody {
    pub name: String,
    pub product: String,
    pub email: String,
    pub confirm: bool,
}

/// The slot's post: one press is the confirmation (it names its price on the button's title).
pub fn new_estate_body(name: &str, product: &str, email: &str) -> NewEstateBody {
    NewEstateBody {
        name: name.trim().to_owned(),
        product: product.trim().to_owned(),
        email: email.trim().to_owned(),
        confirm: true,
    }
}

/// **Whether the slot's button is pressable, and why not** — only that every field is filled
/// and the operator is an admin; every rule of a name is monetize's, said by its refusal.
pub fn may_make(name: &str, product: &str, email: &str, admin: bool) -> (bool, &'static str) {
    if !admin {
        (false, "only an admin orders an estate")
    } else if name.trim().is_empty() {
        (false, "name the estate")
    } else if product.trim().is_empty() {
        (false, "pick its product")
    } else if email.trim().is_empty() {
        (false, "the address its ready mail goes to")
    } else {
        (
            true,
            "orders it: the payment page names its price, and this box lays it",
        )
    }
}

/// `POST /api/estates/new`'s answer.
#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct NewEstateJson {
    #[serde(default)]
    pub estate: String,
    #[serde(default)]
    pub domain: String,
    #[serde(default)]
    pub created: bool,
    #[serde(default)]
    pub checkout_url: String,
    #[serde(default)]
    pub price_words: String,
    #[serde(default)]
    pub words: String,
}

/// **Where the operator pays** for what the slot ordered: `Pay <amount>` and the vendor's own
/// https page. Nothing else is ever a link.
pub fn new_pay_link(n: &NewEstateJson) -> Option<(String, String)> {
    n.checkout_url
        .starts_with("https://")
        .then(|| (format!("Pay {}", n.price_words), n.checkout_url.clone()))
}

/// The slot's endpoint.
pub const NEW_ESTATE_API: &str = "/api/estates/new";

/// The month's money, by each PAYMENT's own mode, minor units per currency.
#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct RevenueJson {
    #[serde(default)]
    pub live: BTreeMap<String, u64>,
    #[serde(default)]
    pub test: BTreeMap<String, u64>,
    #[serde(default)]
    pub unsaid: BTreeMap<String, u64>,
    #[serde(default)]
    pub window_days: u64,
}

#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct VendorModeJson {
    pub vendor: String,
    #[serde(default)]
    pub livemode: Option<bool>,
}

#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct ReachJson {
    pub product: String,
    #[serde(default)]
    pub state: String,
}

/// `/api/estates/{estate}/summary`: one Overview row. Every part that could
/// not be asked is `None`, which is not zero.
#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
pub struct SummaryJson {
    #[serde(default)]
    pub estate: String,
    #[serde(default)]
    pub control: Option<String>,
    /// The public fingerprint of the key this console signs the estate's calls with.
    #[serde(default)]
    pub signed_with: Option<String>,
    #[serde(default)]
    pub reached: bool,
    /// The estate's monetize refused this console's key: it was never fed it.
    #[serde(default)]
    pub unfed: bool,
    #[serde(default)]
    pub error: Option<String>,
    #[serde(default)]
    pub products: Vec<String>,
    #[serde(default)]
    pub reachability: Vec<ReachJson>,
    /// The machines that reported in (front, main, twin…), live.
    #[serde(default)]
    pub machines: Option<u32>,
    /// Their roles, in estate order.
    #[serde(default)]
    pub machine_roles: Vec<String>,
    /// `Some(false)`: nothing of this estate is installed yet.
    #[serde(default)]
    pub installed: Option<bool>,
    /// Each reported machine's host (`serves_at`), by role.
    #[serde(default)]
    pub machine_hosts: BTreeMap<String, String>,
    /// The site's machines were laid by the operator's kit, not by this box.
    #[serde(default)]
    pub laid_by_operator: Option<bool>,
    #[serde(default)]
    pub customers: Option<u64>,
    #[serde(default)]
    pub revenue: Option<RevenueJson>,
    #[serde(default)]
    pub vendors: Vec<VendorModeJson>,
    /// The vendors by name: an estate whose monetize predates `vendors` still
    /// names its Stripe here.
    #[serde(default)]
    pub vendor_names: Vec<String>,
    #[serde(default)]
    pub clouds: Vec<String>,
    #[serde(default)]
    pub default_cloud: String,
    #[serde(default)]
    pub paid_at_vendor_open: Option<u64>,
    /// The vendor this site's orders are billed through (`stripe/gunnar`);
    /// empty when the site names none.
    #[serde(default)]
    pub site_vendor: String,
}

// ── The picker ────────────────────────────────────────────────────────

/// One heading of the picker: a product, and its estates.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PickerGroup {
    pub heading: String,
    pub estates: Vec<String>,
}

/// The heading for estates whose monetize did not answer: nobody can say
/// which product they serve, and the picker does not guess.
pub const UNANSWERED_GROUP: &str = "not answering";

/// **The picker, grouped by product** — the product is a heading and only a
/// heading. Headings in the order their first estate appears; estates in the
/// console's own order under each (`gunnar` → `gunnar.rs`, `gunnar.acme.se`,
/// `test2.gunnar.rs`). An estate that did not answer goes under
/// [`UNANSWERED_GROUP`], last — still pickable, never dropped.
pub fn picker_groups(estates: &[EstateJson]) -> Vec<PickerGroup> {
    let mut groups: Vec<PickerGroup> = Vec::new();
    let mut silent = Vec::new();
    for e in estates {
        let Some(name) = e.name.clone() else { continue };
        let heading = match e.products.first() {
            Some(p) if e.serving => p.clone(),
            _ => {
                silent.push(name);
                continue;
            }
        };
        match groups.iter_mut().find(|g| g.heading == heading) {
            Some(g) => g.estates.push(name),
            None => groups.push(PickerGroup {
                heading,
                estates: vec![name],
            }),
        }
    }
    if !silent.is_empty() {
        groups.push(PickerGroup {
            heading: UNANSWERED_GROUP.into(),
            estates: silent,
        });
    }
    groups
}

// ── Overview cells ────────────────────────────────────────────────────

/// `120.00 EUR · 300.00 SEK` — one figure per currency, never summed across
/// them; `—` when there is none.
pub fn money_set(m: &BTreeMap<String, u64>) -> String {
    let parts: Vec<String> = m
        .iter()
        .filter(|(_, v)| **v > 0)
        .map(|(c, v)| money(*v, c))
        .collect();
    if parts.is_empty() {
        "—".into()
    } else {
        parts.join(" · ")
    }
}

/// **One row's Revenue/mo cell**: the live money, then the TEST money in
/// brackets — `(80.00 EUR)` — which is shown and never totalled. Money whose
/// payment did not say its mode is shown apart (`not said: …`). `None` when
/// the estate could not be asked for its sales — not zero.
pub fn revenue_cell(r: Option<&RevenueJson>) -> Option<(String, Option<String>, Option<String>)> {
    let r = r?;
    let test = (r.test.values().any(|v| *v > 0)).then(|| format!("({})", money_set(&r.test)));
    let unsaid =
        (r.unsaid.values().any(|v| *v > 0)).then(|| format!("not said: {}", money_set(&r.unsaid)));
    Some((money_set(&r.live), test, unsaid))
}

/// **Which payment keys the estate holds** — its SETTING, read off the
/// estate's own vendor block: `live`, `test`, `not said` (the block does not
/// say, or the estate's monetize is older than the field and only names its
/// Stripe in `names`), or `no Stripe`.
///
/// A vendor is named by kind and instance (`stripe/gunnar`); a bare `stripe`
/// is an older monetize. `site` is the vendor this site's orders are billed
/// through (`[estates.<site>].vendor`), read first when it is Stripe — so a
/// site whose Stripe is fed never shows as `no Stripe`.
pub fn stripe_word(vendors: &[VendorModeJson], names: &[String], site: &str) -> &'static str {
    let is_stripe = |n: &str| n == "stripe" || n.starts_with("stripe/");
    let found = vendors
        .iter()
        .find(|v| !site.is_empty() && v.vendor == site && is_stripe(&v.vendor))
        .or_else(|| vendors.iter().find(|v| is_stripe(&v.vendor)));
    match found {
        Some(VendorModeJson {
            livemode: Some(true),
            ..
        }) => "live",
        Some(VendorModeJson {
            livemode: Some(false),
            ..
        }) => "test",
        Some(_) => "not said",
        None if names.iter().any(|n| is_stripe(n)) => "not said",
        None => "no Stripe",
    }
}

/// The badge for [`stripe_word`]: `Stripe: live`, `Stripe: test`,
/// `Stripe: not said`, or `no Stripe`.
pub fn stripe_label(word: &str) -> String {
    if word == "no Stripe" {
        word.to_owned()
    } else {
        format!("Stripe: {word}")
    }
}

/// **Every payment vendor and which keys it holds**, one line each —
/// `stripe (test keys)`; a vendor whose block does not say, or whose monetize
/// predates the field (named only in `names`), is `not said`.
pub fn vendor_lines(vendors: &[VendorModeJson], names: &[String]) -> Vec<String> {
    let said = |v: &VendorModeJson| match v.livemode {
        Some(true) => "live keys",
        Some(false) => "test keys",
        None => "not said",
    };
    let mut out: Vec<String> = vendors
        .iter()
        .map(|v| format!("{} ({})", v.vendor, said(v)))
        .collect();
    out.extend(
        names
            .iter()
            .filter(|n| !vendors.iter().any(|v| &v.vendor == *n))
            .map(|n| format!("{n} (not said)")),
    );
    out
}

/// The words for an estate that did not answer.
pub const NOT_REACHED: &str = "not reached";
/// The words for an estate nothing of which is installed yet (gunnar.rs
/// before its install on the new account).
pub const NOT_INSTALLED: &str = "not installed";
pub const UNFED: &str = "not fed this console's key";
/// The words for an estate whose `[machines.*]` is empty — said instead of a
/// blank board (gunnar.rs before its upgrade writes the block).
pub const NO_MACHINES: &str = "no machines yet";

/// **One row's Health cell.**
pub fn health_cell(s: &SummaryJson) -> (Tone, String) {
    if !s.reached {
        if s.installed == Some(false) {
            return (Tone::Plain, NOT_INSTALLED.into());
        }
        return (
            Tone::Bad,
            if s.unfed {
                UNFED.into()
            } else {
                NOT_REACHED.into()
            },
        );
    }
    match s.paid_at_vendor_open {
        Some(1) => return (Tone::Bad, "1 paid at the vendor needs you".into()),
        Some(n) if n > 1 => return (Tone::Bad, format!("{n} paid at the vendor need you")),
        _ => {}
    }
    if let Some(r) = s.reachability.iter().find(|r| r.state == "down") {
        return (Tone::Bad, format!("{} down", r.product));
    }
    if let Some(r) = s.reachability.iter().find(|r| r.state != "up") {
        return (Tone::Warn, format!("{} not asked yet", r.product));
    }
    (Tone::Pass, "serving".into())
}

/// **What the thumbnail cell shows.** Never a blank square: the estate's map,
/// or a named placeholder that says why there is none.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Thumb {
    /// The row has not heard from its estate yet.
    Asking,
    /// The estate's own board: `src` to draw, `href` the estate's Map.
    Map { src: String, href: String },
    /// Words instead of a picture, and still a way into the estate.
    Placeholder { words: &'static str, href: String },
}

pub fn thumb(estate: &str, s: Option<&SummaryJson>) -> Thumb {
    let href = estate_href(estate, "map");
    match s {
        None => Thumb::Asking,
        Some(s) if !s.reached && s.installed == Some(false) => Thumb::Placeholder {
            words: NOT_INSTALLED,
            href,
        },
        Some(s) if !s.reached => Thumb::Placeholder {
            words: if s.unfed { UNFED } else { NOT_REACHED },
            href,
        },
        Some(s) if s.machines == Some(0) => Thumb::Placeholder {
            words: NO_MACHINES,
            href,
        },
        Some(_) => Thumb::Map {
            src: map_src(estate),
            href,
        },
    }
}

/// **The Overview's total: LIVE money only.** Test money is each row's
/// bracket and never reaches it; an estate that did not answer is NAMED
/// beside the total rather than silently counted as nothing.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct OverviewTotal {
    pub live: BTreeMap<String, u64>,
    /// Estates whose money is not in the total, and why not, by name.
    pub missing: Vec<String>,
    /// Rows still asking.
    pub asking: usize,
}

pub fn overview_total<'a>(
    rows: impl IntoIterator<Item = (&'a str, Option<&'a SummaryJson>)>,
) -> OverviewTotal {
    let mut t = OverviewTotal::default();
    for (name, s) in rows {
        match s {
            None => t.asking += 1,
            Some(s) => match s.revenue.as_ref().filter(|_| s.reached) {
                Some(r) => {
                    for (c, v) in &r.live {
                        *t.live.entry(c.clone()).or_default() += v;
                    }
                }
                None => t.missing.push(name.to_owned()),
            },
        }
    }
    t
}

/// The sentence under the total.
pub fn total_note(t: &OverviewTotal) -> String {
    let mut parts = vec![
        "live money only — test money is in brackets per row and never in this total".to_owned(),
    ];
    if t.asking > 0 {
        parts.push(format!("{} still answering", t.asking));
    }
    if !t.missing.is_empty() {
        parts.push(format!("not in it: {}", t.missing.join(", ")));
    }
    parts.join(" · ")
}

/// `3`, or `—` when it could not be asked.
/// **A site's machines, as they reported in** — `(role, host, who laid it)`,
/// in estate order. Owner 2026-10-12: test2's three VMs were laid by the
/// operator's kit and carry no label of this box, so the cloud's list ("nothing
/// owned") never showed them; they show here, from their own reports.
pub fn site_machines(s: &SummaryJson) -> Vec<(String, String, &'static str)> {
    let who = match s.laid_by_operator {
        Some(true) => "laid by the operator",
        Some(false) => "laid by this box",
        None => "",
    };
    s.machine_roles
        .iter()
        .map(|r| {
            (
                r.clone(),
                s.machine_hosts.get(r).cloned().unwrap_or_default(),
                who,
            )
        })
        .collect()
}

/// The Machines cell's tooltip: which machines reported in (`front, main, twin`).
pub fn machines_title(s: &SummaryJson) -> String {
    if s.machine_roles.is_empty() {
        "no machine has reported in yet".into()
    } else {
        s.machine_roles.join(", ")
    }
}

pub fn count_cell<T: std::fmt::Display>(n: Option<T>) -> String {
    n.map(|n| n.to_string()).unwrap_or_else(|| "—".into())
}

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

    fn est(name: &str, product: Option<&str>) -> EstateJson {
        EstateJson {
            name: Some(name.into()),
            control: None,
            serving: product.is_some(),
            products: product.into_iter().map(String::from).collect(),
            paid_at_vendor_open: Some(0),
            error: None,
        }
    }

    /// ★ The picker is grouped by PRODUCT, the product a heading only, every
    /// estate under it in the console's own order — and an estate that did
    /// not answer is still in the picker, under its own heading, last.
    #[test]
    fn the_picker_groups_estates_under_their_product() {
        let groups = picker_groups(&[
            est("gunnar.rs", Some("gunnar")),
            est("holger.rs", Some("holger")),
            est("gunnar.acme.se", Some("gunnar")),
            est("test2.holger.rs", Some("holger")),
            est("test2.gunnar.rs", Some("gunnar")),
            est("gone.gunnar.se", None),
        ]);
        assert_eq!(
            groups,
            vec![
                PickerGroup {
                    heading: "gunnar".into(),
                    estates: vec![
                        "gunnar.rs".into(),
                        "gunnar.acme.se".into(),
                        "test2.gunnar.rs".into()
                    ]
                },
                PickerGroup {
                    heading: "holger".into(),
                    estates: vec!["holger.rs".into(), "test2.holger.rs".into()]
                },
                PickerGroup {
                    heading: UNANSWERED_GROUP.into(),
                    estates: vec!["gone.gunnar.se".into()]
                },
            ]
        );
        assert!(
            picker_groups(&[EstateJson {
                name: None,
                serving: true,
                products: vec!["gunnar".into()],
                ..Default::default()
            }])
            .is_empty(),
            "an unnamed estate is not pickable"
        );
    }

    fn summary(live: &[(&str, u64)], test: &[(&str, u64)]) -> SummaryJson {
        SummaryJson {
            reached: true,
            revenue: Some(RevenueJson {
                live: live.iter().map(|(c, v)| (c.to_string(), *v)).collect(),
                test: test.iter().map(|(c, v)| (c.to_string(), *v)).collect(),
                unsaid: BTreeMap::new(),
                window_days: 30,
            }),
            ..Default::default()
        }
    }

    /// ★ **The total is LIVE money only.** Test money is shown in brackets on
    /// its own row and never reaches the total; an estate that did not answer
    /// is named beside it instead of counted as nothing.
    #[test]
    fn the_total_counts_live_money_only_and_names_who_is_missing() {
        let a = summary(&[("EUR", 12_000)], &[]);
        let b = summary(&[("EUR", 3_000), ("SEK", 50_000)], &[("EUR", 8_000)]);
        let c = summary(&[], &[("EUR", 8_000)]);
        let down = SummaryJson {
            reached: false,
            error: Some("not reached: tcp connect error".into()),
            ..Default::default()
        };
        let t = overview_total([
            ("gunnar.rs", Some(&a)),
            ("gunnar.acme.se", Some(&b)),
            ("test2.gunnar.rs", Some(&c)),
            ("holger.rs", Some(&down)),
            ("test2.holger.rs", None),
        ]);
        assert_eq!(
            t.live,
            BTreeMap::from([("EUR".to_string(), 15_000), ("SEK".to_string(), 50_000)]),
            "test money never reaches the total"
        );
        assert_eq!(t.missing, vec!["holger.rs".to_string()]);
        assert_eq!(t.asking, 1);
        assert_eq!(money_set(&t.live), "150.00 EUR · 500.00 SEK");
        let note = total_note(&t);
        assert!(
            note.contains("live money only")
                && note.contains("holger.rs")
                && note.contains("1 still answering"),
            "{note}"
        );

        // The row's own cell: live, then the test money in brackets.
        assert_eq!(
            revenue_cell(b.revenue.as_ref()),
            Some((
                "30.00 EUR · 500.00 SEK".into(),
                Some("(80.00 EUR)".into()),
                None
            ))
        );
        assert_eq!(
            revenue_cell(c.revenue.as_ref()),
            Some(("—".into(), Some("(80.00 EUR)".into()), None)),
            "an estate that took only test money shows a dash and its bracket"
        );
        assert_eq!(revenue_cell(None), None, "not asked is not zero");
        let unsaid = RevenueJson {
            unsaid: BTreeMap::from([("SEK".to_string(), 700)]),
            ..Default::default()
        };
        assert_eq!(
            revenue_cell(Some(&unsaid)).unwrap().2.as_deref(),
            Some("not said: 7.00 SEK")
        );
    }

    /// ★ **A thumbnail per estate, and never a blank square.** The estate's own
    /// map, addressed by estate; a named placeholder for an estate that was not
    /// reached, was not fed this console's key, or names no machines yet.
    #[test]
    fn every_estate_gets_its_map_or_a_named_placeholder() {
        let ok = SummaryJson {
            reached: true,
            machines: Some(3),
            ..Default::default()
        };
        assert_eq!(
            thumb("test2.gunnar.rs", Some(&ok)),
            Thumb::Map {
                src: "/api/estates/test2.gunnar.rs/map".into(),
                href: "/e/test2.gunnar.rs/map".into()
            }
        );
        assert_eq!(thumb("gunnar.rs", None), Thumb::Asking);
        let unfed = SummaryJson {
            reached: false,
            unfed: true,
            ..Default::default()
        };
        assert_eq!(
            thumb("gunnar.acme.se", Some(&unfed)),
            Thumb::Placeholder {
                words: UNFED,
                href: "/e/gunnar.acme.se/map".into()
            }
        );
        let down = SummaryJson {
            reached: false,
            ..Default::default()
        };
        assert_eq!(
            thumb("holger.rs", Some(&down)),
            Thumb::Placeholder {
                words: NOT_REACHED,
                href: "/e/holger.rs/map".into()
            }
        );
        let empty = SummaryJson {
            reached: true,
            machines: Some(0),
            ..Default::default()
        };
        assert_eq!(
            thumb("gunnar.rs", Some(&empty)),
            Thumb::Placeholder {
                words: NO_MACHINES,
                href: "/e/gunnar.rs/map".into()
            }
        );
        let unasked = SummaryJson {
            reached: true,
            machines: None,
            ..Default::default()
        };
        assert!(
            matches!(thumb("gunnar.rs", Some(&unasked)), Thumb::Map { .. }),
            "a count nobody got is not zero: draw the board"
        );
    }

    /// ★ Owner 2026-10-12: gunnar.rs not installed on the new account read
    /// "not reached". It says so plainly; an installed estate that is quiet
    /// still says not reached.
    #[test]
    fn an_estate_with_nothing_installed_says_not_installed() {
        let fresh = SummaryJson {
            reached: false,
            installed: Some(false),
            ..Default::default()
        };
        assert_eq!(
            health_cell(&fresh),
            (Tone::Plain, "not installed".to_owned())
        );
        assert_eq!(
            thumb("gunnar-rs", Some(&fresh)),
            Thumb::Placeholder {
                words: NOT_INSTALLED,
                href: "/e/gunnar-rs/map".into()
            }
        );
        let quiet = SummaryJson {
            reached: false,
            installed: Some(true),
            ..Default::default()
        };
        assert_eq!(health_cell(&quiet).1, NOT_REACHED);
        let three = SummaryJson {
            reached: true,
            machines: Some(3),
            machine_roles: vec!["front".into(), "main".into(), "twin".into()],
            ..Default::default()
        };
        assert_eq!(machines_title(&three), "front, main, twin");
    }

    /// ★ test2's front, main and twin, laid by the operator's kit: shown from
    /// their reports with their hosts, marked as the operator's.
    #[test]
    fn a_sites_machines_show_from_their_reports_marked_by_who_laid_them() {
        let s = SummaryJson {
            reached: true,
            machine_roles: vec!["front".into(), "main".into(), "twin".into()],
            machine_hosts: [("main".to_string(), "10.6.0.2:443".to_string())].into(),
            laid_by_operator: Some(true),
            ..Default::default()
        };
        let rows = site_machines(&s);
        assert_eq!(rows.len(), 3);
        assert_eq!(
            rows[1],
            (
                "main".to_owned(),
                "10.6.0.2:443".to_owned(),
                "laid by the operator"
            )
        );
        assert!(rows.iter().all(|r| r.2 == "laid by the operator"));
    }

    /// ★ live 2026-10-14: gunnar-rs bills through `stripe/gunnar` (fed, test
    /// keys) and the Overview said `no Stripe` — the vendor's instance name
    /// was never read as Stripe.
    #[test]
    fn a_site_billed_through_stripe_slash_instance_shows_its_keys() {
        let vendors = vec![
            VendorModeJson {
                vendor: "invoice/gunnar".into(),
                livemode: None,
            },
            VendorModeJson {
                vendor: "stripe/holger".into(),
                livemode: Some(true),
            },
            VendorModeJson {
                vendor: "stripe/gunnar".into(),
                livemode: Some(false),
            },
        ];
        let names: Vec<String> = vendors.iter().map(|v| v.vendor.clone()).collect();
        assert_eq!(
            stripe_word(&vendors, &names, "stripe/gunnar"),
            "test",
            "the site's own Stripe is read first"
        );
        assert_eq!(
            stripe_word(&vendors[2..], &names[2..], ""),
            "test",
            "a site that names no vendor still finds its Stripe"
        );
        assert_eq!(stripe_word(&[], &["stripe/gunnar".into()], ""), "not said");
    }

    /// The estate's keys are a SETTING, said as one.
    #[test]
    fn the_stripe_cell_is_the_estates_setting() {
        let v = |live| {
            vec![VendorModeJson {
                vendor: "stripe".into(),
                livemode: live,
            }]
        };
        assert_eq!(stripe_word(&v(Some(true)), &[], ""), "live");
        assert_eq!(stripe_word(&v(Some(false)), &[], ""), "test");
        assert_eq!(stripe_word(&v(None), &[], ""), "not said");
        assert_eq!(
            stripe_word(
                &[VendorModeJson {
                    vendor: "invoice".into(),
                    livemode: None
                }],
                &["invoice".into()],
                ""
            ),
            "no Stripe"
        );
        assert_eq!(
            stripe_word(&[], &["stripe".into()], ""),
            "not said",
            "an older monetize names its Stripe and says nothing of its keys"
        );
        assert_eq!(stripe_label("test"), "Stripe: test");
        assert_eq!(
            vendor_lines(&v(Some(false)), &["stripe".into(), "invoice".into()]),
            ["stripe (test keys)", "invoice (not said)"]
        );
        assert_eq!(stripe_label("no Stripe"), "no Stripe");
    }

    #[test]
    fn the_health_cell_names_what_is_wrong() {
        let ok = SummaryJson {
            reached: true,
            paid_at_vendor_open: Some(0),
            reachability: vec![ReachJson {
                product: "gunnar".into(),
                state: "up".into(),
            }],
            ..Default::default()
        };
        assert_eq!(health_cell(&ok), (Tone::Pass, "serving".into()));
        assert_eq!(
            health_cell(&SummaryJson {
                paid_at_vendor_open: Some(2),
                ..ok.clone()
            }),
            (Tone::Bad, "2 paid at the vendor need you".into())
        );
        assert_eq!(
            health_cell(&SummaryJson {
                reachability: vec![ReachJson {
                    product: "gunnar".into(),
                    state: "down".into()
                }],
                ..ok.clone()
            }),
            (Tone::Bad, "gunnar down".into())
        );
        assert_eq!(
            health_cell(&SummaryJson {
                reached: false,
                unfed: true,
                ..Default::default()
            }),
            (Tone::Bad, UNFED.into())
        );
        assert_eq!(
            health_cell(&SummaryJson {
                reached: false,
                ..Default::default()
            }),
            (Tone::Bad, NOT_REACHED.into())
        );
    }

    #[test]
    fn every_estate_has_the_same_six_sections_addressed_by_estate() {
        assert_eq!(
            SECTIONS.iter().map(|(_, l)| *l).collect::<Vec<_>>(),
            ["Map", "Customers", "Money", "Offer", "Events", "Settings"]
        );
        assert_eq!(
            estate_href("gunnar.acme.se", "money"),
            "/e/gunnar.acme.se/money"
        );
        assert_eq!(map_src("gunnar.rs"), "/api/estates/gunnar.rs/map");
        assert_eq!(
            summary_path("test2.gunnar.rs"),
            "/api/estates/test2.gunnar.rs/summary"
        );
        assert_eq!(
            estate_in("/e/test2.gunnar.rs/money"),
            Some(("test2.gunnar.rs".into(), "money".into()))
        );
        assert_eq!(
            estate_in("/e/gunnar.rs"),
            Some(("gunnar.rs".into(), "map".into()))
        );
        assert_eq!(
            estate_in("/e/gunnar.rs/"),
            Some(("gunnar.rs".into(), "map".into()))
        );
        assert_eq!(estate_in("/"), None);
        assert_eq!(estate_in("/login"), None);
    }

    /// ★ **OVW-CLEAN: the Overview's rows are what exists; the rest is one quiet line.** The
    /// wire is monetize's classification, read field for field; nothing here classifies.
    #[test]
    fn the_overview_reads_rows_a_quiet_line_and_the_slot_as_monetize_said_them() {
        let o: OverviewJson = serde_json::from_str(r#"{"estates":[{"name":"gunnar-rs","product":"gunnar","standing":"installed","words":"installed: its machines have reported in","in_fleet":true,"domain":"gunnar.rs"},{"name":"test4-gunnar-rs","product":"gunnar","standing":"laying","words":"ordered: waiting for its payment","in_fleet":false,"domain":"test4.gunnar.rs"}],"ended":[{"name":"acme2","standing":"ended","words":"terminated on 2026-10-27","in_fleet":true},{"name":"polka","standing":"never_installed","in_fleet":true}],"slot":{"products":["gunnar","holger"],"declared":["test2-holger-rs"]},"stale_secs":null}"#).unwrap();
        assert_eq!(
            o.estates
                .iter()
                .map(|e| e.name.as_str())
                .collect::<Vec<_>>(),
            ["gunnar-rs", "test4-gunnar-rs"]
        );
        assert!(
            asks_summary(&o.estates[0]) && !asks_summary(&o.estates[1]),
            "a row being laid before the fleet knows it asks nobody"
        );
        assert_eq!(
            laying_cell(&o.estates[1]),
            (Tone::Warn, "ordered: waiting for its payment".to_string())
        );
        assert_eq!(laying_cell(&EstateName::default()).1, "being laid");
        assert_eq!(
            quiet_line(&o.ended).as_deref(),
            Some("2 ended or never-installed")
        );
        assert_eq!(quiet_line(&[]), None, "no line when nothing ended");
        assert_eq!(
            (o.slot.products.clone(), o.slot.declared.clone()),
            (
                vec!["gunnar".to_string(), "holger".to_string()],
                vec!["test2-holger-rs".to_string()]
            )
        );
        // An older answer (names only) still reads: every row asks nobody it cannot.
        let old: OverviewJson =
            serde_json::from_str(r#"{"estates":[{"name":"gunnar.rs"}]}"#).unwrap();
        assert!(
            old.ended.is_empty() && old.slot.products.is_empty() && !asks_summary(&old.estates[0])
        );
    }

    /// ★ **The slot carries what was typed** and only knows a field is empty; every rule of a
    /// name is monetize's refusal. Its answer's only link is the vendor's own page.
    #[test]
    fn the_slot_carries_the_name_and_product_and_pays_at_the_vendors_page() {
        assert_eq!(
            new_estate_body(" test4-gunnar-rs ", "gunnar", " ops@example.invalid "),
            NewEstateBody {
                name: "test4-gunnar-rs".into(),
                product: "gunnar".into(),
                email: "ops@example.invalid".into(),
                confirm: true
            }
        );
        assert!(may_make("x", "gunnar", "a@b", true).0);
        assert_eq!(
            may_make("x", "gunnar", "a@b", false),
            (false, "only an admin orders an estate")
        );
        assert_eq!(
            may_make(" ", "gunnar", "a@b", true),
            (false, "name the estate")
        );
        assert_eq!(may_make("x", "", "a@b", true), (false, "pick its product"));
        assert!(!may_make("x", "gunnar", "", true).0);
        assert!(
            may_make("Not A Name!", "gunnar", "a@b", true).0,
            "the name's rules are the server's"
        );
        let paid = NewEstateJson {
            checkout_url: "https://pay.example/c/1".into(),
            price_words: "0.50 EUR".into(),
            ..Default::default()
        };
        assert_eq!(
            new_pay_link(&paid),
            Some((
                "Pay 0.50 EUR".to_string(),
                "https://pay.example/c/1".to_string()
            ))
        );
        assert_eq!(
            new_pay_link(&NewEstateJson {
                checkout_url: "javascript:alert(1)".into(),
                ..paid
            }),
            None
        );
    }

    /// ★ **The Overview page draws no "not installed" row**: its rows are monetize's, its
    /// leftovers one quiet line, and it has one New-estate slot.
    #[test]
    fn the_overview_page_draws_rows_the_quiet_line_and_one_slot() {
        let page = include_str!("pages/overview.rs");
        assert!(
            page.contains("quiet_line(") && page.contains("<details class=\"ended\""),
            "the quiet line"
        );
        assert!(
            page.contains("class=\"estate-slot\"")
                && page.contains("NEW_ESTATE_API")
                && page.contains("\"New estate\""),
            "the slot"
        );
        assert!(
            page.contains("asks_summary("),
            "a row asks its summary only when the fleet knows it"
        );
        assert!(
            !page.contains("NOT_INSTALLED"),
            "no row is drawn as not installed"
        );
    }

    /// ★ **No roles, no labels** (owner 2026-10-02): every estate is an equal,
    /// full estate. No word in the console's own sources calls one of them
    /// production, staging, private — nor a "test estate": `test` is a
    /// setting of the Stripe keys, said as `Stripe: test`, and nothing else.
    #[test]
    fn no_estate_is_given_a_role_anywhere_in_the_console() {
        let sources = [
            ("app.rs", include_str!("app.rs")),
            ("pages/overview.rs", include_str!("pages/overview.rs")),
            ("pages/shell.rs", include_str!("pages/shell.rs")),
            ("pages/products.rs", include_str!("pages/products.rs")),
            ("pages/settings.rs", include_str!("pages/settings.rs")),
            ("pages/tenants.rs", include_str!("pages/tenants.rs")),
            ("pages/ledger.rs", include_str!("pages/ledger.rs")),
            ("ui.rs", include_str!("ui.rs")),
            ("console.css", include_str!("../styles/console.css")),
        ];
        let banned = [
            "production",
            "staging",
            "private",
            "prod ",
            "test estate",
            "live estate",
            "sandbox estate",
            "role-test",
            "role-prod",
        ];
        for (file, text) in sources {
            let lower = text.to_ascii_lowercase();
            for word in banned {
                assert!(
                    !lower.contains(word),
                    "{file} names an estate role: {word:?}"
                );
            }
        }
        // And what the picker and the Overview print for an estate carries no
        // such word either — the estate's name, its product, its setting.
        let printed = [
            UNANSWERED_GROUP,
            NOT_REACHED,
            UNFED,
            NO_MACHINES,
            stripe_word(&[], &[], ""),
        ]
        .join(" ")
        .to_ascii_lowercase();
        for word in banned {
            assert!(!printed.contains(word), "{word}");
        }
    }
}