dashboard-v2 PR 2: frontend SPA + serve integration
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Successful in 16s
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user