crates/monetize-ui-web/src/pages/guardian.rs
//! **THE ESTATE GUARDIAN** (5.4 resilience) — every estate's repair cases, the
//! four buttons and the auto-fix opt-in. Every word is a cell of
//! [`crate::guardian`], tested on the host; this only lays them out.
//!
//! A button asks monetize to approve a repair with the operator's fresh admin
//! passkey; monetize queues a SIGNED order the estate pulls (or creates a box
//! through its cloud). Nothing here reaches into an estate.
use leptos::prelude::*;
use leptos_router::hooks::use_query_map;
use serde_json::{json, Value};
use crate::api::Fail;
use crate::guardian::{
auto_fix_api, auto_fix_line, box_cells, buttons, case_cells, estate_api, estate_cells,
estate_href, press_api, remedy_label, state_tone, step_cells, EstateJson, GuardianJson,
BOX_COLUMNS, CASE_COLUMNS, ESTATE_COLUMNS, STEP_COLUMNS,
};
use crate::ui::{resolved, Badge, Empty, FailBox, PageHeader, Panel};
/// `/guardian`: estate | product | main | twin | auto-fix | open case.
#[component]
pub fn Guardian() -> impl IntoView {
crate::api::set_estate(None);
let feed = crate::feed::use_feed();
let list = LocalResource::new(move || {
feed.reread.get();
async { crate::api::get::<GuardianJson>("/api/guardian").await }
});
view! {
<main class="page" id="main">
<PageHeader title="Guardian" subtitle="Repair cases for every estate. Auto-fix is off unless turned on."/>
{move || resolved(list.get(), None, |j: GuardianJson| {
if j.estates.is_empty() {
return view! { <Empty title="No estates" detail="No estate has reported in yet."/> }.into_any();
}
let now = crate::pages::now_ms();
let stale = crate::fmt::stale_note(j.stale_secs);
view! {
{stale.map(|w| view! { <p class="hint stale" role="status">{w}</p> })}
<Panel title="Estates">
<table class="guardian" aria-label="Estates">
<thead><tr>
{ESTATE_COLUMNS.iter().map(|c| view! { <th scope="col">{*c}</th> }).collect_view()}
</tr></thead>
<tbody>
{j.estates.into_iter().map(|e| {
let [instance, product, main, twin, auto, open] = estate_cells(&e, now);
let href = estate_href(&e.instance);
view! {
<tr>
<td class="mono"><a href=href>{instance}</a></td>
<td class="mono">{product}</td>
<td>{main}</td>
<td>{twin}</td>
<td>{auto}</td>
<td>{open}</td>
</tr>
}
}).collect_view()}
</tbody>
</table>
</Panel>
}
.into_any()
})}
</main>
}
}
/// `/guardian/estate?instance=…`: the boxes, the four buttons, the auto-fix
/// switch, the cases, and the newest case's steps, evidence and log.
#[component]
pub fn GuardianEstate() -> impl IntoView {
crate::api::set_estate(None);
let feed = crate::feed::use_feed();
let query = use_query_map();
let instance = move || query.with(|q| q.get("instance").unwrap_or_default());
let reload = RwSignal::new(0u32);
let detail = LocalResource::new(move || {
feed.reread.get();
reload.get();
let path = estate_api(&instance());
async move { crate::api::get::<EstateJson>(&path).await }
});
let reread = Callback::new(move |()| reload.update(|n| *n += 1));
view! {
<main class="page" id="main">
{move || view! { <PageHeader title=format!("Estate {}", instance()) subtitle="Its machines, repair cases and actions."/> }}
<p><a href="/guardian">"← Guardian"</a></p>
{move || resolved(detail.get(), None, move |e: EstateJson| view! { <EstateBody estate=e reread=reread/> })}
// The ONE box updates what it laid (owner 2026-10-14): "Update to release X" here too.
{move || {
let admin = crate::app::session().get().map(|s| s.is_operator()).unwrap_or(false);
view! { <crate::pages::sites::EstateUpdate estate=instance() admin=admin/> }
}}
// The operator terminates a sold estate here (owner 2026-10-22), as its buyer may.
{move || {
let admin = crate::app::session().get().map(|s| s.is_operator()).unwrap_or(false);
view! { <crate::pages::sites::EstateTerminate estate=instance() admin=admin/> }
}}
</main>
}
}
#[component]
fn EstateBody(estate: EstateJson, reread: Callback<()>) -> impl IntoView {
let now = crate::pages::now_ms();
let busy = RwSignal::new(false);
let failed = RwSignal::new(None::<Fail>);
let done = RwSignal::new(None::<String>);
let is_admin = crate::app::session()
.get_untracked()
.map(|s| s.is_operator())
.unwrap_or(false);
let post = move |path: String, body: Value, said: String| {
if busy.get_untracked() {
return;
}
busy.set(true);
failed.set(None);
done.set(None);
leptos::task::spawn_local(async move {
match crate::api::post::<_, Value>(&path, &body).await {
Ok(_) => {
done.set(Some(said));
reread.run(());
}
Err(f) => failed.set(Some(f)),
}
busy.set(false);
});
};
let instance = estate.instance.clone();
let open_id = crate::guardian::open_case(&estate)
.map(|c| c.id.clone())
.unwrap_or_default();
let auto_on = estate.auto_fix;
let auto_line = auto_fix_line(&estate);
let bearer = crate::guardian::bearer_line(&estate, now);
let boxes: Vec<[String; 3]> = estate.boxes.iter().map(|b| box_cells(b, now)).collect();
let newest = estate.cases.first().cloned();
let pad = buttons(&estate);
let cases = estate.cases;
let toggle = {
let path = auto_fix_api(&instance);
move |_| {
post(
path.clone(),
json!({ "on": !auto_on }),
if auto_on {
"Auto-fix turned off".into()
} else {
"Auto-fix turned on".into()
},
)
}
};
view! {
<div class="stack">
<Panel title="Boxes">
<table class="guardian-boxes" aria-label="Boxes">
<thead><tr>{BOX_COLUMNS.iter().map(|c| view! { <th scope="col">{*c}</th> }).collect_view()}</tr></thead>
<tbody>
{boxes.into_iter().map(|cells| view! { <tr>{cells.into_iter().map(|c| view! { <td>{c}</td> }).collect_view()}</tr> }).collect_view()}
</tbody>
</table>
</Panel>
<Panel title="Repair">
<div class="actions" role="group" aria-label="Repair buttons">
{pad.into_iter().map(|b| {
let path = press_api(&instance);
let case_id = open_id.clone();
let remedy = b.remedy;
let class = if b.recommended { "btn danger" } else { "btn" };
let enabled = b.enabled && is_admin;
let title = if !b.why.is_empty() { b.why.clone() } else if !is_admin { "a repair needs an admin passkey".into() } else { String::new() };
view! {
<button class=class title=title disabled=move || busy.get() || !enabled
on:click=move |_| post(path.clone(), json!({ "remedy": remedy, "case_id": case_id }), format!("{} approved — the estate pulls the order on its next call home", remedy_label(remedy)))>
{b.label}
</button>
}
}).collect_view()}
</div>
<p class="hint">{auto_line}</p>
<p class="hint">{bearer}</p>
<button class="btn quiet" disabled=move || busy.get() || !is_admin on:click=toggle>{if auto_on { "Turn auto-fix off" } else { "Turn auto-fix on" }}</button>
{move || done.get().map(|d| view! { <p class="hint">{d}</p> })}
{move || failed.get().map(|f| view! { <FailBox fail=f/> })}
</Panel>
<Panel title="Cases">
{if cases.is_empty() {
view! { <p class="hint">"No case on this estate."</p> }.into_any()
} else {
view! {
<table class="guardian-cases" aria-label="Cases">
<thead><tr>{CASE_COLUMNS.iter().map(|c| view! { <th scope="col">{*c}</th> }).collect_view()}</tr></thead>
<tbody>
{cases.iter().map(|c| {
let [opened, finding, state, remedy, by, outcome] = case_cells(c, now);
let tone = state_tone(&c.state);
view! {
<tr title=c.id.clone()>
<td>{opened}</td>
<td>{finding}</td>
<td><Badge tone=tone label=state/></td>
<td>{remedy}</td>
<td>{by}</td>
<td>{outcome}</td>
</tr>
}
}).collect_view()}
</tbody>
</table>
}.into_any()
}}
</Panel>
{newest.map(|c| view! {
<Panel title=format!("Case {} · {}", c.id, c.state)>
<table class="guardian-steps" aria-label="Steps">
<thead><tr>{STEP_COLUMNS.iter().map(|h| view! { <th scope="col">{*h}</th> }).collect_view()}</tr></thead>
<tbody>
{c.steps.iter().map(|s| view! { <tr>{step_cells(s).into_iter().map(|x| view! { <td>{x}</td> }).collect_view()}</tr> }).collect_view()}
</tbody>
</table>
<ul class="evidence">{c.evidence.iter().map(|e| view! { <li>{e.clone()}</li> }).collect_view()}</ul>
<ul class="log">{c.log.iter().map(|l| view! { <li>{format!("{} {}", crate::fmt::since(l.at_unix_ms, now), l.words)}</li> }).collect_view()}</ul>
{(!c.receipt_json.is_empty()).then(|| view! { <pre class="receipt">{c.receipt_json.clone()}</pre> })}
</Panel>
})}
</div>
}
}