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/admin_storage.rs

//! **Administration → Storage**: everything technical about the box's storage,
//! for an administrator, in the owner's order (2026-10-02, "personal →
//! Account, machine → Administration"):
//!
//! 1. **Data set** — total, used, free, floor, servable, layout, members, and
//!    whether and how it grows;
//! 2. **Engine** — its name linked to its source with its description as the
//!    tooltip, the archive, the cold tier when there is one, the read cache;
//! 3. **Disks** — the system disk, every mount, every block device;
//! 4. **Twin** — its heartbeat, its volume and its mounts, and the restore;
//! 5. **/serialdat** — what the box's own records take;
//! 6. **Layout** — where `/data` and `/serialdat` are, the off-stripe copies,
//!    where gunnar puts what, and (folded) what the installer decided;
//! 7. **Performance** — the disk speed measured at start-up;
//! 8. **Allocation** — the box's disk, cores and RAM.
//!
//! 6 and 8 came from the Machine page when the owner retired it (2026-10-04):
//! every disk detail is said ONCE, here, and the Overview's Machine card
//! carries only the estate's picture and gunnar's few verdict lines.
//!
//! Then the per-repository technical table and the deleted-not-yet-purged
//! bytes with their undo.
//!
//! **A row is drawn only when gunnar reports a value.** The "N/A — not
//! reported" rows (write amplification, priority transfer, the read cache's
//! high/low water and its hits and misses, a cold tier an archive engine does not
//! have) are gone, with the paragraphs that explained them.

use leptos::prelude::*;

use crate::disks::{self, Disks};
use crate::fmt;
use crate::pages::storage::{Deleted, Storage};
use crate::pages::{now_ms, resolved};
use crate::ui::{Badge, Empty, FailBox, Hint, Metric, PageHeader, Panel, Tone};

#[component]
pub fn AdminStorage() -> impl IntoView {
    let session = crate::app::session();
    view! {
        <PageHeader title="Storage"/>
        {move || match session.get() {
            None => view! { <crate::ui::Loading/> }.into_any(),
            Some(s) if !s.is_admin() => view! {
                <Empty
                    title="This page is the administrator's"
                    detail="Your own storage is under Account."
                />
            }
            .into_any(),
            Some(_) => view! {
                <crate::pages::admin::Banner/>
                <Machine/>
            }
            .into_any(),
        }}
    }
}

/// The panels, off `/api/disks`, `/api/storage` and `/api/machine`.
#[component]
fn Machine() -> impl IntoView {
    let disks = LocalResource::new(|| async { crate::api::get::<Disks>("/api/disks").await });
    let storage = LocalResource::new(|| async { crate::api::get::<Storage>("/api/storage").await });
    view! {
        <div class="stack">
            {move || resolved(disks.get(), None, |d: Disks| view! { <DataSet d=d.clone()/> })}
            {move || resolved(storage.get(), None, |s: Storage| view! { <Engine s=s/> })}
            {move || resolved(disks.get(), None, |d: Disks| view! {
                <DisksPanel d=d.clone()/>
                <TwinPanel d=d.clone()/>
                <Serialdat d=d.clone()/>
                <crate::pages::machine::Layout/>
                <Performance d=d/>
                <crate::pages::machine::BoxAllocation/>
            })}
            {move || resolved(storage.get(), None, |s: Storage| view! {
                <PerRepository s=s.clone()/>
                <Deleted held=s.total_tombstoned_bytes/>
            })}
        </div>
    }
}

/// ── 1. The data set ────────────────────────────────────────────────────────
#[component]
fn DataSet(d: Disks) -> impl IntoView {
    let warnings = d.warnings.clone();
    d.data_set.map(|set| {
        let members = disks::members_line(&set);
        let used = (set.total_bytes > 0).then(|| format!("{} % of the total", set.used_bytes.saturating_mul(100) / set.total_bytes));
        let growth = set.growth.clone();
        let growth_known = crate::dataset::growth_label(&growth) != "Unknown";
        view! {
            <Panel title="Data set">
                <div class="panel-body">
                    {warnings.into_iter().map(|w| view! { <p class="bad" role="alert">{w}</p> }).collect_view()}
                    <dl class="metrics">
                        <Metric label="Total" value=fmt::bytes(set.total_bytes) unit=set.root.clone()/>
                        <Metric label="Used" value=fmt::bytes(set.used_bytes) unit=used.unwrap_or_default()/>
                        <Metric label="Free" value=fmt::bytes(set.free_bytes)/>
                        <Metric label="Floor" value=fmt::bytes(set.floor_bytes) unit="never handed out"/>
                        <Metric label="Servable" value=fmt::bytes(set.servable_bytes)/>
                        {(!set.layout.is_empty()).then(|| view! { <Metric label="Layout" value=set.layout.clone()/> })}
                        {growth_known.then(|| view! { <Metric label="Growth" value=crate::dataset::growth_label(&growth)/> })}
                    </dl>
                    {members.map(|line| view! { <p class="meta">{format!("Members ({} {}): {line}", set.device, set.raid_level)}</p> })}
                    {growth_known.then(|| view! { <p class="hint">{crate::dataset::growth_detail(&set.growth)}</p> })}
                </div>
            </Panel>
        }
    })
}

/// ── 2. The engine ──────────────────────────────────────────────────────────
#[component]
fn Engine(s: Storage) -> impl IntoView {
    let budget = if s.total_explode_budget_bytes > 0 { format!("of {}", fmt::bytes(s.total_explode_budget_bytes)) } else { "no budget".to_owned() };
    view! {
        <Panel title="Engine">
            <dl class="metrics">
                {s.engine.clone().map(|engine| view! {
                    <div class="metric">
                        <dt>"Engine"</dt>
                        <dd>
                            {if engine.repo_url.is_empty() {
                                view! { <span title=engine.description.clone()>{engine.name.clone()}</span> }.into_any()
                            } else {
                                view! {
                                    <a href=engine.repo_url title=engine.description rel="external noopener">{engine.name}</a>
                                }
                                .into_any()
                            }}
                        </dd>
                    </div>
                })}
                {if s.cold_measured {
                    view! {
                        <Metric label="Hot tier" value=fmt::bytes(s.total_hot_bytes)/>
                        <Metric label="Cold tier" value=fmt::bytes(s.total_cold_bytes)/>
                    }
                    .into_any()
                } else {
                    view! { <Metric label="Archive" value=fmt::bytes(s.total_hot_bytes) unit="one per repository"/> }.into_any()
                }}
                <Metric
                    label="Read cache"
                    value=fmt::bytes(s.total_explode_bytes)
                    unit=budget
                />
            </dl>
        </Panel>
    }
}

/// ── 3. Every disk ──────────────────────────────────────────────────────────
#[component]
fn DisksPanel(d: Disks) -> impl IntoView {
    view! {
        <Panel title="Disks">
            <div class="panel-body">
                {d.system.clone().map(|m| view! {
                    <h3>"System disk"</h3>
                    <p>{format!("{} on {} ({}) \u{2014} {}", m.mount_point, m.source, m.fs_type, disks::mount_line(&m))}</p>
                    <p class="meta">{disks::mount_room(&m)}</p>
                })}
                {d.read_cache.clone().map(|c| view! {
                    <h3>"Read cache (exploded objects)"</h3>
                    <p>{disks::cache_line(&c)}</p>
                    {c.hysteresis.clone().map(|h| view! { <p class="meta">{format!("High/low water (80 % / 75 %): {h}")}</p> })}
                })}
                {(!d.mounts.is_empty()).then(|| view! {
                    <h3>"Every mount"</h3>
                    <div class="table-wrap">
                        <table class="table">
                            <thead><tr><th>"Mount"</th><th>"Device"</th><th>"Gunnar puts"</th><th>"Use"</th><th>"Room"</th></tr></thead>
                            <tbody>
                                {d.mounts.iter().map(|m| view! {
                                    <tr>
                                        <td>{format!("{} ({})", m.mount_point, m.fs_type)}</td>
                                        <td>{m.source.clone()}</td>
                                        <td>{disks::labels_line(m)}</td>
                                        <td>{disks::mount_line(m)}</td>
                                        <td>{disks::mount_room(m)}</td>
                                    </tr>
                                }).collect_view()}
                            </tbody>
                        </table>
                    </div>
                })}
                {(!d.devices.is_empty()).then(|| view! {
                    <h3>"Block devices"</h3>
                    <div class="table-wrap">
                        <table class="table">
                            <thead><tr><th>"Device"</th><th>"Size"</th><th>"Partitions"</th><th>"Held by / members"</th></tr></thead>
                            <tbody>
                                {d.devices.iter().map(|b| view! {
                                    <tr>
                                        <td>{if b.raid_level.is_empty() { b.name.clone() } else { format!("{} ({})", b.name, b.raid_level) }}</td>
                                        <td>{fmt::bytes(b.size_bytes)}</td>
                                        <td>{b.partitions.iter().map(|(n, s)| format!("{n} {}", fmt::bytes(*s))).collect::<Vec<_>>().join(", ")}</td>
                                        <td>{if b.members.is_empty() { b.holders.join(", ") } else { b.members.join(", ") }}</td>
                                    </tr>
                                }).collect_view()}
                            </tbody>
                        </table>
                    </div>
                })}
                {d.problems.iter().map(|p| view! { <p class="hint">{p.clone()}</p> }).collect_view()}
                {d.unanswered.iter().map(|u| view! { <p class="hint">{format!("Not answered: {u}")}</p> }).collect_view()}
            </div>
        </Panel>
    }
}

/// **The twin's heartbeat**: one row per twin gunnar reports — state, last
/// seen, lag, fill and address — red when a twin is behind or silent.
/// `GET /api/twin`, an operator's read, so a viewer's session draws no panel.
/// Every sentence comes from [`crate::twin`], where it is tested.
#[component]
fn TwinPanel(d: Disks) -> impl IntoView {
    use crate::twin::{self, TwinPanel as Answer};

    let may_read = Memo::new(move |_| crate::app::session().get().is_some_and(|s| s.may_write()));
    let answer = LocalResource::new(|| async { crate::api::get::<Answer>("/api/twin").await });

    view! { {move || {
        let d = d.clone();
        if !may_read.get() {
            return ().into_any();
        }
        resolved(answer.get(), None, |panel: Answer| {
            let alarm = twin::panel_alarm(&panel);
            let unanswered = panel.unanswered.clone();
            let primary = panel.primary.clone();
            let rows = panel.twins.clone();
            // What "no rows" means here — a box with no twin, or a configured
            // twin this primary has never heard from. See `twin::no_twin_detail`.
            let detail = twin::no_twin_detail(&panel);
            view! {
                <div class=if alarm { "twin twin-alarm" } else { "twin" }>
                    <Panel title="Twin">
                        {(!unanswered.is_empty()).then(|| view! {
                            <div class="panel-body"><p class="hint">{unanswered.clone()}</p></div>
                        })}
                        {primary.map(|p| {
                            let tone = if twin::alarmed(&p.state) { Tone::Bad } else { Tone::Pass };
                            view! {
                                <div class="panel-body">
                                    <p>
                                        <Badge tone=tone label=twin::state_label(&p.state)/>
                                        {format!(
                                            " This box is a twin of {}: applied seq {} of the primary's {}, last answer {}. {}",
                                            p.address,
                                            p.applied_seq,
                                            p.primary_head_seq,
                                            if p.last_answer_unix_ms.is_some() { twin::ago(p.answer_ago_ms) } else { "never".to_owned() },
                                            p.why,
                                        )}
                                    </p>
                                </div>
                            }
                        })}
                        {if rows.is_empty() {
                            if unanswered.is_empty() {
                                view! { <Empty title="No twin reported" detail=detail.clone()/> }.into_any()
                            } else {
                                ().into_any()
                            }
                        } else {
                            view! {
                                <div class="table-wrap">
                                    <table>
                                        <thead>
                                            <tr>
                                                <th>"Twin"</th>
                                                <th>"State"</th>
                                                <th>"Last seen"</th>
                                                <th>"Lag"</th>
                                                <th>"Fill"</th>
                                                <th>"Address"</th>
                                            </tr>
                                        </thead>
                                        <tbody>
                                            {rows.into_iter().map(|row| {
                                                let red = twin::alarmed(&row.state);
                                                let tone = if red { Tone::Bad } else { Tone::Pass };
                                                view! {
                                                    <tr class=if red { "alarm" } else { "" } title=row.why.clone()>
                                                        <td>{row.principal.clone()}</td>
                                                        <td><Badge tone=tone label=twin::state_label(&row.state)/></td>
                                                        <td>{twin::seen_line(&row)}</td>
                                                        <td>{twin::lag_line(&row)}</td>
                                                        <td>{twin::fill_line(&row)}</td>
                                                        <td><code>{row.address.clone()}</code></td>
                                                    </tr>
                                                }
                                            }).collect_view()}
                                        </tbody>
                                    </table>
                                </div>
                            }.into_any()
                        }}
                        // ★ The restore lives INSIDE this panel, not beside it.
                        // Reading a twin and acting on it are one subject, and
                        // two panels made the action look like a separate
                        // feature that might or might not be installed — which
                        // is the confusion `restore_absence` already exists to
                        // end. It draws its own heading and nothing at all for
                        // a non-admin, so an operator's pane is unchanged.
                        <TwinVolumes d=d.clone()/>
                        <RestoreAction/>
                        <RebuildAction/>
                    </Panel>
                </div>
            }
        })
    }} }
}

/// ★ **The Twin panel's admin half** (5.4 resilience) — ALWAYS drawn for an
/// administrator, nothing for anyone else: **[Review twin]** (gunnar-repair's
/// review: health, head vs primary, last heartbeat, what a recharge would
/// lose) and **[Recharge from twin]**, armed only when `GET /api/restore`
/// names a phrase. A main that answers as LIVE gets only the fence-first
/// override, with its own phrase. The server asks for a fresh login
/// (step-up); its refusal is shown as it said it. See [`crate::twin::show_restore`].
#[component]
fn RestoreAction() -> impl IntoView {
    use crate::twin::{self, Eligibility, Restored};

    let is_admin = Memo::new(move |_| crate::app::session().get().is_some_and(|s| s.is_admin()));
    let offer = LocalResource::new(|| async { crate::api::get::<Eligibility>("/api/restore").await });
    let armed = RwSignal::new(false);
    let typed = RwSignal::new(String::new());
    let busy = RwSignal::new(false);
    let failed = RwSignal::new(None::<crate::api::Fail>);
    let done = RwSignal::new(None::<Restored>);

    view! { {move || {
        if !is_admin.get() {
            return ().into_any();
        }
        if let Some(r) = done.get() {
            return view! {
                <div class="panel-body">
                    <h3 class="disks-head">"Recharge from twin"</h3>
                    <p>{twin::restored_line(&r)}</p>
                </div>
            }
            .into_any();
        }
        let answer = offer.get().and_then(|r| r.ok());
        let review = answer.as_ref().and_then(|e| e.review.clone());
        // The answer's own reason is drawn once, below; never again in the review.
        let said = answer.as_ref().map(|e| e.why.clone()).unwrap_or_default();
        let plain = answer.as_ref().and_then(twin::plain_why);
        let review_view = view! {
            <p>
                <button id="twin-review" class="btn quiet" disabled=move || busy.get() on:click=move |_| offer.refetch()>
                    "Review twin"
                </button>
            </p>
            {review.map(|r| twin::review_lines(&r, &said).into_iter().map(|l| view! { <p class="hint">{l}</p> }).collect_view())}
        };
        let overriding = twin::show_override(true, answer.as_ref());
        if !twin::show_restore(true, answer.as_ref()) && !overriding {
            // Not the action — the REASON there is none. See `twin::restore_absence`.
            let why = twin::restore_absence(true, answer.as_ref());
            return view! {
                <div class="panel-body">
                    <h3 class="disks-head">"Recharge from twin"</h3>
                    {review_view}
                    {match plain {
                        Some((line, detail)) => view! { <Hint line=line more=detail/> }.into_any(),
                        None => why.map(|why| view! { <p class="hint">"Not offered here: "{why}</p> }).into_any(),
                    }}
                </div>
            }
            .into_any();
        }
        let e = answer.unwrap_or_default();
        let offer_text = if overriding { e.why.clone() } else { twin::offer_line(&e) };
        let phrase = if overriding { e.override_confirm.clone() } else { e.confirm.clone() };
        let check = e.clone();
        let body_of = e.clone();
        let send = move |_| {
            busy.set(true);
            failed.set(None);
            let body = twin::restore_body(&typed.get_untracked(), &body_of, overriding);
            leptos::task::spawn_local(async move {
                match crate::api::post::<_, Restored>("/api/restore", &body).await {
                    Ok(r) => {
                        armed.set(false);
                        done.set(Some(r));
                    }
                    Err(fail) => failed.set(Some(fail)),
                }
                busy.set(false);
            });
        };
        view! {
            <div class="restore">
                    <div class="panel-body">
                        <h3 class="disks-head">"Recharge from twin"</h3>
                        {review_view}
                        {match plain.clone() {
                            Some((line, detail)) => view! { <Hint line=line more=detail/> }.into_any(),
                            None => view! { <p class="hint">{offer_text}</p> }.into_any(),
                        }}
                        {move || if armed.get() {
                            let check = check.clone();
                            let send = send.clone();
                            view! {
                                <div class="consequence" role="alert">
                                    {if overriding {
                                        view! {
                                            <Hint
                                                line="The main is LIVE: this fences it first and streams nothing into it."
                                                more="Fenced = read-only, no copies shipped. Nothing is streamed into a live main: \
                                                      re-lay it, then recharge the fresh box."
                                            />
                                        }.into_any()
                                    } else {
                                        view! { <p>{twin::warning()}</p> }.into_any()
                                    }}
                                    {twin::fence_hint(&check).map(|(line, more)| view! { <Hint line=line more=more /> })}
                                    <p>"Type " <code>{phrase.clone()}</code> " to confirm."</p>
                                    <p>
                                        <input
                                            id="restore-confirm"
                                            type="text"
                                            autocomplete="off"
                                            prop:value=move || typed.get()
                                            on:input=move |ev| typed.set(event_target_value(&ev))
                                        />
                                    </p>
                                    <p>
                                        <button
                                            id="restore-send"
                                            class="btn danger"
                                            disabled=move || busy.get() || !(if overriding { twin::may_override(&typed.get(), &check) } else { twin::may_send(&typed.get(), &check) })
                                            on:click=send
                                        >
                                            {if overriding { "Fence main now" } else { "Recharge now" }}
                                        </button>
                                        " "
                                        <button
                                            id="restore-cancel"
                                            class="btn quiet"
                                            disabled=move || busy.get()
                                            on:click=move |_| {
                                                armed.set(false);
                                                typed.set(String::new());
                                            }
                                        >
                                            "Cancel"
                                        </button>
                                    </p>
                                </div>
                            }
                            .into_any()
                        } else {
                            view! {
                                <p>
                                    <button id="restore-arm" class="btn danger" on:click=move |_| armed.set(true)>
                                        {if overriding { "Fence main and recharge…" } else { "Recharge from twin…" }}
                                    </button>
                                </p>
                            }
                            .into_any()
                        }}
                        {move || failed.get().map(|fail| view! { <FailBox fail=fail/> })}
                    </div>
            </div>
        }
        .into_any()
    }} }
}

/// ★ **[Rebuild main]** (main rebuild, owner 2026-10-12) — drawn for an administrator, and
/// only where the console reaches monetize (`GET /api/rebuild` answers): the ONE box's
/// guardian's words about THIS estate's main, its case, and the press with its typed phrase.
/// The twin is never promoted; the guardian rebuilds the main from it. See
/// [`crate::twin::rebuild_lines`].
#[component]
fn RebuildAction() -> impl IntoView {
    use crate::twin::{self, Rebuild};

    let is_admin = Memo::new(move |_| crate::app::session().get().is_some_and(|s| s.is_admin()));
    let look = LocalResource::new(|| async { crate::api::get::<Rebuild>("/api/rebuild").await });
    let armed = RwSignal::new(false);
    let typed = RwSignal::new(String::new());
    let busy = RwSignal::new(false);
    let failed = RwSignal::new(None::<crate::api::Fail>);

    view! { {move || {
        if !is_admin.get() {
            return ().into_any();
        }
        let Some(r) = look.get().and_then(|r| r.ok()) else { return ().into_any() };
        let lines = twin::rebuild_lines(&r);
        let pressable = !r.confirm.is_empty();
        let phrase = r.confirm.clone();
        let check = r.clone();
        let send = move |_| {
            busy.set(true);
            failed.set(None);
            let body = serde_json::json!({ "confirm": typed.get_untracked() });
            leptos::task::spawn_local(async move {
                match crate::api::post::<_, Rebuild>("/api/rebuild", &body).await {
                    Ok(_) => {
                        armed.set(false);
                        typed.set(String::new());
                        look.refetch();
                    }
                    Err(fail) => failed.set(Some(fail)),
                }
                busy.set(false);
            });
        };
        view! {
            <div class="panel-body">
                <h3 class="disks-head">"Rebuild main"</h3>
                {lines.into_iter().map(|l| view! { <p class="hint">{l}</p> }).collect_view()}
                {move || if !pressable {
                    ().into_any()
                } else if armed.get() {
                    let check = check.clone();
                    view! {
                        <div class="consequence" role="alert">
                            <Hint
                                line="Its provider rebuilds the main from the twin."
                                more="The twin is never promoted. A disk that still holds data is never reformatted without the operator's confirmation."
                            />
                            <p>"Type " <code>{phrase.clone()}</code> " to confirm."</p>
                            <p>
                                <input id="rebuild-confirm" type="text" autocomplete="off"
                                    prop:value=move || typed.get()
                                    on:input=move |ev| typed.set(event_target_value(&ev)) />
                            </p>
                            <p>
                                <button id="rebuild-send" class="btn danger"
                                    disabled=move || busy.get() || !twin::may_rebuild(&typed.get(), &check)
                                    on:click=send>"Rebuild now"</button>
                                " "
                                <button id="rebuild-cancel" class="btn quiet" disabled=move || busy.get()
                                    on:click=move |_| { armed.set(false); typed.set(String::new()); }>"Cancel"</button>
                            </p>
                        </div>
                    }.into_any()
                } else {
                    let label = twin::rebuild_button(&check);
                    view! { <p><button id="rebuild-arm" class="btn danger" on:click=move |_| armed.set(true)>{label}</button></p> }.into_any()
                }}
                {move || failed.get().map(|fail| view! { <FailBox fail=fail/> })}
            </div>
        }
        .into_any()
    }} }
}

/// ── 4. The twin's volume and mounts ────────────────────────────────────────
/// Only what gunnar reported: a twin whose volume was not reported has no row.
#[component]
fn TwinVolumes(d: Disks) -> impl IntoView {
    let volumes: Vec<_> = d.twins.iter().filter(|t| t.used_bytes.is_some() && t.total_bytes.is_some()).cloned().collect();
    view! {
        <div class="panel-body">
            {volumes.iter().map(|t| view! {
                <p>{format!("{} at {} ({}): {}", t.principal, t.address, t.state, disks::twin_line(t))}</p>
            }).collect_view()}
            {d.twin_box.clone().map(|tb| view! {
                <h3>{format!("Twin at {}", tb.address)}</h3>
                {(!tb.unanswered.is_empty()).then(|| view! { <p class="hint">{tb.unanswered.clone()}</p> })}
                {tb.mounts.iter().map(|m| view! {
                    <p>{format!("{} ({}) \u{2014} {}", m.mount_point, m.source, disks::mount_line(m))}</p>
                }).collect_view()}
            })}
        </div>
    }
}

/// ── 5. /serialdat ──────────────────────────────────────────────────────────
#[component]
fn Serialdat(d: Disks) -> impl IntoView {
    d.serialdat.map(|sd| {
        let rows: Vec<_> = sd.breakdown.iter().filter(|p| p.bytes.is_some()).cloned().collect();
        view! {
            <Panel title="/serialdat">
                <div class="panel-body">
                    <p>{format!("{} on {} \u{2014} {}", sd.mount.mount_point, sd.mount.source, disks::mount_line(&sd.mount))}</p>
                    <ul>{rows.iter().map(|p| view! { <li>{disks::path_line(p)}</li> }).collect_view()}</ul>
                </div>
            </Panel>
        }
    })
}

/// ── 6. Performance ─────────────────────────────────────────────────────────
#[component]
fn Performance(d: Disks) -> impl IntoView {
    d.disk_speed.filter(|sp| sp.test == "measured").map(|sp| view! {
        <Panel title="Performance">
            <div class="panel-body">
                <h3>"Disk speed (measured at start-up)"</h3>
                <p>{disks::speed_line(&sp)}</p>
                {disks::speed_basis(&sp).map(|b| view! { <p class="meta">{b}</p> })}
            </div>
        </Panel>
    })
}

/// The technical per-repository table: archive, cold (only when an engine
/// has a cold tier), loose objects, read cache, references, last repack.
#[component]
fn PerRepository(s: Storage) -> impl IntoView {
    if s.repos.is_empty() {
        return ().into_any();
    }
    let now = now_ms();
    let cold = s.repos.iter().any(|r| r.cold_measured);
    let mut rows = s.repos.clone();
    rows.sort_by_key(|r| std::cmp::Reverse(r.hot_bytes + r.cold_bytes));
    view! {
        <Panel title="Every repository">
            <div class="table-wrap">
                <table>
                    <thead>
                        <tr>
                            <th scope="col">"Repository"</th>
                            <th scope="col">"Archive"</th>
                            {cold.then(|| view! { <th scope="col">"Cold"</th><th scope="col">"Cold objects"</th> })}
                            <th scope="col">"Loose objects"</th>
                            <th scope="col">"Read cache"</th>
                            <th scope="col">"References"</th>
                            <th scope="col">"Last repacked"</th>
                        </tr>
                    </thead>
                    <tbody>
                        {rows.into_iter().map(|row| view! {
                            <tr>
                                <td class="mono">{row.repo.clone()}</td>
                                <td class="num">{fmt::bytes(row.hot_bytes)}</td>
                                {cold.then(|| view! {
                                    <td class="num">{if row.cold_measured { fmt::bytes(row.cold_bytes) } else { "\u{2014}".to_owned() }}</td>
                                    <td class="num">{if row.cold_measured { fmt::count(row.cold_object_count) } else { "\u{2014}".to_owned() }}</td>
                                })}
                                <td class="num">{fmt::count(row.loose_object_count)}</td>
                                <td class="num">{if row.explode_bytes > 0 { fmt::bytes(row.explode_bytes) } else { "\u{2014}".to_owned() }}</td>
                                <td class="num">{fmt::count(row.ref_count)}</td>
                                <td class="num">{row.last_repack_at_unix_ms.map(|at| fmt::since(at, now)).unwrap_or_else(|| "never".to_owned())}</td>
                            </tr>
                        }).collect_view()}
                    </tbody>
                </table>
            </div>
        </Panel>
    }
    .into_any()
}