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
+103 -41
View File
@@ -1,13 +1,13 @@
import { useState } from 'react';
import { Route, Switch, Link, Router, useLocation } from 'wouter';
import { CommandCenter } from './pages/CommandCenter';
import { NodeDetail } from './pages/NodeDetail';
import { StorageBrowser } from './pages/StorageBrowser';
import { RefTrackingPage } from './pages/RefTrackingPage';
// Base path — matches the deploy mount. When served locally at
// :7700/v2/ the base is /v2; via Tailscale it's /clawstor.
// Detected from the browser's current pathname so a single SPA
// build works both places without env-var wiring.
// Base path — matches the deploy mount. Detected from
// window.location so a single SPA build serves both local
// (:7700/v2/) and Tailscale (/clawstor).
const BASE = (() => {
if (typeof window === 'undefined') return '';
const p = window.location.pathname;
@@ -25,53 +25,22 @@ export default function App() {
}
function Shell() {
const [loc] = useLocation();
const tab = (path: string, label: string) => {
const active = loc === path || (path !== '/' && loc.startsWith(path));
return (
<Link href={path}>
<a
className={[
'px-3 py-2 text-sm border-b-2 transition-colors',
active
? 'border-emerald-400 text-emerald-300'
: 'border-transparent text-slate-400 hover:text-slate-100',
].join(' ')}
>
{label}
</a>
</Link>
);
};
return (
<div className="min-h-screen">
<header className="border-b border-slate-800 bg-slate-950/80 backdrop-blur sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-4 flex items-center gap-6">
<Link href="/">
<a className="font-mono text-lg text-emerald-300 py-3">
clawstor · command center
</a>
</Link>
<nav className="flex">
{tab('/', 'Overview')}
{tab('/storage/blobs', 'Blobs')}
{tab('/storage/tags', 'Tags')}
{tab('/storage/refs', 'Refs')}
{tab('/storage/snapshots', 'Snapshots')}
{tab('/refs/tracking', 'Ref-tracking')}
</nav>
</div>
</header>
<NavBar />
<main className="max-w-7xl mx-auto p-4">
<Switch>
<Route path="/" component={CommandCenter} />
<Route path="/nodes/:name">
{(params) => <NodeDetail name={params.name} />}
</Route>
<Route path="/storage/:tab">
<Route path="/advanced">
<AdvancedRedirect />
</Route>
<Route path="/advanced/:tab">
{(params) => <StorageBrowser tab={params.tab as any} />}
</Route>
<Route path="/refs/tracking" component={RefTrackingPage} />
<Route path="/advanced/refs/tracking" component={RefTrackingPage} />
<Route>
<div className="text-slate-400 py-12 text-center">404</div>
</Route>
@@ -80,3 +49,96 @@ function Shell() {
</div>
);
}
function NavBar() {
const [loc] = useLocation();
const [advOpen, setAdvOpen] = useState(false);
const advActive = loc.startsWith('/advanced');
const primary = [{ path: '/', label: 'Fleet health' }];
const advanced = [
{ path: '/advanced/blobs', label: 'Blobs' },
{ path: '/advanced/tags', label: 'Tags' },
{ path: '/advanced/refs', label: 'Refs' },
{ path: '/advanced/snapshots', label: 'Snapshots' },
{ path: '/advanced/refs/tracking', label: 'Ref-tracking' },
];
return (
<header className="border-b border-slate-800 bg-slate-950/80 backdrop-blur sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-4 flex items-center gap-6">
<Link href="/">
<a className="font-mono text-lg text-emerald-300 py-3">
clawstor · command center
</a>
</Link>
<nav className="flex items-center">
{primary.map((t) => (
<Tab key={t.path} path={t.path} label={t.label} active={loc === t.path} />
))}
<div
className="relative"
onMouseEnter={() => setAdvOpen(true)}
onMouseLeave={() => setAdvOpen(false)}
>
<span
className={[
'px-3 py-2 text-sm border-b-2 cursor-pointer transition-colors select-none',
advActive
? 'border-emerald-400 text-emerald-300'
: 'border-transparent text-slate-400 hover:text-slate-100',
].join(' ')}
>
View Advanced
</span>
{advOpen && (
<div className="absolute left-0 top-full mt-0 bg-slate-900 border border-slate-800 rounded shadow-lg min-w-[12rem] z-20">
{advanced.map((t) => (
<Link key={t.path} href={t.path}>
<a
className={[
'block px-3 py-2 text-sm hover:bg-slate-800',
loc === t.path ? 'text-emerald-300' : 'text-slate-300',
].join(' ')}
>
{t.label}
</a>
</Link>
))}
</div>
)}
</div>
</nav>
</div>
</header>
);
}
function Tab({
path,
label,
active,
}: {
path: string;
label: string;
active: boolean;
}) {
return (
<Link href={path}>
<a
className={[
'px-3 py-2 text-sm border-b-2 transition-colors',
active
? 'border-emerald-400 text-emerald-300'
: 'border-transparent text-slate-400 hover:text-slate-100',
].join(' ')}
>
{label}
</a>
</Link>
);
}
function AdvancedRedirect() {
const [, nav] = useLocation();
nav('/advanced/blobs', { replace: true });
return null;
}