dashboard-v2 PR 2: frontend SPA + serve integration
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Successful in 16s

React 19 + Vite + Tailwind + wouter (tiny router, no external
state library). Consumes the /api/v2/* endpoints shipped in PR 1.
Serves under /v2/* so the legacy dashboard at / stays live.

Pages:
* CommandCenter (/)      — fleet strip + this-node stat tiles
* NodeDetail (/nodes/:name) — per-node deep dive
* StorageBrowser (/storage/{blobs,tags,refs,snapshots}) — tables
  with prefix filter
* RefTrackingPage (/refs/tracking) — grouped by repo

Backend changes:
* claw-store serve grows --v2-static-dir <path>
* build_app split into build_app_with_v2 for the extra static
  mount
* /v2/* falls through to index.html so wouter client routing works

New systemd unit: clawstor-dashboard.service. Points at both
static dirs; installs on any node.

dashboard/ (legacy) untouched. dashboard-v2/ built to
target/dashboard-v2/dist for deploy.

Deploy sequence per node:
1. cp target/release/claw-store  ~/clawstor-deploy/
2. rsync dashboard-v2/dist/      ~/clawstor-deploy/dashboard-v2/
3. cp deploy/systemd/clawstor-dashboard.service ~/.config/systemd/user/
4. systemctl --user daemon-reload && enable --now clawstor-dashboard.service

Cross-node fan-out for /api/v2/node/:name/status is PR 3.
Action POSTs (scrub/gc/snapshot/pin) are PR 4.
This commit is contained in:
Omar Sobh
2026-07-14 15:59:47 -07:00
parent b431475af7
commit f33468b7c2
32 changed files with 3264 additions and 5 deletions
+62
View File
@@ -0,0 +1,62 @@
import { Route, Switch, Link, useLocation } from 'wouter';
import { CommandCenter } from './pages/CommandCenter';
import { NodeDetail } from './pages/NodeDetail';
import { StorageBrowser } from './pages/StorageBrowser';
import { RefTrackingPage } from './pages/RefTrackingPage';
export default function App() {
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>
<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">
{(params) => <StorageBrowser tab={params.tab as any} />}
</Route>
<Route path="/refs/tracking" component={RefTrackingPage} />
<Route>
<div className="text-slate-400 py-12 text-center">404</div>
</Route>
</Switch>
</main>
</div>
);
}