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