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

nordisk/gunnar-ui

crates/gunnar-ui-web/src/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>
            })
        })}
    }
}