crates/gunnar-ui-web/src/machine.rs
//! **What the Machine page decides**, on the host side of the wasm boundary.
//!
//! Same argument as [`crate::room`] and [`crate::activity`], and the same
//! layout: `pages::machine` is `#[cfg(target_arch = "wasm32")]`, so nothing
//! written beside the view is compiled — let alone run — by `cargo test` on
//! the host. That is fine for a view and not fine for this page, whose whole
//! value is a handful of rules about what may and may not be said. No leptos,
//! no `web-sys`; the page below re-exports these and does nothing but draw.
//!
//! # The page exists because the appliance has no shell
//!
//! `:22` is closed and PID 1 is `gunnar-server`. Nobody will ever run `lsblk`
//! on this box. So this is the only place an administrator can see what the
//! machine is — and that raises the cost of every sentence on it, because
//! there is nothing to check it against.
//!
//! # Three layers, and they age differently
//!
//! 1. **What the installer decided** ([`InstallRecord`]) — historical, written
//! once, and carried as gunnar sends it: a key/value table this console
//! does not parse. See [`InstallRecord::fields`] for why that is a feature.
//! 2. **What is true now** ([`Filesystem`]) — measured on the call, and only
//! for the paths gunnar was told to use.
//! 3. **What gunnar is configured to do** ([`Placement`]) — a setting, which
//! is a statement about the next byte and not about the bytes already
//! there.
//!
//! Conflating them is how a reader takes a stale fact for a live one, so the
//! page draws them as three panels with three different words for time, and
//! **nothing here derives a layer-1 answer from layer-2 data**. See
//! [`layer_one`].
//!
//! # The two rules that outrank completeness
//!
//! **[`OffStripe::SameDevice`] is never good news.** A copy that shares a
//! device with what it copies dies in the event it exists for, and it is worse
//! than no copy at all, because somebody will trust it. It gets an alarm, and
//! the state where there simply is no copy gets none — see [`OffStripe`].
//!
//! **A number that was not read is not rendered.** Every byte figure arrives
//! as an `Option` with a reason beside it, and every function here that turns
//! one into text answers `None` rather than a zero, a dash-shaped guess or a
//! half-measured total.
use serde::Deserialize;
use crate::fmt;
/// The machine, as `GET /api/machine` sends it.
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
pub struct Machine {
/// The server's own words when it could not be asked about the machine at
/// all — either because it does not serve `Admin.StorageInfo`, or because
/// it serves it and predates the `reality` field. Empty when it answered.
#[serde(default)]
pub unanswered: String,
#[serde(default)]
pub record: Option<InstallRecord>,
#[serde(default)]
pub record_path: String,
#[serde(default)]
pub record_absent: String,
#[serde(default)]
pub filesystems: Vec<Filesystem>,
#[serde(default)]
pub placement: Option<Placement>,
/// **gunnar's own few lines** (`StorageReality.glance`): `/data`'s shape,
/// the off-stripe copies, the free space, the disk speed — each with
/// gunnar's verdict. The Overview's Machine card draws them AS THEY CAME;
/// see [`glance_grade`]. Empty from a gunnar older than the field.
#[serde(default)]
pub glance: Vec<Glance>,
}
/// **How often the Overview's Machine card asks again** while its page is
/// visible (owner, 2026-10-04: "the picture needs to be a bit dynamic, if twin
/// dies etc."). gunnar's own silence bound is a minute; ten seconds is how
/// quickly the card shows what gunnar decided, not a judgement of its own.
pub const MACHINE_CARD_POLL_MS: u32 = 10_000;
/// `GET /api/machine/topology`, as the card reads it: the rows (for the words
/// a screen reader is given) and the live drawing.
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
pub struct Topology {
#[serde(default)]
pub unanswered: String,
#[serde(default)]
pub boxes: Vec<TopologyBox>,
/// The shared renderer's live board of these rows, drawn by the BFF.
#[serde(default)]
pub svg: String,
}
/// One box as gunnar sent it.
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
pub struct TopologyBox {
#[serde(default)]
pub name: String,
#[serde(default)]
pub role: String,
#[serde(default)]
pub state: String,
#[serde(default)]
pub headline: String,
}
/// ★ **The picture in words** — its accessible name: each box, its role and
/// gunnar's own words for its state, as sent. Nothing judged here.
pub fn estate_words(t: &Topology) -> String {
if t.boxes.is_empty() {
return format!("The estate: {}", t.unanswered);
}
let rows: Vec<String> = t.boxes.iter().map(|b| format!("{} ({}): {}", b.name, b.role, if b.headline.is_empty() { &b.state } else { &b.headline })).collect();
format!("The estate, {} box{}: {}", t.boxes.len(), if t.boxes.len() == 1 { "" } else { "es" }, rows.join("; "))
}
/// ★ **Swap the drawing in, or keep the one on screen** — only when gunnar's
/// answer drew something DIFFERENT, so an unchanged estate never repaints (no
/// flicker, the pulses keep their phase), and never to nothing: an answer with
/// no drawing keeps the last picture until gunnar answers again.
///
/// And never to a drawing that could do more than draw ([`drawable`]): the
/// card puts it in with `inner_html`, so this is the only gate between the
/// answer and the page (stinky #20).
pub fn swap(on_screen: &str, answer: &str) -> bool {
!answer.is_empty() && answer != on_screen && drawable(answer)
}
/// The elements the suite's estate renderer (`nordisk-estate-svg`) draws.
/// Nothing that links, embeds, styles or scripts.
const DRAWN_ELEMENTS: &[&str] = &["svg", "g", "defs", "linearGradient", "stop", "rect", "circle", "line", "text", "animate"];
/// The attributes it writes. No `href`, no `style`, no `on…`.
const DRAWN_ATTRIBUTES: &[&str] = &[
"xmlns", "viewBox", "id", "x", "y", "x1", "y1", "x2", "y2", "cx", "cy", "r", "rx", "width", "height", "offset",
"fill", "stroke", "stroke-width", "stroke-dasharray", "stop-color", "font-family", "font-size", "font-weight",
"text-anchor", "attributeName", "values", "dur", "repeatCount", "data-box", "data-link", "data-state",
];
/// What an `<animate>` may change: how strongly a line is drawn, never what
/// it links to.
const ANIMATED: &[&str] = &["stroke-opacity", "opacity", "fill-opacity"];
const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg";
/// ★ **Could this answer only draw?** One `<svg>` root holding nothing but
/// the elements and attributes the shared renderer writes, every value
/// double-quoted, every `&` one of the five XML entities, no comment, CDATA or
/// processing instruction, no colon outside `xmlns` and the `data-` words
/// (so no `javascript:`), no `url(` but to a gradient in the same drawing,
/// and every element closed in order. Anything else, including what a browser
/// would forgive, is not drawn: a checker that is stricter than the HTML
/// parser cannot be talked past by it.
pub fn drawable(svg: &str) -> bool {
let s = svg.trim();
let mut open: Vec<&str> = Vec::new();
let mut closed_root = false;
let mut at = 0;
while at < s.len() {
let rest = &s[at..];
let Some(lt) = rest.find('<') else { return false };
let words = &rest[..lt];
if closed_root || !escaped(words) || (open.is_empty() && !words.trim().is_empty()) {
return false;
}
let tag = &rest[lt + 1..];
let Some(gt) = tag.find('>') else { return false };
let inner = &tag[..gt];
at += lt + 1 + gt + 1;
if let Some(name) = inner.strip_prefix('/') {
if open.pop() != Some(name) {
return false;
}
closed_root = open.is_empty();
continue;
}
let (inner, empty) = match inner.strip_suffix('/') {
Some(inner) => (inner, true),
None => (inner, false),
};
let name_end = inner.find(|c: char| c.is_ascii_whitespace()).unwrap_or(inner.len());
let name = &inner[..name_end];
if !DRAWN_ELEMENTS.contains(&name) || open.is_empty() != (name == "svg") || !attributes(name, &inner[name_end..]) {
return false;
}
if empty {
closed_root = open.is_empty();
} else {
open.push(name);
}
}
closed_root
}
/// `name="value"` pairs, each after whitespace, each one [`attribute`] allows.
fn attributes(element: &str, mut rest: &str) -> bool {
loop {
let pair = rest.trim_start_matches(|c: char| c.is_ascii_whitespace());
if pair.is_empty() {
return true;
}
if pair.len() == rest.len() {
return false;
}
let Some((name, after)) = pair.split_once('=') else { return false };
let Some(after) = after.strip_prefix('"') else { return false };
let Some((value, after)) = after.split_once('"') else { return false };
if !attribute(element, name, value) {
return false;
}
rest = after;
}
}
fn attribute(element: &str, name: &str, value: &str) -> bool {
if !DRAWN_ATTRIBUTES.contains(&name) || !escaped(value) || value.contains('<') {
return false;
}
match name {
"xmlns" => element == "svg" && value == SVG_NAMESPACE,
"attributeName" => element == "animate" && ANIMATED.contains(&value),
"id" => !value.is_empty() && value.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'),
_ if name.starts_with("data-") => true,
_ => !value.contains(':') && value.split("url(").skip(1).all(|after| after.starts_with('#')),
}
}
/// Text with every `&` one of the five XML entities the renderer writes —
/// no numeric reference that could spell a word the checks above refuse.
fn escaped(text: &str) -> bool {
text.split('&').skip(1).all(|after| ["amp;", "lt;", "gt;", "quot;", "apos;"].iter().any(|e| after.starts_with(e)))
}
/// One line of the machine at a glance, as gunnar sent it.
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
pub struct Glance {
/// `data` | `serialdat` | `free` | `speed`.
#[serde(default)]
pub key: String,
#[serde(default)]
pub line: String,
/// `ok` | `warn` | `bad` | `unknown`.
#[serde(default)]
pub state: String,
/// Why — behind the line's "?".
#[serde(default)]
pub detail: String,
}
/// ★ **gunnar's verdict word, drawn — never re-decided.** `ok` is the green
/// lamp, `warn` amber, `bad` red; anything else (`unknown`, or a word from a
/// gunnar newer than this build) is a plain, colourless marker — never the
/// safest-looking colour this console knows.
pub fn glance_grade(state: &str) -> Grade {
match state {
"ok" => Grade::Good,
"warn" => Grade::Caution,
"bad" => Grade::Alarm,
_ => Grade::Neutral,
}
}
/// The word on a glance line's badge: gunnar's own, upper-cased for the
/// three it judged, `unknown` for anything else.
pub fn glance_label(state: &str) -> &'static str {
match state {
"ok" => "OK",
"warn" => "LOOK",
"bad" => "BAD",
_ => "unknown",
}
}
/// Layer 1: what the installer decided, **in the installer's own keys**.
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
pub struct InstallRecord {
#[serde(default)]
pub version: u32,
/// gunnar's two words: `verified` or `INTEGRITY ONLY`.
#[serde(default)]
pub signature: String,
#[serde(default)]
pub signature_verified: bool,
/// The installer's own sentence saying what is NOT verified.
#[serde(default)]
pub signature_note: String,
/// ★ **Every field of the config, unparsed, in key order.**
///
/// Not folded back into disks, partitions and sets, and gunnar's own
/// comment is why: *a key this build does not know is still shown, because
/// the admin reading the page may know what it means when this binary does
/// not*. A console that reconstructed a structure here would go blind on
/// every key a later installer adds, and on a box with no shell there is
/// no second way to see it.
#[serde(default)]
pub fields: Vec<RecordField>,
}
/// One `key=value` from the install record, as the installer wrote it.
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
pub struct RecordField {
#[serde(default)]
pub key: String,
#[serde(default)]
pub value: String,
}
/// Layer 2: one configured filesystem, measured on this call.
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
pub struct Filesystem {
/// `data root` | `configured directory`.
#[serde(default)]
pub what: String,
#[serde(default)]
pub path: String,
pub total_bytes: Option<u64>,
pub free_bytes: Option<u64>,
/// Why there are no numbers. Empty when there are.
#[serde(default)]
pub error: String,
}
/// Layer 3: gunnar's placement settings.
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
pub struct Placement {
#[serde(default)]
pub data_root: String,
/// **`/data`'s own shape** (`data.shape` from the installer's layout
/// config: `raid0` | `plain` | `p3`). Empty when nothing said.
#[serde(default)]
pub data_shape: String,
/// `data.<N>.dev`: the devices `/data` is made of.
#[serde(default)]
pub data_devices: Vec<String>,
#[serde(default)]
pub slow_root: String,
/// **How gunnar learned where to write.** "We were told" and "we fell
/// back" are different facts, and this is the field that holds the
/// difference. See [`slow_root_line`].
#[serde(default)]
pub slow_root_source: String,
/// **`/serialdat`'s disk** (`serialdat.dev`) — NOT `/data`'s. The old
/// Machine page drew it as "Backing device" right under "Data root", and
/// every estate read as if its repositories lived on the boot disk's P3
/// (owner, 2026-10-04). See [`serialdat_on`].
#[serde(default)]
pub backing_device: String,
/// `p3` | `device` | `fallback` — `/serialdat`'s class.
#[serde(default)]
pub backing_class: String,
pub off_stripe: Option<String>,
/// Whole sentences, each naming a copy AND its limitation. Never "OK".
#[serde(default)]
pub off_stripe_health: Vec<String>,
#[serde(default)]
pub rows: Vec<PlacementRow>,
}
/// Layer 3: one thing gunnar writes, and where.
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
pub struct PlacementRow {
#[serde(default)]
pub what: String,
#[serde(default)]
pub path: String,
#[serde(default)]
pub requires_off_stripe: bool,
pub off_stripe: Option<String>,
/// **Does gunnar write here today?** A directory that exists is not a
/// feature.
#[serde(default)]
pub wired: bool,
/// When `wired` is false, why not.
#[serde(default)]
pub note: String,
}
/// **How many paths are configured and not yet written to**, in a sentence
/// that agrees with its number (owner 2026-10-14: "2 of these paths is …").
pub fn not_written_line(n: usize) -> String {
format!("{} of these paths {} configured and not yet written to.", crate::fmt::count(n as u64), if n == 1 { "is" } else { "are" })
}
#[cfg(test)]
mod not_written_line_tests {
#[test]
fn the_sentence_agrees_with_its_number() {
assert_eq!(super::not_written_line(1), "1 of these paths is configured and not yet written to.");
assert_eq!(super::not_written_line(2), "2 of these paths are configured and not yet written to.");
}
}
/// How loudly something should be drawn.
///
/// The console's own four states live in `ui::Tone`, which is behind the wasm
/// gate; this is the same ladder on the testable side, and `pages::machine`
/// maps it across. Keeping a second copy is deliberate — the alternative is
/// deciding severity inside a view, where no test can reach the decision.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Grade {
/// Working as intended.
Good,
/// True, and somebody should know.
Caution,
/// Not working, or a promise that will not be kept.
Alarm,
/// A fact with no verdict attached.
Neutral,
}
/// ★ **Whether a copy lives somewhere that can die on its own — in three
/// honest states, and a fourth for "nobody said".**
///
/// The three are **not a scale**, and this type exists so that no view can
/// treat them as one:
///
/// * [`OffStripe::Elsewhere`] — the copy is in another failure domain. Good.
/// * [`OffStripe::SameDevice`] — there is a copy and it will die with its
/// original. **Never good news, and never a green tick.** It is worse than
/// having none, because a copy that is shown as present is a copy somebody
/// will reach for on the day the original is gone. It is graded
/// [`Grade::Alarm`] on purpose, one step ABOVE the state where there is no
/// copy at all.
/// * [`OffStripe::None`] — there is no copy. Honest, and not an alarm by
/// itself: nobody is going to trust a thing that is not there.
/// * [`OffStripe::NotSaid`] — nothing was reported about this. A state of the
/// ANSWER, not of the machine — it is also what a placement row that does
/// not REQUIRE an off-stripe copy carries — and the page draws it as a dash
/// with the reason in the title rather than as any of the three above.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OffStripe {
Elsewhere,
SameDevice,
None,
NotSaid,
}
impl OffStripe {
/// Read the word the server sent.
///
/// gunnar spells these `off-stripe` / `same-device` / `absent`; the BFF
/// translates them into this console's older vocabulary once, in
/// `api::machine::off_stripe_word`, and this is the only reader of the
/// result. An unknown word is [`OffStripe::NotSaid`] and never a guess: a
/// console that mapped a word it does not know onto the nearest one it
/// does would be inventing a topology, which is the whole failure this
/// page is built against.
pub fn read(word: Option<&str>) -> OffStripe {
match word {
Some("elsewhere") => OffStripe::Elsewhere,
Some("same_device") => OffStripe::SameDevice,
Some("none") => OffStripe::None,
_ => OffStripe::NotSaid,
}
}
/// The badge's word, or `None` when there is nothing honest to badge.
pub fn label(self) -> Option<&'static str> {
match self {
OffStripe::Elsewhere => Some("off-stripe"),
OffStripe::SameDevice => Some("same device"),
OffStripe::None => Some("no second copy"),
OffStripe::NotSaid => None,
}
}
/// How loudly. **[`OffStripe::SameDevice`] is never [`Grade::Good`]**, and
/// it is louder than [`OffStripe::None`].
pub fn grade(self) -> Grade {
match self {
OffStripe::Elsewhere => Grade::Good,
OffStripe::SameDevice => Grade::Alarm,
OffStripe::None => Grade::Neutral,
OffStripe::NotSaid => Grade::Neutral,
}
}
/// The sentence under the badge, in the words an operator reads.
pub fn detail(self) -> &'static str {
match self {
OffStripe::Elsewhere => {
"On a device that is not in the striped set, so it survives the set."
}
OffStripe::SameDevice => {
"This copy shares a device with what it copies: it dies in the \
event it exists for. That is worse than having none, because it \
is there to be trusted."
}
OffStripe::None => "There is no second copy. Nothing here is claiming otherwise.",
OffStripe::NotSaid => {
"Nothing was reported about where this sits, and nothing is being \
inferred from the filesystems below."
}
}
}
}
/// Why there is no install record to draw.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Absence {
/// **gunnar answered, and this machine has no layout config.** Expected on
/// any appliance installed before `korp-installer` began writing one, and
/// also on one whose config did not verify — gunnar reports that as ABSENT
/// rather than as present-and-suspect, and `why` is what says which.
NoRecord { path: String, why: String },
/// **gunnar could not be asked** — it does not serve `Admin.StorageInfo`,
/// or it does and predates the `reality` field. Not a fact about the
/// machine at all.
NotAnswered { why: String },
}
impl Absence {
/// The heading. Says which of the two absences this is, because the fixes
/// are unrelated: one is a reinstall, the other is a server upgrade.
pub fn title(&self) -> &'static str {
match self {
Absence::NoRecord { .. } => "The install record is absent",
Absence::NotAnswered { .. } => "This Gunnar does not report the machine",
}
}
/// ★ The paragraph, and the promise it makes is a NEGATIVE one: the page
/// will not answer layer 1 out of layer 2.
///
/// Saying so is not padding. The filesystems are on the same screen, and
/// the obvious thing for a reader — or for a later maintainer — to do is
/// to fill this panel in from them. `statvfs` can say how full `/data` is.
/// It can never say that a fifth disk was refused for being 512e, and a
/// page that quietly reconstructed the first would be read as having
/// answered the second.
pub fn detail(&self) -> String {
match self {
Absence::NoRecord { path, why } => {
let why = if why.trim().is_empty() {
"Gunnar gave no reason.".to_owned()
} else {
format!("Gunnar says: {}", sentence(why))
};
let path = if path.trim().is_empty() {
String::new()
} else {
format!(" It was looked for at {path}.")
};
format!(
"{why}{path} What the installer decided — which disk became boot and \
why it won, which disks were rejected and on what grounds — is not \
recoverable from a running machine, and this page will not guess at \
it from the filesystems below. An appliance installed before the \
record existed simply has none."
)
}
Absence::NotAnswered { why } => format!(
"Gunnar says: {} The server this console is talking to is older than \
the machine report, so nothing on this page was measured — neither the \
installer's decisions nor the filesystems. Nothing below is being \
inferred in its place.",
sentence(why)
),
}
}
}
/// gunnar's words, ended once.
///
/// Its messages arrive with and without a full stop — `no file at that path`
/// and `statvfs failed.` are both real — and both are quoted mid-paragraph
/// here. Without this the page reads *no file at that path It was looked for
/// at …*, which is the kind of seam that makes a careful page look careless.
fn sentence(words: &str) -> String {
let trimmed = words.trim_end();
if trimmed.ends_with(['.', '!', '?', ':', ';']) {
trimmed.to_owned()
} else {
format!("{trimmed}.")
}
}
/// ★ **Layer 1, or the honest reason there is none.**
///
/// The one function the page's first panel goes through, and the reason it is
/// a `Result` rather than an `Option`: an `Option` has one empty state, and
/// this has two that must never be drawn the same way. *This machine has no
/// record* is a fact about the machine. *This gunnar cannot be asked* is a
/// fact about the server, on a machine whose layout may be perfectly well
/// recorded.
pub fn layer_one(machine: &Machine) -> Result<&InstallRecord, Absence> {
if !machine.unanswered.trim().is_empty() {
return Err(Absence::NotAnswered {
why: machine.unanswered.clone(),
});
}
match &machine.record {
Some(record) => Ok(record),
None => Err(Absence::NoRecord {
path: machine.record_path.clone(),
why: machine.record_absent.clone(),
}),
}
}
/// ★ **What the record's signature is worth, as a badge and a grade.**
///
/// Three states and gunnar's own two words for the first two. `INTEGRITY
/// ONLY` is [`Grade::Caution`] and never [`Grade::Good`]: the body's blake3
/// matched and **nothing else is claimed**, which is a checksum and not a
/// signature. It is not [`Grade::Alarm`] either, because it is the state every
/// appliance in the field is in today and a permanent red is a red nobody
/// reads by the second week.
///
/// A signature that FAILED to verify never reaches here — gunnar makes the
/// whole record absent instead — so this is never a third, quieter kind of
/// failure.
pub fn signature(record: &InstallRecord) -> (String, Grade) {
if record.signature_verified {
return (
"Signed, and the signature verified against the root key enrolled on this \
estate."
.to_owned(),
Grade::Good,
);
}
let note = if record.signature_note.trim().is_empty() {
String::new()
} else {
format!(" {}", sentence(&record.signature_note))
};
if record.signature.trim().is_empty() {
return (
format!(
"Gunnar did not say what this record's signature is worth, so nothing here \
is claiming it was checked.{note}"
),
Grade::Neutral,
);
}
(
format!(
"{}: the body's checksum matches and nothing else is claimed. This is not a \
signature.{note}",
record.signature.trim()
),
Grade::Caution,
)
}
/// The badge's own short word for the same thing.
pub fn signature_label(record: &InstallRecord) -> &'static str {
if record.signature_verified {
"verified"
} else if record.signature.trim().is_empty() {
"not reported"
} else {
"integrity only"
}
}
/// `94.5 GB free of 137 GB`, or `None` when it was not measured.
///
/// Both halves or neither. "94.5 GB free of 0 B" is the shape a proto3 zero
/// makes of a failed `statvfs`, and it is the most dangerous line this page
/// could print — it reads as a disk with nothing on it.
pub fn free_of_total(fs: &Filesystem) -> Option<String> {
match (fs.free_bytes, fs.total_bytes) {
(Some(free), Some(total)) => {
Some(format!("{} free of {}", fmt::bytes(free), fmt::bytes(total)))
}
_ => None,
}
}
/// How full, 0–100, for the bar. `None` when it was not measured, and `None`
/// on a zero-sized filesystem rather than a division by zero.
///
/// Used is `total - free`, which is arithmetic on two numbers that WERE read
/// and not a third number nobody measured. `saturating_sub` because the
/// reserved blocks are already out of `free`, which can make the two cross.
pub fn fill_percent(fs: &Filesystem) -> Option<u32> {
let (total, free) = (fs.total_bytes?, fs.free_bytes?);
if total == 0 {
return None;
}
let used = total.saturating_sub(free);
Some(((used.saturating_mul(100)) / total).min(100) as u32)
}
/// Why a filesystem has no numbers, in words. Empty is never returned as a
/// reason.
pub fn unmeasured_reason(fs: &Filesystem) -> String {
if fs.error.trim().is_empty() {
"Gunnar sent no figures and no reason for their absence.".to_owned()
} else {
fs.error.clone()
}
}
/// ★ **The slow root, and whether gunnar was TOLD or fell back.**
///
/// `slow_root_source` and not "is the string empty": a box whose layout config
/// names `/serialdat` and a box with no config at all can end up writing to
/// the same directory, and they are not the same machine. One is configured;
/// the other is a default that will move the day the default moves.
pub fn slow_root_line(placement: &Placement) -> String {
let source = placement.slow_root_source.trim();
let root = placement.slow_root.trim();
match (root.is_empty(), source.is_empty()) {
(true, true) => "Gunnar did not report a slow root, and nothing here is standing \
in for one."
.to_owned(),
(true, false) => format!(
"No slow root is named. Gunnar writes where it does because: {}",
sentence(source)
),
(false, true) => format!(
"Slow root: {root}. Gunnar did not say how it learned that, so this page \
cannot tell a configured root from a default."
),
(false, false) => format!("Slow root: {root} — {source}"),
}
}
/// ★ **What `/data` is made of**, from the installer's `data.*` keys and
/// nothing else: `raid0 over /dev/vdb, /dev/vdc`. `None` when nothing said —
/// never filled from `/serialdat`'s device.
pub fn data_on(placement: &Placement) -> Option<String> {
let shape = placement.data_shape.trim();
let devices = placement.data_devices.join(", ");
match (shape.is_empty(), devices.is_empty()) {
(true, true) => None,
(false, true) => Some(shape.to_owned()),
(true, false) => Some(devices),
(false, false) => Some(format!("{shape} over {devices}")),
}
}
/// ★ **Where `/serialdat` is**, labelled as `/serialdat`'s: `/dev/vda3 (p3)`.
/// `None` when nothing said. The ONE place `backing_device` becomes text, and
/// the page puts it under "/serialdat on", never under the data root.
pub fn serialdat_on(placement: &Placement) -> Option<String> {
let dev = placement.backing_device.trim();
let class = placement.backing_class.trim();
match (dev.is_empty(), class.is_empty()) {
(true, true) => None,
(true, false) => Some(class.to_owned()),
(false, true) => Some(dev.to_owned()),
(false, false) => Some(format!("{dev} ({class})")),
}
}
/// ★ **The rows that need another failure domain, and the ones that do not.**
///
/// Split rather than sorted: a placement row that does not require an
/// off-stripe copy carries no verdict about one, and listing it beside the
/// three that do — under a column whose empty cell reads as "fine" — is how a
/// reader concludes the mirror is somewhere it is not. The required ones come
/// first because they are the reason the table is on the page.
pub fn by_requirement(placement: &Placement) -> (Vec<PlacementRow>, Vec<PlacementRow>) {
let (required, rest): (Vec<_>, Vec<_>) = placement
.rows
.iter()
.cloned()
.partition(|row| row.requires_off_stripe);
(required, rest)
}
/// ★ **How many configured rows gunnar does not actually write to yet.**
///
/// `wired` is gunnar's own field and this page does not second-guess it: a
/// directory that exists is not a feature. The count is what lets the panel
/// say so once at the top instead of leaving a reader to notice it row by row.
pub fn unwired(placement: &Placement) -> usize {
placement.rows.iter().filter(|row| !row.wired).count()
}
#[cfg(test)]
mod tests {
use super::*;
fn filesystem(path: &str) -> Filesystem {
Filesystem {
what: "configured directory".to_owned(),
path: path.to_owned(),
..Filesystem::default()
}
}
/// ★ **`same_device` is never good news, and the four states stay four.**
///
/// The assertion the page is FOR. A view that treated the three states as
/// a scale would draw the worst of them as an almost-good one, and the
/// person reading this page is often reading it because something already
/// broke.
#[test]
fn a_copy_on_the_same_device_is_graded_worse_than_no_copy_at_all() {
assert_eq!(OffStripe::read(Some("elsewhere")), OffStripe::Elsewhere);
assert_eq!(OffStripe::read(Some("same_device")), OffStripe::SameDevice);
assert_eq!(OffStripe::read(Some("none")), OffStripe::None);
assert_eq!(OffStripe::read(None), OffStripe::NotSaid);
// A word from a gunnar newer than this console is not rounded to the
// nearest word this console knows.
assert_eq!(OffStripe::read(Some("mirrored")), OffStripe::NotSaid);
assert_eq!(OffStripe::Elsewhere.grade(), Grade::Good);
assert_ne!(
OffStripe::SameDevice.grade(),
Grade::Good,
"a copy that dies with its original was drawn as working"
);
assert_eq!(OffStripe::SameDevice.grade(), Grade::Alarm);
// Louder than having nothing, which is the ordering that matters.
assert_eq!(OffStripe::None.grade(), Grade::Neutral);
// And the silence is not one of the three.
assert_eq!(OffStripe::NotSaid.label(), None);
for state in [OffStripe::Elsewhere, OffStripe::SameDevice, OffStripe::None] {
assert!(state.label().is_some());
}
}
/// ★ **The two absences are two.**
///
/// A missing record and a gunnar that cannot be asked have nothing in
/// common: one is a fact about this machine, the other about the server
/// talking about it, and they are fixed by unrelated things.
#[test]
fn a_missing_record_and_an_older_gunnar_are_not_the_same_absence() {
let no_record = Machine {
record_path: "/boot/korp/layout.conf".to_owned(),
record_absent: "no file at that path".to_owned(),
..Machine::default()
};
let older = Machine {
unanswered: "this gunnar serves Admin.StorageInfo but sends no `reality`"
.to_owned(),
..Machine::default()
};
let a = layer_one(&no_record).expect_err("no record");
let b = layer_one(&older).expect_err("no answer");
assert_ne!(a.title(), b.title());
assert!(a.detail().contains("/boot/korp/layout.conf"));
assert!(a.detail().contains("no file at that path"));
assert!(b.detail().contains("older than"));
}
/// ★ **The page promises, in words, not to reconstruct layer 1 from layer
/// 2** — and the promise is asserted, because the whole point is that the
/// filesystems are right there on the same screen to be tempted by.
#[test]
fn an_absent_record_says_the_page_will_not_infer_one() {
let machine = Machine {
record_absent: "no file".to_owned(),
filesystems: vec![filesystem("/data")],
..Machine::default()
};
let absence = layer_one(&machine).expect_err("no record");
let detail = absence.detail().to_lowercase();
assert!(detail.contains("not recoverable from a running machine"));
assert!(detail.contains("will not guess"));
}
/// A record that IS there comes back as itself, keys and all.
#[test]
fn a_present_record_is_handed_over_whole() {
let machine = Machine {
record: Some(InstallRecord {
version: 3,
fields: vec![RecordField {
key: "data.raid".to_owned(),
value: "raid0".to_owned(),
}],
..InstallRecord::default()
}),
..Machine::default()
};
let record = layer_one(&machine).expect("present");
assert_eq!(record.version, 3);
assert_eq!(record.fields[0].key, "data.raid");
}
/// An older gunnar wins over a record: with no answer there is no record
/// to have an opinion about.
#[test]
fn no_answer_outranks_everything_else_in_the_message() {
let machine = Machine {
unanswered: "no such method".to_owned(),
record: Some(InstallRecord::default()),
..Machine::default()
};
assert!(matches!(
layer_one(&machine),
Err(Absence::NotAnswered { .. })
));
}
/// ★ **An unsigned record is never drawn as a verified one.**
///
/// `INTEGRITY ONLY` is the state every appliance in the field is in, and
/// it is a checksum: the body matches itself. Grading it [`Grade::Good`]
/// would put a green tick on the sentence *nothing else is claimed*.
#[test]
fn integrity_only_is_a_caution_and_never_a_verified_signature() {
let verified = InstallRecord {
signature: "verified".to_owned(),
signature_verified: true,
..InstallRecord::default()
};
let (line, grade) = signature(&verified);
assert_eq!(grade, Grade::Good);
assert!(line.contains("verified"));
assert_eq!(signature_label(&verified), "verified");
let integrity = InstallRecord {
signature: "INTEGRITY ONLY".to_owned(),
signature_verified: false,
signature_note: "no KORP_ROOT_SIGNING_KEY is enrolled on this estate".to_owned(),
..InstallRecord::default()
};
let (line, grade) = signature(&integrity);
assert_ne!(grade, Grade::Good, "a checksum was drawn as a signature");
assert_eq!(grade, Grade::Caution);
assert!(line.contains("not a signature"));
// The installer's own sentence about what is NOT verified is on the
// page, not swallowed.
assert!(line.contains("KORP_ROOT_SIGNING_KEY"));
assert_eq!(signature_label(&integrity), "integrity only");
// And a gunnar that said nothing at all gets no verdict either way.
let silent = InstallRecord::default();
let (line, grade) = signature(&silent);
assert_eq!(grade, Grade::Neutral);
assert!(line.contains("did not say"));
assert_eq!(signature_label(&silent), "not reported");
}
/// ★ **Half a measurement is no measurement.**
///
/// The failed-`statvfs` shape: a total with no free, or a free with no
/// total, must draw nothing at all rather than "0 B" or "94 GB free of
/// 0 B".
#[test]
fn a_half_measured_filesystem_renders_no_figure_and_no_bar() {
let measured = Filesystem {
total_bytes: Some(128_000_000_000),
free_bytes: Some(96_000_000_000),
..filesystem("/data")
};
assert!(free_of_total(&measured).is_some());
assert_eq!(fill_percent(&measured), Some(25));
for half in [
Filesystem {
total_bytes: Some(128_000_000_000),
..filesystem("/data")
},
Filesystem {
free_bytes: Some(96_000_000_000),
..filesystem("/data")
},
filesystem("/data"),
] {
assert_eq!(free_of_total(&half), None);
assert_eq!(fill_percent(&half), None);
}
// A zero-sized filesystem is not 100 % full and is not a panic.
let empty = Filesystem {
total_bytes: Some(0),
free_bytes: Some(0),
..filesystem("/data")
};
assert_eq!(fill_percent(&empty), None);
// And more free than total — the reserved blocks crossing over — is
// 0 % rather than an underflow.
let crossed = Filesystem {
total_bytes: Some(100),
free_bytes: Some(120),
..filesystem("/data")
};
assert_eq!(fill_percent(&crossed), Some(0));
}
/// An unmeasured filesystem always has a reason on screen, even when
/// gunnar sent none.
#[test]
fn an_unmeasured_filesystem_always_carries_a_reason() {
assert!(unmeasured_reason(&filesystem("/data")).contains("no reason"));
assert_eq!(
unmeasured_reason(&Filesystem {
error: "statvfs: os error 5".to_owned(),
..filesystem("/data")
}),
"statvfs: os error 5"
);
}
/// ★ **A row that does not require an off-stripe copy is not listed as a
/// row that has one.**
///
/// The column's empty cell reads as "fine", so the rows that make no such
/// promise are drawn apart from the ones that do rather than mixed in
/// under it.
#[test]
fn the_rows_that_must_be_off_stripe_are_drawn_apart_from_the_ones_that_need_not_be() {
let placement = Placement {
rows: vec![
PlacementRow {
what: "packs".to_owned(),
requires_off_stripe: false,
wired: true,
..PlacementRow::default()
},
PlacementRow {
what: "metadata mirror".to_owned(),
requires_off_stripe: true,
off_stripe: Some("same_device".to_owned()),
wired: true,
..PlacementRow::default()
},
PlacementRow {
what: "archives".to_owned(),
requires_off_stripe: false,
wired: false,
note: "no archive-out has run on this box".to_owned(),
..PlacementRow::default()
},
],
..Placement::default()
};
let (required, rest) = by_requirement(&placement);
assert_eq!(required.len(), 1);
assert_eq!(required[0].what, "metadata mirror");
assert_eq!(
OffStripe::read(required[0].off_stripe.as_deref()),
OffStripe::SameDevice
);
assert_eq!(rest.len(), 2);
// And the one gunnar does not write to yet is counted, so the panel
// can say so once rather than leave it to be noticed.
assert_eq!(unwired(&placement), 1);
}
/// ★ A slow root gunnar was TOLD about and one it fell back to are two
/// different configurations, and the page says which.
#[test]
fn a_configured_slow_root_is_not_a_default_one() {
let told = slow_root_line(&Placement {
slow_root: "/serialdat".to_owned(),
slow_root_source: "named by the layout config".to_owned(),
..Placement::default()
});
assert!(told.contains("/serialdat"));
assert!(told.contains("named by the layout config"));
let fell_back = slow_root_line(&Placement {
slow_root: "/data/slow".to_owned(),
slow_root_source: "the documented default under the data root, because there \
is no layout config"
.to_owned(),
..Placement::default()
});
assert!(fell_back.contains("documented default"));
assert_ne!(told, fell_back);
// Nothing said at all is not a configuration either.
let silent = slow_root_line(&Placement::default());
assert!(silent.contains("did not report"));
// A root with no source does not get to pass as a configured one.
let rootless_source = slow_root_line(&Placement {
slow_root: "/serialdat".to_owned(),
..Placement::default()
});
assert!(rootless_source.contains("did not say how it learned"));
}
/// ★ **The mislabel, fixed for good** (owner, 2026-10-04): `/data` names
/// ITS devices, `/serialdat`'s disk is said as `/serialdat`'s — and the
/// page that draws them puts `backing_device` under a "/serialdat" label
/// and nowhere else.
#[test]
fn data_and_serialdat_each_name_their_own_disk() {
let p = Placement {
data_root: "/data".into(),
data_shape: "raid0".into(),
data_devices: vec!["/dev/vdb".into(), "/dev/vdc".into()],
backing_device: "/dev/vda3".into(),
backing_class: "p3".into(),
..Placement::default()
};
assert_eq!(data_on(&p).as_deref(), Some("raid0 over /dev/vdb, /dev/vdc"));
assert!(!data_on(&p).unwrap().contains("vda3"), "the data line names the boot disk");
assert_eq!(serialdat_on(&p).as_deref(), Some("/dev/vda3 (p3)"));
assert_eq!(data_on(&Placement::default()), None, "nothing said is not a shape");
// The view: `backing_device` is drawn ONLY through `serialdat_on`, and
// that is drawn only under a label that names /serialdat.
// The CODE of the page — its comments may tell the story of the old
// labels, the view may not draw them.
let page = crate::code::of(include_str!("pages/machine.rs"));
assert!(!page.contains("backing_device"), "the page reads backing_device itself instead of serialdat_on");
assert!(!page.contains("Backing device") && !page.contains("Backing class"), "the old labels are back");
let at = page.find("serialdat_on(").expect("the page draws serialdat_on");
let from = (0..=at.saturating_sub(400)).rev().find(|i| page.is_char_boundary(*i)).unwrap_or(0);
let before = &page[from..at];
assert!(before.contains("/serialdat on"), "serialdat_on is drawn under a label that does not name /serialdat");
}
/// gunnar's verdict word is drawn as it came, and a word this build does
/// not know is colourless.
#[test]
fn a_glance_state_is_drawn_never_re_decided() {
assert_eq!(glance_grade("ok"), Grade::Good);
assert_eq!(glance_grade("warn"), Grade::Caution);
assert_eq!(glance_grade("bad"), Grade::Alarm);
for other in ["unknown", "", "OK", "green"] {
assert_eq!(glance_grade(other), Grade::Neutral, "{other}");
assert_eq!(glance_label(other), "unknown");
}
}
fn bx(name: &str, role: &str, state: &str, headline: &str) -> TopologyBox {
TopologyBox { name: name.into(), role: role.into(), state: state.into(), headline: headline.into() }
}
/// The picture's words are gunnar's rows, verbatim.
#[test]
fn the_picture_is_named_in_gunnars_words() {
let t = Topology { boxes: vec![bx("vm2", "primary", "up", "answering"), bx("vm3", "twin", "silent", "silent for 4 min")], ..Default::default() };
assert_eq!(estate_words(&t), "The estate, 2 boxes: vm2 (primary): answering; vm3 (twin): silent for 4 min");
let none = Topology { unanswered: "this gunnar does not serve Admin.Topology".into(), ..Default::default() };
assert_eq!(estate_words(&none), "The estate: this gunnar does not serve Admin.Topology");
}
/// ★ **Swapped in only when the drawing changed, and never to nothing.**
#[test]
fn the_drawing_is_swapped_only_when_it_changed() {
assert!(!swap("<svg>a</svg>", "<svg>a</svg>"), "an unchanged answer repainted the picture");
assert!(swap("<svg>a</svg>", "<svg>b</svg>"));
assert!(swap("", "<svg>a</svg>"), "the first answer is drawn");
assert!(!swap("<svg>a</svg>", ""), "an answer with no drawing blanked the picture");
}
/// ★ **A drawing that could do anything but draw never reaches the page**
/// (stinky #20). The card puts the drawing in with `inner_html`, so
/// whatever the answer holds is parsed as markup: a `<script>`, an `on…`
/// handler, a `javascript:` link, a `<foreignObject>` with HTML in it.
/// The shared renderer escapes every word it draws today, but this side
/// must not depend on that: such an answer is never swapped in.
#[test]
fn a_drawing_that_could_run_script_is_never_swapped_in() {
let hostile = [
r#"<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script></svg>"#,
r#"<svg xmlns="http://www.w3.org/2000/svg" onload="alert(1)"></svg>"#,
r#"<svg xmlns="http://www.w3.org/2000/svg"><rect onclick="alert(1)"/></svg>"#,
r#"<svg xmlns="http://www.w3.org/2000/svg"><a href="javascript:alert(1)"><text>x</text></a></svg>"#,
r#"<svg xmlns="http://www.w3.org/2000/svg"><foreignObject><img src=x onerror="alert(1)"></foreignObject></svg>"#,
r#"<svg xmlns="http://www.w3.org/2000/svg"><animate attributeName="href" values="javascript:alert(1)"/></svg>"#,
r#"<svg xmlns="http://www.w3.org/2000/svg"><rect fill="url(https://elsewhere.example/x#y)"/></svg>"#,
r#"<svg xmlns="http://www.w3.org/2000/svg"><text>javascript</text><style>*{}</style></svg>"#,
r#"<svg xmlns="http://www.w3.org/2000/svg"><!-- --><text>x</text></svg>"#,
r#"<svg xmlns="http://www.w3.org/2000/svg"><text><![CDATA[<script>alert(1)</script>]]></text></svg>"#,
r#"<svg xmlns="http://www.w3.org/2000/svg"><text x=1>unquoted</text></svg>"#,
r#"<svg xmlns="http://www.w3.org/2000/svg"><text>never closed</svg>"#,
r#"<img src=x onerror="alert(1)">"#,
r#"<svg xmlns="http://www.w3.org/2000/svg"></svg><script>alert(1)</script>"#,
];
for answer in hostile {
assert!(!swap("", answer), "drawn: {answer}");
assert!(!swap("<svg>a</svg>", answer), "swapped in: {answer}");
}
}
/// ★ **…and every drawing the suite's one renderer makes is drawn** — the
/// live board and the still one, both themes, with somebody else's words
/// on the chips (a box named `<script>`, a headline with quotes and `&`).
#[test]
fn every_drawing_the_shared_renderer_makes_is_swapped_in() {
use nordisk_estate_svg::{estate_svg, estate_svg_themed, Disk, Estate, Liveness, Machine, Role, Theme};
let chip = |id: &str, role: Role, state: &str, headline: &str| Machine {
id: id.to_owned(),
role,
title: "this box · gunnar-server · 4 vCPU 16 GB".to_owned(),
cloud: String::new(),
server_id: "10.13.8.101".to_owned(),
plan: None,
declared_vcpu: None,
declared_ram_mb: None,
measured_cpu_millicores: None,
measured_ram_bytes: None,
disk: Some(Disk { used_bytes: 3 << 30, total_bytes: 40 << 30, growable: false }),
liveness: Liveness::from_word(state),
note: "said \"it's <b>gone</b>\" & left".to_owned(),
headline: headline.to_owned(),
};
let machines = vec![
chip("vm1", Role::Appliance, "up", "following"),
chip("<script>alert(1)</script>", Role::Twin, "behind", "behind 120 entries"),
chip("vm3\" onload=\"alert(1)", Role::Standby, "silent", "silent for 4 min"),
chip("vm4", Role::Console, "unknown", ""),
];
for live in [true, false] {
for disk_only in [true, false] {
let estate = Estate {
product: "Gunnar & <co>".to_owned(),
machines: machines.clone(),
subtitle: Some("4 boxes \"as laid\"".to_owned()),
disk_only,
live,
..Estate::default()
};
for drawing in [estate_svg(&estate), estate_svg_themed(&estate, &Theme::MONOCHROME)] {
assert!(swap("", &drawing), "the renderer's own drawing was refused (live={live}): {drawing}");
}
}
}
let nothing = Estate { product: "Gunnar".to_owned(), subtitle: Some("this Gunnar does not serve Admin.Topology".to_owned()), live: true, disk_only: true, ..Estate::default() };
assert!(swap("", &estate_svg(¬hing)), "the board for an unanswered gunnar was refused");
}
/// ★ **The card is live and thin** (owner, 2026-10-04: "make it svg
/// dynamic"): it asks again on its own while the page is visible, draws the
/// SVG INLINE (no <img> to blank between fetches), and swaps only through
/// `swap`. Held to the page's source.
#[test]
fn the_machine_card_polls_inline_and_swaps_only_on_change() {
let page = crate::code::of(include_str!("pages/overview.rs"));
let at = page.find("fn MachineCard").expect("the Machine card");
let card = &page[at..at + page[at..].find("\n}\n").expect("its end")];
for must in ["MACHINE_CARD_POLL_MS", ".hidden()", "inner_html", "machine::swap(", "machine::estate_words("] {
assert!(card.contains(must), "the Machine card lacks `{must}`");
}
assert!(!card.contains("<img"), "the card still draws the picture as an <img>");
}
}