crates/estate-svg/src/lib.rs
//! **The estate, drawn**: one board, one chip per machine, the numbers a buyer
//! never sees and an operator always needs.
//!
//! # Why this exists
//!
//! monetize's model is deliberately blind to machines. An order names a METER
//! and an AMOUNT — `pack_bytes = 10 GiB` — and `Change::Ensure` decides
//! what iron that becomes and where. That abstraction is right, and it is the
//! reason a buyer's receipt says "10 GiB" and never "on the twin".
//!
//! What it left behind is an operator with no picture. `[clouds.server]` is one
//! anonymous `pool_gib`; the machines are named in three unrelated places
//! (`monetize_cloud::ServerId` on every `Resource`, the twin's provider ids in
//! the retired `[products.<id>.twin_grow]`, and nowhere at all for the front), and none of
//! them is a list. So: this module takes what the config already knows, plus
//! whatever the live boxes answered, and draws it.
//!
//! **It is a rendering, not an authority.** Nothing here measures anything, and
//! nothing here decides placement. Every number arrives in [`Estate`] from
//! somebody who measured it, and a number nobody measured is drawn as `—`
//! rather than guessed — the same rule `Figure::Unmeasured` applies on the
//! console's own screens, and for the same reason: a plan's declared vCPU count
//! is the provider's claim about a box, not a reading off it.
//!
//! # Fairly static, with one moving part
//!
//! The topology does not change much: a front, an appliance, a twin. What
//! changes is whether the twin is THERE — and that is exactly the thing an
//! operator opens this picture to find out. A twin that has never been seen is
//! drawn, dimmed, with its own sentence, rather than left out: a missing box on
//! a diagram reads as "there is no twin in this design", and there is one, it
//! is just dark.
//!
//! # ONE renderer, two doors (extracted 2026-10-04)
//!
//! * **monetize** — `EstateAdmin.Draw` (`monetize-server`'s
//! `services::estate`) puts the config's half and the product plugin's half
//! in front of [`estate_svg`] and hands the document to monetize's console
//! BFF, which serves it as `image/svg+xml` behind an operator's session.
//! * **gunnar** — the console's Machine card: the BFF asks gunnar's
//! `Admin.Topology` for the boxes gunnar has evidence of (itself, its twins
//! and standbys, the consoles that call it, the monetize it registers with),
//! maps every row onto a [`Machine`] AS IT CAME — gunnar decided each state,
//! the console decides none — and draws it here with [`Estate::disk_only`]
//! on, because a topology carries no plan and no growth verdict.
//!
//! Nothing re-draws the board on the far side of either wire: a second renderer
//! would be free to disagree about which number was read and which was merely
//! claimed, which is what this crate exists to prevent.
//!
//! # Read left to right (owner, 2026-10-05)
//!
//! The top row is always front | main | twin — the front on the left, the
//! server it talks to in the middle, the twin on the right — whatever order the
//! rows arrive in; the links run front → main → twin. Every other box (a
//! console, monetize, a standby, a second twin) is on a row below, hanging from
//! the main box, and never sits between those three.
//!
//! # The style is skade's, the colours are the page's
//!
//! A dark PCB — board gradient, chips on rails, cyan slots, mono for anything
//! an operator might paste into a command. The palette and the geometry are
//! taken from `skade/.nornir/assets/skade_flow.svg` so the suite's diagrams look
//! like one suite. Nothing is loaded from anywhere: no fonts, no images, no
//! script.
//!
//! Every colour is a page token with skade's colour behind it
//! (`var(--estate-ink, #eaf6ff)`, the names in [`TOKENS`]): a console that puts
//! the board INLINE maps the tokens onto its own theme and the board follows
//! that theme live (owner, 2026-10-05: "make it follow the chosen theme of the
//! web site"); anywhere nothing maps them — an `<img>`, a README, a PDF — the
//! output is skade's dark board.
use std::fmt::Write as _;
/// What a machine is for. The role decides where it sits on the board and what
/// is said about it, never what it is allowed to do.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Role {
/// The public face: Caddy, the console, the money services.
Front,
/// The appliance itself — gunnar, PID 1, no shell. gunnar's own word for
/// it is `primary`.
Appliance,
/// The follower. No customers, and therefore the one box allowed a restart.
Twin,
/// A replication standby gunnar pushes to (gunnar's `standby`).
Standby,
/// A console gunnar saw call it from a box no pairing names.
Console,
/// The monetize gunnar registers its plugin with, on a box of its own.
Monetize,
/// **Where the main box's bytes come FROM** (holger, 2026-10-23): a
/// registry it proxies (crates.io, PyPI, npm), or an upstream holger it
/// mirrors. Drawn on a row of its own ABOVE front | main | twin, each with
/// a link down into the main box carrying the upstream's state.
Upstream,
}
impl Role {
/// The published 0.1.0 name of [`Role::from_word`], kept for its callers until their next release.
pub fn from_gunnar(word: &str) -> Option<Role> {
Self::from_word(word)
}
/// The role's own word, drawn when a machine carries no title.
pub fn title(self) -> &'static str {
match self {
Role::Front => "front",
Role::Appliance => "appliance",
Role::Twin => "twin",
Role::Standby => "standby",
Role::Console => "console",
Role::Monetize => "monetize",
Role::Upstream => "upstream",
}
}
/// **gunnar's role word** (`Admin.Topology`'s `TopologyBox.role`), read as
/// it came. A word this build does not know is drawn as a console-sized
/// box with its own word as the title by the caller — never refused here:
/// this is a renderer, and a picture that panics is worse than a box in the
/// wrong column.
pub fn from_word(word: &str) -> Option<Role> {
match word {
"primary" => Some(Role::Appliance),
"twin" => Some(Role::Twin),
"front" => Some(Role::Front),
"standby" => Some(Role::Standby),
"console" => Some(Role::Console),
"monetize" => Some(Role::Monetize),
"upstream" => Some(Role::Upstream),
_ => None,
}
}
}
/// Whether the box answered, the last time anybody asked.
///
/// `Unknown` is not a synonym for `Down`. A box nobody has asked about and a box
/// that was asked and said nothing are different facts, and an operator acts
/// differently on each.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Liveness {
/// It answered.
Up,
/// It answers, and does not keep up — a twin whose lag grows. Lit, with an
/// amber lamp: it is THERE, which is a different drawing from dark.
Behind,
/// It was asked and did not answer.
Silent,
/// Nobody asked.
Unknown,
/// **It does not exist yet**: a machine on a QUOTE — the private-gunnar
/// buy page draws what the buyer would get. Lit (it is the offer, not a
/// dead box), with an open lamp and the word `planned`.
Planned,
}
impl Liveness {
/// The published 0.1.0 name of [`Liveness::from_word`], kept for its callers until their next release.
pub fn from_gunnar(word: &str) -> Liveness {
Self::from_word(word)
}
/// **gunnar's state word** (`TopologyBox.state`: `up` · `behind` ·
/// `silent` · `unknown`), read as it came. A word this build does not know
/// is `Unknown` — never the safest-looking of the words it does know.
pub fn from_word(word: &str) -> Liveness {
match word {
"up" => Liveness::Up,
"behind" => Liveness::Behind,
"silent" => Liveness::Silent,
_ => Liveness::Unknown,
}
}
/// The word a live board names the state by (`data-state`).
pub fn word(self) -> &'static str {
match self {
Liveness::Up => "up",
Liveness::Behind => "behind",
Liveness::Silent => "silent",
Liveness::Unknown => "unknown",
Liveness::Planned => "planned",
}
}
/// Present and not answering, or never asked: drawn dark.
fn dark(self) -> bool {
matches!(self, Liveness::Silent | Liveness::Unknown)
}
}
/// A measured pair of bytes. There is no constructor from a declaration: if a
/// number was declared rather than read, it does not belong in here.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Disk {
pub used_bytes: u64,
pub total_bytes: u64,
/// Whether THIS box can gain capacity while it runs. The twin can (stop →
/// resize → start, it has no customers); the appliance grows in place and
/// only where the boot stack carries the engine.
pub growable: bool,
}
/// One machine on the board.
#[derive(Clone, Debug)]
pub struct Machine {
/// The short name an operator uses, and the id everything else references.
pub id: String,
pub role: Role,
/// The estate's own one-line description of this box, when it has one.
/// Drawn instead of the bare role word, because "the forge, no shell" tells
/// a reader more than "appliance" does.
pub title: String,
/// The `[clouds.<name>]` this box belongs to.
pub cloud: String,
/// The provider's own id, for the operator who is about to open a support
/// ticket. Drawn in mono because it gets pasted.
pub server_id: String,
/// The provider's plan name, if the config names one.
pub plan: Option<String>,
/// **Declared by the provider, not measured.** Drawn with the word `plan`
/// beside it so nobody reads it as a reading.
pub declared_vcpu: Option<u32>,
/// The same, for memory.
pub declared_ram_mb: Option<u64>,
/// **MEASURED cpu, in millicores**, when the product read it off the box.
/// gunnar's `Admin.Capacity` reports `cpu_millicores` as one of its
/// ceilings; a plugin that cannot reach its box reports nothing, and a
/// meter ABSENT there was not measured — never 0. When this is present the
/// drawing shows it INSTEAD of the plan, because a reading beats a claim.
pub measured_cpu_millicores: Option<u64>,
/// The same, for memory: `ram_bytes`.
pub measured_ram_bytes: Option<u64>,
/// Measured, or `None` — never a declaration dressed up as one.
pub disk: Option<Disk>,
pub liveness: Liveness,
/// One short sentence when there is something to say: why it is silent, why
/// it cannot grow. Rendered verbatim.
pub note: String,
/// **The state in a few words, from whoever judged it** — gunnar's
/// `silent for 4 min`, `behind 120 entries`, `following`. Drawn on a
/// [`Estate::live`] board under the title, in the lamp's colour; empty draws
/// nothing. Never composed here.
pub headline: String,
}
/// A cloud, as the board's footer lists it.
#[derive(Clone, Debug)]
pub struct CloudRow {
pub name: String,
/// False for the box's own iron.
pub spends: bool,
/// True for the one a purchase actually goes to.
pub is_default: bool,
}
/// Everything the picture is drawn from.
#[derive(Clone, Debug, Default)]
pub struct Estate {
/// The product this estate serves, for the title (`<product> estate`).
pub product: String,
pub machines: Vec<Machine>,
/// The clouds along the bottom. Empty = no footer at all: a board whose
/// caller knows no cloud does not draw an empty "iron comes from" row.
pub clouds: Vec<CloudRow>,
/// How many tenants the product holds, when the caller knows.
pub tenants: Option<u64>,
/// **The line under the title, when the caller has its own.** `None` is
/// monetize's line — a buyer names a meter and an amount, never a machine
/// — which is monetize's rule and not every console's to state (gunnar's
/// open console says nothing about buying at all).
pub subtitle: Option<String>,
/// **Draw only what a topology knows**: the name, the role, the lamp, the
/// address, the measured disk and the sentence — no cpu/memory row and no
/// growth line. gunnar's `Admin.Topology` carries neither a plan nor a
/// growth verdict per box, and a row of dashes captioned "no plan" on every
/// chip would be a claim about a thing nobody was asked.
pub disk_only: bool,
/// **A board that is the estate NOW** (owner, 2026-10-04: "the picture
/// needs to be a bit dynamic, if twin dies etc.") — drawn to be swapped in
/// place on an open page: each chip a group naming its box and state word
/// (`data-box`, `data-state`); a silent chip's edge red, a behind one's
/// amber; [`Machine::headline`] under the title in the lamp's colour; and
/// the link INTO each box (the rail on its left, or the drop above a box
/// on the row below) carrying that box's state — a pulse for `up`, a
/// slower amber pulse for `behind`, a still red line for `silent`, a dark
/// dashed one for `unknown`. Every one of those comes
/// from the box's [`Liveness`]; nothing here judges a box. Off, the board
/// is the one monetize has always drawn: no groups, no pulses, no
/// headlines.
pub live: bool,
}
// ── the palettes: the page's tokens, a theme's colours behind them ──────────
//
// Owner, 2026-10-05: "make it follow the chosen theme of the web site". Every
// colour on the board is a CSS custom property of the page that shows it,
// `--estate-<name>`, with the THEME's colour as the fallback:
// `fill="var(--estate-ink, #eaf6ff)"`. Drawn INLINE (gunnar's Machine card),
// the page's stylesheet maps each name onto its own theme tokens and the board
// repaints with the page when its theme switches — no fetch, no second
// drawing. Drawn as an `<img>` (monetize's console), in a README or a PDF,
// nothing reaches it and it is the theme's own board: skade's dark one, or
// the white buy page's black and white ([`Theme::MONOCHROME`]).
/// How a machine's state is told apart besides its word.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Lamps {
/// By HUE: green, amber, red, grey dots (skade's board).
Hue,
/// By LINE AND FILL, never by hue: up is a solid dot, behind a hollow
/// ring, silent a dashed ring (and the chip's own outline dashed), not
/// asked a dotted grey ring. For a page printed or read in black and white.
Shape,
}
/// **The colours a board is drawn in** — each one the FALLBACK of its page
/// token (`var(--estate-<name>, <colour>)`, the names in [`Theme::tokens`]).
/// [`Theme::SKADE`] is the console's dark board; [`Theme::MONOCHROME`] is
/// black and white for the white buy page (owner, 2026-10-04 18:15).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Theme {
pub ink: &'static str,
pub ink_dim: &'static str,
pub ink_body: &'static str,
pub ink_faint: &'static str,
pub slot_ink: &'static str,
/// The accent: slot outlines, pins, the default cloud, the disk bar.
pub accent: &'static str,
pub rail_edge: &'static str,
pub chip_edge: &'static str,
pub rule: &'static str,
pub via: &'static str,
/// A dark box keeps its outline and loses its light — it is present and
/// not answering, which is a different drawing from absent.
pub dark_edge: &'static str,
pub dark_ink: &'static str,
pub pin: &'static str,
pub bar_track: &'static str,
/// A disk 80 % full.
pub bar_high: &'static str,
/// Gradient stops `(top, bottom)`: board, chip, the appliance's chip
/// (`rail`), an inset slot, and an unlit chip.
pub board: (&'static str, &'static str),
pub chip: (&'static str, &'static str),
pub rail: (&'static str, &'static str),
pub slot: (&'static str, &'static str),
pub dark: (&'static str, &'static str),
/// The four state colours, one per [`Liveness`] word (`up`, `behind`,
/// `silent`, `unknown`). A page maps them onto its own pass / warn / bad;
/// each state also keeps its line and fill (a pulse, a slower pulse, a
/// still line, a dashed one; an unlit chip), so no theme has to carry the
/// meaning by hue alone.
pub up: &'static str,
pub behind: &'static str,
pub silent: &'static str,
pub unknown: &'static str,
pub lamps: Lamps,
}
impl Theme {
/// skade's dark board — what monetize's `EstateAdmin.Draw` and gunnar-ui's
/// Machine card draw, and every token's fallback in [`TOKENS`]. Pinned
/// byte for byte by `tests/golden.rs`.
pub const SKADE: Theme = Theme {
ink: "#eaf6ff",
ink_dim: "#9fc9e0",
ink_body: "#86b3cc",
ink_faint: "#7fa8c4",
slot_ink: "#bfe6ff",
accent: "#6fd3f5",
rail_edge: "#7fc8ea",
chip_edge: "#5aa6cf",
rule: "#143a52",
via: "#16415c",
dark_edge: "#3d5f74",
dark_ink: "#6a8ba0",
pin: "#bfecff",
bar_track: "#0a2032",
bar_high: "#e0a34f",
board: ("#0b1c2e", "#06111d"),
chip: ("#14384f", "#0e2a3e"),
rail: ("#173d57", "#0c2438"),
slot: ("#123448", "#0a2032"),
dark: ("#0d1f2e", "#081521"),
up: "#5fd39a",
behind: "#e0a34f",
silent: "#e0574f",
unknown: "#7fa8c4",
lamps: Lamps::Hue,
};
/// **Black and white on white**: black strokes and text, grey for the
/// secondary lines; state by line style, fill and word, never by hue.
pub const MONOCHROME: Theme = Theme {
ink: "#000000",
ink_dim: "#555555",
ink_body: "#555555",
ink_faint: "#777777",
slot_ink: "#000000",
accent: "#000000",
rail_edge: "#000000",
chip_edge: "#000000",
rule: "#cccccc",
via: "#aaaaaa",
dark_edge: "#000000",
dark_ink: "#555555",
pin: "#000000",
bar_track: "#e6e6e6",
bar_high: "#000000",
board: ("#ffffff", "#ffffff"),
chip: ("#ffffff", "#ffffff"),
rail: ("#ffffff", "#ffffff"),
slot: ("#ffffff", "#ffffff"),
dark: ("#ffffff", "#ffffff"),
up: "#000000",
behind: "#000000",
silent: "#000000",
unknown: "#777777",
lamps: Lamps::Shape,
};
/// **The page tokens this theme fills**: every custom property the drawing
/// reads, `(name, this theme's colour)`, in one fixed order.
pub const fn tokens(&self) -> [(&'static str, &'static str); 29] {
[
("--estate-board-top", self.board.0),
("--estate-board-bottom", self.board.1),
("--estate-chip-top", self.chip.0),
("--estate-chip-bottom", self.chip.1),
("--estate-rail-top", self.rail.0),
("--estate-rail-bottom", self.rail.1),
("--estate-slot-top", self.slot.0),
("--estate-slot-bottom", self.slot.1),
("--estate-dark-top", self.dark.0),
("--estate-dark-bottom", self.dark.1),
("--estate-ink", self.ink),
("--estate-ink-dim", self.ink_dim),
("--estate-ink-body", self.ink_body),
("--estate-ink-faint", self.ink_faint),
("--estate-slot-ink", self.slot_ink),
("--estate-accent", self.accent),
("--estate-rail-edge", self.rail_edge),
("--estate-chip-edge", self.chip_edge),
("--estate-rule", self.rule),
("--estate-via", self.via),
("--estate-dark-edge", self.dark_edge),
("--estate-dark-ink", self.dark_ink),
("--estate-pin", self.pin),
("--estate-bar-track", self.bar_track),
("--estate-bar-high", self.bar_high),
("--estate-up", self.up),
("--estate-behind", self.behind),
("--estate-silent", self.silent),
("--estate-unknown", self.unknown),
]
}
}
const SKADE_TOKENS: [(&str, &str); 29] = Theme::SKADE.tokens();
/// **The page tokens the board is drawn with**: every custom property the
/// drawing reads, `(name, skade's fallback)`. A console that shows the board
/// inline maps each onto its own theme (gunnar-ui: `.estate-picture` in
/// `styles/console.css`, which a test holds to this list); a name it does not
/// map falls back to the theme's colour.
pub const TOKENS: &[(&str, &str)] = &SKADE_TOKENS;
/// A theme as the drawing writes it: every colour its page token with the
/// theme's colour behind it. The names are the drawing's own.
#[allow(non_snake_case)]
struct Pal {
BOARD_TOP: String,
BOARD_BOTTOM: String,
CHIP_TOP: String,
CHIP_BOTTOM: String,
RAIL_TOP: String,
RAIL_BOTTOM: String,
SLOT_TOP: String,
SLOT_BOTTOM: String,
DARK_TOP: String,
DARK_BOTTOM: String,
INK: String,
INK_DIM: String,
INK_BODY: String,
INK_FAINT: String,
SLOT_INK: String,
CYAN: String,
RAIL_EDGE: String,
CHIP_EDGE: String,
RULE: String,
VIA: String,
DARK_EDGE: String,
DARK_INK: String,
PIN: String,
BAR_TRACK: String,
BAR_HIGH: String,
UP: String,
AMBER: String,
RED: String,
NOT_ASKED: String,
lamps: Lamps,
}
impl Pal {
#[allow(non_snake_case)]
fn of(t: &Theme) -> Pal {
let [BOARD_TOP, BOARD_BOTTOM, CHIP_TOP, CHIP_BOTTOM, RAIL_TOP, RAIL_BOTTOM, SLOT_TOP, SLOT_BOTTOM, DARK_TOP, DARK_BOTTOM, INK, INK_DIM, INK_BODY, INK_FAINT, SLOT_INK, CYAN, RAIL_EDGE, CHIP_EDGE, RULE, VIA, DARK_EDGE, DARK_INK, PIN, BAR_TRACK, BAR_HIGH, UP, AMBER, RED, NOT_ASKED] =
t.tokens()
.map(|(name, colour)| format!("var({name}, {colour})"));
Pal {
BOARD_TOP,
BOARD_BOTTOM,
CHIP_TOP,
CHIP_BOTTOM,
RAIL_TOP,
RAIL_BOTTOM,
SLOT_TOP,
SLOT_BOTTOM,
DARK_TOP,
DARK_BOTTOM,
INK,
INK_DIM,
INK_BODY,
INK_FAINT,
SLOT_INK,
CYAN,
RAIL_EDGE,
CHIP_EDGE,
RULE,
VIA,
DARK_EDGE,
DARK_INK,
PIN,
BAR_TRACK,
BAR_HIGH,
UP,
AMBER,
RED,
NOT_ASKED,
lamps: t.lamps,
}
}
}
const MONO: &str = "'DejaVu Sans Mono', monospace";
/// The board's width for front | main | twin. A wider row widens the board
/// rather than running off its edge.
const W: u32 = 1180;
/// The board's height for one row of chips; a second row adds a chip and a
/// [`ROW_GAP`].
const H: u32 = 560;
/// Chip geometry. The top row is front | main | twin, in reading order; every
/// other box is on the row below ([`layout`]).
const CHIP_W: u32 = 300;
const CHIP_H: u32 = 268;
const CHIP_Y: u32 = 132;
const GAP: u32 = 40;
/// Between the top row and the row below: the bus the boxes below hang from.
const ROW_GAP: u32 = 72;
/// **Reading order** (owner, 2026-10-05: "the front must be on the left, going
/// to the server in the middle, and the twin on the right, in reading order"):
/// the top row is the first front, the first appliance and the first twin, in
/// that order, whichever of them the estate has — whatever order the rows came
/// in (gunnar sends THIS box first). Every other box — a console, monetize, a
/// standby, a second twin — is on the row below, by role and then as it came,
/// so none of them ever sits between front, main and twin. An estate with none
/// of the three draws its boxes on the top row, as they came.
struct Layout {
/// Indices into [`Estate::machines`], left to right: the upstreams' row
/// (empty on a board that names none), front | main | twin, the row below.
above: Vec<usize>,
top: Vec<usize>,
below: Vec<usize>,
}
fn layout(machines: &[Machine]) -> Layout {
let first = |role: Role| machines.iter().position(|m| m.role == role);
let above: Vec<usize> = (0..machines.len())
.filter(|&i| machines[i].role == Role::Upstream)
.collect();
let top: Vec<usize> = [Role::Front, Role::Appliance, Role::Twin]
.into_iter()
.filter_map(first)
.collect();
let mut below: Vec<usize> = (0..machines.len())
.filter(|i| !top.contains(i) && !above.contains(i))
.collect();
if top.is_empty() {
return Layout {
above,
top: below,
below: Vec::new(),
};
}
// Stable: boxes of one role keep the order they came in.
below.sort_by_key(|&i| rank(machines[i].role));
Layout { above, top, below }
}
/// The order of the row below — the box that keeps a copy before the boxes
/// that call.
fn rank(role: Role) -> u8 {
match role {
Role::Front => 0,
Role::Appliance => 1,
Role::Twin => 2,
Role::Standby => 3,
Role::Monetize => 4,
Role::Console => 5,
Role::Upstream => 6,
}
}
/// The width a row of `n` chips takes (at least one, so an empty board is a
/// board).
fn span(n: usize) -> u32 {
let n = n.max(1) as u32;
n * CHIP_W + (n - 1) * GAP
}
/// **Draw the estate.** Deterministic: the same [`Estate`] always yields the
/// same bytes, so the output can be committed and a diff means something
/// changed on the estate rather than in a layout engine.
pub fn estate_svg(estate: &Estate) -> String {
estate_svg_themed(estate, &Theme::SKADE)
}
/// **Draw the estate in `theme`**: the same layout, the same words; only the
/// colours — every one a page token with the theme's colour behind it — and,
/// for [`Lamps::Shape`], the lamps' and a dark chip's line differ.
#[allow(non_snake_case)]
pub fn estate_svg_themed(estate: &Estate, theme: &Theme) -> String {
let p = &Pal::of(theme);
let Pal {
BOARD_TOP,
BOARD_BOTTOM,
CHIP_TOP,
CHIP_BOTTOM,
RAIL_TOP,
RAIL_BOTTOM,
SLOT_TOP,
SLOT_BOTTOM,
DARK_TOP,
DARK_BOTTOM,
INK,
INK_DIM,
INK_BODY,
SLOT_INK,
CYAN,
RULE,
VIA,
DARK_EDGE,
PIN,
..
} = p;
// Laid out first, so the board is as wide and as deep as its rows need.
let lay = layout(&estate.machines);
let row = |r: &[usize]| if r.is_empty() { 0 } else { span(r.len()) + 80 };
let w = W
.max(span(lay.top.len()) + 80)
.max(row(&lay.below))
.max(row(&lay.above));
// The upstreams' row, when there is one, pushes every other row down by one.
let lift = if lay.above.is_empty() {
0
} else {
CHIP_H + ROW_GAP
};
let top_y = CHIP_Y + lift;
let h = if lay.below.is_empty() {
H
} else {
H + CHIP_H + ROW_GAP
} + lift;
let mut s = String::with_capacity(8 * 1024);
let _ = write!(
s,
r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" font-family="'DejaVu Sans', 'Segoe UI', sans-serif">
<defs>
<linearGradient id="board" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="{BOARD_TOP}"/><stop offset="1" stop-color="{BOARD_BOTTOM}"/>
</linearGradient>
<linearGradient id="chip" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="{CHIP_TOP}"/><stop offset="1" stop-color="{CHIP_BOTTOM}"/>
</linearGradient>
<linearGradient id="rail" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="{RAIL_TOP}"/><stop offset="1" stop-color="{RAIL_BOTTOM}"/>
</linearGradient>
<linearGradient id="slot" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="{SLOT_TOP}"/><stop offset="1" stop-color="{SLOT_BOTTOM}"/>
</linearGradient>
<linearGradient id="dark" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="{DARK_TOP}"/><stop offset="1" stop-color="{DARK_BOTTOM}"/>
</linearGradient>
</defs>
<rect x="0" y="0" width="{w}" height="{h}" fill="url(#board)"/>
<g stroke="{RULE}" stroke-width="1">
<line x1="0" y1="76" x2="{w}" y2="76"/>
<line x1="0" y1="{rule_low}" x2="{w}" y2="{rule_low}"/>
</g>
<g fill="{VIA}">
<circle cx="40" cy="40" r="2"/><circle cx="{via_r}" cy="40" r="2"/>
<circle cx="40" cy="{via_b}" r="2"/><circle cx="{via_r}" cy="{via_b}" r="2"/>
</g>
"##,
rule_low = h - 76,
via_r = w - 40,
via_b = h - 28,
);
// ── the title band ──────────────────────────────────────────────────────
let title = if estate.product.is_empty() {
"estate".to_owned()
} else {
format!("{} estate", estate.product)
};
let _ = writeln!(
s,
r##" <text x="40" y="46" fill="{INK}" font-size="17" font-weight="700">{}</text>"##,
esc(&title)
);
let subtitle = match (&estate.subtitle, estate.tenants) {
(Some(own), _) => own.clone(),
(None, tenants) => match tenants {
Some(n) => format!(
"{n} tenant{} · a buyer names a meter and an amount, never a machine",
if n == 1 { "" } else { "s" }
),
None => "a buyer names a meter and an amount, never a machine".to_owned(),
},
};
let _ = writeln!(
s,
r##" <text x="40" y="64" fill="{INK_BODY}" font-size="10.5">{}</text>"##,
esc(&subtitle)
);
// ── the machines: front | main | twin, then the row below ──────────────
//
// Each row centred, so a two-machine estate is not glued to the left edge
// of a board sized for three.
let x0 = |n: usize| (w - span(n)) / 2;
let one = |s: &mut String, x: u32, y: u32, m: &Machine| {
if estate.live {
let _ = writeln!(
s,
r#" <g data-box="{}" data-state="{}">"#,
esc(&m.id),
m.liveness.word()
);
}
chip(s, x, y, m, estate.disk_only, estate.live, p);
if estate.live {
s.push_str(" </g>\n");
}
};
let top_x = |i: usize| x0(lay.top.len()) + i as u32 * (CHIP_W + GAP);
let hub = lay
.top
.iter()
.position(|&i| estate.machines[i].role == Role::Appliance)
.unwrap_or(0);
let hub_x = top_x(hub) + CHIP_W / 2;
// The upstreams' row: each upstream drops onto a bus, and the bus runs down
// into the main box — on a live board each drop carries that upstream's
// state, so a dead registry is a red line into the box it fed.
if !lay.above.is_empty() {
let bus_y = CHIP_Y + CHIP_H + ROW_GAP / 2;
let above_x = |j: usize| x0(lay.above.len()) + j as u32 * (CHIP_W + GAP);
let (first, last) = (
above_x(0) + CHIP_W / 2,
above_x(lay.above.len() - 1) + CHIP_W / 2,
);
let _ = write!(
s,
r##" <g stroke="{RULE}" stroke-width="1.5">
<line x1="{}" y1="{bus_y}" x2="{}" y2="{bus_y}"/>
<line x1="{hub_x}" y1="{bus_y}" x2="{hub_x}" y2="{top_y}"/>
</g>
"##,
first.min(hub_x),
last.max(hub_x),
);
for (j, &at) in lay.above.iter().enumerate() {
let x = above_x(j);
let m = &estate.machines[at];
one(&mut s, x, CHIP_Y, m);
drop_link(
&mut s,
x + CHIP_W / 2,
CHIP_Y + CHIP_H,
bus_y,
m,
estate.live,
p,
);
}
}
for (i, &at) in lay.top.iter().enumerate() {
let x = top_x(i);
one(&mut s, x, top_y, &estate.machines[at]);
let Some(&next) = lay.top.get(i + 1) else {
continue;
};
// On a live board the link INTO the next box carries that box's state:
// front → main, main → twin.
if estate.live {
live_link(&mut s, x, top_y, &estate.machines[next], p);
continue;
}
// The rail between neighbours: a pin out, a pin in, a line across. It
// says "these are on one board", not "bytes flow this way" — the twin's
// log does flow, and the front's gRPC does, but a single line cannot
// mean both and a diagram that implies the wrong one is worse than a
// diagram that implies neither.
let y = top_y + CHIP_H / 2;
let _ = write!(
s,
r##" <rect x="{}" y="{}" width="6" height="8" fill="{CYAN}"/>
<line x1="{}" y1="{y}" x2="{}" y2="{y}" stroke="{RULE}" stroke-width="1.5"/>
<circle cx="{}" cy="{y}" r="4" fill="{PIN}"/>
"##,
x + CHIP_W,
y - 4,
x + CHIP_W + 6,
x + CHIP_W + GAP,
x + CHIP_W + GAP,
);
}
// The row below hangs from the main box (else the first on top): a line
// down from it to a bus, and from the bus a drop into each box — on a live
// board the drop carries that box's state, as the links on top do.
if !lay.below.is_empty() {
let below_y = top_y + CHIP_H + ROW_GAP;
let bus_y = top_y + CHIP_H + ROW_GAP / 2;
let below_x = |j: usize| x0(lay.below.len()) + j as u32 * (CHIP_W + GAP);
let (first, last) = (
below_x(0) + CHIP_W / 2,
below_x(lay.below.len() - 1) + CHIP_W / 2,
);
let _ = write!(
s,
r##" <g stroke="{RULE}" stroke-width="1.5">
<line x1="{hub_x}" y1="{}" x2="{hub_x}" y2="{bus_y}"/>
<line x1="{}" y1="{bus_y}" x2="{}" y2="{bus_y}"/>
</g>
"##,
top_y + CHIP_H,
first.min(hub_x),
last.max(hub_x),
);
for (j, &at) in lay.below.iter().enumerate() {
let x = below_x(j);
let m = &estate.machines[at];
one(&mut s, x, below_y, m);
drop_link(&mut s, x + CHIP_W / 2, bus_y, below_y, m, estate.live, p);
}
}
// ── the clouds, along the bottom ────────────────────────────────────────
//
// Only when the caller knows a cloud: an empty "iron comes from" row says
// there is none, which nobody measured either.
if estate.clouds.is_empty() {
s.push_str("</svg>\n");
return s;
}
let y = h - 48;
let _ = writeln!(
s,
r##" <text x="40" y="{y}" fill="{INK_DIM}" font-size="10.5" font-weight="700">iron comes from</text>"##
);
let mut cx = 176;
for c in &estate.clouds {
let label = if c.is_default {
format!("{} · purchases go here", c.name)
} else if !c.spends {
format!("{} · this box · cannot spend", c.name)
} else {
c.name.clone()
};
let w = 14 + (label.chars().count() as u32) * 6;
let _ = write!(
s,
r##" <rect x="{cx}" y="{}" width="{w}" height="22" rx="5" fill="url(#slot)" stroke="{}" stroke-width="1.4"/>
<text x="{}" y="{}" fill="{SLOT_INK}" font-size="10" text-anchor="middle" font-family="{MONO}">{}</text>
"##,
y - 15,
if c.is_default { CYAN } else { DARK_EDGE },
cx + w / 2,
y,
esc(&label)
);
cx += w + 12;
}
s.push_str("</svg>\n");
s
}
/// **The link into `next`, on a live board**: the rail between it and the chip
/// on its left, carrying `next`'s state — a pulse for `up`, a slower amber
/// pulse for `behind`, a still red line for `silent`, a dark dashed one for
/// `unknown`.
fn live_link(s: &mut String, x: u32, top: u32, next: &Machine, p: &Pal) {
let y = top + CHIP_H / 2;
let (x1, x2) = (x + CHIP_W + 6, x + CHIP_W + GAP);
let (stroke, extra, pulse) = link_style(next, p);
let _ = write!(
s,
r##" <g data-link="{}" data-state="{}">
<rect x="{}" y="{}" width="6" height="8" fill="{stroke}"/>
<line x1="{x1}" y1="{y}" x2="{x2}" y2="{y}" stroke="{stroke}" stroke-width="2.5"{extra}>"##,
esc(&next.id),
next.liveness.word(),
x + CHIP_W,
y - 4,
);
if let Some(dur) = pulse {
let _ = write!(
s,
r#"<animate attributeName="stroke-opacity" values="1;0.25;1" dur="{dur}" repeatCount="indefinite"/>"#
);
}
let _ = write!(
s,
r##"</line>
<circle cx="{x2}" cy="{y}" r="4" fill="{stroke}"/>
</g>
"##
);
}
/// How the link into `m` is drawn on a live board: its colour, its line, and
/// its pulse — from `m`'s [`Liveness`], never judged here.
fn link_style<'p>(m: &Machine, p: &'p Pal) -> (&'p str, &'static str, Option<&'static str>) {
match m.liveness {
Liveness::Up => (&p.UP, "", Some("1.6s")),
Liveness::Behind => (&p.AMBER, "", Some("3.2s")),
Liveness::Silent => (&p.RED, "", None),
// A planned box is not on a live board; drawn as unknown if it is.
Liveness::Unknown | Liveness::Planned => (&p.DARK_EDGE, r#" stroke-dasharray="4 4""#, None),
}
}
/// **The drop into a box on the row below**, from the bus at `bus_y` to the
/// chip's top at `top`, at `cx`. On a live board it carries the box's state as
/// the links on the top row do; off, it is a rail like theirs.
#[allow(non_snake_case)]
fn drop_link(s: &mut String, cx: u32, bus_y: u32, top: u32, m: &Machine, live: bool, p: &Pal) {
let Pal {
CYAN, RULE, PIN, ..
} = p;
let y1 = bus_y + 6;
if !live {
let _ = write!(
s,
r##" <rect x="{}" y="{bus_y}" width="8" height="6" fill="{CYAN}"/>
<line x1="{cx}" y1="{y1}" x2="{cx}" y2="{top}" stroke="{RULE}" stroke-width="1.5"/>
<circle cx="{cx}" cy="{top}" r="4" fill="{PIN}"/>
"##,
cx - 4,
);
return;
}
let (stroke, extra, pulse) = link_style(m, p);
let _ = write!(
s,
r##" <g data-link="{}" data-state="{}">
<rect x="{}" y="{bus_y}" width="8" height="6" fill="{stroke}"/>
<line x1="{cx}" y1="{y1}" x2="{cx}" y2="{top}" stroke="{stroke}" stroke-width="2.5"{extra}>"##,
esc(&m.id),
m.liveness.word(),
cx - 4,
);
if let Some(dur) = pulse {
let _ = write!(
s,
r#"<animate attributeName="stroke-opacity" values="1;0.25;1" dur="{dur}" repeatCount="indefinite"/>"#
);
}
let _ = write!(
s,
r##"</line>
<circle cx="{cx}" cy="{top}" r="4" fill="{stroke}"/>
</g>
"##
);
}
/// One machine's chip, its top-left corner at (`x`, `top`).
#[allow(non_snake_case)]
fn chip(s: &mut String, x: u32, top: u32, m: &Machine, disk_only: bool, live: bool, p: &Pal) {
let Pal {
INK,
INK_DIM,
INK_FAINT,
CYAN,
RAIL_EDGE,
CHIP_EDGE,
DARK_EDGE,
DARK_INK,
UP,
AMBER,
RED,
NOT_ASKED,
CHIP_TOP,
BAR_TRACK,
BAR_HIGH,
..
} = p;
let dark = m.liveness.dark();
// In black and white a dark chip says so by its outline: dashed.
let dash = if dark && p.lamps == Lamps::Shape {
r#" stroke-dasharray="6 4""#
} else {
""
};
let (fill, edge, ink, dim) = if dark {
// On a live board a SILENT box is red-edged: present, and not
// answering, which is the thing the picture is opened to see.
(
"url(#dark)",
if live && m.liveness == Liveness::Silent {
RED
} else {
DARK_EDGE
},
DARK_INK,
DARK_INK,
)
} else if live && m.liveness == Liveness::Behind {
("url(#chip)", AMBER, INK, INK_DIM)
} else {
(
if m.role == Role::Appliance {
"url(#rail)"
} else {
"url(#chip)"
},
if m.role == Role::Appliance {
RAIL_EDGE
} else {
CHIP_EDGE
},
INK,
INK_DIM,
)
};
let mid = x + CHIP_W / 2;
let _ = write!(
s,
r##" <rect x="{x}" y="{top}" width="{CHIP_W}" height="{CHIP_H}" rx="12" fill="{fill}" stroke="{edge}" stroke-width="2.2"{dash}/>
<text x="{mid}" y="{}" fill="{ink}" font-size="14.5" font-weight="700" text-anchor="middle">{}</text>
<text x="{mid}" y="{}" fill="{dim}" font-size="10.5" text-anchor="middle">{}</text>
"##,
top + 28,
esc(&m.id),
top + 46,
esc(if m.title.is_empty() {
m.role.title()
} else {
&m.title
}),
);
// The liveness lamp, top-right of the chip. A dot and a word: the colour is
// the glance and the word is the answer, because a colour alone cannot be
// read by everyone and this one decides whether there is a second copy of
// the data.
// `lamp` is the dot's attributes; `colour` the judge's words beside it.
let (lamp, word, colour): (String, &str, &str) = match (p.lamps, m.liveness) {
(Lamps::Hue, Liveness::Up) => (format!(r#"fill="{UP}""#), "up", UP),
(Lamps::Hue, Liveness::Behind) => (format!(r#"fill="{AMBER}""#), "behind", AMBER),
(Lamps::Hue, Liveness::Silent) => (format!(r#"fill="{RED}""#), "silent", RED),
(Lamps::Hue, Liveness::Unknown) => {
(format!(r#"fill="{NOT_ASKED}""#), "not asked", NOT_ASKED)
}
(Lamps::Hue, Liveness::Planned) => (
format!(r#"fill="none" stroke="{NOT_ASKED}" stroke-width="1.5""#),
"planned",
NOT_ASKED,
),
// By line and fill: solid, hollow, dashed, dotted grey — the hollow
// ones filled with the chip's own ground.
(Lamps::Shape, Liveness::Up) => (format!(r#"fill="{UP}""#), "up", UP),
(Lamps::Shape, Liveness::Behind) => (
format!(r#"fill="{CHIP_TOP}" stroke="{AMBER}" stroke-width="1.5""#),
"behind",
AMBER,
),
(Lamps::Shape, Liveness::Silent) => (
format!(
r#"fill="{CHIP_TOP}" stroke="{RED}" stroke-width="1.5" stroke-dasharray="2 1.5""#
),
"silent",
RED,
),
(Lamps::Shape, Liveness::Unknown) => (
format!(
r#"fill="{CHIP_TOP}" stroke="{NOT_ASKED}" stroke-width="1" stroke-dasharray="1 1.5""#
),
"not asked",
NOT_ASKED,
),
(Lamps::Shape, Liveness::Planned) => (
format!(r#"fill="{CHIP_TOP}" stroke="{INK_DIM}" stroke-width="1.5""#),
"planned",
INK_DIM,
),
};
// The judge's own words, under the title, in the lamp's colour.
if live && !m.headline.is_empty() {
let _ = writeln!(
s,
r##" <text x="{}" y="{}" fill="{}" font-size="10.5" font-weight="700" text-anchor="middle">{}</text>"##,
x + CHIP_W / 2,
top + 62,
if m.liveness == Liveness::Unknown {
INK_DIM.as_str()
} else {
colour
},
esc(&m.headline)
);
}
let _ = write!(
s,
r##" <circle cx="{}" cy="{}" r="4.5" {lamp}/>
<text x="{}" y="{}" fill="{dim}" font-size="9.5" text-anchor="end">{word}</text>
"##,
x + 20,
top + 22,
x + CHIP_W - 16,
top + 26,
);
// The provider id, in mono, because it is what gets pasted into a ticket.
let mut y = top + 72;
if !m.server_id.is_empty() {
let label = if m.cloud.is_empty() {
m.server_id.clone()
} else {
format!("{} · {}", m.cloud, m.server_id)
};
slot(s, x, y, &label, dark, p);
y += 38;
}
// The three numbers. Disk is MEASURED and says so by carrying a bar; vCPU
// and memory are the provider's plan and carry the word `plan`, because
// nothing on this estate reads them off the box.
match m.disk {
Some(d) if d.total_bytes > 0 => {
let pct = (d.used_bytes.saturating_mul(100) / d.total_bytes).min(100);
slot(
s,
x,
y,
&format!("disk {} of {}", bytes(d.used_bytes), bytes(d.total_bytes)),
dark,
p,
);
// The fill bar, BELOW the slot and on its width. Drawn across the
// slot's lower border at first, which clipped the rounded corner
// and read as a rendering fault rather than a reading.
let bw = CHIP_W - 44;
let fw = (bw as u64 * pct / 100) as u32;
let _ = write!(
s,
r##" <rect x="{}" y="{}" width="{bw}" height="4" rx="2" fill="{BAR_TRACK}"/>
<rect x="{}" y="{}" width="{fw}" height="4" rx="2" fill="{}"/>
"##,
x + 22,
y + 34,
x + 22,
y + 34,
if dark {
DARK_EDGE
} else if pct >= 80 {
BAR_HIGH
} else {
CYAN
},
);
y += 50;
}
_ => {
slot(s, x, y, "disk — · not measured", dark, p);
y += 50;
}
}
// A topology knows no plan and no growth verdict: the sentence follows the
// disk directly, and nothing is drawn for what nobody was asked.
if disk_only {
note(s, x, y - 6, m, p);
return;
}
// **A reading beats a claim.** Where the product measured the box's own
// ceilings, those are what is drawn and the caption says `measured`; where
// it did not, the provider's plan is drawn and the caption says `plan says`.
// The two must never be mixed on one chip without the caption changing with
// them, or a picture becomes evidence for something nobody read.
let measured = m.measured_cpu_millicores.is_some() || m.measured_ram_bytes.is_some();
let cpu = match (m.measured_cpu_millicores, m.declared_vcpu) {
// Millicores to cores, one decimal: 2000 is 2.0, and 1500 is a box
// somebody capped at one and a half rather than a rounding error.
(Some(mc), _) => format!("{:.1} cores", mc as f64 / 1000.0),
(None, Some(c)) => format!("{c} vCPU"),
(None, None) => "— vCPU".to_owned(),
};
let ram = match (m.measured_ram_bytes, m.declared_ram_mb) {
(Some(b), _) => bytes(b),
(None, Some(mb)) if mb >= 1024 => format!("{} GB", mb / 1024),
(None, Some(mb)) => format!("{mb} MB"),
(None, None) => "—".to_owned(),
};
slot(s, x, y, &format!("{cpu} · {ram}"), dark, p);
let _ = writeln!(
s,
r##" <text x="{}" y="{}" fill="{INK_FAINT}" font-size="9" text-anchor="middle">{}</text>"##,
x + CHIP_W / 2,
y + 44,
if measured {
match &m.plan {
Some(p) => esc(&format!("measured · plan {p}")),
None => "measured".to_owned(),
}
} else if m.plan.is_some() || m.declared_vcpu.is_some() {
match &m.plan {
Some(p) => esc(&format!("plan says · {p}")),
None => "plan says · not measured".to_owned(),
}
} else {
"no plan declared".to_owned()
}
);
y += 62;
// Growth, and then whatever sentence the caller had.
let grow = match m.disk {
Some(d) if d.growable => "can grow",
Some(_) => "cannot grow",
None => "growth unknown",
};
let _ = writeln!(
s,
r##" <text x="{}" y="{y}" fill="{dim}" font-size="9.5" text-anchor="middle">{grow}</text>"##,
x + CHIP_W / 2,
);
note(s, x, y, m, p);
}
/// The machine's own sentence, at most two wrapped lines under `y`.
#[allow(non_snake_case)]
fn note(s: &mut String, x: u32, y: u32, m: &Machine, p: &Pal) {
let Pal { INK_FAINT, .. } = p;
if !m.note.is_empty() {
for (i, line) in wrap(&m.note, 42).into_iter().enumerate().take(2) {
let _ = writeln!(
s,
r##" <text x="{}" y="{}" fill="{INK_FAINT}" font-size="9" text-anchor="middle">{}</text>"##,
x + CHIP_W / 2,
y + 18 + (i as u32) * 12,
esc(&line)
);
}
}
}
/// One inset slot with a mono label — skade's inner-rectangle idiom.
#[allow(non_snake_case)]
fn slot(s: &mut String, x: u32, y: u32, label: &str, dark: bool, p: &Pal) {
let Pal {
SLOT_INK,
CYAN,
DARK_EDGE,
DARK_INK,
..
} = p;
let _ = write!(
s,
r##" <rect x="{}" y="{y}" width="{}" height="28" rx="6" fill="url(#slot)" stroke="{}" stroke-width="1.5"/>
<text x="{}" y="{}" fill="{}" font-size="10" text-anchor="middle" font-family="{MONO}">{}</text>
"##,
x + 22,
CHIP_W - 44,
if dark { DARK_EDGE } else { CYAN },
x + CHIP_W / 2,
y + 18,
if dark { DARK_INK } else { SLOT_INK },
esc(label)
);
}
/// XML-escape. Five entities, because a provider id or an operator's note is
/// somebody else's string and one `&` in it is a document that will not parse.
fn esc(raw: &str) -> String {
let mut out = String::with_capacity(raw.len());
for c in raw.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(c),
}
}
out
}
/// Greedy wrap on spaces. SVG does not reflow text, so a sentence that must fit
/// a chip is broken here or it runs off the board.
fn wrap(text: &str, cols: usize) -> Vec<String> {
let mut lines = Vec::new();
let mut line = String::new();
for word in text.split_whitespace() {
if !line.is_empty() && line.chars().count() + 1 + word.chars().count() > cols {
lines.push(std::mem::take(&mut line));
}
if !line.is_empty() {
line.push(' ');
}
line.push_str(word);
}
if !line.is_empty() {
lines.push(line);
}
lines
}
/// Bytes in the units an operator reads. Binary, like every other byte figure
/// in this system.
fn bytes(n: u64) -> String {
const K: u64 = 1024;
let (v, unit) = match n {
n if n >= K * K * K * K => (n as f64 / (K * K * K * K) as f64, "TiB"),
n if n >= K * K * K => (n as f64 / (K * K * K) as f64, "GiB"),
n if n >= K * K => (n as f64 / (K * K) as f64, "MiB"),
n if n >= K => (n as f64 / K as f64, "KiB"),
n => return format!("{n} B"),
};
format!("{v:.1} {unit}")
}
#[cfg(test)]
mod tests {
use super::*;
/// skade's state colours as the drawing writes them.
const RED: &str = "var(--estate-silent, #e0574f)";
const AMBER: &str = "var(--estate-behind, #e0a34f)";
fn front() -> Machine {
Machine {
id: "gunnar-front".into(),
role: Role::Front,
title: String::new(),
cloud: "upcloud".into(),
server_id: "00f9e1a2".into(),
plan: Some("2xCPU-4GB".into()),
declared_vcpu: Some(2),
declared_ram_mb: Some(4096),
disk: Some(Disk {
used_bytes: 9 << 30,
total_bytes: 50 << 30,
growable: false,
}),
liveness: Liveness::Up,
measured_cpu_millicores: None,
measured_ram_bytes: None,
note: String::new(),
headline: String::new(),
}
}
fn appliance() -> Machine {
Machine {
id: "gunnar-appliance".into(),
role: Role::Appliance,
title: String::new(),
cloud: "upcloud".into(),
server_id: "0057bb10".into(),
plan: Some("2xCPU-4GB".into()),
declared_vcpu: Some(2),
declared_ram_mb: Some(4096),
disk: Some(Disk {
used_bytes: 43_714_560,
total_bytes: 40_588_369_920,
growable: false,
}),
liveness: Liveness::Up,
measured_cpu_millicores: Some(2000),
measured_ram_bytes: Some(4 << 30),
note: "grows in place; this boot stack carries no engine".into(),
headline: String::new(),
}
}
fn twin_dark() -> Machine {
Machine {
id: "gunnar-twin".into(),
role: Role::Twin,
title: String::new(),
cloud: "upcloud".into(),
server_id: "01a2ff03".into(),
plan: None,
declared_vcpu: None,
declared_ram_mb: None,
disk: None,
liveness: Liveness::Silent,
measured_cpu_millicores: None,
measured_ram_bytes: None,
note: "has not polled since this server started".into(),
headline: String::new(),
}
}
fn estate(machines: Vec<Machine>) -> Estate {
Estate {
product: "gunnar".into(),
machines,
clouds: vec![
CloudRow {
name: "upcloud".into(),
spends: true,
is_default: true,
},
CloudRow {
name: "server".into(),
spends: false,
is_default: false,
},
],
tenants: Some(11),
..Estate::default()
}
}
/// ★ **The question this picture is opened to answer.** A twin that has
/// never been seen is DRAWN — dimmed, lamp red, with its own sentence —
/// because leaving it out reads as "this design has no twin", and it has
/// one. It is dark.
#[test]
fn a_dark_twin_is_drawn_and_says_so() {
let svg = estate_svg(&estate(vec![front(), appliance(), twin_dark()]));
assert!(svg.contains("gunnar-twin"), "the twin is not on the board");
assert!(svg.contains("silent"), "the twin's state is not named");
assert!(
svg.contains("has not polled"),
"the twin's reason is not carried"
);
assert!(
svg.contains("url(#dark)"),
"a silent box is drawn as a live one"
);
}
/// A twin that answered is an ordinary chip: no dark fill, and the lamp
/// says so in a word as well as a colour.
#[test]
fn a_live_twin_is_an_ordinary_chip() {
let mut up = twin_dark();
up.liveness = Liveness::Up;
up.disk = Some(Disk {
used_bytes: 26_175_638,
total_bytes: 21_474_836_480,
growable: true,
});
up.note = String::new();
let svg = estate_svg(&estate(vec![front(), appliance(), up]));
assert!(!svg.contains("url(#dark)"), "a live estate is drawn dimmed");
assert!(svg.contains(">up<"), "the lamp does not say up");
assert!(svg.contains("can grow"), "the twin's growth is not stated");
}
/// **A declaration is never drawn as a measurement.** vCPU and memory come
/// off the provider's plan, and the picture says `plan says` beside them;
/// the disk is read off the box and carries a bar instead. Getting this
/// backwards is how a diagram becomes evidence for something nobody
/// measured.
#[test]
fn declared_numbers_are_labelled_and_missing_ones_are_dashes() {
let svg = estate_svg(&estate(vec![front(), appliance(), twin_dark()]));
assert!(svg.contains("plan says"), "a declared plan is not labelled");
assert!(svg.contains("— vCPU"), "an unknown vCPU count was invented");
assert!(
svg.contains("not measured"),
"an unmeasured disk is not admitted"
);
}
/// The buyer's rule, on the picture, because the picture is where somebody
/// would otherwise get the idea that a customer picks a box.
#[test]
fn the_board_states_that_a_buyer_names_no_machine() {
let svg = estate_svg(&estate(vec![front()]));
assert!(
svg.contains("never a machine"),
"the placement rule is not on the board"
);
}
/// Deterministic, so the output can be committed and a diff means the
/// ESTATE changed.
#[test]
fn the_same_estate_draws_the_same_bytes() {
let e = estate(vec![front(), appliance(), twin_dark()]);
assert_eq!(estate_svg(&e), estate_svg(&e));
}
/// Somebody else's string reaches this document — a provider id, an
/// operator's note — and one `&` in it is an SVG that will not parse.
#[test]
fn a_hostile_note_cannot_break_the_document() {
let mut m = front();
m.note = "a & b <script>alert('x')</script>".into();
m.server_id = "id\"with"es".into();
let svg = estate_svg(&estate(vec![m]));
assert!(
!svg.contains("<script>"),
"markup from a note reached the document"
);
assert!(svg.contains("&"), "an ampersand was not escaped");
}
/// ★ **A reading beats a claim, and the caption moves with it.** The
/// appliance's cpu and memory are MEASURED (`Admin.Capacity`'s ceilings),
/// so its chip says `measured` and shows cores; the front was not measured,
/// so its chip keeps the plan and says `plan says`. Mixing the two under
/// one caption is how a picture becomes evidence for a number nobody read.
#[test]
fn a_measured_box_says_measured_and_an_unmeasured_one_says_plan() {
let svg = estate_svg(&estate(vec![front(), appliance(), twin_dark()]));
assert!(
svg.contains("2.0 cores"),
"the measured cpu is not drawn: {svg}"
);
assert!(
svg.contains("measured · plan 2xCPU-4GB"),
"the measured caption is missing"
);
assert!(
svg.contains("plan says · 2xCPU-4GB"),
"the unmeasured box lost its plan caption"
);
}
/// An empty estate is a board with nothing on it, not a panic and not a
/// zero-width viewBox.
#[test]
fn an_empty_estate_still_draws_a_board() {
let svg = estate_svg(&Estate::default());
assert!(svg.starts_with("<svg"), "{svg}");
assert!(svg.ends_with("</svg>\n"));
}
// ── the gunnar door (2026-10-04): a topology drawn, disk-only ──────────
fn gunnar_box(id: &str, role: &str, state: &str, address: &str) -> Machine {
Machine {
id: id.into(),
role: Role::from_word(role).unwrap_or(Role::Console),
title: String::new(),
cloud: String::new(),
server_id: address.into(),
plan: None,
declared_vcpu: None,
declared_ram_mb: None,
measured_cpu_millicores: None,
measured_ram_bytes: None,
disk: None,
liveness: Liveness::from_word(state),
note: String::new(),
headline: String::new(),
}
}
fn topology(machines: Vec<Machine>) -> Estate {
Estate {
product: "gunnar".into(),
machines,
subtitle: Some("the boxes this gunnar has evidence of".into()),
disk_only: true,
..Estate::default()
}
}
/// ★ **A topology chip says only what a topology knows**: no cpu/memory
/// row, no "plan" caption, no growth line — and no cloud footer and no
/// buyer's line, which are monetize's to state and not a gunnar console's.
#[test]
fn a_disk_only_board_draws_no_plan_no_growth_and_no_shop() {
let mut primary = gunnar_box("vm2", "primary", "up", "10.13.8.121");
primary.disk = Some(Disk {
used_bytes: 1 << 30,
total_bytes: 40 << 30,
growable: false,
});
let svg = estate_svg(&topology(vec![
primary,
gunnar_box("gunnar-ui", "console", "up", "127.0.0.1:41000"),
]));
for gone in [
"vCPU",
"plan",
"grow",
"iron comes from",
"buyer",
"never a machine",
] {
assert!(!svg.contains(gone), "`{gone}` on a topology board: {svg}");
}
assert!(
svg.contains("the boxes this gunnar has evidence of"),
"the caller's own subtitle"
);
assert!(
svg.contains("disk 1.0 GiB of 40.0 GiB"),
"the measured disk is drawn"
);
assert!(
svg.contains(">10.13.8.121<"),
"an address with no cloud is drawn bare, not ` · addr`"
);
assert!(
svg.contains(">console<"),
"a console with no title draws its role word"
);
}
/// `behind` is LIT with an amber lamp — the twin is there and answering —
/// and an unknown word is `unknown`, never `up`.
#[test]
fn behind_is_lit_amber_and_an_unknown_word_is_not_up() {
let svg = estate_svg(&topology(vec![gunnar_box("vm3", "twin", "behind", "")]));
assert!(svg.contains(">behind<") && svg.contains("#e0a34f"), "{svg}");
assert!(
!svg.contains("url(#dark)"),
"a twin that answers is not drawn dark"
);
assert_eq!(Liveness::from_word("weird"), Liveness::Unknown);
assert_eq!(Role::from_word("weird"), None);
assert_eq!(Role::from_word("primary"), Some(Role::Appliance));
}
/// front | main | twin keep the 1180 × 560 board; a box beyond them goes
/// to a second row (the board grows down, not off its edge), and a row of
/// four widens it: 4 × 300 + 3 × 40 + 2 × 40 = 1400.
#[test]
fn more_than_three_machines_widen_the_board() {
let spine = estate_svg(&topology(vec![
gunnar_box("f", "front", "up", ""),
gunnar_box("m", "primary", "up", ""),
gunnar_box("t", "twin", "up", ""),
]));
assert!(spine.contains(r#"viewBox="0 0 1180 560""#), "{spine}");
let three = estate_svg(&topology(
(0..3)
.map(|i| gunnar_box(&format!("t{i}"), "twin", "up", ""))
.collect(),
));
assert!(
three.contains(r#"viewBox="0 0 1180 900""#),
"one twin on the spine, two below: {three}"
);
let five = estate_svg(&topology(
(0..5)
.map(|i| gunnar_box(&format!("t{i}"), "twin", "up", ""))
.collect(),
));
assert!(
five.contains(r#"viewBox="0 0 1400 900""#),
"four below: 4 × 300 + 3 × 40 + 2 × 40 = 1400: {five}"
);
}
// ── the live board (2026-10-04): the state, drawn to be swapped in place ──
fn live_estate() -> Estate {
let mut main = gunnar_box("vm2", "primary", "up", "10.13.8.121");
main.headline = "this box".into();
let mut twin = gunnar_box("vm3", "twin", "silent", "10.13.8.122");
twin.headline = "silent for 4 min".into();
let mut front = gunnar_box("front", "front", "up", "10.13.8.120");
front.headline = "calling".into();
let mut e = topology(vec![main, twin, front]);
e.live = true;
e
}
/// ★ **A dead twin is obvious** (owner, 2026-10-04): its chip names its box
/// and its state, its edge is red, gunnar's own words ("silent for 4 min")
/// sit under the title in red, and the link into it is a STILL red line —
/// while the link into a box that answers pulses. The links run in
/// reading order (owner, 2026-10-05): front → main, main → twin, whatever
/// order gunnar sent the rows in.
#[test]
fn a_live_board_draws_a_silent_twin_red_and_a_live_link_pulsing() {
let svg = estate_svg(&live_estate());
let (red, red_stroke) = (format!(r#"fill="{RED}""#), format!(r#"stroke="{RED}""#));
assert!(
svg.contains(r#"data-box="vm3" data-state="silent""#),
"the silent twin is not a named group: {svg}"
);
assert!(svg.contains(r#"data-box="front" data-state="up""#));
let twin = &svg[svg.find(r#"data-box="vm3""#).unwrap()..];
let twin = &twin[..twin.find("</g>").unwrap()];
assert!(
twin.contains(&red_stroke),
"the silent chip's edge is not red: {twin}"
);
assert!(
twin.contains(">silent for 4 min<") && twin.contains(&red),
"gunnar's words are not drawn red: {twin}"
);
let links: Vec<&str> = svg
.match_indices("<g data-link=")
.map(|(i, _)| &svg[i..i + svg[i..].find("</g>").unwrap()])
.collect();
assert_eq!(links.len(), 2, "front → main and main → twin: {svg}");
assert!(
links[0].contains(r#"data-link="vm2" data-state="up""#)
&& links[0].contains("<animate"),
"the link front → main does not pulse into a live main: {}",
links[0]
);
assert!(
links[1].contains(r#"data-link="vm3" data-state="silent""#)
&& links[1].contains(&red_stroke)
&& !links[1].contains("<animate"),
"the link main → twin is not a still red line: {}",
links[1]
);
}
/// `behind` is amber on the edge, the words and the link — and still lit.
#[test]
fn a_live_board_draws_behind_amber() {
let mut e = live_estate();
e.machines[1].liveness = Liveness::Behind;
e.machines[1].headline = "behind 120 entries".into();
let svg = estate_svg(&e);
let twin = &svg[svg
.find(r#"data-box="vm3" data-state="behind""#)
.expect("behind group")..];
let twin = &twin[..twin.find("</g>").unwrap()];
assert!(
twin.contains(&format!(r#"stroke="{AMBER}""#)) && twin.contains(">behind 120 entries<"),
"{twin}"
);
assert!(!twin.contains("url(#dark)"));
}
/// Off, nothing of it: monetize's board carries no groups, no animation,
/// no headline, and a silent chip keeps its dark edge.
#[test]
fn a_board_that_is_not_live_is_unchanged() {
let mut e = live_estate();
e.live = false;
let svg = estate_svg(&e);
for gone in ["data-box", "data-link", "<animate", "silent for 4 min"] {
assert!(!svg.contains(gone), "`{gone}` on a board that is not live");
}
assert!(svg.contains("url(#dark)") && !svg.contains(&format!(r#"stroke="{RED}""#)));
}
// ── reading order (owner, 2026-10-05): front | main | twin, always ──────
/// Where a chip is: the (x, y) its name is drawn at (the 14.5 title).
fn at(svg: &str, id: &str) -> (u32, u32) {
let end = svg
.find(&format!(
r#"font-size="14.5" font-weight="700" text-anchor="middle">{}</text>"#,
esc(id)
))
.unwrap_or_else(|| panic!("no chip named {id}: {svg}"));
let start = svg[..end].rfind("<text ").unwrap();
let num = |attr: &str| -> u32 {
let tag = &svg[start..end];
let v = &tag[tag.find(attr).unwrap() + attr.len()..];
v[..v.find('"').unwrap()].parse().unwrap()
};
(num(r#" x=""#), num(r#" y=""#))
}
/// Every chip's name, row by row, left to right — what a reader reads.
fn reading(svg: &str, ids: &[&str]) -> Vec<Vec<String>> {
let mut placed: Vec<(u32, u32, &str)> = ids
.iter()
.map(|id| {
let (x, y) = at(svg, id);
(y, x, *id)
})
.collect();
placed.sort();
let mut rows: Vec<Vec<String>> = Vec::new();
let mut last = None;
for (y, _, id) in placed {
if last != Some(y) {
rows.push(Vec::new());
last = Some(y);
}
rows.last_mut().unwrap().push(id.to_owned());
}
rows
}
fn shaped(rows: &[(&str, &str)], live: bool) -> String {
let mut e = topology(
rows.iter()
.map(|(id, role)| gunnar_box(id, role, "up", ""))
.collect(),
);
e.live = live;
estate_svg(&e)
}
/// ★ **front on the left, the server in the middle, the twin on the
/// right** (owner, 2026-10-05: "in reading order") — for every estate
/// shape, and whatever order the rows arrive in (gunnar sends THIS box
/// first; monetize sorts by role): solo, front + server, server + twin
/// with no front, and all six orders of the three.
#[test]
fn the_board_reads_front_main_twin_left_to_right_for_every_shape() {
for live in [false, true] {
assert_eq!(
reading(&shaped(&[("main", "primary")], live), &["main"]),
[["main"]],
"solo"
);
for rows in [
[("main", "primary"), ("front", "front")],
[("front", "front"), ("main", "primary")],
] {
let svg = shaped(&rows, live);
assert_eq!(
reading(&svg, &["front", "main"]),
[["front", "main"]],
"front + server from {rows:?}"
);
assert!(at(&svg, "front").0 < at(&svg, "main").0);
}
for rows in [
[("twin", "twin"), ("main", "primary")],
[("main", "primary"), ("twin", "twin")],
] {
let svg = shaped(&rows, live);
assert_eq!(
reading(&svg, &["main", "twin"]),
[["main", "twin"]],
"server + twin, no front, from {rows:?}"
);
}
for rows in [
[("front", "front"), ("main", "primary"), ("twin", "twin")],
[("front", "front"), ("twin", "twin"), ("main", "primary")],
[("main", "primary"), ("front", "front"), ("twin", "twin")],
[("main", "primary"), ("twin", "twin"), ("front", "front")],
[("twin", "twin"), ("front", "front"), ("main", "primary")],
[("twin", "twin"), ("main", "primary"), ("front", "front")],
] {
let svg = shaped(&rows, live);
let (f, m, t) = (at(&svg, "front"), at(&svg, "main"), at(&svg, "twin"));
assert!(
f.0 < m.0 && m.0 < t.0 && f.1 == m.1 && m.1 == t.1,
"front {f:?} main {m:?} twin {t:?} from {rows:?}"
);
}
}
}
/// ★ **Extra boxes never break front | main | twin**: consoles, monetize,
/// standbys and a second twin go on a row BELOW, and the top row is
/// exactly the three, in reading order.
#[test]
fn extra_boxes_go_on_a_row_below_and_never_between() {
let rows = [
("vm2", "primary"),
("gunnar-ui", "console"),
("vm3", "twin"),
("mon", "monetize"),
("front", "front"),
("sb", "standby"),
("vm4", "twin"),
];
let ids: Vec<&str> = rows.iter().map(|r| r.0).collect();
for live in [false, true] {
let svg = shaped(&rows, live);
let read = reading(&svg, &ids);
assert_eq!(read.len(), 2, "two rows: {read:?}");
assert_eq!(
read[0],
["front", "vm2", "vm3"],
"the top row is front | main | twin"
);
let mut below = read[1].clone();
below.sort();
assert_eq!(
below,
["gunnar-ui", "mon", "sb", "vm4"],
"every extra box is on the row below"
);
assert!(
svg.contains(r#"viewBox="0 0 1400 900""#),
"four below widen the board, the second row deepens it: {svg}"
);
}
}
/// The links follow the reading order: on a live board the link INTO main
/// lies between front and main, the link into the twin between main and
/// twin — and every box below still carries its state on a link of its own.
#[test]
fn the_links_run_front_to_main_and_main_to_twin() {
let svg = shaped(
&[
("main", "primary"),
("twin", "twin"),
("front", "front"),
("ui", "console"),
],
true,
);
let line_x = |id: &str| -> (u32, u32) {
let g = &svg[svg
.find(&format!(r#"<g data-link="{id}""#))
.unwrap_or_else(|| panic!("no link into {id}: {svg}"))..];
let g = &g[..g.find("</g>").unwrap()];
let n = |attr: &str| -> u32 {
let v = &g[g.find(attr).unwrap() + attr.len()..];
v[..v.find('"').unwrap()].parse().unwrap()
};
(n(r#" x1=""#), n(r#" x2=""#))
};
let (f, m, t) = (at(&svg, "front").0, at(&svg, "main").0, at(&svg, "twin").0);
let into_main = line_x("main");
let into_twin = line_x("twin");
assert!(
f < into_main.0 && into_main.1 < m,
"front → main lies between them: {into_main:?}, front {f}, main {m}"
);
assert!(
m < into_twin.0 && into_twin.1 < t,
"main → twin lies between them: {into_twin:?}, main {m}, twin {t}"
);
assert!(
!svg.contains(r#"<g data-link="front""#),
"nothing on the board's left links into the front"
);
assert!(
svg.contains(r#"<g data-link="ui" data-state="up""#),
"the console below carries its state on its own link"
);
}
// ── the page's theme (owner, 2026-10-05): one drawing, both themes ──────
/// Every colour the board is drawn in, both kinds of board, every state.
fn rich() -> Vec<String> {
let mut full = front();
full.disk = Some(Disk {
used_bytes: 45 << 30,
total_bytes: 50 << 30,
growable: true,
});
let mut behind = twin_dark();
behind.liveness = Liveness::Behind;
behind.id = "behind".into();
let mut unknown = twin_dark();
unknown.liveness = Liveness::Unknown;
unknown.id = "unknown".into();
unknown.headline = "never asked".into();
let mut console = gunnar_box("ui", "console", "up", "127.0.0.1");
console.headline = "calling".into();
let machines = vec![full, appliance(), twin_dark(), behind, unknown, console];
let mut e = estate(machines);
let flat = estate_svg(&e);
e.live = true;
e.machines[2].headline = "silent for 4 min".into();
vec![flat, estate_svg(&e)]
}
/// The value of every `name="…"` attribute in the document.
fn values<'a>(svg: &'a str, name: &str) -> Vec<&'a str> {
let key = format!(r#" {name}=""#);
svg.match_indices(&key)
.map(|(i, _)| {
let v = &svg[i + key.len()..];
&v[..v.find('"').unwrap()]
})
.collect()
}
/// ★ **No colour is hard-coded** (owner, 2026-10-05: "make it follow the
/// chosen theme of the web site"): every fill, stroke and gradient stop is
/// a page token with skade's colour as its fallback —
/// `var(--estate-<name>, #rrggbb)` — or a gradient reference or `none`.
/// Drawn INLINE, the page's tokens reach it and a theme switch repaints
/// it with no fetch; drawn as an `<img>`, in a README or a PDF, nothing
/// reaches it and it is skade's board.
#[test]
fn every_colour_is_a_page_token_with_a_fallback() {
let token = |v: &str| -> bool {
let Some(inner) = v
.strip_prefix("var(--estate-")
.and_then(|r| r.strip_suffix(')'))
else {
return false;
};
let Some((name, hex)) = inner.split_once(", #") else {
return false;
};
!name.is_empty()
&& name.chars().all(|c| c.is_ascii_lowercase() || c == '-')
&& hex.len() == 6
&& hex.chars().all(|c| c.is_ascii_hexdigit())
};
for svg in rich() {
let mut seen = 0;
for attr in ["fill", "stroke", "stop-color", "color"] {
for v in values(&svg, attr) {
seen += 1;
assert!(
token(v) || v == "none" || (v.starts_with("url(#") && v.ends_with(')')),
"{attr}=\"{v}\" is not a page token with a fallback"
);
}
}
assert!(seen > 50, "the board draws in colour at all: {seen}");
// And no colour hides anywhere else: every `#` is a fallback or a
// gradient reference.
let hashes = svg.matches('#').count();
let fallbacks = svg.matches(", #").count();
let refs = svg.matches("url(#").count();
assert_eq!(
hashes,
fallbacks + refs,
"a colour outside a token's fallback: {svg}"
);
}
}
/// The fallbacks are skade's palette exactly, so a board nobody themes —
/// monetize's `<img>`, a README, a PDF — looks as it always has.
#[test]
fn the_fallbacks_are_skades_board() {
let svg = estate_svg(&live_estate());
for (token, hex) in [
("board-top", "#0b1c2e"),
("board-bottom", "#06111d"),
("chip-top", "#14384f"),
("ink", "#eaf6ff"),
("chip-edge", "#5aa6cf"),
("rail-edge", "#7fc8ea"),
("up", "#5fd39a"),
("silent", "#e0574f"),
("dark-edge", "#3d5f74"),
] {
assert!(
svg.contains(&format!("var(--estate-{token}, {hex})")),
"--estate-{token} does not fall back to skade's {hex}: {svg}"
);
}
let mut e = live_estate();
e.machines[1].liveness = Liveness::Behind;
assert!(
estate_svg(&e).contains("var(--estate-behind, #e0a34f)"),
"behind is skade's amber"
);
}
/// ★ **The state stays legible in every theme**: up, behind and silent are
/// three tokens a page maps to its own pass / warn / bad — and besides the
/// hue each keeps its line and fill: up pulses, behind pulses slower,
/// silent stands still, unknown is dashed; a silent or unasked chip is
/// drawn unlit.
#[test]
fn the_states_are_tokens_and_keep_their_line_and_fill() {
let mut e = live_estate();
e.machines.push(gunnar_box("vm4", "twin", "behind", ""));
e.machines.push(gunnar_box("vm5", "standby", "unknown", ""));
let svg = estate_svg(&e);
let link = |id: &str| -> String {
let g = &svg[svg
.find(&format!(r#"<g data-link="{id}""#))
.unwrap_or_else(|| panic!("no link into {id}: {svg}"))..];
g[..g.find("</g>").unwrap()].to_owned()
};
let chip = |id: &str| -> String {
let g = &svg[svg.find(&format!(r#"<g data-box="{id}""#)).unwrap()..];
g[..g.find("</g>").unwrap()].to_owned()
};
let (up, behind, silent, unknown) = (link("vm2"), link("vm4"), link("vm3"), link("vm5"));
assert!(
up.contains("var(--estate-up, ") && up.contains(r#"dur="1.6s""#),
"{up}"
);
assert!(
behind.contains("var(--estate-behind, ") && behind.contains(r#"dur="3.2s""#),
"{behind}"
);
assert!(
silent.contains("var(--estate-silent, ")
&& !silent.contains("<animate")
&& !silent.contains("dasharray"),
"{silent}"
);
assert!(
unknown.contains("stroke-dasharray") && !unknown.contains("<animate"),
"{unknown}"
);
assert!(
chip("vm3").contains("url(#dark)") && chip("vm5").contains("url(#dark)"),
"silent and unasked chips are unlit"
);
assert!(
!chip("vm4").contains("url(#dark)") && !chip("vm2").contains("url(#dark)"),
"behind and up are lit"
);
}
/// ★ **An upstream is drawn ABOVE the main box** (holger A13, 2026-10-23):
/// where the bytes come from sits over the box that takes them, the
/// downstreams hang below, and the link from each upstream into the main
/// box carries the upstream's state. A board without one is unchanged.
#[test]
fn an_upstream_sits_on_a_row_above_the_main_box() {
let up = |id: &str, live: Liveness| Machine {
id: id.into(),
role: Role::Upstream,
title: "upstream · proxy".into(),
server_id: "https://index.crates.io".into(),
liveness: live,
headline: "answering".into(),
..appliance()
};
let mut down = appliance();
down.id = "onprem-1".into();
down.role = Role::Standby;
let base = Estate {
product: "holger".into(),
machines: vec![front(), appliance(), down.clone()],
clouds: vec![],
tenants: None,
subtitle: None,
disk_only: true,
live: true,
};
let plain = estate_svg(&base);
let mut with = base.clone();
with.machines.insert(0, up("crates-io", Liveness::Up));
with.machines.push(up("pypi", Liveness::Silent));
let drawn = estate_svg(&with);
let height = |svg: &str| {
svg.split("viewBox=\"0 0 ")
.nth(1)
.unwrap()
.split('"')
.next()
.unwrap()
.split(' ')
.nth(1)
.unwrap()
.parse::<u32>()
.unwrap()
};
assert_eq!(
height(&drawn),
height(&plain) + CHIP_H + ROW_GAP,
"one more row"
);
let at = |svg: &str, id: &str| {
svg.find(&format!(r#"<g data-box="{id}""#))
.unwrap_or_else(|| panic!("{id} not drawn"))
};
assert!(
at(&drawn, "crates-io") < at(&drawn, "gunnar-appliance")
&& at(&drawn, "pypi") < at(&drawn, "gunnar-appliance"),
"the upstreams come first, on the row above"
);
assert!(at(&drawn, "gunnar-appliance") < at(&drawn, "onprem-1"));
let link = |id: &str| {
drawn
.split(&format!(r#"<g data-link="{id}""#))
.nth(1)
.map(|r| r.split("</g>").next().unwrap().to_string())
};
assert!(
link("crates-io").unwrap().contains(r#"data-state="up""#)
&& link("crates-io").unwrap().contains("<animate")
);
assert!(
link("pypi").unwrap().contains(r#"data-state="silent""#)
&& link("pypi").unwrap().contains(RED)
);
// The main box's chip is pushed down by exactly one row.
let chip_y = |svg: &str, id: &str| {
svg[at(svg, id)..]
.split("<rect x=\"")
.nth(1)
.unwrap()
.split("y=\"")
.nth(1)
.unwrap()
.split('"')
.next()
.unwrap()
.parse::<u32>()
.unwrap()
};
assert_eq!(
chip_y(&drawn, "gunnar-appliance"),
chip_y(&plain, "gunnar-appliance") + CHIP_H + ROW_GAP
);
assert_eq!(
chip_y(&drawn, "crates-io"),
chip_y(&plain, "gunnar-appliance")
);
assert_eq!(Role::Upstream.title(), "upstream");
assert_eq!(Role::from_word("upstream"), Some(Role::Upstream));
}
}