crates/monetize-ui-web/src/guardian.rs
//! **THE ESTATE GUARDIAN** (5.4 resilience) — every decision the guardian
//! screens take, compiled for the host and tested there; `pages::guardian`
//! only lays them out.
//!
//! One estate (a Private Gunnar: main + twin) has repair CASES:
//! `detected → investigating → approved → running → done | failed`. A human
//! presses one of four buttons — recharge from twin, review twin, new twin,
//! new server — with a fresh admin passkey; or the estate's AUTO-FIX opt-in
//! (off by default) lets the guardian approve its own case. A button never
//! reaches into the estate: monetize queues a SIGNED order the box pulls, or
//! creates a box through its cloud and the installer lays it.
//!
//! ```text
//! Guardian: estate | product | main | twin | auto-fix | open case
//! One estate: boxes (role | last heard | address), the four buttons,
//! the auto-fix switch, cases (opened | finding | state | remedy | approved by | outcome),
//! and the newest case's steps and log
//! ```
//!
//! [`estate_text`] draws the screen as plain text — what the tests (and
//! `ui-case.md`) read.
use serde::Deserialize;
use crate::accounts::text_table;
use crate::fmt::{percent_encode, since, Tone};
/// The four buttons: (remedy, label), in screen order.
pub const BUTTONS: [(&str, &str); 4] = [
("recharge_from_twin", "Recharge from twin"),
("review_twin", "Review twin"),
("new_twin", "New twin"),
("new_server", "New server"),
];
/// The Guardian list's columns.
pub const ESTATE_COLUMNS: [&str; 6] =
["estate", "product", "main", "twin", "auto-fix", "open case"];
/// One estate's box columns.
pub const BOX_COLUMNS: [&str; 3] = ["role", "last heard", "address"];
/// One estate's case columns.
pub const CASE_COLUMNS: [&str; 6] = [
"opened",
"finding",
"state",
"remedy",
"approved by",
"outcome",
];
/// One case's step columns.
pub const STEP_COLUMNS: [&str; 3] = ["step", "state", "detail"];
/// A cell with nothing to say.
pub const NONE: &str = "—";
/// The principal the guardian records when auto-fix approved the case.
pub const AUTO_WORDS: &str = "guardian (auto-fix)";
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
pub struct BoxJson {
pub role: String,
#[serde(default)]
pub heard_unix_ms: u64,
#[serde(default)]
pub silent: bool,
#[serde(default)]
pub laid_ipv4: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
pub struct StepJson {
#[serde(default)]
pub seq: u64,
pub words: String,
pub state: String,
#[serde(default)]
pub detail: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
pub struct LineJson {
pub at_unix_ms: u64,
pub words: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
pub struct CaseJson {
pub id: String,
pub state: String,
#[serde(default)]
pub opened_unix_ms: u64,
#[serde(default)]
pub updated_unix_ms: u64,
#[serde(default)]
pub finding_code: String,
#[serde(default)]
pub finding_words: String,
#[serde(default)]
pub evidence: Vec<String>,
#[serde(default)]
pub buttons: Vec<String>,
#[serde(default)]
pub remedy: String,
#[serde(default)]
pub approved_by: String,
#[serde(default)]
pub auto: bool,
#[serde(default)]
pub steps: Vec<StepJson>,
#[serde(default)]
pub log: Vec<LineJson>,
#[serde(default)]
pub outcome: String,
#[serde(default)]
pub receipt_json: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
pub struct EstateJson {
pub instance: String,
#[serde(default)]
pub product: String,
#[serde(default)]
pub auto_fix: bool,
#[serde(default)]
pub boxes: Vec<BoxJson>,
/// Newest first.
#[serde(default)]
pub cases: Vec<CaseJson>,
/// Its estate bearer (ST-13): until when it opens (0: none, or revoked).
#[serde(default)]
pub bearer_until_unix_ms: u64,
/// When the operator revoked it (0: never).
#[serde(default)]
pub bearer_revoked_unix_ms: u64,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
pub struct GuardianJson {
#[serde(default)]
pub estates: Vec<EstateJson>,
/// PROD-4: the box did not answer within its bound; this is its state this old.
#[serde(default)]
pub stale_secs: Option<u64>,
}
/// One button as the screen draws it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ButtonState {
pub remedy: &'static str,
pub label: &'static str,
pub enabled: bool,
/// The open case recommends it.
pub recommended: bool,
/// Why it is disabled; empty when enabled.
pub why: String,
}
/// `/guardian/estate?instance=…`
pub fn estate_href(instance: &str) -> String {
format!("/guardian/estate?instance={}", percent_encode(instance))
}
/// The BFF's path for one estate.
pub fn estate_api(instance: &str) -> String {
format!("/api/guardian/{}", percent_encode(instance))
}
/// The BFF's path for a press on one estate.
pub fn press_api(instance: &str) -> String {
format!("{}/press", estate_api(instance))
}
/// The BFF's path for the estate's auto-fix switch.
pub fn auto_fix_api(instance: &str) -> String {
format!("{}/auto-fix", estate_api(instance))
}
fn in_hand(state: &str) -> bool {
matches!(state, "detected" | "investigating" | "approved" | "running")
}
fn busy(state: &str) -> bool {
matches!(state, "approved" | "running")
}
/// The case still in hand (detected, investigating, approved or running), if any.
pub fn open_case(e: &EstateJson) -> Option<&CaseJson> {
e.cases.iter().find(|c| in_hand(&c.state))
}
/// A remedy's label.
pub fn remedy_label(remedy: &str) -> String {
BUTTONS
.iter()
.find(|(r, _)| *r == remedy)
.map(|(_, l)| (*l).to_owned())
.unwrap_or_else(|| remedy.to_owned())
}
/// What colour a case state carries.
pub fn state_tone(state: &str) -> Tone {
match state {
"done" => Tone::Pass,
"failed" => Tone::Bad,
"approved" | "running" => Tone::Live,
"detected" | "investigating" => Tone::Warn,
_ => Tone::Plain,
}
}
/// **The four buttons.** Pressable unless a case is already approved or
/// running (one repair at a time); the open case's recommendations are marked.
pub fn buttons(e: &EstateJson) -> Vec<ButtonState> {
let open = open_case(e);
let why = open
.filter(|c| busy(&c.state))
.map(|c| format!("case {} is {}: one repair at a time", c.id, c.state))
.unwrap_or_default();
BUTTONS
.iter()
.map(|(remedy, label)| ButtonState {
remedy,
label,
enabled: why.is_empty(),
recommended: open.is_some_and(|c| c.buttons.iter().any(|b| b == remedy)),
why: why.clone(),
})
.collect()
}
/// The auto-fix switch's line.
pub fn auto_fix_line(e: &EstateJson) -> String {
if e.auto_fix {
"Auto-fix: ON — the guardian approves its own cases".into()
} else {
"Auto-fix: OFF (opt-in) — a person approves every repair".into()
}
}
/// **The estate bearer's line** (ST-13): live until a date, expired, revoked, or none — and
/// how it is renewed or taken away.
pub fn bearer_line(e: &EstateJson, now_ms: u64) -> String {
let rotate = "a new enrolment token replaces it";
match (e.bearer_until_unix_ms, e.bearer_revoked_unix_ms) {
(_, r) if r > 0 => format!("Estate bearer: REVOKED {} — {rotate}", crate::fmt::date(r)),
(0, _) => {
format!("Estate bearer: none — the estate has not enrolled here with one; {rotate}")
}
(u, _) if u <= now_ms => {
format!("Estate bearer: EXPIRED {} — {rotate}", crate::fmt::date(u))
}
(u, _) if u == u64::MAX => format!(
"Estate bearer: live, no expiry — revoke it at the door (attach-revoke); {rotate}"
),
(u, _) => format!(
"Estate bearer: live until {} — revoke it at the door (attach-revoke); {rotate}",
crate::fmt::date(u)
),
}
}
fn heard(b: &BoxJson, now_ms: u64) -> String {
if b.heard_unix_ms == 0 {
return "never".into();
}
let ago = since(b.heard_unix_ms, now_ms);
if b.silent {
format!("{ago} · SILENT")
} else {
ago
}
}
fn or_none(s: &str) -> String {
if s.is_empty() {
NONE.into()
} else {
s.to_owned()
}
}
/// One box: role | last heard | address.
pub fn box_cells(b: &BoxJson, now_ms: u64) -> [String; 3] {
[b.role.clone(), heard(b, now_ms), or_none(&b.laid_ipv4)]
}
/// One case: opened | finding | state | remedy | approved by | outcome.
pub fn case_cells(c: &CaseJson, now_ms: u64) -> [String; 6] {
let by = if c.auto {
AUTO_WORDS.to_owned()
} else {
or_none(&c.approved_by)
};
let finding = if c.finding_words.is_empty() {
c.finding_code.clone()
} else {
c.finding_words.clone()
};
let remedy = if c.remedy.is_empty() {
NONE.into()
} else {
remedy_label(&c.remedy)
};
[
since(c.opened_unix_ms, now_ms),
finding,
c.state.clone(),
remedy,
by,
or_none(&c.outcome),
]
}
/// One step: step | state | detail.
pub fn step_cells(s: &StepJson) -> [String; 3] {
let words = if s.seq > 0 {
format!("{} (order #{})", s.words, s.seq)
} else {
s.words.clone()
};
[words, s.state.clone(), or_none(&s.detail)]
}
/// One estate on the Guardian list.
pub fn estate_cells(e: &EstateJson, now_ms: u64) -> [String; 6] {
let role = |r: &str| {
e.boxes
.iter()
.find(|b| b.role == r)
.map(|b| heard(b, now_ms))
.unwrap_or_else(|| NONE.into())
};
let open = open_case(e)
.map(|c| {
format!(
"{}: {}",
c.state,
if c.finding_words.is_empty() {
&c.finding_code
} else {
&c.finding_words
}
)
})
.unwrap_or_else(|| NONE.into());
[
e.instance.clone(),
e.product.clone(),
role("main"),
role("twin"),
if e.auto_fix {
"on".into()
} else {
"off".into()
},
open,
]
}
/// The buttons as one text line: `[ Label ]` pressable, `[*Label*]`
/// recommended, `( Label )` disabled.
pub fn buttons_line(e: &EstateJson) -> String {
buttons(e)
.iter()
.map(|b| match (b.enabled, b.recommended) {
(false, _) => format!("( {} )", b.label),
(true, true) => format!("[*{}*]", b.label),
(true, false) => format!("[ {} ]", b.label),
})
.collect::<Vec<_>>()
.join(" ")
}
/// **The Guardian list as text.**
pub fn guardian_text(j: &GuardianJson, now_ms: u64) -> String {
let rows: Vec<Vec<String>> = j
.estates
.iter()
.map(|e| estate_cells(e, now_ms).to_vec())
.collect();
format!("Guardian\n{}", text_table(&ESTATE_COLUMNS, &rows))
}
/// **One estate's case screen as text** — boxes, buttons, auto-fix, cases,
/// and the newest case's steps and log.
pub fn estate_text(e: &EstateJson, now_ms: u64) -> String {
let mut t = format!("Estate {} ({})\n\n", e.instance, e.product);
let boxes: Vec<Vec<String>> = e
.boxes
.iter()
.map(|b| box_cells(b, now_ms).to_vec())
.collect();
t.push_str(&text_table(&BOX_COLUMNS, &boxes));
t.push('\n');
t.push_str(&buttons_line(e));
t.push('\n');
if let Some(why) = buttons(e)
.first()
.map(|b| b.why.clone())
.filter(|w| !w.is_empty())
{
t.push_str(&format!(" {why}\n"));
}
t.push_str(&format!("{}\n", auto_fix_line(e)));
t.push_str(&format!("{}\n\n", bearer_line(e, now_ms)));
let cases: Vec<Vec<String>> = e
.cases
.iter()
.map(|c| case_cells(c, now_ms).to_vec())
.collect();
t.push_str(&text_table(&CASE_COLUMNS, &cases));
if let Some(c) = e.cases.first() {
let mut head = format!("\nCase {} · {}", c.id, c.state);
if !c.remedy.is_empty() {
head.push_str(&format!(" · {}", remedy_label(&c.remedy)));
}
if c.auto {
head.push_str(&format!(" · approved by {AUTO_WORDS}"));
} else if !c.approved_by.is_empty() {
head.push_str(&format!(" · approved by {}", c.approved_by));
}
t.push_str(&head);
t.push('\n');
if !c.steps.is_empty() {
let steps: Vec<Vec<String>> = c.steps.iter().map(|s| step_cells(s).to_vec()).collect();
t.push_str(&text_table(&STEP_COLUMNS, &steps));
}
if !c.evidence.is_empty() {
t.push_str("Evidence\n");
for ev in &c.evidence {
t.push_str(&format!(" · {ev}\n"));
}
}
if !c.log.is_empty() {
t.push_str("Log\n");
for l in &c.log {
t.push_str(&format!(" {} {}\n", since(l.at_unix_ms, now_ms), l.words));
}
}
if !c.receipt_json.is_empty() {
t.push_str(&format!("Receipt {}\n", c.receipt_json));
}
}
t
}
#[cfg(test)]
mod tests {
use super::*;
const NOW: u64 = 1_760_090_000_000;
fn twin_lost(state: &str) -> EstateJson {
EstateJson {
instance: "acme".into(),
product: "gunnar".into(),
auto_fix: false,
boxes: vec![
BoxJson {
role: "main".into(),
heard_unix_ms: NOW - 20_000,
silent: false,
laid_ipv4: "94.237.1.10".into(),
},
BoxJson {
role: "twin".into(),
heard_unix_ms: NOW - 6 * 60_000,
silent: true,
laid_ipv4: "94.237.1.11".into(),
},
],
cases: vec![
CaseJson {
id: "acme-2".into(),
state: state.into(),
opened_unix_ms: NOW - 5 * 60_000,
finding_code: "twin_silent".into(),
finding_words: "the twin has not called home for 6 min".into(),
evidence: vec!["twin last heard 6 min ago".into()],
buttons: vec!["new_twin".into(), "review_twin".into()],
remedy: if state == "detected" || state == "investigating" {
String::new()
} else {
"new_twin".into()
},
approved_by: if state == "detected" || state == "investigating" {
String::new()
} else {
"rickard".into()
},
steps: vec![
StepJson {
seq: 0,
words: "fence the old twin (cloud: destroy server)".into(),
state: "met".into(),
detail: String::new(),
},
StepJson {
seq: 0,
words: "lay a new twin (installer)".into(),
state: "running".into(),
detail: "relay 94.237.1.12".into(),
},
StepJson {
seq: 12,
words: "main pulls adopt_twin".into(),
state: "pending".into(),
detail: String::new(),
},
],
log: vec![LineJson {
at_unix_ms: NOW - 5 * 60_000,
words: "detected: twin silent".into(),
}],
..Default::default()
},
CaseJson {
id: "acme-1".into(),
state: "done".into(),
opened_unix_ms: NOW - 3 * 86_400_000,
finding_code: "main_data_lost".into(),
finding_words: "main lost its data set".into(),
remedy: "recharge_from_twin".into(),
approved_by: "rickard".into(),
outcome: "recharged 1.2 GiB from the twin".into(),
..Default::default()
},
],
..Default::default()
}
}
/// ★ **No open case: all four buttons are pressable** — a human may start
/// any repair; none is recommended.
#[test]
fn with_no_case_open_all_four_buttons_are_pressable() {
let mut e = twin_lost("done");
e.cases.remove(0);
let b = buttons(&e);
assert_eq!(
b.iter().map(|b| b.label).collect::<Vec<_>>(),
[
"Recharge from twin",
"Review twin",
"New twin",
"New server"
]
);
assert!(
b.iter()
.all(|b| b.enabled && !b.recommended && b.why.is_empty()),
"{b:?}"
);
assert!(open_case(&e).is_none());
}
/// ★ **A detected case recommends its remedies; every button stays pressable.**
#[test]
fn a_detected_case_marks_what_it_recommends() {
let e = twin_lost("detected");
assert_eq!(open_case(&e).map(|c| c.id.as_str()), Some("acme-2"));
let b = buttons(&e);
assert!(b.iter().all(|b| b.enabled), "{b:?}");
assert_eq!(
b.iter()
.filter(|b| b.recommended)
.map(|b| b.remedy)
.collect::<Vec<_>>(),
["review_twin", "new_twin"]
);
assert_eq!(
buttons_line(&e),
"[ Recharge from twin ] [*Review twin*] [*New twin*] [ New server ]"
);
}
/// ★ **One repair at a time**: while a case is approved or running, every
/// button is disabled and says which case holds the estate.
#[test]
fn a_running_case_disables_every_button_and_says_why() {
for state in ["approved", "running"] {
let e = twin_lost(state);
let b = buttons(&e);
assert!(
b.iter()
.all(|b| !b.enabled && b.why.contains("acme-2") && b.why.contains(state)),
"{state}: {b:?}"
);
assert_eq!(
buttons_line(&e),
"( Recharge from twin ) ( Review twin ) ( New twin ) ( New server )"
);
}
}
/// ★ **Auto-fix is an opt-in, OFF by default**, and the line says what
/// each position means; an auto-approved case names the guardian.
#[test]
fn the_estate_bearer_is_listed_live_expired_revoked_or_none() {
let mut e = twin_lost("detected");
assert!(
bearer_line(&e, NOW).starts_with("Estate bearer: none"),
"{}",
bearer_line(&e, NOW)
);
e.bearer_until_unix_ms = NOW + 86_400_000;
assert!(bearer_line(&e, NOW).starts_with(&format!(
"Estate bearer: live until {}",
crate::fmt::date(NOW + 86_400_000)
)));
assert!(bearer_line(&e, NOW + 86_400_000).starts_with("Estate bearer: EXPIRED"));
e.bearer_revoked_unix_ms = NOW;
assert!(bearer_line(&e, NOW).starts_with("Estate bearer: REVOKED"));
assert!(
estate_text(&e, NOW).contains("Estate bearer: REVOKED"),
"the case screen lists it"
);
}
#[test]
fn auto_fix_is_an_opt_in_and_an_auto_case_names_the_guardian() {
let mut e = twin_lost("running");
assert_eq!(
auto_fix_line(&e),
"Auto-fix: OFF (opt-in) — a person approves every repair"
);
e.auto_fix = true;
assert_eq!(
auto_fix_line(&e),
"Auto-fix: ON — the guardian approves its own cases"
);
e.cases[0].auto = true;
assert_eq!(case_cells(&e.cases[0], NOW)[4], AUTO_WORDS);
assert!(!EstateJson::default().auto_fix, "absent means off");
}
/// The cells: a silent box says so; a case reads opened | finding | state |
/// remedy | approved by | outcome; a step carries its order seq.
#[test]
fn boxes_cases_and_steps_read_as_cells() {
let e = twin_lost("running");
assert_eq!(
box_cells(&e.boxes[0], NOW),
["main".to_string(), "just now".into(), "94.237.1.10".into()]
);
assert_eq!(
box_cells(&e.boxes[1], NOW),
[
"twin".to_string(),
"6 min ago · SILENT".into(),
"94.237.1.11".into()
]
);
assert_eq!(
box_cells(
&BoxJson {
role: "twin".into(),
..Default::default()
},
NOW
)[1],
"never"
);
assert_eq!(
case_cells(&e.cases[0], NOW),
[
"5 min ago".to_string(),
"the twin has not called home for 6 min".into(),
"running".into(),
"New twin".into(),
"rickard".into(),
NONE.into()
]
);
assert_eq!(
case_cells(&e.cases[1], NOW)[3..],
[
"Recharge from twin".to_string(),
"rickard".into(),
"recharged 1.2 GiB from the twin".into()
]
);
assert_eq!(
case_cells(&twin_lost("detected").cases[0], NOW)[3..5],
[NONE.to_string(), NONE.into()]
);
assert_eq!(
step_cells(&e.cases[0].steps[2]),
[
"main pulls adopt_twin (order #12)".to_string(),
"pending".into(),
NONE.into()
]
);
assert_eq!(step_cells(&e.cases[0].steps[1])[2], "relay 94.237.1.12");
assert_eq!(
(
state_tone("done"),
state_tone("failed"),
state_tone("running"),
state_tone("detected")
),
(Tone::Pass, Tone::Bad, Tone::Live, Tone::Warn)
);
}
/// The list: one line per estate, the boxes' last heard, auto-fix and the
/// open case (or none).
#[test]
fn the_guardian_list_reads_one_line_per_estate() {
let e = twin_lost("detected");
assert_eq!(
estate_cells(&e, NOW),
[
"acme".to_string(),
"gunnar".into(),
"just now".into(),
"6 min ago · SILENT".into(),
"off".into(),
"detected: the twin has not called home for 6 min".into()
]
);
let mut quiet = twin_lost("done");
quiet.cases.remove(0);
quiet.auto_fix = true;
assert_eq!(
estate_cells(&quiet, NOW)[4..],
["on".to_string(), NONE.into()]
);
let text = guardian_text(
&GuardianJson {
estates: vec![e],
..Default::default()
},
NOW,
);
assert!(
text.starts_with("Guardian\nestate | product | main"),
"{text}"
);
assert!(text.contains("acme"), "{text}");
}
/// ★ **The case screen as text** — what `ui-case.md` shows the owner.
#[test]
fn one_estate_reads_as_boxes_buttons_auto_fix_cases_then_the_newest_case() {
let t = estate_text(&twin_lost("running"), NOW);
let at = |needle: &str| {
t.find(needle)
.unwrap_or_else(|| panic!("{needle:?} missing from:\n{t}"))
};
assert!(
at("Estate acme (gunnar)") < at("role | last heard")
&& at("role | last heard") < at("( Recharge from twin )")
);
assert!(
at("( New server )") < at("Auto-fix: OFF") && at("Auto-fix: OFF") < at("| finding")
);
assert!(at("| finding") < at("Case acme-2 · running · New twin · approved by rickard"));
assert!(
at("Case acme-2") < at("\nstep ")
&& at("\nstep ") < at("main pulls adopt_twin (order #12)")
);
assert!(at("Evidence") < at("twin last heard 6 min ago"));
assert!(
t.contains("Log") && t.contains("detected: twin silent"),
"{t}"
);
}
/// Instances are path segments and query values: encoded.
#[test]
fn an_estate_is_addressed_by_its_instance() {
assert_eq!(
estate_href("acme ab"),
"/guardian/estate?instance=acme%20ab"
);
assert_eq!(estate_api("acme"), "/api/guardian/acme");
assert_eq!(press_api("acme"), "/api/guardian/acme/press");
assert_eq!(auto_fix_api("acme"), "/api/guardian/acme/auto-fix");
assert_eq!(remedy_label("new_server"), "New server");
assert_eq!(remedy_label("odd"), "odd");
}
}