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

//! **What a namespace holds.** gunnar's own meters, one namespace at a time.
//!
//! # Why this is not the Storage page again
//!
//! The Storage page answers a server operator's question — which repositories
//! are big, what is hot, what is cold, what has never been repacked — and it
//! reads `Admin.StorageInfo`, which is every repository on the box and takes a
//! server-wide grant.
//!
//! This page answers a *namespace's own* question: how much of this disk is
//! mine, and which part of it is the part that cannot be thrown away? It reads
//! `Usage.Read`, which names one namespace and takes `Read` **on that
//! namespace** — so it is the page that still works when the credential looking
//! at it is not the server's.
//!
//! Nothing here is a price. gunnar counts these numbers whether or not
//! anything ever reads them, and a person running gunnar on their own disk gets
//! exactly this page.
//!
//! # Room: used, allowed, remaining
//!
//! Since 2026-09-05 the same `Usage.Read` carries the namespace's
//! **allowance** — per meter, the cap the box holds for it, absent when there
//! is none — and the page draws each meter as three numbers: what is used,
//! what is allowed (`no cap` when nothing caps it; the community appliance
//! caps nobody), and what is left. "Left" is the console's one derived number
//! and it has one writer (`api::usage::remaining` on the server): what the cap
//! leaves, and for a byte meter no more than the box can serve. Without a cap
//! it is the box alone; without the box either, there is nothing honest to say
//! and a dash is drawn. A cap is a resource number and nothing else: no plan
//! name, no date, no link, so this panel is exactly as useful to a self-hoster
//! who set no caps as to a tenant who has some.
//!
//! # A cap is not automatically a wall, and the table draws the difference
//!
//! Three of the seven meters are a DOOR and four are a READING, and until
//! gunnar grew `Allowance.enforcement` all seven arrived in one shape under one
//! message called `Allowance`. A tenant whose signed fact carried
//! `caps["repos"]=5` was shown a limit of 5 in the Allowed column, and nothing
//! on the box refused the sixth repository. A number shown as a limit that no
//! code enforces is a promise the product does not keep, and drawing both kinds
//! identically is what made it one.
//!
//! So the row's SHAPE carries it, not only its words:
//!
//! * an **ENFORCED** cap is a WALL — a bar under the meter, filled to how far
//!   in the tenant is, ending at the cap. A bar is the only thing on a page
//!   that says "and then it stops";
//! * a **MEASURED** cap is a READING — a figure and no bar at all, because a
//!   bar IS a ceiling and drawing one where nothing refuses is the defect
//!   itself.
//!
//! The sentence beside each is `monetize_product::Enforcement::describe()`
//! verbatim — *"the product refuses when over"*, *"counted and reported;
//! nothing refuses"*. gunnar put monetize's vocabulary on the wire on purpose;
//! a third phrasing invented here would be the one that goes stale, because
//! nothing compares a console's prose to another repository's enum.
//!
//! A gunnar older than the field says nothing, and **nothing is not "nothing is
//! enforced"** ([`Shape::NotSaid`]): that box still cuts a push mid-stream at
//! `pack_bytes`. It gets no bar — this console will not draw a ceiling it was
//! not told about — and no claim that nothing refuses either.
//!
//! The box line under it is `Admin.Capacity`: how much of the disk gunnar can
//! still serve, and whether the set can grow at all — `cannot grow while
//! running` is the appliance's answer, `an operator can grow it` a box with a
//! shell beside gunnar. That RPC takes a server-wide `Read`; a console whose
//! key holds less gets a dash and gunnar's reason, never a figure it did not
//! read.
//!
//! # The meters are not one number
//!
//! They are split because the splits are what a person acts on:
//!
//! * **`pack_bytes`** is what is not re-derivable. Everything else on the disk
//!   can be rebuilt from it.
//! * **`cache_bytes`** is the exploded read cache. gunnar will re-derive it; it
//!   is here because it is on the disk today, not because it is owed.
//! * **`tombstoned_bytes`** is the answer to *I deleted a repository and my
//!   usage did not go down.* It did not, and this is by how much. Those bytes
//!   are in none of the others: a tombstoned repository has no catalog row.
//! * **`objects`** is not bytes at all. It is the indexed objects of the stores
//!   that are OPEN right now, which is the RAM the server is spending, at
//!   roughly 382 B each.
//!
//! `lfs_bytes` is 0 on every gunnar there is — the server has no LFS store yet —
//! and it is drawn as a real zero with the reason attached rather than hidden,
//! because a meter that vanishes reads as a meter nobody counted.
//!
//! # Activity: what MOVED, beside what is held
//!
//! Every meter above is a **stock** meter, and that is the thing this page
//! could not say until 2026-09-10: a namespace pushed to this morning and one
//! whose last transfer was in March draw an identical page. `Usage.Activity`
//! answers the other question — one row per UTC day that moved something — and
//! the panel draws the last thirty of them as two charts, bytes in and out and
//! then pushes and fetches, with the days transfers were turned away on marked.
//!
//! Three things about it are decisions and not drawing, so all three live in
//! [`crate::activity`] where `cargo test` can reach them:
//!
//! * **"never" is not 1970.** gunnar sends 0 for a namespace nothing has
//!   touched inside the kept window; the "last seen" line says *never*.
//! * **A day with no row is a gap, not a zero.** gunnar writes no row for a
//!   quiet day and the server carries the absence through, so the chart lays
//!   the rows it has into a fixed thirty-day axis and leaves the rest empty.
//! * **An empty chart means one of three different things** — nothing moved,
//!   this box does not count activity, or this could not be read — and a page
//!   with one empty state would tell an operator their busiest namespace is
//!   idle when what happened is that gunnar is older than the ledger.

use leptos::prelude::*;
use leptos_router::components::A;
use leptos_router::hooks::use_params_map;
use serde::Deserialize;

use crate::activity::{
    bars, last_seen_line, no_bars, no_bars_after_refusal, no_bars_detail, no_bars_title, Activity,
    Bar,
};
use crate::api::{segment, Page};
use crate::fmt;
use crate::pages::{now_ms, resolved, when};
use crate::ui::{Empty, FailBox, Hint, Loading, Metric, PageHeader, Panel, Truncation, Unset};

/// **The Room table's types and its decisions live in [`crate::room`]**, not
/// here, and the reason is that this module is `#[cfg(target_arch = "wasm32")]`
/// — nothing in it is compiled by `cargo test` on the host, so a test written
/// beside this view would never run. What is left here is the drawing.
pub use crate::room::{
    box_line, describe, fill_pct, growth_phrase, room_rows, shape_of, Machine, PerMeter, RoomRow,
    Shape, Usage,
};

/// The Room panel: used / allowed / remaining per meter, and the box line.
#[component]
fn Room(usage: Usage) -> impl IntoView {
    let rows = room_rows(&usage);
    let line = box_line(usage.machine.as_ref());
    let why_no_box = usage.box_unreadable.clone();
    view! {
        <Panel title="Room">
            <div class="table-wrap">
                <table class="room">
                    <caption class="vh">"Used, allowed and remaining, per meter"</caption>
                    <thead>
                        <tr>
                            <th scope="col">"Meter"</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.meter;
                                view! {
                                    <tr>
                                        <th scope="row">
                                            {label}
                                            // **The wall, drawn.** A bar with a
                                            // ceiling, under the meter it belongs
                                            // to and only where gunnar said it
                                            // refuses. A reading gets no bar at
                                            // all: a bar IS a ceiling, and drawing
                                            // one where nothing stops is the
                                            // defect this table was carrying.
                                            {match row.fill {
                                                Some(pct) => view! {
                                                    <div
                                                        class="wall"
                                                        role="progressbar"
                                                        aria-label=format!("{label}, against a cap the box refuses at")
                                                        aria-valuenow=pct.to_string()
                                                        aria-valuemin="0"
                                                        aria-valuemax="100"
                                                    >
                                                        <span style=format!("width:{pct}%")></span>
                                                    </div>
                                                }
                                                .into_any(),
                                                None => ().into_any(),
                                            }}
                                        </th>
                                        <td class="num">{row.used}</td>
                                        <td class="num">
                                            {match row.allowed {
                                                Some(cap) => view! { {cap} }.into_any(),
                                                None => view! { "no cap" }.into_any(),
                                            }}
                                        </td>
                                        <td class="num">
                                            {match row.remaining {
                                                Some(left) => view! { {left} }.into_any(),
                                                None => view! {
                                                    <Unset why="nothing this console can read bounds it"/>
                                                }
                                                .into_any(),
                                            }}
                                        </td>
                                        // gunnar's word, in monetize's own
                                        // sentence. Never this console's paraphrase.
                                        <td class="does">
                                            {match row.shape {
                                                Shape::Wall | Shape::Reading => {
                                                    view! { <span>{row.phrase}</span> }.into_any()
                                                }
                                                Shape::NotSaid => view! {
                                                    <Unset why="this Gunnar does not report what a cap on this meter does; \
                                                                it is older than the field that says so, and it still \
                                                                refuses on the meters it always refused on"/>
                                                }
                                                .into_any(),
                                            }}
                                        </td>
                                    </tr>
                                }
                            })
                            .collect_view()}
                    </tbody>
                </table>
            </div>
            <div class="panel-body">
                {match line {
                    Some(line) => view! { <p class="meta box-line">{line}</p> }.into_any(),
                    None => view! {
                        <p class="meta box-line">
                            "This box: "
                            <Unset why=why_no_box.clone()/>
                        </p>
                        <p class="hint" title="So \u{201c}remaining\u{201d} is what each cap leaves and nothing more.">
                            "The box could not be read with this console's key. Gunnar said: " {why_no_box}
                        </p>
                    }
                    .into_any(),
                }}
                <Hint
                    line="A row with a bar is a wall Gunnar refuses at; a row without one is a reading."
                    more="\u{201c}Allowed\u{201d} is the cap this box holds for the meter, or \
                          \u{201c}no cap\u{201d} when nothing caps it. \u{201c}Remaining\u{201d} is \
                          what the cap leaves, and for bytes never more than the box can still \
                          serve; with no cap it is the box alone. \u{201c}At the cap\u{201d} is what \
                          the box does when the number crosses it, and it is not the same for every \
                          meter. A row with a BAR is a wall: Gunnar refuses when over, and the bar \
                          is how full it is. A row with no bar is a reading \u{2014} the number is \
                          counted and reported, and crossing it is not an event here."
                />
            </div>
        </Panel>
    }
}

/// **One chart: thirty slots, two series, one scale.**
///
/// `first` and `second` pick the two percentages off each slot, so the bytes
/// chart and the counts chart are the same component and cannot drift apart.
/// The percentages were computed against one peak per chart in
/// [`crate::activity::bars`] — two bars scaled independently would show a day
/// that served a gigabyte and accepted a kilobyte as two bars of the same
/// height.
#[component]
fn DayChart(
    #[prop(into)] title: String,
    rows: Vec<Bar>,
    first: fn(&Bar) -> u32,
    second: fn(&Bar) -> u32,
) -> impl IntoView {
    let (from, to) = (
        rows.first().map(|b| b.label.clone()).unwrap_or_default(),
        rows.last().map(|b| b.label.clone()).unwrap_or_default(),
    );
    view! {
        <div class="flow">
            <h3>{title}</h3>
            <ol class="days">
                {rows
                    .into_iter()
                    .map(|bar| {
                        // The sentence a screen reader gets, and the one a
                        // pointer gets, are the SAME sentence: a chart whose
                        // meaning is only in a tooltip is a chart half the
                        // operators of this console cannot read.
                        let title = bar.hint.clone();
                        let hint = bar.hint.clone();
                        let marked = if bar.refused > 0 { "refused" } else { "" };
                        view! {
                            <li class=marked title=title>
                                // A day gunnar sent NO row for gets no bar at
                                // all — not a bar of height zero, which reads
                                // as a measurement that was taken.
                                {bar
                                    .moved
                                    .then(|| {
                                        view! {
                                            <span
                                                class="a"
                                                style=format!("height:{}%", first(&bar))
                                            ></span>
                                            <span
                                                class="b"
                                                style=format!("height:{}%", second(&bar))
                                            ></span>
                                        }
                                    })}
                                <span class="vh">{hint}</span>
                            </li>
                        }
                    })
                    .collect_view()}
            </ol>
            <p class="axis">
                <span>{from}</span>
                <span>{to}</span>
            </p>
        </div>
    }
}

/// **The Activity panel: the last thirty UTC days of what moved.**
///
/// Its own read, because it is its own RPC: `Usage.Activity` takes the same
/// grant `Usage.Read` does — `Read` on the namespace — but a page that folded
/// the two into one request would make a gunnar too old to answer the series
/// refuse the meters as well.
///
/// The refusal is therefore handled here rather than by [`resolved`]: one
/// refusal, `Unimplemented`, is a fact about the BOX — a gunnar older than the
/// FLOW ledger — and it must not be drawn as an idle tenant. Every other
/// refusal is *this could not be read*, and gunnar's own words go under it in
/// the usual [`FailBox`].
#[component]
fn Flow(
    namespace: Signal<String>,
    last_push_unix_ms: Option<i64>,
    last_fetch_unix_ms: Option<i64>,
) -> impl IntoView {
    let series = LocalResource::new(move || {
        let url = format!("/api/namespaces/{}/activity", segment(&namespace.get()));
        async move { crate::api::get::<Activity>(&url).await }
    });

    view! {
        <Panel title="Activity">
            <div class="panel-body">
                // The "last seen" pair. It comes off the METERS read, not off
                // the series: it looks back over gunnar's whole kept window and
                // is true for a namespace whose last push was outside the
                // thirty days below.
                <p class="meta">{move || last_seen_line(last_push_unix_ms, last_fetch_unix_ms, now_ms())}</p>
            </div>
            {move || {
                match series.get() {
                    None => view! { <Loading rows=2/> }.into_any(),
                    Some(Err(fail)) => {
                        let state = no_bars_after_refusal(fail.status, &fail.code);
                        view! {
                            <div class="panel-body">
                                // 0 retained days: gunnar refused, so it never
                                // said how far back it remembers, and the two
                                // sentences reachable from a refusal do not
                                // quote a window. Only the QUIET one does, and
                                // it is only reachable from an answer.
                                <Empty
                                    title=no_bars_title(state)
                                    detail=no_bars_detail(state, 0)
                                />
                                <FailBox fail=fail/>
                            </div>
                        }
                            .into_any()
                    }
                    Some(Ok(series)) => {
                        if series.days.is_empty() {
                            let state = no_bars(series.retained_days);
                            return view! {
                                <div class="panel-body">
                                    <Empty
                                        title=no_bars_title(state)
                                        detail=no_bars_detail(state, series.retained_days)
                                    />
                                </div>
                            }
                                .into_any();
                        }
                        let rows = bars(&series, now_ms());
                        view! {
                            <DayChart
                                title="Bytes in and out"
                                rows=rows.clone()
                                first=|b| b.bytes_in_pct
                                second=|b| b.bytes_out_pct
                            />
                            <DayChart
                                title="Pushes and fetches"
                                rows=rows
                                first=|b| b.pushes_pct
                                second=|b| b.fetches_pct
                            />
                            <div class="panel-body">
                                <p class="legend">
                                    <span><i class="a"></i>"in / pushes"</span>
                                    <span><i class="b"></i>"out / fetches"</span>
                                    <span><i class="mark"></i>"transfers refused"</span>
                                </p>
                                <Hint
                                    line="One bar per UTC day, oldest on the left. Hover or focus a day for its figures."
                                    more="The two bars in a chart share a scale so they can be compared. \
                                          A day with no bar moved nothing: Gunnar writes no row for a \
                                          quiet day and this console draws none rather than a zero it \
                                          did not measure. A marked day had transfers turned away — \
                                          hover or focus a day for its figures and the reasons."
                                />
                            </div>
                        }
                            .into_any()
                    }
                }
            }}
        </Panel>
    }
}

/// **The Admission panel**: `Usage.AdmitPlan` — the budget line, then per
/// kind of work the class gunnar would give the next transfer, its estimate
/// and gunnar's own sentence. Its own read, for [`Flow`]'s reason.
#[component]
fn Admission(namespace: Signal<String>) -> impl IntoView {
    let plan = LocalResource::new(move || {
        let url = format!("/api/namespaces/{}/admit", segment(&namespace.get()));
        async move { crate::api::get::<crate::admit::AdmitPlan>(&url).await }
    });
    view! {
        <Panel title="Admission">
            {move || match plan.get() {
                None => view! { <Loading rows=2/> }.into_any(),
                Some(Err(fail)) => view! {
                    <div class="panel-body"><FailBox fail=fail/></div>
                }
                    .into_any(),
                Some(Ok(plan)) => {
                    let line = crate::admit::budget_line(&plan);
                    let rows = plan
                        .kinds
                        .iter()
                        .map(|k| {
                            let estimate = crate::admit::estimate_line(k);
                            view! {
                                <tr>
                                    <td>{k.kind.clone()}</td>
                                    <td>{k.class.clone()}</td>
                                    <td>{estimate}</td>
                                    <td class="meta">{k.why.clone()}</td>
                                </tr>
                            }
                        })
                        .collect_view();
                    view! {
                        <div class="panel-body">
                            <p>{line}</p>
                            <div class="table-wrap">
                                <table class="table">
                                    <thead><tr><th>"work"</th><th>"class"</th><th>"estimate"</th><th>"Gunnar says"</th></tr></thead>
                                    <tbody>{rows}</tbody>
                                </table>
                            </div>
                        </div>
                    }
                        .into_any()
                }
            }}
        </Panel>
    }
}

/// A namespace row, from `GET /api/namespaces`.
#[derive(Debug, Clone, Deserialize, PartialEq)]
pub struct Namespace {
    pub name: String,
    #[serde(default)]
    pub description: String,
    /// gunnar's kind of the principal that owns it (`user`, `machine`, …).
    #[serde(default)]
    pub owner_kind: String,
}

/// `/usage` — pick a namespace.
///
/// A list rather than a dropdown: the names are links, so one can be bookmarked
/// and sent to whoever the namespace belongs to.
#[component]
pub fn UsageIndex() -> impl IntoView {
    let namespaces = LocalResource::new(|| async {
        crate::api::get::<Page<Namespace>>("/api/namespaces").await
    });
    // The box's own namespaces — owned by a principal that is not a person,
    // by gunnar's `owner_kind` (`nav::is_system_namespace`) — are hidden until
    // somebody asks for them.
    let show_system = RwSignal::new(false);

    view! {
        <PageHeader
            title="Usage"
        >
            <label class="toggle">
                <input type="checkbox" prop:checked=move || show_system.get() on:change=move |ev| show_system.set(event_target_checked(&ev))/>
                " Show system principals"
            </label>
        </PageHeader>
        {move || resolved(namespaces.get(), None, |page: Page<Namespace>| {
            let all = show_system.get();
            let page = Page {
                items: page
                    .items
                    .into_iter()
                    .filter(|n| all || !crate::nav::is_system_namespace(&n.owner_kind))
                    .collect(),
                ..page
            };
            if page.items.is_empty() {
                return view! {
                    <Empty
                        title="No namespaces yet"
                        detail="A namespace appears here once one exists to measure."
                    />
                }
                .into_any();
            }
            let rows = page.items.clone();
            let (shown, total) = (rows.len(), page.total);
            view! {
                <Panel title="Namespaces">
                    <div class="table-wrap">
                        <table>
                            <caption class="vh">"Namespaces"</caption>
                            <thead>
                                <tr>
                                    <th scope="col">"Namespace"</th>
                                    <th scope="col">"Description"</th>
                                </tr>
                            </thead>
                            <tbody>
                                {rows
                                    .into_iter()
                                    .map(|ns| {
                                        let href = format!("/usage/{}", segment(&ns.name));
                                        view! {
                                            <tr>
                                                <td class="mono">
                                                    <A href=href>{ns.name.clone()}</A>
                                                </td>
                                                <td>
                                                    {if ns.description.is_empty() {
                                                        view! { <Unset why="no description"/> }.into_any()
                                                    } else {
                                                        view! { {ns.description.clone()} }.into_any()
                                                    }}
                                                </td>
                                            </tr>
                                        }
                                    })
                                    .collect_view()}
                            </tbody>
                        </table>
                    </div>
                    <Truncation shown=shown total=total/>
                </Panel>
            }
            .into_any()
        })}
    }
}

/// `/usage/:namespace` — one namespace's meters.
///
/// A refusal is rendered as a refusal. gunnar answers `PermissionDenied` for a
/// namespace the caller may not read and the console passes its words through,
/// so this page never shows a namespace as empty when what it means is that it
/// was not allowed to look.
#[component]
pub fn NamespaceUsage() -> impl IntoView {
    let params = use_params_map();
    let namespace = Memo::new(move |_| params.read().get("namespace").unwrap_or_default());

    let usage = LocalResource::new(move || {
        let url = format!("/api/namespaces/{}/usage", segment(&namespace.get()));
        async move { crate::api::get::<Usage>(&url).await }
    });

    // A namespace that does not exist for this caller — absent, or private
    // and not theirs; the server answers both with the same 404 — draws
    // "does not exist" and nothing else: no header naming it, no panes.
    let absent = move || crate::gate::gate(&usage.get()) == crate::gate::Gate::Absent;
    view! {
        <Show when=move || !absent() fallback=|| view! { <super::NotHere/> }>
        <PageHeader
            title=move || namespace.get()
        >
            <A href="/usage" attr:class="btn quiet">"All namespaces"</A>
        </PageHeader>

        {move || {
            let now = now_ms();
            resolved(usage.get(), None, move |usage: Usage| {
                view! {
                    <div class="stack">
                        <Room usage=usage.clone()/>

                        // **Directly under the number that made somebody want
                        // it**, and only in a `metered` build: the community
                        // console does not contain the panel at all, so this
                        // page keeps its promise — "nothing here is a price" —
                        // by construction rather than by a runtime check.
                        //
                        // Even in a metered build the panel renders NOTHING
                        // unless the server has a monetize bearer. `Room` above
                        // it is unaffected either way: a cap is a resource
                        // number, and the table is the same table for a
                        // self-hoster who will never pay anyone.
                        {
                            // Not on a SOLD (provided) Gunnar (one shop,
                            // 2026-10-07): nothing is bought there.
                            #[cfg(feature = "metered")]
                            {
                                let session = crate::app::session();
                                move || session.get().filter(|s| s.sells_here()).map(|_| view! {
                                    <crate::pages::increase::MoreRoom
                                        namespace=Signal::derive(move || namespace.get())
                                    />
                                })
                            }
                        }

                        // **What moved**, under what is held. Below the Room
                        // table because a tenant reading this page has come for
                        // the disk; above the per-tier breakdown because "am I
                        // still being used, and am I hitting walls" is the
                        // question the tiers cannot answer at all.
                        <Flow
                            namespace=Signal::derive(move || namespace.get())
                            last_push_unix_ms=usage.last_push_unix_ms
                            last_fetch_unix_ms=usage.last_fetch_unix_ms
                        />

                        <Admission namespace=Signal::derive(move || namespace.get())/>

                        <Panel title="On disk">
                            <dl class="metrics">
                                <Metric
                                    label="Not re-derivable"
                                    value=fmt::bytes(usage.pack_bytes)
                                    unit="packs and their indexes"
                                />
                                <Metric
                                    label="Read cache"
                                    value=fmt::bytes(usage.cache_bytes)
                                    unit="re-derivable"
                                />
                                <Metric
                                    label="Cold tier"
                                    value=fmt::bytes(usage.cold_bytes)
                                    unit="archived"
                                />
                                <Metric
                                    label="LFS"
                                    value=fmt::bytes(usage.lfs_bytes)
                                    unit="Gunnar has no LFS store yet"
                                />
                            </dl>
                            <Hint
                                line="\"Not re-derivable\" is the number that matters: the rest can be rebuilt from it."
                                more="Everything else on this row can be rebuilt from it. The read cache \
                                      is there because it is on the disk today, not because it is owed \
                                      — Gunnar will make it again if it is dropped."
                            />
                        </Panel>

                        <Panel title="Held after a delete">
                            <div class="panel-body">
                                <dl class="metrics">
                                    <Metric
                                        label="Tombstoned"
                                        value=fmt::bytes(usage.tombstoned_bytes)
                                        unit="not in the numbers above"
                                    />
                                </dl>
                                {if usage.tombstoned_bytes > 0 {
                                    view! {
                                        <Hint
                                            line="Deleted repositories hold their bytes until the retention window closes."
                                            more="This is the answer to \u{201c}I deleted a repository and my \
                                                  usage did not go down\u{201d}. A delete is a SOFT delete: \
                                                  the name stops working immediately and the bytes stay \
                                                  until the retention window closes. A tombstoned \
                                                  repository has no catalog row, so these bytes are in \
                                                  none of the meters above."
                                        />
                                    }
                                    .into_any()
                                } else {
                                    view! {
                                        <p class="hint">
                                            "Nothing deleted is still being held. When something \
                                             is, its bytes appear here and in none of the meters \
                                             above."
                                        </p>
                                    }
                                    .into_any()
                                }}
                            </div>
                        </Panel>

                        <Panel title="What is in it">
                            <dl class="metrics">
                                <Metric label="Repositories" value=fmt::count(usage.repos)/>
                                <Metric
                                    label="Indexed objects"
                                    value=fmt::count(usage.objects)
                                    unit="in stores open right now"
                                />
                            </dl>
                            <div class="panel-body">
                                <p class="meta">
                                    "Measured "{move || when(usage.measured_at_unix_ms, now)}
                                </p>
                                <Hint
                                    line="Indexed objects is not a byte count: it is the stores OPEN right now."
                                    more="It is the objects indexed by the stores that are OPEN in the \
                                          server right now — roughly 382 B of RAM each — so it falls when \
                                          a store is closed without anything leaving the disk."
                                />
                            </div>
                        </Panel>
                    </div>
                }
            })
        }}
        </Show>
    }
}