crates/monetize-ui-web/src/pages/settings.rs
//! **SETTINGS** — one estate's settings, as its own monetize answers them.
//!
//! The one thing that tells two estates of the same product apart lives here,
//! as the SETTING it is (owner 2026-10-02): which payment keys the estate
//! holds, live or test — read off the estate's own vendor block, never off its
//! name. Beside it: the names it is reached by, the clouds it may spend at,
//! and the key this console signs every call to it with.
//!
//! ★ UW-18: and the console's own doors an operator needs with no shell on the box —
//! the passkeys that sign in here and "sign every session out" (`/auth/passkey/credentials`,
//! `/auth/sessions/revoke-all`), and a ledger backup now (`/api/backup`, `Ledger.Export`
//! into monetize's own backup directory; restored by the box's restore verb).
use leptos::prelude::*;
use serde_json::{json, Value};
use crate::estate::{stripe_word, summary_path, SummaryJson};
use crate::pages::money::MoneyAction;
use crate::pages::shell::use_estate;
use crate::ui::{resolved, Badge, PageHeader, Panel, Tone};
#[component]
pub fn SettingsPage() -> impl IntoView {
let estate = use_estate();
let summary = LocalResource::new(move || {
let e = estate.get();
async move { crate::api::get::<SummaryJson>(&summary_path(&e)).await }
});
view! {
<PageHeader title=move || format!("{} · Settings", estate.get()) subtitle="Current settings. They change with an install or upgrade."/>
{move || resolved(summary.get(), None, move |s: SummaryJson| {
let stripe = stripe_word(&s.vendors, &s.vendor_names, &s.site_vendor);
let stripe_tone = match stripe { "live" => Tone::Live, "test" => Tone::Plain, _ => Tone::Warn };
let host = s.control.as_deref().map(|c| c.trim_start_matches("https://").trim_end_matches('/').to_owned());
view! {
<div class="stack">
<Panel title="Payment keys">
<dl class="facts panel-body">
<dt>"Stripe"</dt>
<dd><Badge tone=stripe_tone label=crate::estate::stripe_label(stripe)/></dd>
<dt>"Every vendor"</dt>
<dd class="mono">{{ let lines = crate::estate::vendor_lines(&s.vendors, &s.vendor_names); if lines.is_empty() { "none".to_owned() } else { lines.join(" · ") } }}</dd>
</dl>
<p class="panel-body hint">"Which keys the estate holds is the one setting that tells two estates of the same product apart. It is read off the estate's own [vendors.stripe] block, which its install writes from the key it feeds. Money is counted live or test by each payment's own word, not by this setting."</p>
</Panel>
<Panel title="Names">
<dl class="facts panel-body">
<dt>"Estate"</dt>
<dd class="mono">{s.estate.clone()}</dd>
<dt>"Reached at"</dt>
<dd class="mono">{host.unwrap_or_else(|| "this console's own loopback".into())}</dd>
<dt>"Serves"</dt>
<dd class="mono">{if s.products.is_empty() { "—".to_owned() } else { s.products.join(" · ") }}</dd>
<dt>"Clouds"</dt>
<dd class="mono">{if s.clouds.is_empty() { "none".to_owned() } else { s.clouds.iter().map(|c| if *c == s.default_cloud { format!("{c} (purchases go here)") } else { c.clone() }).collect::<Vec<_>>().join(" · ") }}</dd>
</dl>
</Panel>
<Panel title="Keys">
<dl class="facts panel-body">
<dt>"This console signs with"</dt>
<dd class="mono">{s.signed_with.clone().unwrap_or_else(|| "no key: a loopback bearer".into())}</dd>
<dt>"Answering"</dt>
<dd>{if s.reached { view! { <Badge tone=Tone::Pass label="this key is accepted"/> }.into_any() } else if s.unfed { view! { <Badge tone=Tone::Bad label=crate::estate::UNFED/> }.into_any() } else { view! { <Badge tone=Tone::Bad label=crate::estate::NOT_REACHED/> }.into_any() }}</dd>
</dl>
<p class="panel-body hint">"The public fingerprint of the SSH key every call to this estate is signed with — the monetize-ui principal its install fed it. The estate's certificate is pinned by this console."</p>
</Panel>
<Passkeys/>
<GuestCalls/>
<Panel title="Backup">
<p class="panel-body hint">"The ledger and the usage readings, exported now into monetize's own backup directory (no secret is in it). The box's restore takes it back."</p>
<div class="panel-body">
<MoneyAction label="Back up now…" confirm_label="Confirm: export the ledger" path="/api/backup".to_owned() base=json!({}) on_done=Callback::new(|()| {})/>
</div>
</Panel>
</div>
}.into_any()
})}
}
}
/// One passkey as its line says it: its device name, whose it is, when it was made and
/// last used, and DISABLED when it is.
pub fn passkey_line(c: &Value) -> String {
let when = |k: &str| {
c[k].as_i64()
.filter(|s| *s > 0)
.map(|s| crate::fmt::date(s as u64 * 1000))
};
let mut out = vec![
c["label"]
.as_str()
.filter(|l| !l.is_empty())
.unwrap_or("unnamed key")
.to_owned(),
c["operator"].as_str().unwrap_or("operator").to_owned(),
];
if let Some(d) = when("created") {
out.push(format!("since {d}"));
}
out.push(
when("last_used")
.map(|d| format!("last used {d}"))
.unwrap_or_else(|| "never used".into()),
);
if c["disabled"].as_bool() == Some(true) {
out.push("DISABLED".into());
}
out.join(" · ")
}
/// **The passkeys that sign in to this console**, and one press that signs every session
/// out (each device signs in again with its passkey; this one too).
#[component]
fn Passkeys() -> impl IntoView {
let data = LocalResource::new(|| async {
crate::api::get::<Value>("/auth/passkey/credentials").await
});
view! {
<Panel title="Passkeys">
{move || {
let read = data.get().and_then(|r| r.ok()).unwrap_or_default();
let lines: Vec<String> = read["credentials"].as_array().map(|a| a.iter().map(passkey_line).collect()).unwrap_or_default();
view! {
<ul class="panel-body mono">
{if lines.is_empty() { view! { <li class="hint">"none read"</li> }.into_any() } else { lines.into_iter().map(|l| view! { <li>{l}</li> }).collect_view().into_any() }}
</ul>
}
}}
<div class="panel-body">
<MoneyAction label="Sign every session out…" confirm_label="Confirm: sign out everywhere" path="/auth/sessions/revoke-all".to_owned() base=json!({}) on_done=Callback::new(|()| {})/>
</div>
</Panel>
}
}
/// One guest counter row as its line says it: `vendor guest-stripe.wasm status: 10 calls, 1
/// failed, 25 ms on average, 90 ms at most`.
pub fn counter_line(c: &Value) -> String {
let n = |k: &str| c[k].as_u64().unwrap_or_default();
let s = |k: &str| c[k].as_str().unwrap_or("?").to_owned();
let (calls, faults) = (n("calls"), n("faults"));
let avg = n("total_ms").checked_div(calls).unwrap_or_default();
let mut out = format!(
"{} {} {}: {calls} call{}",
s("kind"),
s("guest"),
s("op_name"),
if calls == 1 { "" } else { "s" }
);
if faults > 0 {
out.push_str(&format!(", {faults} failed"));
}
out.push_str(&format!(
", {avg} ms on average, {} ms at most",
n("max_ms")
));
out
}
/// **The calls this monetize made to its signed guests**, one line per guest op and vendor
/// call, as monetize counts them (`/api/counters`); since its last start.
#[component]
fn GuestCalls() -> impl IntoView {
let data = LocalResource::new(|| async { crate::api::get::<Value>("/api/counters").await });
view! {
<Panel title="Guest calls">
{move || {
let read = data.get().and_then(|r| r.ok()).unwrap_or_default();
let lines: Vec<String> = read["rows"].as_array().map(|a| a.iter().map(counter_line).collect()).unwrap_or_default();
let since = read["since_ms"].as_u64().filter(|s| *s > 0).map(crate::fmt::date);
view! {
<ul class="panel-body mono">
{if lines.is_empty() { view! { <li class="hint">"no guest called yet"</li> }.into_any() } else { lines.into_iter().map(|l| view! { <li>{l}</li> }).collect_view().into_any() }}
</ul>
<p class="panel-body hint">{format!("Every call to a signed guest (product, sign-up, payment vendor, cloud, DNS), counted by monetize since {}. A failed call is one the host ended (a bound, a trap): nothing came back.", since.unwrap_or_else(|| "its start".into()))}</p>
}
}}
</Panel>
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_guest_counter_reads_as_its_calls_its_failures_and_its_times() {
let c = json!({"kind": "vendor", "guest": "guest-stripe.wasm", "op": 3, "op_name": "status", "calls": 10, "faults": 1, "total_ms": 250, "max_ms": 90});
assert_eq!(
counter_line(&c),
"vendor guest-stripe.wasm status: 10 calls, 1 failed, 25 ms on average, 90 ms at most"
);
let one = json!({"kind": "cloud", "guest": "guest-upcloud.wasm", "op_name": "lay", "calls": 1, "faults": 0, "total_ms": 4, "max_ms": 4});
assert_eq!(
counter_line(&one),
"cloud guest-upcloud.wasm lay: 1 call, 4 ms on average, 4 ms at most"
);
}
/// ★ UW-18: a passkey's line names the device, its operator, its days and a disabled key.
#[test]
fn a_passkey_reads_as_its_device_operator_and_days() {
let k = json!({"id": "k1", "label": "laptop", "operator": "Rickard", "created": 1_760_000_000, "last_used": null, "disabled": false});
assert_eq!(
passkey_line(&k),
format!(
"laptop · Rickard · since {} · never used",
crate::fmt::date(1_760_000_000_000)
)
);
let off = json!({"label": "", "operator": "Henrik", "created": 0, "last_used": 1_760_000_000, "disabled": true});
assert!(
passkey_line(&off).starts_with("unnamed key · Henrik · last used ")
&& passkey_line(&off).ends_with(" · DISABLED"),
"{}",
passkey_line(&off)
);
}
}