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/pages/mod.rs

//! The console's pages. The JSON shapes they read live in [`crate::model`],
//! compiled for the host as well, so every decision over them is testable
//! without a browser.
//!
//! Since 2026-10-02 the ESTATE is the top level: [`overview`] is the one page
//! that spans every estate, and everything else is one estate's, under
//! `/e/<estate>/…` with the same left menu for every estate ([`shell`]).

/// Every (product, tenant) account of ONE monetize, and one account's rows (5.4).
pub mod accounts;
pub mod guardian;
/// The invite page and the Operators panel (owner 2026-11-03): one-time per-person invites.
pub mod invite;
/// The account's iron, the operator's (MAP-SEG 1).
pub mod iron;
/// The data-set growth panel: the OUTSIDE actor's state, on the estate's Map.
pub mod ledger;
pub mod login;
/// ★ UW-3: the operator's money fixes (refund, hand-settle, an order's settle), as buttons.
pub mod money;
/// Every estate, one row each — the only page that spans them all.
pub mod overview;
/// The product lines, the spend against the ceilings, and the terms — pieces
/// of an estate's Map, Money and Offer.
pub mod products;
/// One estate's Settings: which keys, which names, which key this console signs with.
pub mod settings;
/// One estate: the left menu, and its Map, Customers, Money, Offer and Events.
pub mod shell;
/// The operator's TEST BUTTON: each product's declared test actions and their runs.
pub mod sites;
pub mod tax;
pub mod tenant;
pub mod tenants;
pub mod test_buy;

pub use crate::model::*;

/// Milliseconds since the Unix epoch, from the browser's clock.
pub fn now_ms() -> u64 {
    js_sys::Date::now() as u64
}

/// `/e/<estate>/<section>` for the estate the shell is showing; `/` (the
/// Overview) when none is.
pub fn here(section: &str) -> String {
    match crate::api::estate() {
        Some(e) => crate::estate::estate_href(&e, section),
        None => "/".to_owned(),
    }
}

/// The tenant page's address — a customer of the estate being shown. The
/// tenant rides in the query because tenant names carry slashes (`team/sub`).
pub fn tenant_href(product: &str, tenant: &str) -> String {
    format!(
        "{}?product={}&tenant={}",
        here("customer"),
        crate::fmt::percent_encode(product),
        crate::fmt::percent_encode(tenant)
    )
}

/// One product's customers, in the estate being shown.
pub fn product_href(product: &str) -> String {
    format!(
        "{}?product={}",
        here("customers"),
        crate::fmt::percent_encode(product)
    )
}

/// The estate's Money, scoped to one product and optionally one tenant.
pub fn activity_href(product: &str, tenant: Option<&str>) -> String {
    match tenant {
        Some(t) => format!(
            "{}?product={}&tenant={}",
            here("money"),
            crate::fmt::percent_encode(product),
            crate::fmt::percent_encode(t)
        ),
        None => format!(
            "{}?product={}",
            here("money"),
            crate::fmt::percent_encode(product)
        ),
    }
}

/// ★ Owner 2026-10-12: no file names, internal terms or mechanism talk on any
/// console page. Every page's source is held to it, so a reword cannot come back.
#[cfg(test)]
mod plain_words_tests {
    const PAGES: [(&str, &str); 15] = [
        ("accounts", include_str!("accounts.rs")),
        ("guardian", include_str!("guardian.rs")),
        ("ledger", include_str!("ledger.rs")),
        ("login", include_str!("login.rs")),
        ("money", include_str!("money.rs")),
        ("overview", include_str!("overview.rs")),
        ("products", include_str!("products.rs")),
        ("settings", include_str!("settings.rs")),
        ("shell", include_str!("shell.rs")),
        ("tax", include_str!("tax.rs")),
        ("tenant", include_str!("tenant.rs")),
        ("tenants", include_str!("tenants.rs")),
        ("sites", include_str!("sites.rs")),
        ("test_buy", include_str!("test_buy.rs")),
        ("ui", include_str!("../ui.rs")),
    ];
    const BANNED: [&str; 10] = [
        "monetize.toml",
        "\"Market\"",
        "every market",
        "[machines.*]",
        "the poller",
        "webhook captures",
        "priced on the difference",
        "MONETIZE_UI_",
        "open-source answer",
        "EstateAdmin.Draw",
    ];

    #[test]
    fn no_page_shows_file_names_internal_terms_or_mechanisms() {
        for (page, src) in PAGES {
            // Only what is shown: lines outside comments.
            for line in src.lines().filter(|l| !l.trim_start().starts_with("//")) {
                for word in BANNED {
                    assert!(
                        !line.contains(word),
                        "page {page} shows {word:?}: {}",
                        line.trim()
                    );
                }
            }
        }
    }
}