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

nordisk/holger-ui

src/security.rs

//! **The security workflow — what its pages show**, pure and host-tested;
//! `pages/security.rs` renders it.
//!
//! Two pages, both read from holger's intake chain and nothing else:
//!
//! * **the dashboard** (`/security`): holger's queues — Incoming, Quarantined,
//!   Awaiting approval, Blocked, Approved / available — as tiles with the
//!   server's counts, and the rows of one queue (`ListIntakeArtifacts`), under
//!   the gateway as holger words it (`AdminService.SecuritySummary`: the chain,
//!   the sources and scanners, what waits and what is held, who reads, the sync
//!   signatures, the last security events). The estate board is the Overview's;
//! * **the artifact view** (`/security/artifact?ns=&name=&version=`): its state
//!   and votes (`GetArtifactState`), its scan evidence pane by pane — Integrity,
//!   Malware, Vulnerabilities, License, Provenance, SBOM, Policy
//!   (`GetArtifactEvidence`) — and four buttons that each send one
//!   `AdvanceArtifact`; and the audit trail of its bytes with the chain's
//!   verdict (`AuditService.GetArtifactTrail`).
//!
//! **Thin.** Which states make up a queue, how many votes count, who may press
//! which button: all of it is the server's. The buttons are the same four for
//! every artifact and every visitor; a press the server refuses shows the
//! server's code and words, nothing else. This module only
//! turns the server's answers into lines.
//!
//! **The door is a trait** ([`IntakeDoor`]) so the pages and the tests drive
//! the same loaders: the wasm build talks gRPC-web to holger, the tests talk to
//! a mock server with canned answers.

#![cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
#![allow(async_fn_in_trait)]

#[cfg(not(target_arch = "wasm32"))]
use crate::admin_queue::pb;
#[cfg(target_arch = "wasm32")]
use crate::grpc::pb;
use crate::util::{display_name, enc, Tone};

/// The page of the dashboard.
pub const DASHBOARD: &str = "/security";
/// The queue a dashboard opened without one shows.
pub const DEFAULT_QUEUE: &str = "awaiting-approval";

/// A call the server answered with a refusal: the gRPC code's name and the
/// server's message, verbatim.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Refused {
    pub code: String,
    pub message: String,
}

/// **holger's intake door**, as the security pages use it.
pub trait IntakeDoor {
    async fn list(&self, req: pb::ListIntakeArtifactsRequest) -> Result<pb::ListIntakeArtifactsResponse, Refused>;
    async fn state(&self, req: pb::GetArtifactStateRequest) -> Result<pb::GetArtifactStateResponse, Refused>;
    async fn evidence(&self, req: pb::GetArtifactEvidenceRequest) -> Result<pb::GetArtifactEvidenceResponse, Refused>;
    async fn advance(&self, req: pb::AdvanceArtifactRequest) -> Result<pb::AdvanceArtifactResponse, Refused>;
    /// `AdminService.Topology`: the estate board's rows (its line under the drawing).
    async fn topology(&self) -> Result<pb::TopologyReply, Refused>;
    /// `AdminService.FlowSvg`: the artifact flow, drawn by the server from its RON, with the
    /// machines `open` names unfolded (`""` is nothing open, `all` everything).
    async fn flow(&self, open: &str) -> Result<pb::FlowSvgReply, Refused>;
    /// `AuditService.GetArtifactTrail`: the audit trail of the bytes.
    async fn trail(&self, req: pb::GetArtifactTrailRequest) -> Result<pb::GetArtifactTrailResponse, Refused>;
    /// `AdminService.SecuritySummary`: the Security page, every word the server's.
    async fn security(&self) -> Result<pb::SecuritySummaryReply, Refused>;
}

// ── the server's words, shown ───────────────────────────────────────────────

/// A refusal split into the server's code (the bracketed tag, when its message
/// starts with one in brackets) and the rest of its sentence.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RefusalView {
    pub code: String,
    pub text: String,
}

/// Pure: how a refusal reads. The code is the server's bracketed tag; a
/// message without one shows the gRPC code's name instead.
pub fn refusal(r: &Refused) -> RefusalView {
    let m = r.message.trim();
    if let Some(rest) = m.strip_prefix('[') {
        if let Some((code, text)) = rest.split_once(']') {
            if !code.is_empty() && !code.contains(' ') {
                return RefusalView { code: code.to_string(), text: text.trim().to_string() };
            }
        }
    }
    RefusalView { code: r.code.clone(), text: m.to_string() }
}

/// Pure: the badge tone of a state word the server sent (a word this console
/// does not know is shown as it came, untoned).
pub fn state_tone(state: &str) -> Tone {
    match state {
        "AVAILABLE" | "APPROVED" | "SEALED" | "PROMOTED" => Tone::Good,
        "POLICY_CHECKED" | "QUARANTINED" => Tone::Warn,
        "BLOCKED" => Tone::Bad,
        _ => Tone::Neutral,
    }
}

/// Pure: the badge tone of a verdict word the server sent.
pub fn verdict_tone(verdict: &str) -> Tone {
    match verdict {
        "pass" => Tone::Good,
        "warn" => Tone::Warn,
        "block" | "fail" | "error" => Tone::Bad,
        _ => Tone::Neutral,
    }
}

/// Pure: the server's vote count in words (`1 of 2 approvals`).
pub fn votes_line(approvals: u32, required: u32) -> String {
    format!("{approvals} of {required} approval{}", if required == 1 { "" } else { "s" })
}

/// Pure: the link to one artifact's security view.
pub fn artifact_link(id: &pb::ArtifactId) -> String {
    format!("{DASHBOARD}/artifact?ns={}&name={}&version={}", enc(&id.namespace), enc(&id.name), enc(&id.version))
}

/// Pure: a coordinate as an operator reads it.
pub fn label(id: &pb::ArtifactId) -> String {
    let base = display_name(&id.namespace, &id.name);
    if id.version.is_empty() {
        base
    } else {
        format!("{base} {}", id.version)
    }
}

/// Pure: a Unix second as `YYYY-MM-DD HH:MM UTC`.
pub fn utc(secs: i64) -> String {
    if secs <= 0 {
        return "—".into();
    }
    let days = secs.div_euclid(86_400);
    let rem = secs.rem_euclid(86_400);
    // Civil from days (Howard Hinnant's algorithm).
    let z = days + 719_468;
    let era = z.div_euclid(146_097);
    let doe = z.rem_euclid(146_097);
    let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let y = yoe + era * 400 + i64::from(m <= 2);
    format!("{y:04}-{m:02}-{d:02} {:02}:{:02} UTC", rem / 3_600, rem % 3_600 / 60)
}

fn short(hex: &str) -> String {
    hex.chars().take(12).collect()
}

// ── the dashboard ───────────────────────────────────────────────────────────

/// One tile: a queue of the server's, its count, its link.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Tile {
    pub name: String,
    pub title: String,
    pub count: u64,
    pub href: String,
    pub active: bool,
}

/// One row of the open queue.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Row {
    pub label: String,
    pub href: String,
    pub state: String,
    pub tone: Tone,
    pub sha256: String,
    pub importer: String,
    /// The server's vote count, on a row awaiting approval; empty otherwise.
    pub votes: String,
    pub last: String,
    pub note: String,
}

/// What the dashboard shows.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DashboardView {
    pub tiles: Vec<Tile>,
    /// The title of the open queue.
    pub open: String,
    pub rows: Vec<Row>,
    pub truncated: bool,
}

/// Pure: the dashboard from the server's answer, `queue` open.
pub fn dashboard(resp: &pb::ListIntakeArtifactsResponse, queue: &str) -> DashboardView {
    let tiles = resp
        .queues
        .iter()
        .map(|q| Tile {
            name: q.name.clone(),
            title: q.title.clone(),
            count: q.count,
            href: format!("{DASHBOARD}?queue={}", enc(&q.name)),
            active: q.name == queue,
        })
        .collect();
    let open = resp.queues.iter().find(|q| q.name == queue).map(|q| q.title.clone()).unwrap_or_else(|| queue.to_string());
    let rows = resp
        .artifacts
        .iter()
        .map(|a| {
            let id = a.artifact.clone().unwrap_or_default();
            let mut note = a.reason.clone();
            if !a.held_from.is_empty() {
                note = format!("held at {}{}", a.held_from, if note.is_empty() { String::new() } else { format!(": {note}") });
            }
            Row {
                label: label(&id),
                href: artifact_link(&id),
                state: a.state.clone(),
                tone: state_tone(&a.state),
                sha256: short(&a.sha256),
                importer: if a.importer.is_empty() { "—".into() } else { a.importer.clone() },
                votes: if a.state == "POLICY_CHECKED" { votes_line(a.approvals, a.approvals_required) } else { String::new() },
                last: format!("{} · {}", if a.actor.is_empty() { "—" } else { &a.actor }, utc(a.at)),
                note,
            }
        })
        .collect();
    DashboardView { tiles, open, rows, truncated: resp.truncated }
}

/// **Load the dashboard** from the door: the counts and the open queue's rows
/// in one call.
pub async fn load_dashboard(door: &impl IntakeDoor, queue: &str) -> Result<DashboardView, RefusalView> {
    let queue = if queue.trim().is_empty() { DEFAULT_QUEUE } else { queue.trim() };
    let resp = door
        .list(pb::ListIntakeArtifactsRequest { queue: queue.to_string(), limit: 0 })
        .await
        .map_err(|r| refusal(&r))?;
    Ok(dashboard(&resp, queue))
}

/// **The artifact flow as the console shows it** (lane HOL-SVG-RON): the drawing holger
/// made of its RON, the estate and the counters (`AdminService.FlowSvg`), shown as an
/// image, and the server's line about the estate under it. The console draws nothing.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Board {
    /// The drawing as an image source (`data:image/svg+xml,…`). An image runs
    /// no script.
    pub src: String,
    pub estate: String,
    /// The board in words, for the image's text alternative and the line under
    /// it: the server's own line (`TopologyReply.summary`), never one computed here.
    pub summary: String,
    /// Where the drawing is clickable (HOL-FLOW-SMART): holger's places, each laid over
    /// the image as a plain link. An image follows no link inside it.
    pub links: Vec<BoardLink>,
}

/// One clickable place of the drawing, as holger sent it (`FlowLink`).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BoardLink {
    /// Relative, holger's: `flow?open=main`.
    pub href: String,
    /// What following it does, in holger's words: `unfold main`.
    pub label: String,
    /// Where it lies over the image: holger's shares, as CSS percentages.
    pub style: String,
}

/// A share of the drawing (0..1) as a CSS percentage, without noise digits.
fn percent(share: f64) -> String {
    format!("{}%", (share * 100_000.0).round() / 1000.0)
}

/// Pure: **the Flow page's zoom from its address**: `?open=` as given (empty is nothing
/// open), main when the address names none.
pub fn flow_open(query: Option<&str>) -> String {
    query.unwrap_or("main").to_string()
}

/// Pure: the board from the server's two answers; `None` when holger drew nothing.
pub fn board(flow: &pb::FlowSvgReply, t: Option<&pb::TopologyReply>) -> Option<Board> {
    if flow.svg.trim().is_empty() {
        return None;
    }
    Some(Board {
        src: format!("data:image/svg+xml;charset=utf-8,{}", enc(&flow.svg)),
        estate: t.map(|t| t.estate.clone()).unwrap_or_default(),
        summary: t.map(summary).unwrap_or_default(),
        links: flow
            .links
            .iter()
            .map(|l| BoardLink {
                href: l.href.clone(),
                label: l.label.clone(),
                style: format!(
                    "left:{};top:{};width:{};height:{}",
                    percent(l.left),
                    percent(l.top),
                    percent(l.width),
                    percent(l.height)
                ),
            })
            .collect(),
    })
}

/// Pure: the server's board in words — its own line (`TopologyReply.summary`: the
/// estate's boxes, upstreams, downstreams, who used it), rendered as it came (lane A13).
/// No count of the console's own (OX-HOLF): the server counts the rows it sends.
pub fn summary(t: &pb::TopologyReply) -> String {
    t.summary.clone()
}

/// **Load the estate board** from the door, at the zoom `open` names. Whether it may be
/// read is the server's call; a refusal comes back as its code and words.
pub async fn load_board(door: &impl IntakeDoor, open: &str) -> Result<Option<Board>, RefusalView> {
    let flow = door.flow(open).await.map_err(|r| refusal(&r))?;
    // The line under it is the estate board's; without it the drawing still shows.
    let topology = door.topology().await.ok();
    Ok(board(&flow, topology.as_ref()))
}

// ── the security summary (lane HOL-SEC) ─────────────────────────────────────

/// One row of a section, as the server worded it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SectionRow {
    pub name: String,
    /// The artifact's security view, when the row is about one.
    pub href: Option<String>,
    pub state: String,
    pub tone: Tone,
    pub detail: String,
}

/// One section of the Security page: the chain, the scanners, who reads, …
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Section {
    /// The server's stable key (`chain`, `scanners`, …); the markup's `data-section`.
    pub key: String,
    pub title: String,
    pub summary: String,
    pub empty: String,
    pub rows: Vec<SectionRow>,
}

/// What the top of the Security page shows.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SummaryView {
    pub headline: String,
    pub sections: Vec<Section>,
}

/// Pure: the badge tone of the server's tone word.
pub fn row_tone(tone: &str) -> Tone {
    match tone {
        "good" => Tone::Good,
        "warn" => Tone::Warn,
        "bad" => Tone::Bad,
        _ => Tone::Neutral,
    }
}

/// Pure: the Security page's sections, as the server sent them, in its order.
pub fn security_view(r: &pb::SecuritySummaryReply) -> SummaryView {
    let sections = r
        .sections
        .iter()
        .map(|s| Section {
            key: s.key.clone(),
            title: s.title.clone(),
            summary: s.summary.clone(),
            empty: s.empty.clone(),
            rows: s
                .rows
                .iter()
                .map(|row| SectionRow {
                    name: row.name.clone(),
                    href: row.artifact.as_ref().map(artifact_link),
                    state: row.state.clone(),
                    tone: row_tone(&row.tone),
                    detail: row.detail.clone(),
                })
                .collect(),
        })
        .collect();
    SummaryView { headline: r.headline.clone(), sections }
}

/// **Load the Security page's summary** from the door. Who may read it is the server's call.
pub async fn load_security(door: &impl IntakeDoor) -> Result<SummaryView, RefusalView> {
    door.security().await.map(|r| security_view(&r)).map_err(|r| refusal(&r))
}

// ── the artifact view ───────────────────────────────────────────────────────

/// One line of a pane: a label and the server's value.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Line {
    pub label: String,
    pub value: String,
}

/// One scanner's evidence, as a pane lists it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ScannerView {
    pub scanner: String,
    pub verdict: String,
    pub tone: Tone,
    pub detail: String,
    /// "required", "not installed", both or neither — the server's flags in words.
    pub flags: Vec<&'static str>,
    pub findings: Vec<Line>,
}

/// One pane of the artifact view.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Pane {
    pub title: &'static str,
    /// The pane's verdict: the server's, or empty when the pane has none.
    pub verdict: String,
    pub tone: Tone,
    pub lines: Vec<Line>,
    pub scanners: Vec<ScannerView>,
    /// What the pane says when the server sent nothing for it.
    pub empty: String,
}

/// The SBOM document the server holds for the artifact, for a download link.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SbomFile {
    pub file_name: String,
    pub json: String,
    /// One line about it: the scanner's sentence and the component count.
    pub summary: String,
}

/// One button. The four are the same for every artifact and every visitor.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Action {
    pub label: &'static str,
    pub verb: pb::IntakeVerb,
    /// The state the press asks for (`APPROVED` for Approve), or empty.
    pub to_state: &'static str,
    /// The CSS class of the button.
    pub class: &'static str,
}

/// The four buttons. The server decides whether a press is allowed.
pub const ACTIONS: [Action; 4] = [
    Action { label: "Approve", verb: pb::IntakeVerb::Advance, to_state: "APPROVED", class: "" },
    Action { label: "Quarantine", verb: pb::IntakeVerb::Quarantine, to_state: "", class: "quiet" },
    Action { label: "Release", verb: pb::IntakeVerb::Release, to_state: "", class: "quiet" },
    Action { label: "Block", verb: pb::IntakeVerb::Block, to_state: "", class: "danger" },
];

/// One step of the artifact's intake history.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Step {
    pub when: String,
    pub hop: String,
    pub actor: String,
    pub policy: String,
    pub reason: String,
    pub evidence: Vec<Line>,
}

/// The audit trail as the artifact view shows it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TrailView {
    /// The server's words on the hash chain, and its verdict.
    pub chain: String,
    pub intact: bool,
    pub entries: Vec<TrailLine>,
}

/// One entry of the audit trail.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TrailLine {
    pub seq: u64,
    pub when: String,
    pub kind: String,
    pub who: String,
    pub repo: String,
    pub detail: String,
    pub hash: String,
}

/// Pure: the trail from the server's answer.
pub fn trail(t: &pb::GetArtifactTrailResponse) -> TrailView {
    TrailView {
        chain: t.chain_summary.clone(),
        intact: t.chain_intact,
        entries: t
            .entries
            .iter()
            .map(|e| TrailLine {
                seq: e.seq,
                when: utc(e.ts_nanos.div_euclid(1_000_000_000)),
                kind: e.kind.clone(),
                who: e.ident.clone(),
                repo: e.repo.clone(),
                detail: e.detail.clone(),
                hash: short(&e.hash),
            })
            .collect(),
    }
}

/// What the artifact view shows.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ArtifactView {
    pub label: String,
    pub state: String,
    pub tone: Tone,
    pub importer: String,
    pub votes: String,
    /// Whose votes the server recorded short of the quorum.
    pub voters: Vec<String>,
    pub panes: Vec<Pane>,
    pub sbom: Option<SbomFile>,
    pub history: Vec<Step>,
    pub actions: Vec<Action>,
    /// The evidence read's refusal, when the server refused only that.
    pub evidence_refused: Option<RefusalView>,
    /// The audit trail, or the server's refusal of it.
    pub trail: Option<Result<TrailView, RefusalView>>,
}

/// The panes, in order: `(title, the evidence kinds it shows)`. Integrity and
/// Policy are built from the record itself; every scanner kind the server sends
/// lands in exactly one pane, an unknown one in "Other checks".
const KIND_PANES: [(&str, &[&str]); 6] = [
    ("Malware", &["malware"]),
    ("Vulnerabilities", &["vulnerability"]),
    ("License", &["license"]),
    ("Provenance", &["provenance"]),
    ("SBOM", &["sbom"]),
    ("Other checks", &["inspection", "secrets"]),
];

fn scanner_view(r: &pb::EvidenceRecord) -> ScannerView {
    let mut flags = Vec::new();
    if r.required {
        flags.push("required");
    }
    if r.not_configured {
        flags.push("not installed");
    }
    ScannerView {
        scanner: if r.scanner_version.is_empty() { r.scanner_id.clone() } else { format!("{} {}", r.scanner_id, r.scanner_version) },
        verdict: r.verdict.clone(),
        tone: verdict_tone(&r.verdict),
        detail: r.detail.clone(),
        flags,
        findings: r
            .findings
            .iter()
            .map(|f| Line { label: format!("{} · {}", f.id, f.severity), value: f.summary.clone() })
            .collect(),
    }
}

/// The verdict a pane shows: the worst of its records, in the server's words.
fn pane_verdict(records: &[&pb::EvidenceRecord]) -> String {
    const ORDER: [&str; 4] = ["block", "error", "warn", "pass"];
    ORDER
        .iter()
        .find(|v| records.iter().any(|r| r.verdict == **v))
        .map(|v| v.to_string())
        .or_else(|| records.first().map(|r| r.verdict.clone()))
        .unwrap_or_default()
}

fn sbom_file(ev: &pb::GetArtifactEvidenceResponse) -> Option<SbomFile> {
    let r = ev.records.iter().find(|r| r.kind == "sbom" && !r.document_json.is_empty())?;
    let components = serde_json::from_str::<serde_json::Value>(&r.document_json)
        .ok()
        .and_then(|v| v.get("components").and_then(|c| c.as_array()).map(|c| c.len()));
    let stem = ev.file_name.rsplit('/').next().filter(|s| !s.is_empty()).unwrap_or("artifact");
    let mut summary = r.detail.clone();
    if let Some(n) = components {
        summary = format!("{summary}{}{n} component{}", if summary.is_empty() { "" } else { " · " }, if n == 1 { "" } else { "s" });
    }
    Some(SbomFile { file_name: format!("{stem}.cdx.json"), json: r.document_json.clone(), summary })
}

/// Pure: the panes from the server's evidence answer.
pub fn panes(ev: &pb::GetArtifactEvidenceResponse) -> Vec<Pane> {
    let not_scanned = "no scan has recorded evidence for these bytes yet".to_string();
    let mut out = Vec::new();
    let mut integrity = vec![Line { label: "sha256 at ingest".into(), value: ev.sha256.clone() }];
    integrity.extend(ev.hashes.iter().map(|h| Line { label: format!("{} scanned", h.alg), value: h.hex.clone() }));
    if ev.scanned {
        integrity.push(Line { label: "file".into(), value: ev.file_name.clone() });
        integrity.push(Line { label: "size".into(), value: crate::util::fmt_bytes(ev.size as i64) });
        integrity.push(Line { label: "ecosystem".into(), value: ev.ecosystem.clone() });
        integrity.push(Line { label: "scanned".into(), value: utc(ev.scanned_at) });
    }
    out.push(Pane { title: "Integrity", verdict: String::new(), tone: Tone::Neutral, lines: integrity, scanners: vec![], empty: String::new() });
    let known: Vec<&str> = KIND_PANES.iter().flat_map(|(_, k)| k.iter().copied()).collect();
    for (title, kinds) in KIND_PANES {
        let records: Vec<&pb::EvidenceRecord> = ev
            .records
            .iter()
            .filter(|r| kinds.contains(&r.kind.as_str()) || (title == "Other checks" && !known.contains(&r.kind.as_str())))
            .collect();
        if title == "Other checks" && records.is_empty() {
            continue;
        }
        let verdict = pane_verdict(&records);
        let mut lines = Vec::new();
        if title == "SBOM" {
            if let Some(f) = sbom_file(ev) {
                lines.push(Line { label: "summary".into(), value: f.summary });
            }
        }
        out.push(Pane {
            title,
            tone: verdict_tone(&verdict),
            verdict,
            lines,
            scanners: records.iter().map(|r| scanner_view(r)).collect(),
            empty: if ev.scanned { "the server sent no evidence of this kind".into() } else { not_scanned.clone() },
        });
    }
    let mut policy = Vec::new();
    if !ev.policy.is_empty() {
        policy.push(Line { label: "last transition".into(), value: ev.policy.clone() });
    }
    policy.extend(ev.reasons.iter().map(|r| Line { label: "reason".into(), value: r.clone() }));
    out.push(Pane {
        title: "Policy",
        verdict: ev.verdict.clone(),
        tone: verdict_tone(&ev.verdict),
        lines: policy,
        scanners: vec![],
        empty: if ev.scanned { "the policy recorded no words".into() } else { not_scanned },
    });
    out
}

/// Pure: the history from the server's state answer.
pub fn history(st: &pb::GetArtifactStateResponse) -> Vec<Step> {
    st.history
        .iter()
        .map(|t| Step {
            when: utc(t.at),
            hop: format!("{} → {}", t.from_state, t.to_state),
            actor: t.actor.clone(),
            policy: t.policy.clone(),
            reason: t.reason.clone(),
            evidence: t
                .evidence
                .iter()
                .map(|e| Line { label: format!("{} · {}", e.kind, e.verdict), value: format!("{} {}", e.source, e.reference).trim().to_string() })
                .collect(),
        })
        .collect()
}

/// Pure: the artifact view from the server's two answers. The evidence read may
/// be refused on its own; the state read may not (the page then shows that
/// refusal instead).
pub fn artifact(id: &pb::ArtifactId, st: &pb::GetArtifactStateResponse, ev: Result<&pb::GetArtifactEvidenceResponse, &Refused>) -> ArtifactView {
    let (panes, sbom, evidence_refused) = match ev {
        Ok(ev) => (panes(ev), sbom_file(ev), None),
        Err(r) => (Vec::new(), None, Some(refusal(r))),
    };
    ArtifactView {
        label: label(id),
        state: st.state.clone(),
        tone: state_tone(&st.state),
        importer: if st.importer.is_empty() { "—".into() } else { st.importer.clone() },
        votes: votes_line(st.approvals, st.approvals_required),
        voters: st.pending_approvals.iter().map(|v| v.source.clone()).collect(),
        panes,
        sbom,
        history: history(st),
        actions: ACTIONS.to_vec(),
        evidence_refused,
        trail: None,
    }
}

/// **Load the artifact view** from the door: its state, then its evidence.
pub async fn load_artifact(door: &impl IntakeDoor, id: &pb::ArtifactId) -> Result<ArtifactView, RefusalView> {
    let st = door
        .state(pb::GetArtifactStateRequest { artifact: Some(id.clone()) })
        .await
        .map_err(|r| refusal(&r))?;
    let ev = door.evidence(pb::GetArtifactEvidenceRequest { artifact: Some(id.clone()) }).await;
    let mut v = artifact(id, &st, ev.as_ref());
    if !st.sha256.is_empty() {
        let t = door.trail(pb::GetArtifactTrailRequest { sha256: st.sha256.clone() }).await;
        v.trail = Some(t.map(|t| trail(&t)).map_err(|r| refusal(&r)));
    }
    Ok(v)
}

/// What a press shows.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Pressed {
    /// The server moved it, or recorded a vote: its sentence and its vote count.
    Done { message: String, state: String, votes: String },
    /// The server refused: its code and its words.
    Refused(RefusalView),
}

/// **Press one button**: one `AdvanceArtifact`, and the server's answer.
pub async fn press(door: &impl IntakeDoor, id: &pb::ArtifactId, action: Action, reason: &str) -> Pressed {
    let req = pb::AdvanceArtifactRequest {
        artifact: Some(id.clone()),
        verb: action.verb as i32,
        to_state: action.to_state.to_string(),
        reason: reason.trim().to_string(),
        evidence: Vec::new(),
    };
    match door.advance(req).await {
        Ok(a) => Pressed::Done { message: a.message, state: a.state, votes: votes_line(a.approvals, a.approvals_required) },
        Err(r) => Pressed::Refused(refusal(&r)),
    }
}

/// **The wasm door**: holger's PromotionService over gRPC-web.
#[cfg(target_arch = "wasm32")]
pub struct GrpcDoor;

#[cfg(target_arch = "wasm32")]
fn refused(s: tonic::Status) -> Refused {
    Refused { code: format!("{:?}", s.code()), message: crate::grpc::status_text(&s) }
}

#[cfg(target_arch = "wasm32")]
impl IntakeDoor for GrpcDoor {
    async fn list(&self, req: pb::ListIntakeArtifactsRequest) -> Result<pb::ListIntakeArtifactsResponse, Refused> {
        crate::grpc::promotion().list_intake_artifacts(req).await.map(|r| r.into_inner()).map_err(refused)
    }
    async fn state(&self, req: pb::GetArtifactStateRequest) -> Result<pb::GetArtifactStateResponse, Refused> {
        crate::grpc::promotion().get_artifact_state(req).await.map(|r| r.into_inner()).map_err(refused)
    }
    async fn evidence(&self, req: pb::GetArtifactEvidenceRequest) -> Result<pb::GetArtifactEvidenceResponse, Refused> {
        crate::grpc::promotion().get_artifact_evidence(req).await.map(|r| r.into_inner()).map_err(refused)
    }
    async fn advance(&self, req: pb::AdvanceArtifactRequest) -> Result<pb::AdvanceArtifactResponse, Refused> {
        crate::grpc::promotion().advance_artifact(req).await.map(|r| r.into_inner()).map_err(refused)
    }
    async fn topology(&self) -> Result<pb::TopologyReply, Refused> {
        crate::grpc::admin().topology(pb::TopologyRequest {}).await.map(|r| r.into_inner()).map_err(refused)
    }
    async fn flow(&self, open: &str) -> Result<pb::FlowSvgReply, Refused> {
        crate::grpc::admin().flow_svg(pb::FlowSvgRequest { open: open.to_string() }).await.map(|r| r.into_inner()).map_err(refused)
    }
    async fn trail(&self, req: pb::GetArtifactTrailRequest) -> Result<pb::GetArtifactTrailResponse, Refused> {
        crate::grpc::audit().get_artifact_trail(req).await.map(|r| r.into_inner()).map_err(refused)
    }
    async fn security(&self) -> Result<pb::SecuritySummaryReply, Refused> {
        crate::grpc::admin().security_summary(pb::SecuritySummaryRequest {}).await.map(|r| r.into_inner()).map_err(refused)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    /// Run a future that never waits on anything outside itself (the mock
    /// server answers at once).
    fn block_on<F: std::future::Future>(f: F) -> F::Output {
        let mut f = std::pin::pin!(f);
        let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
        loop {
            if let std::task::Poll::Ready(v) = f.as_mut().poll(&mut cx) {
                return v;
            }
        }
    }

    /// **The mock server**: canned answers, and every request it was sent.
    #[derive(Default)]
    struct Mock {
        list: Option<Result<pb::ListIntakeArtifactsResponse, Refused>>,
        state: Option<Result<pb::GetArtifactStateResponse, Refused>>,
        evidence: Option<Result<pb::GetArtifactEvidenceResponse, Refused>>,
        advance: Option<Result<pb::AdvanceArtifactResponse, Refused>>,
        sent_list: RefCell<Vec<pb::ListIntakeArtifactsRequest>>,
        sent_advance: RefCell<Vec<pb::AdvanceArtifactRequest>>,
        topology: Option<Result<pb::TopologyReply, Refused>>,
        flow: Option<Result<pb::FlowSvgReply, Refused>>,
        sent_flow: RefCell<Vec<String>>,
        trail: Option<Result<pb::GetArtifactTrailResponse, Refused>>,
        sent_trail: RefCell<Vec<pb::GetArtifactTrailRequest>>,
        security: Option<Result<pb::SecuritySummaryReply, Refused>>,
    }

    fn unanswered<T>() -> Result<T, Refused> {
        Err(Refused { code: "Unimplemented".into(), message: "the mock was not given this answer".into() })
    }

    impl IntakeDoor for Mock {
        async fn list(&self, req: pb::ListIntakeArtifactsRequest) -> Result<pb::ListIntakeArtifactsResponse, Refused> {
            self.sent_list.borrow_mut().push(req);
            self.list.clone().unwrap_or_else(unanswered)
        }
        async fn state(&self, _req: pb::GetArtifactStateRequest) -> Result<pb::GetArtifactStateResponse, Refused> {
            self.state.clone().unwrap_or_else(unanswered)
        }
        async fn evidence(&self, _req: pb::GetArtifactEvidenceRequest) -> Result<pb::GetArtifactEvidenceResponse, Refused> {
            self.evidence.clone().unwrap_or_else(unanswered)
        }
        async fn advance(&self, req: pb::AdvanceArtifactRequest) -> Result<pb::AdvanceArtifactResponse, Refused> {
            self.sent_advance.borrow_mut().push(req);
            self.advance.clone().unwrap_or_else(unanswered)
        }
        async fn topology(&self) -> Result<pb::TopologyReply, Refused> {
            self.topology.clone().unwrap_or_else(unanswered)
        }
        async fn flow(&self, open: &str) -> Result<pb::FlowSvgReply, Refused> {
            self.sent_flow.borrow_mut().push(open.to_string());
            self.flow.clone().unwrap_or_else(unanswered)
        }
        async fn trail(&self, req: pb::GetArtifactTrailRequest) -> Result<pb::GetArtifactTrailResponse, Refused> {
            self.sent_trail.borrow_mut().push(req);
            self.trail.clone().unwrap_or_else(unanswered)
        }
        async fn security(&self) -> Result<pb::SecuritySummaryReply, Refused> {
            self.security.clone().unwrap_or_else(unanswered)
        }
    }

    /// ★ **The smart flow's zoom and its clickable places are holger's** (HOL-FLOW-SMART):
    /// the console asks for the machines the page names (`open`) and lays a plain link over
    /// each place holger says is clickable, where holger says it is, as holger worded it.
    /// The image still runs nothing; the console computes no layout.
    #[test]
    fn the_board_asks_holger_for_the_zoom_and_lays_its_links_over_the_drawing() {
        let mock = Mock {
            flow: Some(Ok(pb::FlowSvgReply {
                svg: "<svg/>".into(),
                graph_json: "{}".into(),
                links: vec![
                    pb::FlowLink { href: "flow?open=main".into(), label: "unfold main".into(), left: 0.25, top: 0.5, width: 0.125, height: 0.2 },
                    pb::FlowLink { href: "flow?open=all".into(), label: "show the full RON".into(), left: 0.9, top: 0.0, width: 0.1, height: 0.02 },
                ],
            })),
            ..Default::default()
        };
        let b = block_on(load_board(&mock, "main,lab-1")).unwrap().unwrap();
        assert_eq!(mock.sent_flow.borrow().as_slice(), ["main,lab-1"], "the page's zoom, as it named it");
        assert_eq!(b.links.len(), 2);
        assert_eq!(b.links[0].href, "flow?open=main");
        assert_eq!(b.links[0].label, "unfold main");
        assert_eq!(b.links[0].style, "left:25%;top:50%;width:12.5%;height:20%", "where holger says, as a share of the drawing");
        assert_eq!(b.links[1].href, "flow?open=all");
        block_on(load_board(&mock, "")).unwrap();
        assert_eq!(mock.sent_flow.borrow().last().map(String::as_str), Some(""), "nothing open: level 1");
    }

    /// The Flow page's zoom from its address: `?open=` as given (empty is nothing open),
    /// main when it names none.
    #[test]
    fn the_flow_page_asks_for_what_its_address_names_and_main_by_default() {
        assert_eq!(flow_open(None), "main");
        assert_eq!(flow_open(Some("")), "");
        assert_eq!(flow_open(Some("main,lab-1")), "main,lab-1");
        assert_eq!(flow_open(Some("all")), "all");
    }

    // ── the security summary (lane HOL-SEC) ─────────────────────────────────

    #[test]
    fn the_security_summary_is_the_servers_sections_as_sent() {
        let held = pb::SecurityRow {
            name: "evil 1.0".into(),
            state: "quarantined".into(),
            tone: "warn".into(),
            detail: "invalidated upstream: malware. By security.".into(),
            artifact: Some(pb::ArtifactId { namespace: String::new(), name: "evil.whl".into(), version: "1.0".into() }),
        };
        let reply = pb::SecuritySummaryReply {
            headline: "Holger checks every package before it serves it.".into(),
            sections: vec![
                pb::SecuritySection { key: "chain".into(), title: "Intake chain".into(), summary: "Every package passes these steps.".into(), empty: String::new(), rows: vec![] },
                pb::SecuritySection { key: "held".into(), title: "Held or refused".into(), summary: String::new(), empty: "Nothing is held.".into(), rows: vec![held] },
            ],
        };
        let mock = Mock { security: Some(Ok(reply)), ..Default::default() };
        let v = block_on(load_security(&mock)).expect("the summary");
        assert_eq!(v.headline, "Holger checks every package before it serves it.");
        let keys: Vec<&str> = v.sections.iter().map(|s| s.key.as_str()).collect();
        assert_eq!(keys, ["chain", "held"], "the server's order");
        let r = &v.sections[1].rows[0];
        assert_eq!((r.state.as_str(), r.tone), ("quarantined", Tone::Warn));
        assert_eq!(r.href.as_deref(), Some("/security/artifact?ns=&name=evil.whl&version=1.0"));
        assert_eq!(r.detail, "invalidated upstream: malware. By security.");
    }

    #[test]
    fn a_refused_security_summary_shows_the_servers_code_and_words() {
        let mock = Mock {
            security: Some(Err(Refused { code: "PermissionDenied".into(), message: "[holger-auth-2] 'dev' may not read the Security page".into() })),
            ..Default::default()
        };
        let r = block_on(load_security(&mock)).unwrap_err();
        assert_eq!((r.code.as_str(), r.text.as_str()), ("holger-auth-2", "'dev' may not read the Security page"));
    }

    fn queue(name: &str, title: &str, count: u64) -> pb::IntakeQueue {
        pb::IntakeQueue { name: name.into(), title: title.into(), states: vec![], count }
    }

    fn wheel() -> pb::ArtifactId {
        pb::ArtifactId { namespace: String::new(), name: "demo_pkg-1.0.0-py3-none-any.whl".into(), version: String::new() }
    }

    fn listing() -> pb::ListIntakeArtifactsResponse {
        pb::ListIntakeArtifactsResponse {
            queues: vec![
                queue("incoming", "Incoming", 3),
                queue("quarantined", "Quarantined", 1),
                queue("awaiting-approval", "Awaiting approval", 2),
                queue("blocked", "Blocked", 0),
                queue("available", "Approved / available", 7),
            ],
            artifacts: vec![
                pb::IntakeArtifactRow {
                    artifact: Some(wheel()),
                    state: "POLICY_CHECKED".into(),
                    sha256: "ab".repeat(32),
                    repository: "quarantine".into(),
                    actor: "ida".into(),
                    at: 1_800_000_000,
                    importer: "ida".into(),
                    approvals: 1,
                    approvals_required: 2,
                    ..Default::default()
                },
                pb::IntakeArtifactRow {
                    artifact: Some(pb::ArtifactId { namespace: "acme".into(), name: "tool".into(), version: "2.0".into() }),
                    state: "POLICY_CHECKED".into(),
                    approvals: 0,
                    approvals_required: 2,
                    ..Default::default()
                },
            ],
            truncated: false,
        }
    }

    fn evidence() -> pb::GetArtifactEvidenceResponse {
        let rec = |kind: &str, verdict: &str| pb::EvidenceRecord {
            scanner_id: format!("{kind}-scanner"),
            scanner_version: "1".into(),
            kind: kind.into(),
            verdict: verdict.into(),
            detail: format!("{kind} said {verdict}"),
            ..Default::default()
        };
        let mut vuln = rec("vulnerability", "warn");
        vuln.findings = vec![pb::EvidenceFinding { id: "PYSEC-1".into(), severity: "low".into(), summary: "a low one".into() }];
        let mut malware = rec("malware", "error");
        malware.scanner_id = "not-configured".into();
        malware.scanner_version = "0".into();
        malware.not_configured = true;
        malware.required = true;
        let mut sbom = rec("sbom", "pass");
        sbom.detail = "CycloneDX SBOM for demo-pkg 1.0.0".into();
        sbom.document_json = r#"{"bomFormat":"CycloneDX","components":[{"name":"demo-pkg"}]}"#.into();
        pb::GetArtifactEvidenceResponse {
            state: "POLICY_CHECKED".into(),
            sha256: "ab".repeat(32),
            scanned: true,
            file_name: "demo_pkg-1.0.0-py3-none-any.whl".into(),
            ecosystem: "pip".into(),
            size: 2_048,
            hashes: vec![
                pb::EvidenceHash { alg: "sha256".into(), hex: "ab".repeat(32) },
                pb::EvidenceHash { alg: "blake3".into(), hex: "cd".repeat(32) },
            ],
            verdict: "block".into(),
            reasons: vec!["malware: required, not configured".into()],
            records: vec![rec("inspection", "pass"), malware, vuln, rec("license", "pass"), rec("provenance", "error"), sbom],
            scanned_at: 1_800_000_000,
            policy: "structural check only".into(),
            // W5's stored records, signer and SBOM link (holger 0cb5f11f): not shown here.
            ..Default::default()
        }
    }

    fn state() -> pb::GetArtifactStateResponse {
        pb::GetArtifactStateResponse {
            state: "POLICY_CHECKED".into(),
            sha256: "ab".repeat(32),
            importer: "ida".into(),
            approvals: 1,
            approvals_required: 2,
            history: vec![pb::IntakeTransition {
                from_state: "UNTRUSTED".into(),
                to_state: "INGESTED".into(),
                actor: "ida".into(),
                at: 1_800_000_000,
                ..Default::default()
            }],
            pending_approvals: vec![pb::IntakeEvidence { kind: "approval".into(), source: "bob".into(), verdict: "pass".into(), reference: "sha256:x".into() }],
            // holger f86b9627 (tier 1): the artifact's source and its effective policy; not shown here.
            source: String::new(),
            effective_policy: String::new(),
        }
    }

    /// ★ **The dashboard renders the server's five queues and counts**, and the
    /// rows of the open queue with the server's vote count.
    #[test]
    fn the_dashboard_renders_the_servers_queues_counts_and_rows() {
        let mock = Mock { list: Some(Ok(listing())), ..Default::default() };
        let v = block_on(load_dashboard(&mock, "")).expect("the mock answers");
        assert_eq!(mock.sent_list.borrow()[0].queue, DEFAULT_QUEUE, "a bare dashboard opens awaiting approval");
        let tiles: Vec<(&str, u64, bool)> = v.tiles.iter().map(|t| (t.title.as_str(), t.count, t.active)).collect();
        assert_eq!(
            tiles,
            vec![
                ("Incoming", 3, false),
                ("Quarantined", 1, false),
                ("Awaiting approval", 2, true),
                ("Blocked", 0, false),
                ("Approved / available", 7, false),
            ]
        );
        assert_eq!(v.tiles[1].href, "/security?queue=quarantined");
        assert_eq!(v.open, "Awaiting approval");
        assert_eq!(v.rows.len(), 2);
        let r = &v.rows[0];
        assert_eq!((r.label.as_str(), r.state.as_str(), r.tone), ("demo_pkg-1.0.0-py3-none-any.whl", "POLICY_CHECKED", Tone::Warn));
        assert_eq!(r.votes, "1 of 2 approvals");
        assert_eq!(r.sha256, "abababababab");
        assert_eq!(r.last, "ida · 2027-01-15 08:00 UTC");
        assert_eq!(r.href, "/security/artifact?ns=&name=demo_pkg-1.0.0-py3-none-any.whl&version=");
        assert_eq!(v.rows[1].label, "acme/tool 2.0");
        assert_eq!(v.rows[1].votes, "0 of 2 approvals");
        // Another queue is asked for by its server name.
        block_on(load_dashboard(&mock, "blocked")).unwrap();
        assert_eq!(mock.sent_list.borrow()[1].queue, "blocked");
    }

    #[test]
    fn a_dashboard_the_server_refuses_shows_its_code_and_words() {
        let mock = Mock {
            list: Some(Err(Refused { code: "FailedPrecondition".into(), message: "this server declares no intake chain".into() })),
            ..Default::default()
        };
        let e = block_on(load_dashboard(&mock, "incoming")).expect_err("refused");
        assert_eq!(e, RefusalView { code: "FailedPrecondition".into(), text: "this server declares no intake chain".into() });
    }

    /// ★ **The artifact view renders every pane from the server's evidence**:
    /// Integrity (the hashes), Malware, Vulnerabilities, License, Provenance,
    /// SBOM (a download and a summary), Policy, and the vote count.
    #[test]
    fn the_artifact_view_renders_integrity_scans_sbom_policy_and_votes() {
        let mock = Mock { state: Some(Ok(state())), evidence: Some(Ok(evidence())), ..Default::default() };
        let v = block_on(load_artifact(&mock, &wheel())).expect("the mock answers");
        let titles: Vec<&str> = v.panes.iter().map(|p| p.title).collect();
        assert_eq!(titles, ["Integrity", "Malware", "Vulnerabilities", "License", "Provenance", "SBOM", "Other checks", "Policy"]);
        let pane = |t: &str| v.panes.iter().find(|p| p.title == t).unwrap();
        let integrity: Vec<(&str, &str)> = pane("Integrity").lines.iter().map(|l| (l.label.as_str(), l.value.as_str())).collect();
        assert_eq!(integrity[1], ("sha256 scanned", "ab".repeat(32).as_str()));
        assert_eq!(integrity[2], ("blake3 scanned", "cd".repeat(32).as_str()));
        assert!(integrity.contains(&("size", "2.0 kB")), "{integrity:?}");
        let malware = pane("Malware");
        assert_eq!((malware.verdict.as_str(), malware.tone), ("error", Tone::Bad));
        assert_eq!(malware.scanners[0].flags, vec!["required", "not installed"]);
        let vulns = pane("Vulnerabilities");
        assert_eq!((vulns.verdict.as_str(), vulns.tone), ("warn", Tone::Warn));
        assert_eq!(vulns.scanners[0].findings, vec![Line { label: "PYSEC-1 · low".into(), value: "a low one".into() }]);
        assert_eq!(pane("License").tone, Tone::Good);
        assert_eq!(pane("Provenance").verdict, "error");
        assert_eq!(pane("SBOM").lines[0].value, "CycloneDX SBOM for demo-pkg 1.0.0 · 1 component");
        let sbom = v.sbom.as_ref().expect("an SBOM to download");
        assert_eq!(sbom.file_name, "demo_pkg-1.0.0-py3-none-any.whl.cdx.json");
        assert!(sbom.json.contains("CycloneDX"));
        let policy = pane("Policy");
        assert_eq!((policy.verdict.as_str(), policy.tone), ("block", Tone::Bad));
        assert!(policy.lines.iter().any(|l| l.value == "malware: required, not configured"));
        // The two-person approval: the server's count, and whose vote it holds.
        assert_eq!(v.votes, "1 of 2 approvals");
        assert_eq!(v.voters, vec!["bob"]);
        assert_eq!(v.history[0].hop, "UNTRUSTED → INGESTED");
        assert_eq!((v.state.as_str(), v.importer.as_str()), ("POLICY_CHECKED", "ida"));
    }

    /// ★ **The trail is the server's**: asked by the bytes' sha256, shown with
    /// the chain's verdict; a reader the server refuses sees its refusal.
    #[test]
    fn the_trail_is_asked_by_sha256_and_shown_or_refused() {
        let mock = Mock {
            state: Some(Ok(state())),
            evidence: Some(Ok(evidence())),
            trail: Some(Ok(pb::GetArtifactTrailResponse {
                sha256: "ab".repeat(32),
                entries: vec![pb::ArtifactTrailEntry {
                    seq: 7,
                    hash: "f0".repeat(32),
                    ts_nanos: 1_800_000_000_000_000_000,
                    kind: "imported".into(),
                    ident: "ida".into(),
                    repo: "incoming".into(),
                    detail: "intake UNTRUSTED → INGESTED".into(),
                    ..Default::default()
                }],
                chain_intact: true,
                chain_summary: "12 records, chain intact".into(),
            })),
            ..Default::default()
        };
        let v = block_on(load_artifact(&mock, &wheel())).unwrap();
        assert_eq!(mock.sent_trail.borrow()[0].sha256, "ab".repeat(32));
        let t = v.trail.expect("asked").expect("answered");
        assert!(t.intact);
        assert_eq!(t.chain, "12 records, chain intact");
        let e = &t.entries[0];
        assert_eq!((e.seq, e.when.as_str(), e.kind.as_str(), e.who.as_str(), e.hash.as_str()), (7, "2027-01-15 08:00 UTC", "imported", "ida", "f0f0f0f0f0f0"));
        let refused = Mock {
            state: Some(Ok(state())),
            evidence: Some(Ok(evidence())),
            trail: Some(Err(Refused { code: "PermissionDenied".into(), message: "[holger-audit-10] only an Auditor or an Admin reads the trail".into() })),
            ..Default::default()
        };
        let v = block_on(load_artifact(&refused, &wheel())).unwrap();
        assert_eq!(v.trail.unwrap().unwrap_err().code, "holger-audit-10");
    }

    #[test]
    fn an_artifact_not_scanned_yet_says_so_and_a_refused_evidence_read_shows_the_refusal() {
        let mut ev = pb::GetArtifactEvidenceResponse { state: "INGESTED".into(), sha256: "ef".repeat(32), ..Default::default() };
        ev.scanned = false;
        let v = artifact(&wheel(), &state(), Ok(&ev));
        let malware = v.panes.iter().find(|p| p.title == "Malware").unwrap();
        assert!(malware.scanners.is_empty());
        assert_eq!(malware.empty, "no scan has recorded evidence for these bytes yet");
        assert!(v.sbom.is_none());
        let r = Refused { code: "PermissionDenied".into(), message: "[holger-duty-6] Developer reads only AVAILABLE".into() };
        let v = artifact(&wheel(), &state(), Err(&r));
        assert!(v.panes.is_empty());
        assert_eq!(v.evidence_refused.unwrap().code, "holger-duty-6");
    }

    /// ★ **A refusal from the server is shown, code and words**, and the press
    /// sent exactly what the button says: no client-side decision in between.
    #[test]
    fn a_press_the_server_refuses_shows_the_servers_code_and_words() {
        let mock = Mock {
            advance: Some(Err(Refused {
                code: "PermissionDenied".into(),
                message: "[holger-duty-1] ida imported demo_pkg-1.0.0-py3-none-any.whl@ and may never approve it".into(),
            })),
            ..Default::default()
        };
        let got = block_on(press(&mock, &wheel(), ACTIONS[0], " looks fine "));
        assert_eq!(
            got,
            Pressed::Refused(RefusalView {
                code: "holger-duty-1".into(),
                text: "ida imported demo_pkg-1.0.0-py3-none-any.whl@ and may never approve it".into(),
            })
        );
        let sent = mock.sent_advance.borrow();
        assert_eq!(sent.len(), 1, "one press, one call");
        assert_eq!((sent[0].verb, sent[0].to_state.as_str(), sent[0].reason.as_str()), (pb::IntakeVerb::Advance as i32, "APPROVED", "looks fine"));
        assert!(sent[0].evidence.is_empty(), "the console never supplies evidence");
    }

    #[test]
    fn a_vote_short_of_the_quorum_shows_the_servers_count() {
        let mock = Mock {
            advance: Some(Ok(pb::AdvanceArtifactResponse {
                from_state: "POLICY_CHECKED".into(),
                state: "POLICY_CHECKED".into(),
                approvals: 1,
                approvals_required: 2,
                message: "approval 1 of 2 recorded".into(),
                ..Default::default()
            })),
            ..Default::default()
        };
        assert_eq!(
            block_on(press(&mock, &wheel(), ACTIONS[0], "")),
            Pressed::Done { message: "approval 1 of 2 recorded".into(), state: "POLICY_CHECKED".into(), votes: "1 of 2 approvals".into() }
        );
        let verbs: Vec<(&str, pb::IntakeVerb)> = ACTIONS.iter().map(|a| (a.label, a.verb)).collect();
        assert_eq!(
            verbs,
            vec![
                ("Approve", pb::IntakeVerb::Advance),
                ("Quarantine", pb::IntakeVerb::Quarantine),
                ("Release", pb::IntakeVerb::Release),
                ("Block", pb::IntakeVerb::Block),
            ]
        );
    }

    /// ★ **No client-side role logic.** Every artifact, in every state, gets the
    /// same four buttons; and neither the module nor its page reads who the
    /// visitor is to decide anything.
    #[test]
    fn there_is_no_client_side_role_logic() {
        for s in ["INGESTED", "SCANNED", "POLICY_CHECKED", "APPROVED", "AVAILABLE", "QUARANTINED", "BLOCKED"] {
            let st = pb::GetArtifactStateResponse { state: s.into(), ..state() };
            assert_eq!(artifact(&wheel(), &st, Ok(&evidence())).actions, ACTIONS.to_vec(), "{s}");
        }
        let code = |src: &'static str| src.split("#[cfg(test)]").next().unwrap();
        for (file, src) in [
            ("src/security.rs", code(include_str!("security.rs"))),
            ("src/pages/security.rs", code(include_str!("pages/security.rs"))),
        ] {
            // An ARIA `role="alert"` attribute is markup, not a role check.
            let lower = src.to_lowercase().replace("role=\"", "aria=\"");
            for word in ["role", "duty", "duties", "approver", "is_admin", "admin_visible", "who_am_i", "session", "token"] {
                assert!(!lower.contains(word), "{file} mentions `{word}`: the server decides, the console renders");
            }
        }
    }

    /// ★ **The board is holger's flow drawing, shown as sent** (lane HOL-SVG-RON, owner
    /// 2026-10-30: the SVG is the RON's graph, drawn by the server from its RON; the console
    /// draws nothing): the image is `AdminService.FlowSvg`'s bytes, every one escaped; the
    /// line under it is the estate board's; a refusal shows the server's code and words.
    #[test]
    fn the_board_is_holgers_flow_drawing_shown_as_sent_and_its_refusal_is_shown() {
        let svg = r#"<svg xmlns="http://www.w3.org/2000/svg"><g data-node="source:team" data-kind="source"></g><g data-node="downstream:lab-1" data-kind="downstream"></g></svg>"#;
        let mock = Mock {
            flow: Some(Ok(pb::FlowSvgReply { svg: svg.into(), graph_json: "{}".into(), links: vec![] })),
            topology: Some(Ok(pb::TopologyReply { estate: "acme".into(), summary: "3 boxes: front, main, twin".into(), ..Default::default() })),
            ..Default::default()
        };
        let b = block_on(load_board(&mock, "")).expect("answered").expect("a drawing");
        assert_eq!(b.src, format!("data:image/svg+xml;charset=utf-8,{}", enc(svg)), "holger's bytes, as sent");
        assert!(!b.src.contains('<') && !b.src.contains('"') && !b.src.contains('#'), "every byte escaped: {}", b.src);
        assert!(b.src.contains("data-node%3D%22source%3Ateam%22") && b.src.contains("downstream%3Alab-1"), "the RON's nodes, from the server: {}", b.src);
        assert_eq!((b.estate.as_str(), b.summary.as_str()), ("acme", "3 boxes: front, main, twin"), "the server's line under it");
        // The drawing without the line: the topology refused, the drawing still shows.
        let no_line = Mock { flow: Some(Ok(pb::FlowSvgReply { svg: svg.into(), graph_json: String::new(), links: vec![] })), ..Default::default() };
        assert_eq!(block_on(load_board(&no_line, "")).unwrap().unwrap().summary, "");
        let empty = Mock { flow: Some(Ok(pb::FlowSvgReply::default())), ..Default::default() };
        assert_eq!(block_on(load_board(&empty, "")), Ok(None), "holger drew nothing: nothing is shown, never a sample");
        let refused = Mock {
            flow: Some(Err(Refused { code: "PermissionDenied".into(), message: "[holger-auth-3] the artifact flow is admin-read".into() })),
            ..Default::default()
        };
        assert_eq!(
            block_on(load_board(&refused, "")),
            Err(RefusalView { code: "holger-auth-3".into(), text: "the artifact flow is admin-read".into() })
        );
    }

    #[test]
    fn words_and_times_read_plainly() {
        assert_eq!(utc(0), "—");
        assert_eq!(utc(86_400 + 3_660), "1970-01-02 01:01 UTC");
        assert_eq!(votes_line(2, 2), "2 of 2 approvals");
        assert_eq!(votes_line(0, 1), "0 of 1 approval");
        assert_eq!(state_tone("RESYNCING"), Tone::Neutral);
        assert_eq!(verdict_tone("block"), Tone::Bad);
        assert_eq!(
            refusal(&Refused { code: "Internal".into(), message: "boom".into() }),
            RefusalView { code: "Internal".into(), text: "boom".into() }
        );
    }

    /// ★ **The board's words are the server's** (lane A13): its line, as it came. The console
    /// adds no count of its own (OX-HOLF).
    #[test]
    fn the_board_says_what_the_server_wrote() {
        let line = "3 boxes: front, main, twin · no upstream (uploads only) · no downstream · 1 person used it in 30 days";
        let t = pb::TopologyReply { summary: line.into(), ..Default::default() };
        let f = pb::FlowSvgReply { svg: "<svg/>".into(), graph_json: String::new(), links: vec![] };
        assert_eq!(board(&f, Some(&t)).unwrap().summary, line);
    }
}