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()
})}
}
}