crates/monetize-ui-web/src/pages/test_buy.rs
//! **THE TEST BUTTON** (owner 2026-10-11) — `/test-buy`: each product's declared
//! test actions as buttons, and the runs they made with their setup lines.
//! Every word is a cell of [`crate::test_buy`], tested on the host; this only
//! lays them out. Thin: it calls the console's server and renders.
use leptos::prelude::*;
use crate::api::Fail;
use crate::test_buy::{
button_words, buys, by_product, line_cells, pay_link, press, refusal_words, run_cells,
start_body, state_tone, ActionJson, RunJson, TestPurchaseJson, LINE_COLUMNS, LIST_API,
RUN_COLUMNS, START_API,
};
use crate::ui::{resolved, Badge, Empty, PageHeader, Panel};
/// A refusal in plain words: no status, no wire code.
#[component]
fn Refused(fail: Fail) -> impl IntoView {
let (head, detail) = refusal_words(&fail);
view! {
<div class="failure" role="alert">
<h3>{head}</h3>
{(!detail.is_empty()).then(|| view! { <p class="detail">{detail}</p> })}
</div>
}
}
#[component]
pub fn TestBuy() -> impl IntoView {
crate::api::set_estate(None);
let feed = crate::feed::use_feed();
let reload = RwSignal::new(0u32);
let list = LocalResource::new(move || {
feed.reread.get();
reload.get();
async { crate::api::get::<TestPurchaseJson>(LIST_API).await }
});
let busy = RwSignal::new(false);
let failed = RwSignal::new(None::<Fail>);
let shown = RwSignal::new(None::<String>);
let is_admin = move || {
crate::app::session()
.get()
.map(|s| s.is_operator())
.unwrap_or(false)
};
let start = move |a: ActionJson| {
if busy.get_untracked() {
return;
}
busy.set(true);
failed.set(None);
leptos::task::spawn_local(async move {
match crate::api::post::<_, RunJson>(START_API, &start_body(&a)).await {
Ok(run) => {
shown.set(Some(run.id));
reload.update(|n| *n += 1);
}
Err(f) => failed.set(Some(f)),
}
busy.set(false);
});
};
view! {
<main class="page" id="main">
<PageHeader title="Test buy" subtitle="Buy as a customer would. A real account (and machines, for a private instance) is made. Each button says what it costs."/>
{move || failed.get().map(|f| view! { <Refused fail=f/> })}
{move || match list.get() {
Some(Err(f)) if crate::fmt::login_redirect_for(f.status, "/test-buy").is_none() => view! { <div class="panel-body"><Refused fail=f/></div> }.into_any(),
got => resolved(got, None, move |j: TestPurchaseJson| {
let now = crate::pages::now_ms();
let admin = is_admin();
let open = shown.get().or_else(|| j.runs.first().map(|r| r.id.clone()));
let open_run = j.runs.iter().find(|r| Some(&r.id) == open.as_ref()).cloned();
view! {
<div class="stack">
{if j.actions.is_empty() {
view! { <Empty title="No test buttons" detail="No test buttons yet."/> }.into_any()
} else {
by_product(&j.actions).into_iter().map(|(product, actions)| view! {
<Panel title=product.clone()>
<div class="actions" role="group" aria-label=format!("{product} test buttons")>
{actions.into_iter().map(|a| {
let (ok, why) = press(&a, admin);
let pressed = a.clone();
let class = if a.live { "btn danger" } else { "btn" };
view! {
<div class="test-action">
<button class=class title=why.clone() disabled=move || busy.get() || !ok on:click=move |_| start(pressed.clone())>{button_words(&a)}</button>
<span class="hint">{if a.words.is_empty() { buys(&a) } else { a.words.clone() }}</span>
{(!why.is_empty()).then(|| view! { <span class="hint">{format!(" — {why}")}</span> })}
</div>
}
}).collect_view()}
</div>
</Panel>
}).collect_view().into_any()
}}
<Panel title="Runs">
{if j.runs.is_empty() {
view! { <p class="hint">"No test buy yet."</p> }.into_any()
} else {
view! {
<table class="test-runs" aria-label="Runs">
<thead><tr>{RUN_COLUMNS.iter().map(|c| view! { <th scope="col">{*c}</th> }).collect_view()}</tr></thead>
<tbody>
{j.runs.iter().map(|r| {
let [started, product, action, account, instance, state, words] = run_cells(r, now);
let tone = state_tone(&r.state);
let id = r.id.clone();
view! {
<tr title=r.id.clone() on:click=move |_| shown.set(Some(id.clone()))>
<td>{started}</td>
<td class="mono">{product}</td>
<td>{action}</td>
<td class="mono">{account}</td>
<td class="mono">{instance}</td>
<td><Badge tone=tone label=state/></td>
<td>{words}</td>
</tr>
}
}).collect_view()}
</tbody>
</table>
}.into_any()
}}
</Panel>
{open_run.map(|r| view! {
<Panel title=format!("{} · {} · {}", r.id, r.label, r.state)>
{(!r.mailbox.is_empty()).then(|| view! { <p class="hint">{format!("mail goes to {}", r.mailbox)}</p> })}
{pay_link(&r).map(|(words, url)| view! { <p><a class="btn danger" href=url target="_blank" rel="noopener noreferrer">{words}</a></p> })}
<table class="test-lines" aria-label="Setup lines">
<thead><tr>{LINE_COLUMNS.iter().map(|c| view! { <th scope="col">{*c}</th> }).collect_view()}</tr></thead>
<tbody>
{r.lines.iter().map(|l| view! { <tr>{line_cells(l, now).into_iter().map(|x| view! { <td>{x}</td> }).collect_view()}</tr> }).collect_view()}
</tbody>
</table>
</Panel>
})}
</div>
}
}),
}}
</main>
}
}