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