Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar-ui

crates/gunnar-ui-web/src/twin.rs

//! **What the Twin panel and the restore action say**, on the host side of the
//! wasm boundary.
//!
//! Same layout as [`crate::dataset`]: `pages::storage` is wasm-only, so every
//! rule about what the panel shows, when it goes red and when the restore
//! action is drawn lives here, where `cargo test` runs it.
//!
//! The state word is gunnar's (`ok` / `behind` / `silent`). Anything but `ok`
//! is an alarm — including a word from a newer gunnar this build does not
//! know, because a panel that stays calm about a state it cannot read is the
//! failure this panel exists to prevent.

use serde::Deserialize;

use crate::fmt;

/// One twin, as `GET /api/twin` sends it.
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(default)]
pub struct TwinRow {
    pub principal: String,
    pub address: String,
    pub last_seen_unix_ms: Option<i64>,
    pub seen_ago_ms: u64,
    pub applied_seq: u64,
    pub head_seq: u64,
    pub lag_entries: u64,
    pub disk_total_bytes: u64,
    pub disk_used_bytes: u64,
    pub fill_permille: Option<u32>,
    pub state: String,
    pub why: String,
    pub silent_after_ms: u64,
}

/// A twin's view of its primary (only on a twin).
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(default)]
pub struct PrimaryRow {
    pub address: String,
    pub last_answer_unix_ms: Option<i64>,
    pub answer_ago_ms: u64,
    pub primary_head_seq: u64,
    pub applied_seq: u64,
    pub state: String,
    pub why: String,
}

/// `GET /api/twin`.
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(default)]
pub struct TwinPanel {
    pub twins: Vec<TwinRow>,
    pub primary: Option<PrimaryRow>,
    pub unanswered: String,
    /// The twin this console is configured to reach (`GUNNAR_UI_TWIN`), empty
    /// when none is. CONFIGURATION, not a measurement: it says where the
    /// restore would read from, never that anything answered there.
    pub configured_twin: String,
}

/// What a recharge would lose (gunnar-repair's `Loss`).
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(default)]
pub struct Loss {
    pub entries: u64,
    pub unreplicated: u64,
    pub what: String,
}

/// **[Review twin]** — gunnar-repair's `review_twin`, as `GET /api/restore`
/// carries it. The words are the server's; this console only lays them out.
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(default)]
pub struct Review {
    pub twin_addr: String,
    /// `healthy`, `behind`, `primary_silent`, `primary_diverged`, `stopped`,
    /// `empty`, `unreachable` — `None` before it was asked.
    pub health: Option<String>,
    pub head_seq: u64,
    pub first_seq: u64,
    pub primary_head_seq: u64,
    pub applied_seq: u64,
    pub last_heartbeat_unix_ms: i64,
    pub heartbeat_ago_ms: u64,
    pub would_lose: Loss,
    pub recharge_possible: bool,
    pub verdict: String,
    pub refusal: String,
}

/// `GET /api/restore`.
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(default)]
pub struct Eligibility {
    pub eligible: bool,
    pub twin_address: String,
    pub twin_reachable: bool,
    pub twin_first_seq: u64,
    pub twin_head_seq: u64,
    pub window_open: bool,
    pub principals: u64,
    pub keys: u64,
    pub confirm: String,
    pub why: String,
    /// gunnar-repair's refusal code; empty when eligible.
    pub code: String,
    /// The main answers and is not fresh: only a fence-first override.
    pub main_live: bool,
    pub override_confirm: String,
    pub review: Option<Review>,
}

/// `POST /api/restore`'s answer.
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(default)]
pub struct Restored {
    pub head_seq: u64,
    pub applied: u64,
    pub lost: u64,
    pub restart_required: bool,
    pub detail: String,
}

/// Red: anything but gunnar's `ok`.
pub fn alarmed(state: &str) -> bool {
    state != "ok"
}

/// True when any row on the panel is red.
pub fn panel_alarm(panel: &TwinPanel) -> bool {
    panel.twins.iter().any(|t| alarmed(&t.state)) || panel.primary.as_ref().is_some_and(|p| alarmed(&p.state))
}

/// The badge word for a state.
pub fn state_label(state: &str) -> &'static str {
    match state {
        "ok" => "In step",
        "behind" => "Behind",
        "silent" => "Silent",
        _ => "Unknown",
    }
}

/// `4 s`, `3 min`, `2 h`, `5 d`.
pub fn ago(ms: u64) -> String {
    let s = ms / 1000;
    match s {
        0..=59 => format!("{s} s ago"),
        60..=3599 => format!("{} min ago", s / 60),
        3600..=86_399 => format!("{} h ago", s / 3600),
        _ => format!("{} d ago", s / 86_400),
    }
}

/// When the primary last heard from this twin.
pub fn seen_line(row: &TwinRow) -> String {
    match row.last_seen_unix_ms {
        Some(_) => ago(row.seen_ago_ms),
        None => "never heard from".to_owned(),
    }
}

/// How far behind the twin runs.
pub fn lag_line(row: &TwinRow) -> String {
    if row.lag_entries == 0 {
        format!("none (at seq {})", row.applied_seq)
    } else {
        format!("{} entries (seq {} of {})", row.lag_entries, row.applied_seq, row.head_seq)
    }
}

/// How full the twin's data volume is.
pub fn fill_line(row: &TwinRow) -> String {
    match row.fill_permille {
        Some(p) => format!("{}.{} % of {}", p / 10, p % 10, fmt::bytes(row.disk_total_bytes)),
        None => "not measured".to_owned(),
    }
}

/// **What the panel says when gunnar reports no twin row at all.**
///
/// A configured twin gunnar does not report is NOT the same as no twin, and
/// the two must not read alike: the first is a twin this primary has never
/// heard from — which is exactly what a twin that will not boot looks like —
/// and the second is a box that has no second copy. Neither sentence claims
/// anything was measured; the address is configuration.
pub fn no_twin_detail(panel: &TwinPanel) -> String {
    if panel.configured_twin.is_empty() {
        "Gunnar reports no twin for this box, and no twin is configured for the restore.".to_owned()
    } else {
        format!(
            "Gunnar reports no twin row. A twin is configured at {}: until it polls this primary, \
             or is enrolled in group twin, Gunnar has nothing to report about it.",
            panel.configured_twin
        )
    }
}

/// Refusals the person waives by typing the phrase after reading the review
/// (the POST then says `stale_ok` / `accept_lost` / `primary_fenced`).
pub fn waived(e: &Eligibility) -> Option<&'static str> {
    match e.code.as_str() {
        "stale_source" => Some("stale_ok"),
        "would_lose" => Some("accept_lost"),
        "not_fenced" => Some("primary_fenced"),
        _ => None,
    }
}

/// **What the page says first on a `not_fenced` refusal** (R-13): `(line, more)`,
/// the line on screen and the rest behind its "?".
pub fn fence_hint(e: &Eligibility) -> Option<(&'static str, &'static str)> {
    (e.code == "not_fenced").then_some((
        "The twin still hears its old primary: fence or stop it first.",
        "Two writable primaries split the estate. Fence the old primary (standby) or stop its machine, \
         then type the phrase: that says it is fenced. A fenced primary still answers its twin, so the \
         twin cannot tell for you.",
    ))
}

/// Draw the restore action? Only for an administrator, and only when the
/// server said this box is fresh, the twin answered, and named a phrase
/// (eligible, or refused only for a reason the review lets the person waive).
pub fn show_restore(is_admin: bool, e: Option<&Eligibility>) -> bool {
    is_admin
        && e.is_some_and(|e| (e.eligible || waived(e).is_some()) && e.twin_reachable && e.twin_head_seq > 0 && !e.confirm.is_empty())
}

/// Draw the fence-first override? Only for an administrator, on a main that
/// answers as LIVE, with the server's override phrase.
pub fn show_override(is_admin: bool, e: Option<&Eligibility>) -> bool {
    is_admin && e.is_some_and(|e| e.main_live && !e.override_confirm.is_empty())
}

/// The POST body for what the person typed.
pub fn restore_body(typed: &str, e: &Eligibility, override_live_main: bool) -> serde_json::Value {
    serde_json::json!({
        "confirm": typed.trim(),
        "override_live_main": override_live_main,
        "stale_ok": waived(e) == Some("stale_ok"),
        "accept_lost": waived(e) == Some("accept_lost"),
        "primary_fenced": waived(e) == Some("primary_fenced"),
    })
}

/// **The review, line by line**: health, head vs primary, last heartbeat,
/// what a recharge would lose, and the server's verdict.
///
/// `said` is the sentence the panel shows on its own beside the review (the
/// answer's `why`, which IS the review's verdict): it is not repeated here
/// (owner, gunnar.rs 2026-10-14: the `main_live: …` paragraph stood twice).
pub fn review_lines(r: &Review, said: &str) -> Vec<String> {
    let mut lines = vec![format!("Health: {}", r.health.as_deref().unwrap_or("not asked").replace('_', " "))];
    let verdict = (!r.verdict.is_empty() && r.verdict != said).then(|| r.verdict.clone());
    if r.health.as_deref() == Some("unreachable") {
        lines.extend(verdict);
        return lines;
    }
    lines.push(format!(
        "Twin holds seq {}..={}; it has applied {} of the {} its primary last reported.",
        r.first_seq.max(1),
        r.head_seq,
        r.applied_seq,
        r.primary_head_seq
    ));
    lines.push(if r.last_heartbeat_unix_ms > 0 {
        format!("Last heartbeat from its primary: {}.", ago(r.heartbeat_ago_ms))
    } else {
        "Last heartbeat from its primary: never, since the twin started.".to_owned()
    });
    lines.push(if r.would_lose.entries == 0 && r.would_lose.unreplicated == 0 {
        "A recharge would lose nothing the twin was sent.".to_owned()
    } else {
        let mut l = format!("A recharge would lose {} entr{} below the twin's log", r.would_lose.entries, if r.would_lose.entries == 1 { "y" } else { "ies" });
        if r.would_lose.unreplicated > 0 {
            l.push_str(&format!(", and {} the primary had that never reached the twin", r.would_lose.unreplicated));
        }
        if !r.would_lose.what.is_empty() {
            l.push_str(&format!(" ({})", r.would_lose.what));
        }
        l.push('.');
        l
    });
    lines.extend(verdict);
    lines
}

/// What a healthy main reads instead of gunnar-repair's `main_live` paragraph.
pub const MAIN_IS_RUNNING: &str = "Main is running normally, so there is nothing to recharge. Recharge is for a main that is lost or wiped.";

/// **The reason, in a person's words**, with the technical sentence kept as
/// the detail a reader asks for: `Some((line, detail))` for a reason this page
/// has plain words for (a main that is running), `None` for every other, which
/// is shown as the server said it.
pub fn plain_why(e: &Eligibility) -> Option<(&'static str, String)> {
    (e.code == "main_live").then(|| (MAIN_IS_RUNNING, e.why.clone()))
}

/// **Why the restore is not on the page**, for an administrator who is looking
/// for it — and `None` for everyone else, because a missing action is not an
/// operator's problem and an action that never existed needs no excuse.
///
/// The sentence is the server's `why`, verbatim: it names the configured twin
/// and what did not answer, and this console does not decide any of that.
/// Drawing nothing here is what made a front with `GUNNAR_UI_TWIN` unset look,
/// to the one person who could fix it, exactly like a front with no feature.
pub fn restore_absence(is_admin: bool, e: Option<&Eligibility>) -> Option<String> {
    if !is_admin {
        return None;
    }
    let e = e?;
    if show_restore(is_admin, Some(e)) {
        return None;
    }
    Some(if e.why.is_empty() {
        "this Gunnar did not say why.".to_owned()
    } else {
        e.why.clone()
    })
}

/// May the confirmed press be sent? Only with the server's phrase typed exactly.
pub fn may_send(typed: &str, e: &Eligibility) -> bool {
    !e.confirm.is_empty() && typed.trim() == e.confirm
}

/// May the fence-first override be sent? Only with its own phrase typed exactly.
pub fn may_override(typed: &str, e: &Eligibility) -> bool {
    !e.override_confirm.is_empty() && typed.trim() == e.override_confirm
}

/// The one sentence above the button.
pub fn offer_line(e: &Eligibility) -> String {
    format!(
        "This box is fresh (no identities, no keys) and the twin at {} holds transactions {}..={}. \
         Restoring replays all of them into this box.",
        e.twin_address,
        e.twin_first_seq.max(1),
        e.twin_head_seq
    )
}

/// The warning in the confirmation.
pub fn warning() -> &'static str {
    "Every identity, key, grant and repository the twin holds is written into this box. \
     Do not close this page until it answers; a broken stream leaves the box restoring, \
     and running the restore again resumes it."
}

/// What the restore did.
pub fn restored_line(r: &Restored) -> String {
    let mut line = format!("Restored through seq {}: {} applied, {} lost.", r.head_seq, r.applied, r.lost);
    if r.restart_required {
        line.push_str(" Restart Gunnar to serve the restored data.");
    }
    if !r.detail.is_empty() {
        line.push(' ');
        line.push_str(&r.detail);
    }
    line
}

/// **[Rebuild main]**, as `GET /api/rebuild` sends it (main rebuild, owner 2026-10-12): the
/// ONE box's guardian on THIS estate's main — monetize's words, passed through.
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(default)]
pub struct Rebuild {
    pub finding: String,
    pub words: String,
    pub remedy: String,
    pub case_id: String,
    pub case_state: String,
    pub steps: Vec<String>,
    pub outcome: String,
    pub auto_fix: bool,
    /// The phrase a press carries; empty: nothing may be pressed now.
    pub confirm: String,
}

/// The lines the panel shows: the guardian's words, then the case, step by step.
pub fn rebuild_lines(r: &Rebuild) -> Vec<String> {
    let mut out = vec![r.words.clone()];
    if !r.case_id.is_empty() {
        out.push(format!("Case {} is {}.", r.case_id, r.case_state));
        out.extend(r.steps.iter().cloned());
        if !r.outcome.is_empty() {
            out.push(r.outcome.clone());
        }
    }
    if r.auto_fix {
        out.push("Auto-fix is on at its provider: it may start this by itself.".to_string());
    }
    out
}

/// The button's words for the guardian's remedy.
pub fn rebuild_button(r: &Rebuild) -> &'static str {
    match r.remedy.as_str() {
        "recharge_from_twin" => "Recharge the main from the twin…",
        _ => "Rebuild main from the twin…",
    }
}

/// May the press be sent: something to press, and the phrase typed exactly.
pub fn may_rebuild(typed: &str, r: &Rebuild) -> bool {
    !r.confirm.is_empty() && typed.trim() == r.confirm
}

#[cfg(test)]
mod tests {

    /// ★ [Rebuild main]: monetize's words first, then the case; the press only on the exact phrase.
    #[test]
    fn the_rebuild_says_monetizes_words_and_presses_only_on_the_phrase() {
        let r: Rebuild = serde_json::from_str(r#"{"finding":"main_lost","words":"the main is silent (never promoted)","remedy":"new_server","confirm":"rebuild main"}"#).unwrap();
        assert_eq!(rebuild_lines(&r), vec!["the main is silent (never promoted)".to_string()]);
        assert_eq!(rebuild_button(&r), "Rebuild main from the twin…");
        assert!(may_rebuild(" rebuild main", &r) && !may_rebuild("rebuild", &r));
        let running = Rebuild { case_id: "c-1".into(), case_state: "running".into(), steps: vec!["running — fence main".into()], confirm: String::new(), auto_fix: true, ..r };
        assert_eq!(rebuild_lines(&running)[1..], ["Case c-1 is running.".to_string(), "running — fence main".to_string(), "Auto-fix is on at its provider: it may start this by itself.".to_string()]);
        assert!(!may_rebuild("rebuild main", &running), "nothing to press while a case runs");
    }
    use super::*;

    fn row(state: &str, lag: u64, seen: Option<i64>, fill: Option<u32>) -> TwinRow {
        TwinRow {
            principal: "twin".into(),
            address: "10.13.8.122:40112".into(),
            last_seen_unix_ms: seen,
            seen_ago_ms: 125_000,
            applied_seq: 100 - lag,
            head_seq: 100,
            lag_entries: lag,
            disk_total_bytes: 2 << 30,
            fill_permille: fill,
            state: state.into(),
            ..TwinRow::default()
        }
    }

    fn fresh() -> Eligibility {
        Eligibility {
            eligible: true,
            twin_address: "10.13.8.122:50051".into(),
            twin_reachable: true,
            twin_first_seq: 1,
            twin_head_seq: 42,
            window_open: true,
            confirm: "restore 42 from twin".into(),
            ..Eligibility::default()
        }
    }

    #[test]
    fn the_panel_is_red_when_a_twin_is_behind_or_silent_and_only_then() {
        let calm = TwinPanel { twins: vec![row("ok", 0, Some(1), Some(500))], ..TwinPanel::default() };
        assert!(!panel_alarm(&calm));
        for state in ["behind", "silent", "something-newer"] {
            let red = TwinPanel { twins: vec![row("ok", 0, Some(1), Some(500)), row(state, 3, Some(1), None)], ..TwinPanel::default() };
            assert!(panel_alarm(&red), "{state} did not turn the panel red");
        }
        let twin_side = TwinPanel { primary: Some(PrimaryRow { state: "silent".into(), ..PrimaryRow::default() }), ..TwinPanel::default() };
        assert!(panel_alarm(&twin_side));
        assert_eq!((state_label("ok"), state_label("behind"), state_label("silent"), state_label("x")), ("In step", "Behind", "Silent", "Unknown"));
    }

    #[test]
    fn a_row_says_last_seen_lag_and_fill_and_never_invents_a_measurement() {
        let r = row("behind", 7, Some(1_700_000_000_000), Some(503));
        assert_eq!(seen_line(&r), "2 min ago");
        assert_eq!(lag_line(&r), "7 entries (seq 93 of 100)");
        assert_eq!(fill_line(&r), "50.3 % of 2.0 GB".replace("2.0 GB", &fmt::bytes(2 << 30)));
        let never = row("silent", 0, None, None);
        assert_eq!((seen_line(&never).as_str(), fill_line(&never).as_str()), ("never heard from", "not measured"));
        assert_eq!(lag_line(&never), "none (at seq 100)");
        assert_eq!(ago(4_000), "4 s ago");
        assert_eq!(ago(7_200_000), "2 h ago");
    }

    #[test]
    fn the_restore_action_is_drawn_only_for_an_admin_on_a_fresh_box_with_a_reachable_twin() {
        assert!(show_restore(true, Some(&fresh())));
        assert!(!show_restore(false, Some(&fresh())), "an operator saw the restore");
        assert!(!show_restore(true, None), "drawn before the server answered");
        assert!(!show_restore(true, Some(&Eligibility { eligible: false, confirm: String::new(), ..fresh() })));
        assert!(!show_restore(true, Some(&Eligibility { twin_reachable: false, ..fresh() })));
        assert!(!show_restore(true, Some(&Eligibility { confirm: String::new(), ..fresh() })));
        // What a server answers for an enrolled box.
        let enrolled: Eligibility = serde_json::from_str(r#"{"eligible":false,"twin_reachable":true,"twin_head_seq":42,"principals":3,"keys":4,"confirm":"","why":"not fresh"}"#).unwrap();
        assert!(!show_restore(true, Some(&enrolled)));
    }

    /// An administrator looking for the restore is TOLD why it is not there.
    #[test]
    fn an_absent_restore_says_which_of_its_conditions_failed() {
        assert_eq!(restore_absence(true, Some(&fresh())), None, "the action is drawn; it needs no excuse");
        assert_eq!(restore_absence(false, None), None, "an operator was told about an admin's action");
        assert_eq!(restore_absence(false, Some(&Eligibility { eligible: false, ..fresh() })), None);
        assert_eq!(restore_absence(true, None), None, "a sentence before the server answered");

        let unset = Eligibility { why: "no twin is configured (GUNNAR_UI_TWIN is unset)".into(), ..Eligibility::default() };
        assert_eq!(restore_absence(true, Some(&unset)).as_deref(), Some("no twin is configured (GUNNAR_UI_TWIN is unset)"));
        let down = Eligibility {
            eligible: false,
            twin_reachable: false,
            why: "the twin at 10.13.8.122:50051 did not answer Mirror.LogHead: no answer within 10 s".into(),
            ..fresh()
        };
        assert!(restore_absence(true, Some(&down)).is_some_and(|w| w.contains("did not answer")));
        // A server that refused without a reason still gets a sentence, never silence.
        let mute = Eligibility { eligible: false, confirm: String::new(), why: String::new(), ..fresh() };
        assert_eq!(restore_absence(true, Some(&mute)).as_deref(), Some("this Gunnar did not say why."));
    }

    /// No twin reported is not the same thing as no twin configured.
    #[test]
    fn an_unreported_twin_and_an_unconfigured_one_do_not_read_alike() {
        let none = TwinPanel::default();
        assert_eq!(no_twin_detail(&none), "Gunnar reports no twin for this box, and no twin is configured for the restore.");
        let configured = TwinPanel { configured_twin: "10.13.8.122:50051".into(), ..TwinPanel::default() };
        let said = no_twin_detail(&configured);
        assert!(said.contains("10.13.8.122:50051"), "{said}");
        assert_ne!(said, no_twin_detail(&none));
        // Configuration only: nothing here claims the twin was reached.
        for claim in ["reachable", "answered", "silent", "behind", "ok"] {
            assert!(!said.contains(claim), "`{claim}` in {said:?} — that is a measurement");
        }
    }

    /// **[Review twin]** says health, head vs primary, last heartbeat and what
    /// a recharge would lose — the server's numbers, nothing invented.
    #[test]
    fn the_review_reads_health_heads_heartbeat_and_loss() {
        let json = r#"{"twin_addr":"t:1","health":"primary_silent","head_seq":40,"first_seq":30,"primary_head_seq":42,"applied_seq":40,
            "last_heartbeat_unix_ms":5,"heartbeat_ago_ms":125000,"would_lose":{"entries":29,"unreplicated":2,"what":"pushes since"},
            "recharge_possible":false,"verdict":"would_lose: …","refusal":"would_lose","future_field":1}"#;
        let r: Review = serde_json::from_str(json).unwrap();
        let lines = review_lines(&r, "");
        assert_eq!(lines[0], "Health: primary silent");
        assert!(lines[1].contains("30..=40") && lines[1].contains("40 of the 42"), "{}", lines[1]);
        assert_eq!(lines[2], "Last heartbeat from its primary: 2 min ago.");
        assert!(lines[3].contains("29 entries") && lines[3].contains("2 the primary had") && lines[3].contains("pushes since"), "{}", lines[3]);
        assert_eq!(lines[4], "would_lose: …");
        let down = Review { health: Some("unreachable".into()), verdict: "did not answer".into(), ..Review::default() };
        assert_eq!(review_lines(&down, ""), vec!["Health: unreachable".to_owned(), "did not answer".to_owned()]);
    }

    /// ★ Owner, gunnar.rs 2026-10-14: the `main_live: …` paragraph stood TWICE
    /// under Recharge from twin — the review's verdict and the answer's `why`
    /// are the same sentence, and the page drew both. It is drawn once, and a
    /// healthy main reads in a person's words with the technical sentence kept
    /// as the detail.
    #[test]
    fn a_running_mains_reason_is_said_once_and_in_plain_words() {
        let technical = "main_live: the main is NOT fresh, so Bootstrap.Restore would be refused: its identity table holds 3 principals (last seen 1791927777610)";
        let review = Review { health: Some("healthy".into()), head_seq: 42, verdict: technical.into(), refusal: "main_live".into(), ..Review::default() };
        let e = Eligibility { eligible: false, code: "main_live".into(), main_live: true, why: technical.into(), review: Some(review.clone()), ..fresh() };
        let lines = review_lines(&review, &e.why);
        assert!(!lines.iter().any(|l| l == technical), "the verdict is not repeated beside `why`: {lines:?}");
        let (line, detail) = plain_why(&e).expect("plain words for a running main");
        assert_eq!(line, "Main is running normally, so there is nothing to recharge. Recharge is for a main that is lost or wiped.");
        for raw in ["Bootstrap.Restore", "identity table", "1791927777610", "main_live"] {
            assert!(!line.contains(raw), "{raw} in the main text");
        }
        assert_eq!(detail, technical, "the technical sentence is kept as the detail");
        // Any other reason is the server's own sentence.
        assert_eq!(plain_why(&Eligibility { code: "no_twin".into(), why: "no twin is configured".into(), ..fresh() }), None);
        // And the page draws through these.
        let page = include_str!("pages/admin_storage.rs");
        assert!(page.contains("twin::review_lines(&r, &said)") && page.contains("twin::plain_why"), "the panel draws the reason once");
    }

    /// A refusal the review explains (stale, a retention hole) is waived by
    /// typing the phrase, and the POST says which; a live main only overrides.
    #[test]
    fn a_waivable_refusal_draws_the_action_and_a_live_main_only_the_override() {
        let stale = Eligibility { eligible: false, code: "stale_source".into(), ..fresh() };
        assert!(show_restore(true, Some(&stale)));
        let body = restore_body(" restore 42 from twin ", &stale, false);
        assert_eq!((body["stale_ok"].as_bool(), body["accept_lost"].as_bool(), body["confirm"].as_str()), (Some(true), Some(false), Some("restore 42 from twin")));
        let live = Eligibility { eligible: false, code: "main_live".into(), main_live: true, confirm: String::new(), override_confirm: "fence main and restore 42 from twin".into(), ..fresh() };
        assert!(!show_restore(true, Some(&live)));
        assert!(show_override(true, Some(&live)) && !show_override(false, Some(&live)));
        assert!(may_override("fence main and restore 42 from twin", &live) && !may_override("restore 42 from twin", &live));
        assert_eq!(restore_body("x", &live, true)["override_live_main"], true);
    }

    /// **R-13 on the page**: a twin that still hears its primary draws the
    /// action with ONE line on what to do first; the POST says `primary_fenced`.
    #[test]
    fn a_not_fenced_refusal_draws_the_action_with_its_fence_line() {
        let unfenced = Eligibility { eligible: false, code: "not_fenced".into(), ..fresh() };
        assert_eq!(waived(&unfenced), Some("primary_fenced"));
        assert!(show_restore(true, Some(&unfenced)) && !show_restore(false, Some(&unfenced)));
        let body = restore_body("restore 42 from twin", &unfenced, false);
        assert_eq!((body["primary_fenced"].as_bool(), body["stale_ok"].as_bool()), (Some(true), Some(false)));
        let (line, more) = fence_hint(&unfenced).expect("the page says what to do first");
        assert!(line.contains("fence") && line.chars().count() <= crate::hint::ONE_LINE, "{line}");
        assert!(more.contains("stop") && more.contains("twin"), "{more}");
        assert_eq!(fence_hint(&fresh()), None, "nothing to fence on an eligible box");
        assert_eq!(restore_body("x", &fresh(), false)["primary_fenced"], false, "said only for a not_fenced refusal");
    }

    #[test]
    fn the_confirmed_press_needs_the_servers_phrase_exactly() {
        let e = fresh();
        assert!(may_send("restore 42 from twin", &e));
        assert!(may_send("  restore 42 from twin ", &e));
        for typed in ["", "restore", "restore 41 from twin", "RESTORE 42 FROM TWIN"] {
            assert!(!may_send(typed, &e), "{typed:?} was accepted");
        }
        assert!(!may_send("", &Eligibility::default()), "an empty phrase confirms nothing");
    }

    #[test]
    fn the_answer_reads_as_gunnars_numbers() {
        let r = Restored { head_seq: 42, applied: 42, lost: 0, restart_required: true, detail: "restart".into() };
        let line = restored_line(&r);
        assert!(line.contains("seq 42") && line.contains("42 applied") && line.contains("0 lost") && line.contains("Restart"), "{line}");
        assert!(offer_line(&fresh()).contains("1..=42"));
    }

    /// Everything the panel and the action can say, on the open console: no money.
    #[test]
    fn nothing_the_twin_panel_can_say_is_about_money() {
        let (fence_line, fence_more) = fence_hint(&Eligibility { code: "not_fenced".into(), ..fresh() }).expect("a fence hint");
        let mut said = vec![fence_line.to_owned(), fence_more.to_owned(), warning().to_owned(), offer_line(&fresh()), restored_line(&Restored { restart_required: true, detail: "x".into(), ..Restored::default() })];
        for s in ["ok", "behind", "silent", "x"] {
            said.push(state_label(s).to_owned());
        }
        let r = row("behind", 2, None, Some(1));
        said.extend([seen_line(&r), lag_line(&r), fill_line(&r)]);
        said.push(no_twin_detail(&TwinPanel::default()));
        said.push(no_twin_detail(&TwinPanel { configured_twin: "10.13.8.122:50051".into(), ..TwinPanel::default() }));
        said.push(restore_absence(true, Some(&Eligibility { eligible: false, confirm: String::new(), ..fresh() })).expect("a reason"));
        for text in &said {
            for word in text.split(|c: char| !c.is_ascii_alphanumeric()).map(str::to_ascii_lowercase) {
                assert!(
                    !["price", "pay", "paid", "buy", "bought", "sell", "sold", "billed", "billing", "quota", "plan", "invoice", "money", "purchase"].contains(&word.as_str()),
                    "`{word}` in {text:?}"
                );
            }
        }
    }
}