crates/gunnar-ui-web/src/pages/machine.rs
//! ★ **The machine's layout, for a box that has no shell** — the Layout and
//! Allocation panels of Administration → Storage.
//!
//! `:22` is closed on the appliance and PID 1 is `gunnar-server`. There is no
//! way in, no `lsblk`, no `df`, so the console is the only place anybody sees
//! what the machine is. Until 2026-10-04 that was a page of its own, Machine;
//! the owner retired it because it said again what Overview and Storage say.
//! Now the Overview's Machine card carries the estate's picture and gunnar's
//! few verdict lines, and every disk detail is said ONCE, on Storage — these
//! two panels are the part of the old page Storage did not already have:
//!
//! * **Layout** — where `/data` is and what it is made of, where `/serialdat`
//! is, where gunnar puts what (the off-stripe verdict and the placement
//! table), and, folded, what the installer decided (the layout config, key
//! by key, unparsed);
//! * **Allocation** — the box's disk, cores and RAM: used, allowed, remaining.
//!
//! The filesystem table ("what is true now") is gone with the page: Storage's
//! Data set and Disks panels already measure the same filesystems.
//!
//! # The mislabel this fixes
//!
//! The page drew `serialdat.dev` / `serialdat.class` as "Backing device /
//! Backing class" directly under "Data root", so every estate read as if its
//! repositories lived on the boot disk's P3. They are `/serialdat`'s, and are
//! drawn ONLY through [`crate::machine::serialdat_on`] under "/serialdat on";
//! `/data` names its own devices ([`crate::machine::data_on`], from the
//! installer's `data.*` keys via gunnar). `machine::tests::
//! data_and_serialdat_each_name_their_own_disk` holds this file to it.
//!
//! Every decision lives in [`crate::machine`], on the host side of the wasm
//! boundary where `cargo test` can reach it; this module draws and decides
//! nothing.
//!
//! # Admin only, and not only in this file
//!
//! The rows carry device paths, the layout config's every key, and the roots
//! the box writes to. `/api/machine` is `role_floor`'s admin READ, so the
//! boundary is on the server; Storage itself is drawn for an administrator
//! only.
use leptos::prelude::*;
use crate::machine::{
by_requirement, data_on, layer_one, serialdat_on, signature, signature_label, slow_root_line, unwired,
Absence, Grade, InstallRecord, Machine as Report, OffStripe, Placement, PlacementRow, RecordField,
};
use crate::pages::resolved;
use crate::ui::{Badge, Empty, Hint, Metric, Panel, Tone, Unset};
/// The host-side grade, in the console's own four states.
fn tone(grade: Grade) -> Tone {
match grade {
Grade::Good => Tone::Pass,
Grade::Caution => Tone::Warn,
Grade::Alarm => Tone::Bad,
Grade::Neutral => Tone::Plain,
}
}
/// A value gunnar left empty, drawn as a dash with the reason in its title
/// rather than as a blank cell.
#[component]
fn Or(#[prop(into)] value: String, #[prop(into)] why: String) -> impl IntoView {
if value.trim().is_empty() {
view! { <Unset why=why/> }.into_any()
} else {
view! { {value} }.into_any()
}
}
/// ★ **The off-stripe badge — three states and a silence.**
///
/// The one component in this panel whose colour is load-bearing, and the one
/// place a mistake here would be actively dangerous: `same device` takes the
/// same tone as a failure, one step above "no second copy", because a mirror
/// that dies with its original is worse than a mirror nobody has. The
/// sentence under it is always on screen: colour alone is the failure mode
/// this console's whole palette was built to avoid.
#[component]
fn OffStripeBadge(word: Option<String>) -> impl IntoView {
let state = OffStripe::read(word.as_deref());
let detail = state.detail();
match state.label() {
Some(label) => view! {
<Badge tone=tone(state.grade()) label=label/>
<div class="disks-note">{detail}</div>
}
.into_any(),
// Not one of the three. A dash and the reason — never the safest
// looking of the words this console does know.
None => view! {
<Unset why="nothing was reported about this"/>
<div class="disks-note">{detail}</div>
}
.into_any(),
}
}
/// ★ **Administration → Storage's Layout panel**: `/data`, `/serialdat`, where
/// gunnar puts what, and — folded — what the installer decided.
#[component]
pub fn Layout() -> impl IntoView {
let machine = LocalResource::new(|| async { crate::api::get::<Report>("/api/machine").await });
view! {
{move || resolved(machine.get(), None, |machine: Report| view! { <LayoutPanel machine/> })}
}
}
/// The panel, once gunnar answered (or said it could not).
#[component]
fn LayoutPanel(machine: Report) -> impl IntoView {
// **A server that did not answer measured nothing**, and the panel says so
// instead of drawing an empty table as a finding.
if !machine.unanswered.trim().is_empty() {
return view! {
<Panel title="Layout">
<div class="panel-body">
<Empty
title="Not reported"
detail=machine.unanswered.clone()
more="Where /data and /serialdat are and where Gunnar writes what travel in the \
same answer. Nothing was read, so nothing is shown."
/>
</div>
</Panel>
}
.into_any();
}
let Some(placement) = machine.placement.clone() else {
return view! {
<Panel title="Layout">
<div class="panel-body">
<Empty
title="Gunnar did not report its placement"
detail="Where it writes packs, mirrors and archives is not on the wire."
more="The Data set and Disks panels say what EXISTS; they do not say what Gunnar \
will use next, and nothing here is guessing."
/>
</div>
</Panel>
}
.into_any();
};
view! {
<Panel title="Layout">
<Where placement=placement.clone()/>
<OffStripeCopies placement=placement.clone()/>
<Placed placement=placement/>
<Decided machine=machine/>
</Panel>
}
.into_any()
}
/// ★ **`/data` and `/serialdat`, each naming its OWN disk.**
#[component]
fn Where(placement: Placement) -> impl IntoView {
let slow = slow_root_line(&placement);
view! {
<div class="panel-body">
<dl class="metrics">
<div class="metric">
<dt>"Data root"</dt>
<dd class="mono">
<Or value=placement.data_root.clone() why="Gunnar did not name a data root"/>
</dd>
</div>
<div class="metric">
<dt>"/data is"</dt>
<dd class="mono">
<Or
value=data_on(&placement).unwrap_or_default()
why="the installer's layout config names no shape for the data set"
/>
</dd>
</div>
<div class="metric">
<dt>"/serialdat on"</dt>
<dd class="mono">
<Or
value=serialdat_on(&placement).unwrap_or_default()
why="nothing said which disk /serialdat is on"
/>
</dd>
</div>
</dl>
<Hint
line=slow
more="Settings, not measurements: this is where the NEXT byte goes. /data holds the \
repositories; /serialdat holds the box's own records and the copies that must \
survive the data set."
/>
</div>
}
}
/// ★ The one off-stripe verdict, and the sentences behind it.
#[component]
fn OffStripeCopies(placement: Placement) -> impl IntoView {
let health = placement.off_stripe_health.clone();
view! {
<div class="panel-body">
<h3 class="disks-head">"The copies that need another failure domain"</h3>
<OffStripeBadge word=placement.off_stripe.clone()/>
{if health.is_empty() {
view! { <Unset why="Gunnar listed no copies to report on"/> }.into_any()
} else {
view! {
<ul class="off-stripe-health">
{health.into_iter().map(|line| view! { <li class="disks-note">{line}</li> }).collect_view()}
</ul>
}
.into_any()
}}
</div>
}
}
/// Where gunnar puts what: the copies that must be off-stripe, then the rest.
#[component]
fn Placed(placement: Placement) -> impl IntoView {
let (required, rest) = by_requirement(&placement);
let not_wired = unwired(&placement);
view! {
<Rows
title="Must be off-stripe"
note="Copies whose whole value is being on a device that can die on its own."
more="The verdict beside each is one of three, and \"same device\" is never a pass."
rows=required
show_domain=true
/>
<Rows
title="Everything else Gunnar writes"
note="No second failure domain is claimed for these, so none is reported."
more="An empty verdict column here would read as a promise nobody made, which is why \
they are a table of their own."
rows=rest
show_domain=false
/>
{(not_wired > 0).then(|| view! {
<div class="panel-body">
<Hint
line=crate::machine::not_written_line(not_wired)
more="Each says so in its own row. A directory that exists is not a feature."
/>
</div>
})}
}
}
/// **Historical, and folded**: what `korp-installer` decided, written once and
/// never touched again — opened on purpose, because it is what was TRUE THEN.
#[component]
fn Decided(machine: Report) -> impl IntoView {
let summary = match layer_one(&machine) {
Ok(record) => format!("What the installer decided — {} keys, {}", record.fields.len(), signature_label(record)),
Err(_) => "What the installer decided — no record".to_owned(),
};
view! {
<details class="panel-body installer-record">
<summary class="disks-head">{summary}</summary>
{match layer_one(&machine) {
Err(absence) => view! { <NoRecord absence=absence/> }.into_any(),
Ok(record) => view! { <RecordView record=record.clone() path=machine.record_path.clone()/> }.into_any(),
}}
</details>
}
}
/// ★ **The record is absent, and the page says so instead of inventing one.**
///
/// Two absences, drawn apart because they are fixed by unrelated things: a
/// machine installed before the record existed has none, and a gunnar older
/// than the report cannot be asked about a machine that may well have one.
/// [`Absence`] carries the words; the paragraph it writes ends with the
/// promise this panel is here to keep — that nothing below was used to fill
/// this in.
#[component]
fn NoRecord(absence: Absence) -> impl IntoView {
let title = absence.title();
let detail = absence.detail();
view! {
<div class="panel-body">
<Empty
title=title
detail="Nothing below was used to fill this in."
more=detail
/>
</div>
}
}
/// The record, as it was written.
#[component]
fn RecordView(record: InstallRecord, path: String) -> impl IntoView {
let (sentence, grade) = signature(&record);
let label = signature_label(&record);
let fields = record.fields.clone();
view! {
<div class="panel-body">
<Hint
line="Written once, when this machine was installed, and never updated since."
more="Everything here is what was TRUE THEN — a disk that has failed in the meantime \
is still described here. The machine as it is now is the panels above."
/>
<dl class="metrics">
<Metric label="Config version" value=record.version.to_string()/>
<div class="metric">
<dt>"Read from"</dt>
<dd class="mono">
<Or value=path why="Gunnar did not name the path it read"/>
</dd>
</div>
</dl>
<p><Badge tone=tone(grade) label=label/></p>
<p class="disks-note">{sentence}</p>
</div>
<Fields rows=fields/>
}
}
/// ★ **The layout config, key by key, exactly as the installer wrote it.**
///
/// Not folded into disks, partitions and sets — gunnar chose not to, and its
/// reason is the strongest argument on this page: *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 parsed this into a
/// structure would go blind the day a later `korp-installer` writes a key this
/// build has never heard of, and on a box with no shell there is no second way
/// to read it.
#[component]
fn Fields(rows: Vec<RecordField>) -> impl IntoView {
if rows.is_empty() {
return view! {
<div class="panel-body">
<Empty
title="The record carries no fields"
detail="Gunnar reported a record and no keys in it."
more="That is not an empty configuration — it is a record this console was given \
nothing out of, and nothing here is standing in for it."
/>
</div>
}
.into_any();
}
view! {
<div class="panel-body">
<h3 class="disks-head">"The layout config"</h3>
<Hint
line="Every key the installer wrote, in key order, unparsed."
more="A key this build does not recognise is still shown: whoever is reading this \
page may know what it means when this binary does not."
/>
</div>
<div class="table-wrap">
<table>
<caption class="vh">"The install record, key by key"</caption>
<thead>
<tr>
<th scope="col">"Key"</th>
<th scope="col">"Value"</th>
</tr>
</thead>
<tbody>
{rows
.into_iter()
.map(|f| view! {
<tr>
<th scope="row" class="mono">{f.key.clone()}</th>
<td class="mono">
<Or
value=f.value.clone()
why="the installer wrote this key with no value"
/>
</td>
</tr>
})
.collect_view()}
</tbody>
</table>
</div>
}
.into_any()
}
/// One half of the placement table.
///
/// The `show_domain` column exists only where a row promises something about a
/// failure domain. Drawing it for rows that promise nothing would put an empty
/// cell under a heading a reader takes for a verdict.
#[component]
fn Rows(
#[prop(into)] title: String,
#[prop(into)] note: String,
#[prop(into, optional)] more: String,
rows: Vec<PlacementRow>,
show_domain: bool,
) -> impl IntoView {
if rows.is_empty() {
return view! {
<div class="panel-body">
<h3 class="disks-head">{title}</h3>
<Unset why="Gunnar listed no rows of this kind"/>
</div>
}
.into_any();
}
view! {
<div class="panel-body">
<h3 class="disks-head">{title}</h3>
<Hint line=note more=more/>
</div>
<div class="table-wrap">
<table>
<caption class="vh">"Where Gunnar writes what"</caption>
<thead>
<tr>
<th scope="col">"What"</th>
<th scope="col">"Path"</th>
<th scope="col">"Written to"</th>
{show_domain.then(|| view! { <th scope="col">"Failure domain"</th> })}
</tr>
</thead>
<tbody>
{rows
.into_iter()
.map(|r| {
let wired = r.wired;
let note = r.note.clone();
view! {
<tr>
<td>{r.what.clone()}</td>
<td class="mono">
<Or
value=r.path.clone()
why="Gunnar named no path for this"
/>
</td>
<td>
{if wired {
view! {
<Badge tone=Tone::Pass label="yes"/>
}
.into_any()
} else {
view! {
<Badge tone=Tone::Warn label="not yet"/>
<div class="disks-note">
<Or
value=note.clone()
why="Gunnar gave no reason"
/>
</div>
}
.into_any()
}}
</td>
{show_domain.then(|| view! {
<td><OffStripeBadge word=r.off_stripe.clone()/></td>
})}
</tr>
}
})
.collect_view()}
</tbody>
</table>
</div>
}
.into_any()
}
/// ★ **The box's allocation: disk, cores and RAM — used, allowed, remaining.**
///
/// What `Admin.Capacity` measures beyond the disk (the cores gunnar may run
/// on, the one-minute load, MemTotal, MemAvailable, and gunnar's own sentence
/// about how it read them), from `/api/machine/allocation`. Written on
/// lane-allocbuy (e349616 + 9db3bbc, 2026-09-20) for a tenant's usage page and
/// never released; it is the MACHINE's, so it stands under Administration
/// (owner 2026-10-02: machine → Administration). The rows and their
/// wall-versus-reading rules are [`crate::room::box_rows`]; a value gunnar did
/// not measure is gunnar's sentence, never a zero.
#[component]
pub fn BoxAllocation() -> impl IntoView {
let read = LocalResource::new(|| async { crate::api::get::<crate::room::Machine>("/api/machine/allocation").await });
view! {
{move || resolved(read.get(), None, |machine: crate::room::Machine| {
let why = machine.machine_reason.clone();
crate::room::box_rows(Some(&machine)).map(|rows| view! {
<Panel title="Allocation">
<div class="table-wrap">
<table class="room">
<caption class="vh">"The box: used, allowed and remaining, per resource"</caption>
<thead>
<tr>
<th scope="col">"This box"</th>
<th scope="col">"Used"</th>
<th scope="col">"Allowed"</th>
<th scope="col">"Remaining"</th>
<th scope="col">"At the cap"</th>
</tr>
</thead>
<tbody>
{rows.into_iter().map(|row| {
let label = row.resource;
let why = why.clone();
let cell = move |value: Option<String>| match value {
Some(value) => view! { {value} }.into_any(),
None => view! { <Unset why=why.clone()/> }.into_any(),
};
view! {
<tr>
<th scope="row">
{label}
{row.fill.map(|pct| view! {
<div class="wall" role="progressbar"
aria-label=format!("{label}, against what this box may hand out")
aria-valuenow=pct.to_string() aria-valuemin="0" aria-valuemax="100">
<span style=format!("width:{pct}%")></span>
</div>
})}
</th>
<td class="num">{cell(row.used.clone())}</td>
<td class="num">{cell(row.allowed.clone())}</td>
<td class="num">{cell(row.remaining.clone())}</td>
<td class="does">
<span>{row.phrase}</span>
<span class="meta">{row.caveat}</span>
</td>
</tr>
}
}).collect_view()}
</tbody>
</table>
</div>
<Hint
line="Disk is metered per tenant; cores and RAM are the machine's, not a cap anyone holds."
more=format!("How Gunnar read the box: {why}")
/>
</Panel>
})
})}
}
}