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/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>
    }
}