crates/monetize-ui-server/src/iron.rs
//! **Whose iron is whose** (MAP-SEG, owner 2026-10-26, the Map of test2-gunnar-rs).
//!
//! The ONE monetize lists the cloud account ONCE per product (`Usage.Fleet`'s `cloud`
//! block): every estate's machines and disks, the orphans and the missing. Each row names
//! the estate it belongs to (`estate`, `usage.proto`). The Map of ONE estate showed the
//! whole block, so test2-gunnar-rs listed the iron of rickardtest, acme2, ignalina,
//! test2holgerrs, holgerrs and test2gunnarrs alike.
//!
//! The cut is here, on the data, before any page is answered:
//!
//! * [`estate_cloud`] — ONE estate's block: its own owned and missing rows, its totals
//! counted again over them, and no orphan (an orphan is in no order, so it is no
//! estate's) and no count of the account's foreign rows;
//! * [`card_cloud`] — a product card's block: the totals, no list (the card prints a total
//! and a viewer is not handed the account's rows through it);
//! * the account-wide block, whole, is the operator's alone: `GET /api/iron`
//! (an operator's, [`crate::auth::require_auth`]), drawn by the console's Iron page.
use serde_json::{json, Value};
/// The rows of `list` that name `estate`.
fn rows_of(cloud: &Value, list: &str, estate: &str) -> Vec<Value> {
cloud[list]
.as_array()
.into_iter()
.flatten()
.filter(|r| r["estate"].as_str().unwrap_or("") == estate)
.cloned()
.collect()
}
/// **ONE estate's iron**: the block cut to the rows that name `estate` (an empty `estate`:
/// the rows of no ONE estate — a product's own tenants, the box's own data set). Owned
/// totals are counted again over the rows kept; `orphans` are empty and `foreign_count`
/// is 0 (neither is any estate's). `available`, `error`, `provider` and `currency` are
/// kept: a cloud that could not be listed says so on every estate's page.
pub fn estate_cloud(cloud: &Value, estate: &str) -> Value {
estate_cloud_of(cloud, estate, false)
}
/// [`estate_cloud`], and with `own` also the rows of no ONE estate — a product's own tenants'
/// holdings (`<product>/<tenant>/…`, no `@`) and the box's own data set — which belong to the
/// product's own (not sold) estate, as its tenants do ([`crate::estates::tenant_on`]).
pub fn estate_cloud_of(cloud: &Value, estate: &str, own: bool) -> Value {
let rows = |list: &str| {
let mut r = rows_of(cloud, list, estate);
if own && !estate.is_empty() {
r.extend(rows_of(cloud, list, ""));
}
r
};
let owned: Vec<Value> = rows("owned");
let missing: Vec<Value> = rows("missing");
// MAP-3: the backups of this estate's own disks (never an orphan's, never another's).
let backups: Vec<Value> = rows("backups");
let gib = |r: &Value| r["gib"].as_u64().unwrap_or(0);
let priced = |r: &Value| r["priced"].as_bool().unwrap_or(false);
json!({
"provider": cloud["provider"].clone(),
"available": cloud["available"].clone(),
"error": cloud["error"].clone(),
"total_bytes": owned.iter().map(|r| gib(r) << 30).sum::<u64>(),
"monthly_minor": owned.iter().filter(|r| priced(r)).map(|r| r["monthly_minor"].as_u64().unwrap_or(0)).sum::<u64>(),
"unpriced": owned.iter().filter(|r| !priced(r)).count() as u64,
"owned": owned,
"orphans": [],
"missing": missing,
"backup_bytes": backups.iter().map(|r| gib(r) << 30).sum::<u64>(),
"backups": backups,
"foreign_count": 0,
"currency": cloud["currency"].clone(),
})
}
/// **The ACCOUNT's iron, once per cloud account** (MAP-3, owner 2026-10-27: "gunnar · upcloud"
/// listed everything and "holger · upcloud" the same 16 orphans again). Every product's block
/// lists the same linked cloud; `products` is each product's id with its block. Blocks with
/// the same `provider` and `account` are ONE entry naming every product on it.
///
/// Each entry is a cloud block (the same fields, every row once, its totals counted again
/// over the rows kept) plus `account`, `products`, `title` (`<provider> · <products>`) and
/// `estates`: one group per estate (by name; the rows of no ONE estate last, as "the
/// account's own (no estate)") with its owned rows, its backups and their count and bytes.
/// A block with no provider (or the `none` stub) is no account. A block that could not be
/// listed marks its account not listed, with its reason.
pub fn accounts(products: &[(String, Value)]) -> Vec<Value> {
let mut out: Vec<Value> = Vec::new();
for (product, b) in products {
let provider = b["provider"].as_str().unwrap_or_default();
if provider.is_empty() || provider == "none" {
continue;
}
let account = b["account"].as_str().unwrap_or_default();
let at = match out
.iter()
.position(|c| c["provider"] == provider && c["account"] == account)
{
Some(i) => i,
None => {
out.push(json!({ "provider": provider, "account": account, "products": [], "available": b["available"].as_bool().unwrap_or(false), "error": b["error"].clone(), "currency": b["currency"].clone(), "foreign_count": b["foreign_count"].clone(), "owned": [], "orphans": [], "missing": [], "backups": [] }));
out.len() - 1
}
};
let c = &mut out[at];
c["products"]
.as_array_mut()
.expect("a list")
.push(json!(product));
if b["available"].as_bool() != Some(true) {
c["available"] = json!(false);
c["error"] = b["error"].clone();
}
if c["currency"].as_str().unwrap_or_default().is_empty() {
c["currency"] = b["currency"].clone();
}
for list in ["owned", "orphans", "missing", "backups"] {
for r in b[list].as_array().into_iter().flatten() {
let rows = c[list].as_array_mut().expect("a list");
if !rows.iter().any(|x| x["id"] == r["id"]) {
rows.push(r.clone());
}
}
}
}
for c in &mut out {
let products: Vec<&str> = c["products"]
.as_array()
.into_iter()
.flatten()
.filter_map(Value::as_str)
.collect();
c["title"] = json!(format!(
"{} · {}",
c["provider"].as_str().unwrap_or_default(),
products.join(", ")
));
let owned = c["owned"].as_array().cloned().unwrap_or_default();
let backups = c["backups"].as_array().cloned().unwrap_or_default();
let gib = |r: &Value| r["gib"].as_u64().unwrap_or(0);
let priced = |r: &Value| r["priced"].as_bool().unwrap_or(false);
c["total_bytes"] = json!(owned.iter().map(|r| gib(r) << 30).sum::<u64>());
c["monthly_minor"] = json!(owned
.iter()
.filter(|r| priced(r))
.map(|r| r["monthly_minor"].as_u64().unwrap_or(0))
.sum::<u64>());
c["unpriced"] = json!(owned.iter().filter(|r| !priced(r)).count() as u64);
c["backup_bytes"] = json!(backups.iter().map(|r| gib(r) << 30).sum::<u64>());
let estate = |r: &Value| r["estate"].as_str().unwrap_or_default().to_owned();
let named: std::collections::BTreeSet<String> = owned
.iter()
.chain(backups.iter())
.map(estate)
.filter(|e| !e.is_empty())
.collect();
let none = owned
.iter()
.chain(backups.iter())
.any(|r| estate(r).is_empty());
let mut groups: Vec<Value> = Vec::new();
for e in named.iter().map(String::as_str).chain(none.then_some("")) {
let own: Vec<&Value> = owned.iter().filter(|r| estate(r) == e).collect();
let bk: Vec<&Value> = backups.iter().filter(|r| estate(r) == e).collect();
groups.push(json!({
"estate": e,
"name": if e.is_empty() { "the account's own (no estate)" } else { e },
"owned": own,
"backups": bk,
"backup_count": bk.len() as u64,
"backup_bytes": bk.iter().map(|r| gib(r) << 30).sum::<u64>(),
}));
}
c["estates"] = json!(groups);
}
out
}
/// **A product card's iron**: its totals, and no row of any list.
pub fn card_cloud(cloud: &Value) -> Value {
let mut c = cloud.clone();
if let Some(o) = c.as_object_mut() {
for list in ["owned", "orphans", "missing", "backups"] {
o.insert(list.into(), json!([]));
}
o.insert("foreign_count".into(), json!(0));
}
c
}
#[cfg(test)]
mod tests {
use super::*;
fn row(id: &str, kind: &str, gib: u64, estate: &str) -> Value {
json!({ "provider": "upcloud", "id": id, "kind": kind, "gib": gib, "zone": "se-sto1", "attached_to": "", "reference": format!("estate/{}", estate.replace('-', "")), "tenant": "", "priced": true, "monthly_minor": 100, "provenance": "upcloud", "estate": estate })
}
/// The account as the owner's Map of test2-gunnar-rs showed it: six estates' iron, two
/// orphans, one missing row of another estate.
fn account() -> Value {
let mut owned = Vec::new();
for e in [
"rickardtest",
"acme2",
"ignalina",
"test2-holger-rs",
"holger-rs",
"test2-gunnar-rs",
] {
owned.push(row(&format!("server/{e}-front"), "server", 0, e));
owned.push(row(&format!("storage/{e}-front-disk"), "disk", 20, e));
}
json!({
"provider": "upcloud", "available": true, "error": "", "currency": "EUR",
"owned": owned,
"orphans": [row("storage/o1", "disk", 40, "holgerr34e"), row("storage/o2", "disk", 20, "")],
"missing": [{ "provider": "upcloud", "id": "srv-x", "reference": "estate/gunnarrs", "tenant": "gunnarrs", "resource": "server:x", "estate": "gunnar-rs" }],
"foreign_count": 11, "total_bytes": 120u64 << 30, "monthly_minor": 1200, "unpriced": 0,
})
}
/// ★ **RED MAP-SEG 1: the Map of ONE estate holds no other estate's iron.**
#[test]
fn one_estates_iron_holds_no_other_estates_rows_and_no_orphan() {
let c = estate_cloud(&account(), "test2-gunnar-rs");
let ids: Vec<&str> = c["owned"]
.as_array()
.unwrap()
.iter()
.map(|r| r["id"].as_str().unwrap())
.collect();
assert_eq!(
ids,
[
"server/test2-gunnar-rs-front",
"storage/test2-gunnar-rs-front-disk"
],
"{c}"
);
assert!(
c["orphans"].as_array().unwrap().is_empty()
&& c["missing"].as_array().unwrap().is_empty(),
"{c}"
);
assert_eq!(
(
c["total_bytes"].as_u64(),
c["monthly_minor"].as_u64(),
c["foreign_count"].as_u64()
),
(Some(20 << 30), Some(200), Some(0)),
"the totals are this estate's: {c}"
);
assert_eq!(c["currency"], "EUR");
// gunnar-rs's own missing row is on gunnar-rs's Map, not test2's.
assert_eq!(
estate_cloud(&account(), "gunnar-rs")["missing"]
.as_array()
.unwrap()
.len(),
1
);
// A row of no ONE estate is on no named estate's Map.
assert!(estate_cloud(&account(), "")["owned"]
.as_array()
.unwrap()
.is_empty());
// Unlisted, it says so on every estate: the sentence, not an empty account.
let down = estate_cloud(
&json!({ "provider": "upcloud", "available": false, "error": "401", "owned": [], "orphans": [], "missing": [] }),
"acme2",
);
assert_eq!(
(down["available"].as_bool(), down["error"].as_str()),
(Some(false), Some("401"))
);
}
/// ★ A product's OWN (not sold) estate also carries the rows of no ONE estate: its own
/// tenants' holdings and the box's data set (a sold estate's Map never does).
#[test]
fn a_products_own_estate_carries_the_rows_of_no_one_estate() {
let mut a = account();
a["owned"].as_array_mut().unwrap().push(json!({ "id": "storage/alice", "kind": "disk", "gib": 10, "priced": true, "monthly_minor": 50, "reference": "gunnar/alice/2026-10-01", "estate": "" }));
let own = estate_cloud_of(&a, "gunnar-rs", true);
assert_eq!(
own["owned"]
.as_array()
.unwrap()
.iter()
.map(|r| r["id"].as_str().unwrap())
.collect::<Vec<_>>(),
["storage/alice"],
"{own}"
);
let sold = estate_cloud_of(&a, "acme2", false);
assert!(
sold["owned"]
.as_array()
.unwrap()
.iter()
.all(|r| r["estate"] == "acme2"),
"{sold}"
);
}
fn backup(id: &str, estate: &str, server: &str, day: &str) -> Value {
json!({ "provider": "upcloud", "id": id, "gib": 10, "zone": "se-sto1", "origin": format!("storage/{server}-disk"), "server": server, "created": format!("{day}T03:56:43Z"), "title": format!("Simple backup of {server} system"), "estate": estate, "reference": "", "words": format!("backup of {server}, {day}") })
}
/// The account as the ONE box's Iron page showed it on 2026-10-27: the gunnar product's
/// block lists every estate (it sells them), holger's lists none of them — and both list the
/// same orphans and the same backups, because both are the SAME linked UpCloud account.
fn two_products_one_account() -> Vec<(String, Value)> {
let backups = json!([
backup(
"storage/bk1",
"holger-rs",
"holger-web-ui-holgerrs",
"2026-10-27"
),
backup(
"storage/bk2",
"test2-gunnar-rs",
"gunnar-front-test2gunnarrs",
"2026-10-27"
),
backup("storage/bk3", "", "monetize-prod", "2026-10-27")
]);
let mut gunnar = account();
gunnar["account"] = json!("upcloud");
gunnar["backups"] = backups.clone();
gunnar["backup_bytes"] = json!(30u64 << 30);
let holger = json!({
"provider": "upcloud", "account": "upcloud", "available": true, "error": "", "currency": "EUR",
"owned": [], "orphans": gunnar["orphans"].clone(), "missing": [], "backups": backups,
"foreign_count": 11, "total_bytes": 0, "monthly_minor": 0, "unpriced": 0, "backup_bytes": 30u64 << 30,
});
vec![
("gunnar".to_string(), gunnar),
("holger".to_string(), holger),
]
}
/// ★ **RED MAP-3: one cloud account is listed ONCE**, naming every product on it; its rows,
/// orphans and backups each once; every estate a group of its owned rows and its backups
/// (the account's own backups, of no estate, a group of their own, last); a different
/// account stays its own entry.
#[test]
fn one_cloud_account_under_two_products_is_listed_once_naming_both() {
let a = accounts(&two_products_one_account());
assert_eq!(a.len(), 1, "{a:?}");
let a = &a[0];
assert_eq!(a["products"], json!(["gunnar", "holger"]), "{a}");
assert_eq!(a["title"], "upcloud · gunnar, holger", "{a}");
assert_eq!(
a["orphans"].as_array().unwrap().len(),
2,
"an orphan is said once: {a}"
);
assert_eq!(
a["backups"].as_array().unwrap().len(),
3,
"a backup is said once: {a}"
);
assert_eq!(a["owned"].as_array().unwrap().len(), 12, "{a}");
assert_eq!(
(
a["total_bytes"].as_u64(),
a["backup_bytes"].as_u64(),
a["foreign_count"].as_u64()
),
(Some(120 << 30), Some(30 << 30), Some(11)),
"{a}"
);
let names: Vec<&str> = a["estates"]
.as_array()
.unwrap()
.iter()
.map(|e| e["estate"].as_str().unwrap())
.collect();
assert_eq!(
names,
[
"acme2",
"holger-rs",
"ignalina",
"rickardtest",
"test2-gunnar-rs",
"test2-holger-rs",
""
],
"{a}"
);
let holger_rs = &a["estates"][1];
assert_eq!(
holger_rs["owned"].as_array().unwrap().len(),
2,
"{holger_rs}"
);
assert_eq!(
holger_rs["backups"]
.as_array()
.unwrap()
.iter()
.map(|b| b["words"].as_str().unwrap())
.collect::<Vec<_>>(),
["backup of holger-web-ui-holgerrs, 2026-10-27"],
"{holger_rs}"
);
assert_eq!(
(
holger_rs["backup_count"].as_u64(),
holger_rs["backup_bytes"].as_u64()
),
(Some(1), Some(10 << 30)),
"{holger_rs}"
);
let own = &a["estates"][6];
assert_eq!(
(
own["name"].as_str(),
own["owned"].as_array().unwrap().len(),
own["backups"].as_array().unwrap().len()
),
(Some("the account's own (no estate)"), 0, 1),
"{own}"
);
// Another account is another entry.
let mut more = two_products_one_account();
more.push(("lake".into(), json!({ "provider": "sandbox", "account": "sandbox", "available": true, "error": "", "owned": [], "orphans": [], "missing": [], "backups": [] })));
let a = accounts(&more);
assert_eq!(
a.iter()
.map(|x| x["title"].as_str().unwrap())
.collect::<Vec<_>>(),
["upcloud · gunnar, holger", "sandbox · lake"]
);
// A block that could not be listed marks its account not listed.
let mut down = two_products_one_account();
down[1].1 = json!({ "provider": "upcloud", "account": "upcloud", "available": false, "error": "401", "owned": [], "orphans": [], "missing": [] });
let a = accounts(&down);
assert_eq!(
(a.len(), a[0]["available"].as_bool(), a[0]["error"].as_str()),
(1, Some(false), Some("401"))
);
}
/// ★ ONE estate's Map carries its own backups (of its own disks) and no other's; a card none.
#[test]
fn an_estates_map_carries_its_own_backups_and_a_card_none() {
let (_, gunnar) = two_products_one_account().remove(0);
let c = estate_cloud(&gunnar, "holger-rs");
assert_eq!(
c["backups"]
.as_array()
.unwrap()
.iter()
.map(|b| b["id"].as_str().unwrap())
.collect::<Vec<_>>(),
["storage/bk1"],
"{c}"
);
assert_eq!(c["backup_bytes"].as_u64(), Some(10 << 30), "{c}");
assert!(card_cloud(&gunnar)["backups"]
.as_array()
.unwrap()
.is_empty());
}
/// A product card keeps its totals and hands out no row.
#[test]
fn a_product_card_carries_the_totals_and_no_row() {
let c = card_cloud(&account());
assert_eq!(c["total_bytes"].as_u64(), Some(120 << 30));
for list in ["owned", "orphans", "missing"] {
assert!(c[list].as_array().unwrap().is_empty(), "{list}: {c}");
}
assert_eq!(c["foreign_count"].as_u64(), Some(0));
}
}