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/tenants.rs

//! `docs/v2/07-console.md` screen 2 — a product: the fleet.
//!
//! One number is big: `used of capacity` on the product's headline meter, or
//! its honest unmeasured state. Under it, one trend line, `⚠ oversold by …`
//! when the tenants were promised more than the box can serve, and `⚠ cannot
//! grow` when the box is sealed — each a badge holding the product's own
//! paragraph, never the paragraph on the page. Then tenants sorted by who is
//! closest to their cap, folded after a few, and the last outcomes from the
//! ledger with the door to all of them.
//!
//! Then the two lists no STOCK meter could ever produce. Every bar above is
//! *how much is held*, which reads the same for a customer who pushed this
//! morning and one who stopped in March — so this product could be billing
//! nine tenants correctly and not know that six of them had left. **SILENT**
//! is who has not been pushed to or fetched from for the server's threshold,
//! longest first; **FRICTION** is who met a door in the last week, most
//! refused first. Both come whole from the wire — the fleet page makes ONE
//! call, never one per tenant — and the price of that is named on the FRICTION
//! panel itself: the reason breakdown lives on the tenant's Activity panel.
//!
//! Every decision here is `fmt`'s and tested there: `primary_meter`,
//! `fleet_figure`, `trend_line`, `oversell_line`, `growth_badge`, `standing`, `sort_closest`,
//! `fold`, `sale_line`, `sale_mark`, `month_day`, `quiet_line`, `friction_line`. This file lays them out.
//!
//! Last, IRON — what we actually own out there, and the box itself (`cloud_panel_state`,
//! `cloud_summary_line`, `cloud_rows`, `cloud_needs_you`, `cloud_foreign_line`).
//! One row per owned resource, `play iron` on the fake ones, `this box` on the
//! server's own data set; an orphan or a
//! missing resource is a task under NEEDS YOU, because those are the two bugs
//! that cost money. A box with no cloud gets no panel; a cloud that could not be
//! listed gets the sentence, not an empty list.

use leptos::prelude::*;

use crate::fmt::{
    cloud_foreign_line, cloud_needs_you, cloud_panel_state, cloud_rows, cloud_summary_line,
    cloud_title, fleet_figure, fold, friction_empty_line, friction_line, friction_window_note,
    growth_badge, money, month_day, needs_attention_state, no_flow_data_line, oversell_line,
    primary_meter, quiet_line, sale_line, sale_mark, shows_flow_panel, silent_empty_line,
    silent_window_note, sort_closest, standing, trend_line, CloudPanel, Figure, Tone, SHOWN_SALES,
    SHOWN_TENANTS,
};
use crate::pages::{
    activity_href, tenant_href, FrictionTenantJson, SilentTenantJson, TenantRow, TenantsJson,
};
use crate::ui::{resolved, Badge, Empty, Panel, StateBadge, Track};

/// Which half of a product's page to draw: its CUSTOMERS (the tenants, what
/// they bought, who went quiet, who is refused) or its MACHINES (the fleet
/// figure, the iron at the provider, the data-set growth) — the estate's
/// Customers and Map (owner 2026-10-02: the same page, re-homed, nothing dropped).
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Part {
    Customers,
    Machines,
}

#[component]
pub fn Tenants(
    #[prop(into)] product: String,
    part: Part,
    /// The estate whose page this is (MAP-SEG 1): the server cuts the product to ITS
    /// tenants and ITS iron (`?estate=`). Empty: the rows of no ONE estate.
    #[prop(optional, into)]
    estate: String,
) -> impl IntoView {
    let product = Memo::new(move |_| product.clone());
    let estate = Memo::new(move |_| estate.clone());
    let customers = part == Part::Customers;
    let feed = crate::feed::use_feed();
    let reload = RwSignal::new(0u32);
    let data = LocalResource::new(move || {
        reload.get();
        feed.reread.get();
        let p = product.get();
        let e = estate.get();
        async move { crate::api::get::<TenantsJson>(&crate::fmt::tenants_path(&p, &e)).await }
    });
    let retry = Callback::new(move |()| reload.update(|n| *n += 1));

    view! {
        {move || resolved(data.get(), Some(retry), move |mut t: TenantsJson| {
            let meter = primary_meter(t.trend.as_ref(), &t.meters).map(str::to_owned);
            let enforcement = meter.as_deref().and_then(|m| t.meters.iter().find(|x| x.name == m)).map(|m| m.enforcement.clone()).unwrap_or_default();
            let figure = fleet_figure(&t.fleet, meter.as_deref());
            let (used, capacity) = match &figure {
                Figure::Measured { used, capacity, .. } => (*used, Some(*capacity)),
                Figure::Unmeasured { used, .. } => (*used, None),
                Figure::NoMeter => (0, None),
            };
            let trend = trend_line(t.trend.as_ref(), used, capacity);
            let fleet_meter = meter.as_deref().and_then(|m| t.fleet.meters.iter().find(|x| x.meter == m));
            let oversold = fleet_meter.and_then(oversell_line);
            let oversold_detail = fleet_meter.map(|m| m.oversell_reason.clone()).unwrap_or_default();
            let growth = growth_badge(&t.fleet.growth.state);
            let growth_detail = t.fleet.growth.detail.clone();
            if let Some(m) = &meter {
                sort_closest(&mut t.tenants, m, &enforcement);
            }
            let (shown, more) = fold(t.tenants.len(), SHOWN_TENANTS);
            let (sales_shown, sales_more) = fold(t.sales.len(), SHOWN_SALES);
            let product_id = t.product.clone();
            let bar_label = product_id.clone();

            let row_view = {
                let product_id = product_id.clone();
                let meter = meter.clone();
                let enforcement = enforcement.clone();
                move |row: TenantRow| {
                    let href = tenant_href(&product_id, &row.tenant);
                    let attention = needs_attention_state(&row.state);
                    let state = row.state.clone();
                    let tenant = row.tenant.clone();
                    let standing = meter.as_deref().map(|m| standing(&row, m, &enforcement));
                    view! {
                        <a class="tenant-row" href=href>
                            <span class="id mono">{tenant}</span>
                            {match standing {
                                Some(s) => view! {
                                    <span class="figure mono">{s.text()}</span>
                                    <span class="bar"><Track state=s.state width=s.width label=row.tenant.clone()/></span>
                                    <span class=if s.state == crate::fmt::BarState::AtLimit { "word at-cap" } else { "word" }>{s.word()}</span>
                                }.into_any(),
                                None => view! { <span class="figure hint">"no wall to measure against"</span><span class="bar"></span><span class="word"></span> }.into_any(),
                            }}
                            <span class="state">{attention.then(|| view! { <StateBadge state=state/> })}</span>
                            <span class="go" aria-hidden="true">"→"</span>
                        </a>
                    }
                }
            };
            // THE TWO FLOW LISTS. Folded exactly as the tenant list is: the
            // server has already ordered them (longest quiet first, most
            // refused first), so the fold keeps the wire's order and never
            // sorts a second time.
            let (silent_after, friction_days) = (t.silent_after_days, t.friction_days);
            let (silent_shown, silent_more) = fold(t.silent.len(), SHOWN_TENANTS);
            let (friction_shown, friction_more) = fold(t.friction.len(), SHOWN_TENANTS);
            let mut silent = t.silent.clone().into_iter();
            let silent_first: Vec<SilentTenantJson> = silent.by_ref().take(silent_shown).collect();
            let silent_rest: Vec<SilentTenantJson> = silent.collect();
            let mut friction = t.friction.clone().into_iter();
            let friction_first: Vec<FrictionTenantJson> = friction.by_ref().take(friction_shown).collect();
            let friction_rest: Vec<FrictionTenantJson> = friction.collect();
            let silent_row = {
                let product_id = product_id.clone();
                move |r: SilentTenantJson| {
                    let href = tenant_href(&product_id, &r.tenant);
                    let attention = needs_attention_state(&r.state);
                    let (state, tenant) = (r.state.clone(), r.tenant.clone());
                    view! {
                        <a class="flow-row" href=href>
                            <span class="id mono">{tenant}</span>
                            <span class="figure">{quiet_line(r.quiet_days)}</span>
                            <span class="state">{attention.then(|| view! { <StateBadge state=state/> })}</span>
                            <span class="go" aria-hidden="true">"→"</span>
                        </a>
                    }
                }
            };
            let silent_row_rest = silent_row.clone();
            let friction_row = {
                let product_id = product_id.clone();
                move |r: FrictionTenantJson| {
                    let href = tenant_href(&product_id, &r.tenant);
                    let attention = needs_attention_state(&r.state);
                    let (state, tenant) = (r.state.clone(), r.tenant.clone());
                    view! {
                        <a class="flow-row" href=href>
                            <span class="id mono">{tenant}</span>
                            <span class="figure">{friction_line(r.refused, friction_days)}</span>
                            <span class="state">{attention.then(|| view! { <StateBadge state=state/> })}</span>
                            <span class="go" aria-hidden="true">"→"</span>
                        </a>
                    }
                }
            };
            let friction_row_rest = friction_row.clone();
            // No window, no panel: a monetize older than these lists leaves the
            // window at its `serde` default, and a panel drawn on that would
            // give an all-clear over nothing (`fmt::shows_flow_panel`).
            // **The tenants nobody MEASURED**, named under the list rather than
            // folded into it. A product that reports neither `last_*` meter
            // says nothing about whether its tenants are being used, and until
            // 2026-09-10 those tenants were drawn `never seen` at the TOP of
            // this list — a claim about a customer made from a gap in a plugin,
            // ranked as the most urgent row on the page.
            let no_data_note = no_flow_data_line(&t.no_flow_data);
            let silent_panel = shows_flow_panel(silent_after).then(|| {
                view! {
                    <Panel title="Silent" actions=Box::new(move || view! { <span class="hint">{silent_window_note(silent_after)}</span> }.into_any())>
                        {if silent_first.is_empty() {
                            view! { <Empty title="Nobody has gone quiet" detail=silent_empty_line(silent_after)/> }.into_any()
                        } else {
                            view! {
                                <div class="flow-list">
                                    {silent_first.into_iter().map(&silent_row).collect_view()}
                                    {(silent_more > 0).then(|| view! {
                                        <details class="more">
                                            <summary>{format!("+ {silent_more} more")}</summary>
                                            {silent_rest.into_iter().map(&silent_row_rest).collect_view()}
                                        </details>
                                    })}
                                </div>
                            }.into_any()
                        }}
                        // Drawn whether or not anybody is silent: "nobody has
                        // gone quiet" over a product that reports no flow would
                        // be an all-clear nobody measured.
                        {no_data_note.map(|line| view! { <p class="hint no-data">{line}</p> })}
                    </Panel>
                }
                .into_any()
            });
            let friction_panel = shows_flow_panel(friction_days).then(|| {
                view! {
                    <Panel title="Friction" actions=Box::new(move || view! { <span class="hint">{friction_window_note(friction_days)}</span> }.into_any())>
                        {if friction_first.is_empty() {
                            view! { <Empty title="Nobody is being refused" detail=friction_empty_line(friction_days)/> }.into_any()
                        } else {
                            view! {
                                <div class="flow-list">
                                    {friction_first.into_iter().map(&friction_row).collect_view()}
                                    {(friction_more > 0).then(|| view! {
                                        <details class="more">
                                            <summary>{format!("+ {friction_more} more")}</summary>
                                            {friction_rest.into_iter().map(&friction_row_rest).collect_view()}
                                        </details>
                                    })}
                                </div>
                            }.into_any()
                        }}
                    </Panel>
                }
                .into_any()
            });
            let cloud = t.fleet.cloud.clone();
            let cloud_state = cloud_panel_state(cloud.as_ref());
            let cloud_title = cloud.as_ref().map(cloud_title).unwrap_or_default();
            let cloud_panel = match (cloud_state, cloud) {
                (CloudPanel::Hidden, _) | (_, None) => None,
                (CloudPanel::Unavailable(sentence), Some(_)) => Some(view! {
                    <Panel title=cloud_title.clone()>
                        <div class="panel-body cloud-task"><Badge tone=Tone::Warn label="⚠"/>" "{sentence}</div>
                    </Panel>
                }.into_any()),
                (CloudPanel::Show, Some(c)) => {
                    let summary = cloud_summary_line(&c);
                    let rows = cloud_rows(&c);
                    let tasks = cloud_needs_you(&c);
                    let foreign = cloud_foreign_line(c.foreign_count);
                    Some(view! {
                        <Panel title=cloud_title.clone() actions=Box::new(move || view! { <span class="hint mono">{summary.clone()}</span> }.into_any())>
                            {if rows.is_empty() {
                                view! { <Empty title="Nothing owned" detail="Nothing at the cloud provider."/> }.into_any()
                            } else {
                                view! {
                                    <div class="cloud-list">
                                        {rows.into_iter().map(|r| { let place = crate::fmt::cloud_where(&r); view! {
                                            <div class="cloud-row" title=r.reference.clone()>
                                                <span class="id mono">{r.id.clone()}</span>
                                                <span class="size mono">{r.size}</span>
                                                <span class="where hint">{place}</span>
                                                <span class="tenant">{format!("→ {}", r.tenant)}</span>
                                                <span class="ref hint mono">{format!("({})", r.reference)}</span>
                                                <span class="mark">
                                                    {r.play_iron.then(|| view! { <span title="the sandbox cloud: fake iron at real prices, nothing provisioned"><Badge tone=Tone::Plain label="play iron"/></span> })}
                                                    {r.this_box.then(|| view! { <span title="the iron this box already has: the product's own data set — never bought, never an orphan, cannot spend"><Badge tone=Tone::Plain label="this box"/></span> })}
                                                </span>
                                            </div>
                                        }}).collect_view()}
                                    </div>
                                }.into_any()
                            }}
                            {foreign.map(|line| view! { <p class="hint foreign">{line}</p> })}
                            {(!tasks.is_empty()).then(|| view! {
                                <div class="cloud-tasks">
                                    <h3>{format!("Needs you · {}", tasks.len())}</h3>
                                    {tasks.into_iter().map(|task| view! {
                                        <div class="cloud-task">
                                            <Badge tone=Tone::Warn label=format!("⚠ {}", task.what)/>
                                            <span class="mono">{task.subject}</span>
                                            <span class="mono">{task.detail}</span>
                                            <span>{format!("— {}", task.why)}</span>
                                        </div>
                                    }).collect_view()}
                                </div>
                            })}
                        </Panel>
                    }.into_any())
                }
            };
            let mut tenants = t.tenants.into_iter();
            let first: Vec<TenantRow> = tenants.by_ref().take(shown).collect();
            let rest: Vec<TenantRow> = tenants.collect();
            let row_view_rest = row_view.clone();

            view! {
                <div class="stack">
                    <section class="big" aria-label="The fleet">
                        <div class="number mono" title=figure.why().to_owned()>{figure.text()}</div>
                        {figure.bar().map(|(state, width)| view! { <div class="big-bar"><Track state=state width=width label=bar_label.clone()/></div> })}
                        {trend.map(|line| view! { <p class="trend mono">{line}</p> })}
                        {oversold.map(|line| {
                            let badge = view! { <Badge tone=Tone::Warn label=format!("⚠ {line}")/> };
                            if oversold_detail.is_empty() {
                                view! { <div class="holder">{badge}</div> }.into_any()
                            } else {
                                view! {
                                    <details class="holder">
                                        <summary>{badge}</summary>
                                        <p class="detail">{oversold_detail.clone()}</p>
                                    </details>
                                }.into_any()
                            }
                        })}
                        {growth.map(|(word, tone)| view! {
                            <details class="holder">
                                <summary><Badge tone=tone label=word/></summary>
                                <p class="detail">{growth_detail.clone()}</p>
                            </details>
                        })}
                    </section>
                    {customers.then(move || view! {
                        <Panel title="Tenants" actions=Box::new(|| view! { <span class="hint">"sort: closest to cap"</span> }.into_any())>
                            {if first.is_empty() {
                                view! { <Empty title="No tenants yet" detail="No customers yet."/> }.into_any()
                            } else {
                                view! {
                                    <div class="tenant-list">
                                        {first.into_iter().map(&row_view).collect_view()}
                                        {(more > 0).then(|| view! {
                                            <details class="more">
                                                <summary>{format!("+ {more} more")}</summary>
                                                {rest.into_iter().map(&row_view_rest).collect_view()}
                                            </details>
                                        })}
                                    </div>
                                }.into_any()
                            }}
                        </Panel>
                        <Panel title="Recently bought" actions=Box::new({ let p = product_id.clone(); move || view! { <a class="btn quiet" href=activity_href(&p, None)>{if sales_more > 0 { format!("all activity ({sales_more} more) →") } else { "all activity →".to_owned() }}</a> }.into_any() })>
                            {if t.sales.is_empty() {
                                view! { <Empty title="Nothing bought yet" detail="No purchases yet."/> }.into_any()
                            } else {
                                view! {
                                    <div class="sale-list">
                                        {t.sales.into_iter().take(sales_shown).map(|s| {
                                            let (tone, mark) = sale_mark(&s.outcome, s.drawn);
                                            let reason = s.reason.clone();
                                            view! {
                                                <div class="sale-row taxed" title=reason>
                                                    <span class="when hint">{month_day(s.at_unix_ms)}</span>
                                                    <span class="what">{sale_line(&s)}</span>
                                                    <span class="price mono" title=crate::fmt::sale_money_line(&s)>{money(s.price_minor, &s.currency)}</span>
                                                    <span class="tax mono hint">{crate::fmt::sale_tax_line(&s).unwrap_or_default()}</span>
                                                    <span class="mark"><Badge tone=tone label=mark/></span>
                                                </div>
                                            }
                                        }).collect_view()}
                                    </div>
                                }.into_any()
                            }}
                        </Panel>
                        {silent_panel}
                        {friction_panel}
                    })}
                    // The IRON — what we own at a provider — is the estate's MAP
                    // (owner 2026-10-02), beside its board.
                    {(!customers).then(move || view! {
                        {cloud_panel}
                    })}
                </div>
            }.into_any()
        })}
    }
}