FleetHealth PR 2 (frontend): human-oriented landing + advanced menu
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Successful in 18s

Reworks the SPA around the new DashboardStatus fields. The
landing is now a fleet-health dashboard aimed at a layperson —
disk gauges, mount ✓/✗, cache hit rate, next scheduled job.
No hex, no primitives.

Nav restructure:
* Primary: 'Fleet health' (just the landing).
* 'View Advanced ▾' dropdown reveals: Blobs / Tags / Refs /
  Snapshots / Ref-tracking. Routes moved under /advanced/*.

New components:
* StorageBar — horizontal used/total bar with pinned/evictable
  split, health-color threshold at 60/85%.
* NodeCard — traffic-light dot + disk + hot tier bars + mount
  state + cache hit rate + next-timer countdown. Whole card
  is a link into node detail.

New CommandCenter:
* Fleet-wide storage roll-up card (sum of every node's disk).
* Grid of NodeCards.
* 10s poll cadence retained from previous version.

Existing StorageBrowser + RefTrackingPage moved behind
/advanced/* routes; internal component code untouched.
This commit is contained in:
Omar Sobh
2026-07-14 17:23:25 -07:00
parent 9d6badf2aa
commit 22af481c3e
10 changed files with 479 additions and 157 deletions
+11 -15
View File
@@ -1,13 +1,9 @@
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
import { useEffect, useState } from 'react';
import { api, fmtAge, fmtBytes } from '../lib/api';
import { api, fmtBytes, fmtAge } from '../lib/api';
import { NodeCard } from '../components/NodeCard';
import { StatTile } from '../components/StatTile';
// Fleet command center — the landing pane.
//
// Aggregator polls each peer via cluster RPC every 10 s. Numbers
// are ground truth from the daemons themselves; no local
// blob-store reads on the aggregator.
// FleetHealth landing — human-oriented single-pane-of-glass.
// Polls the aggregator's /api/v2/fleet every 10 s.
export function CommandCenter() {
const [fleet, setFleet] = useState(null);
const [err, setErr] = useState(null);
@@ -27,14 +23,14 @@ export function CommandCenter() {
}, []);
const totals = fleet
? fleet.nodes.reduce((a, n) => ({
blobs: a.blobs + n.blob_count,
tags: a.tags + n.tag_count,
refs: a.refs + n.ref_count,
snapshots: a.snapshots + n.snapshot_count,
bytes: a.bytes + n.blob_store_bytes,
diskUsed: a.diskUsed + (n.filesystem?.used_bytes ?? 0),
diskTotal: a.diskTotal + (n.filesystem?.total_bytes ?? 0),
hotUsed: a.hotUsed + (n.hot?.used_bytes ?? 0),
hotMax: a.hotMax + (n.hot?.max_bytes ?? 0),
online: a.online + (n.online ? 1 : 0),
}), { blobs: 0, tags: 0, refs: 0, snapshots: 0, bytes: 0, online: 0 })
mounted: a.mounted + (n.mount?.active ? 1 : 0),
}), { diskUsed: 0, diskTotal: 0, hotUsed: 0, hotMax: 0, online: 0, mounted: 0 })
: null;
return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold text-slate-100", children: "Fleet" }), _jsxs("div", { className: "text-sm text-slate-500", children: ["aggregated by", ' ', _jsx("span", { className: "font-mono text-slate-300", children: fleet?.aggregator_name ?? '…' }), fleet && (_jsxs(_Fragment, { children: [' · updated ', _jsx("span", { className: "font-mono", children: fmtAge(fleet.fetched_at_unix) }), ' · ', _jsx("span", { className: "text-emerald-300", children: totals?.online }), "/", fleet.nodes.length, " online"] }))] })] }), err && (_jsx("div", { className: "rounded border border-red-800 bg-red-950/40 p-3 text-red-300 text-sm", children: err })), totals && fleet && (_jsxs("section", { children: [_jsx("h2", { className: "text-lg font-semibold text-slate-100 mb-3", children: "Totals (fleet-wide)" }), _jsxs("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3", children: [_jsx(StatTile, { label: "online nodes", value: `${totals.online}/${fleet.nodes.length}`, color: totals.online === fleet.nodes.length ? 'ok' : 'warn' }), _jsx(StatTile, { label: "blobs", value: totals.blobs.toLocaleString(), color: "ok" }), _jsx(StatTile, { label: "tags", value: totals.tags, color: "ok" }), _jsx(StatTile, { label: "refs", value: totals.refs, color: "ok" }), _jsx(StatTile, { label: "total bytes", value: fmtBytes(totals.bytes), color: "ok" })] })] })), _jsxs("section", { children: [_jsx("h2", { className: "text-lg font-semibold text-slate-100 mb-3", children: "Nodes" }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4", children: [fleet?.nodes.map((n) => (_jsx(NodeCard, { node: n }, n.node_name))), !fleet &&
[1, 2, 3].map((i) => (_jsx("div", { className: "rounded-lg border border-slate-800 bg-slate-900/40 p-5 h-40 animate-pulse" }, i)))] })] })] }));
return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold text-slate-100", children: "Fleet health" }), _jsx("div", { className: "text-sm text-slate-500 mt-1", children: fleet && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-emerald-300", children: totals?.online }), "/", fleet.nodes.length, " nodes online", ' · ', totals?.mounted, "/", fleet.nodes.length, " mounted", ' · ', "updated ", _jsx("span", { className: "font-mono", children: fmtAge(fleet.fetched_at_unix) }), ' · ', "hosted by ", _jsx("span", { className: "font-mono text-slate-300", children: fleet.aggregator_name })] })) })] }), err && (_jsx("div", { className: "rounded border border-red-800 bg-red-950/40 p-3 text-red-300 text-sm", children: err })), totals && totals.diskTotal > 0 && (_jsxs("div", { className: "rounded-lg border border-slate-800 bg-slate-900/60 p-4 flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("div", { className: "text-xs uppercase tracking-wider text-slate-500", children: "Fleet-wide storage" }), _jsxs("div", { className: "text-2xl font-semibold font-mono mt-1", children: [fmtBytes(totals.diskUsed), ' ', _jsxs("span", { className: "text-slate-500 text-lg", children: ["/ ", fmtBytes(totals.diskTotal)] })] })] }), _jsxs("div", { className: "text-right text-slate-400 text-sm", children: [_jsxs("div", { children: [Math.round((totals.diskUsed / totals.diskTotal) * 100), "% used"] }), _jsxs("div", { className: "text-xs text-slate-500 mt-1", children: ["hot tier: ", fmtBytes(totals.hotUsed), " / ", fmtBytes(totals.hotMax)] })] })] })), _jsxs("section", { children: [_jsx("h2", { className: "text-lg font-semibold text-slate-100 mb-3", children: "Nodes" }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4", children: [fleet?.nodes.map((n) => (_jsx(NodeCard, { node: n }, n.node_name))), !fleet &&
[1, 2, 3].map((i) => (_jsx("div", { className: "rounded-lg border border-slate-800 bg-slate-900/40 p-5 h-64 animate-pulse" }, i)))] })] })] }));
}
+36 -36
View File
@@ -1,13 +1,9 @@
import { useEffect, useState } from 'react';
import { api, FleetSnapshot, fmtAge, fmtBytes } from '../lib/api';
import { api, FleetSnapshot, fmtBytes, fmtAge } from '../lib/api';
import { NodeCard } from '../components/NodeCard';
import { StatTile } from '../components/StatTile';
// Fleet command center — the landing pane.
//
// Aggregator polls each peer via cluster RPC every 10 s. Numbers
// are ground truth from the daemons themselves; no local
// blob-store reads on the aggregator.
// FleetHealth landing — human-oriented single-pane-of-glass.
// Polls the aggregator's /api/v2/fleet every 10 s.
export function CommandCenter() {
const [fleet, setFleet] = useState<FleetSnapshot | null>(null);
const [err, setErr] = useState<string | null>(null);
@@ -31,33 +27,32 @@ export function CommandCenter() {
const totals = fleet
? fleet.nodes.reduce(
(a, n) => ({
blobs: a.blobs + n.blob_count,
tags: a.tags + n.tag_count,
refs: a.refs + n.ref_count,
snapshots: a.snapshots + n.snapshot_count,
bytes: a.bytes + n.blob_store_bytes,
diskUsed: a.diskUsed + (n.filesystem?.used_bytes ?? 0),
diskTotal: a.diskTotal + (n.filesystem?.total_bytes ?? 0),
hotUsed: a.hotUsed + (n.hot?.used_bytes ?? 0),
hotMax: a.hotMax + (n.hot?.max_bytes ?? 0),
online: a.online + (n.online ? 1 : 0),
mounted: a.mounted + (n.mount?.active ? 1 : 0),
}),
{ blobs: 0, tags: 0, refs: 0, snapshots: 0, bytes: 0, online: 0 }
{ diskUsed: 0, diskTotal: 0, hotUsed: 0, hotMax: 0, online: 0, mounted: 0 }
)
: null;
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold text-slate-100">Fleet</h1>
<div className="text-sm text-slate-500">
aggregated by{' '}
<span className="font-mono text-slate-300">
{fleet?.aggregator_name ?? '…'}
</span>
<h1 className="text-2xl font-semibold text-slate-100">Fleet health</h1>
<div className="text-sm text-slate-500 mt-1">
{fleet && (
<>
{' · updated '}
<span className="font-mono">{fmtAge(fleet.fetched_at_unix)}</span>
{' · '}
<span className="text-emerald-300">{totals?.online}</span>
/{fleet.nodes.length} online
/{fleet.nodes.length} nodes online
{' · '}
{totals?.mounted}/{fleet.nodes.length} mounted
{' · '}
updated <span className="font-mono">{fmtAge(fleet.fetched_at_unix)}</span>
{' · '}
hosted by <span className="font-mono text-slate-300">{fleet.aggregator_name}</span>
</>
)}
</div>
@@ -69,19 +64,24 @@ export function CommandCenter() {
</div>
)}
{totals && fleet && (
<section>
<h2 className="text-lg font-semibold text-slate-100 mb-3">
Totals (fleet-wide)
</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
<StatTile label="online nodes" value={`${totals.online}/${fleet.nodes.length}`} color={totals.online === fleet.nodes.length ? 'ok' : 'warn'} />
<StatTile label="blobs" value={totals.blobs.toLocaleString()} color="ok" />
<StatTile label="tags" value={totals.tags} color="ok" />
<StatTile label="refs" value={totals.refs} color="ok" />
<StatTile label="total bytes" value={fmtBytes(totals.bytes)} color="ok" />
{totals && totals.diskTotal > 0 && (
<div className="rounded-lg border border-slate-800 bg-slate-900/60 p-4 flex items-center justify-between">
<div>
<div className="text-xs uppercase tracking-wider text-slate-500">
Fleet-wide storage
</div>
<div className="text-2xl font-semibold font-mono mt-1">
{fmtBytes(totals.diskUsed)}{' '}
<span className="text-slate-500 text-lg">/ {fmtBytes(totals.diskTotal)}</span>
</div>
</div>
</section>
<div className="text-right text-slate-400 text-sm">
<div>{Math.round((totals.diskUsed / totals.diskTotal) * 100)}% used</div>
<div className="text-xs text-slate-500 mt-1">
hot tier: {fmtBytes(totals.hotUsed)} / {fmtBytes(totals.hotMax)}
</div>
</div>
</div>
)}
<section>
@@ -94,7 +94,7 @@ export function CommandCenter() {
[1, 2, 3].map((i) => (
<div
key={i}
className="rounded-lg border border-slate-800 bg-slate-900/40 p-5 h-40 animate-pulse"
className="rounded-lg border border-slate-800 bg-slate-900/40 p-5 h-64 animate-pulse"
/>
))}
</div>