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.
75 lines
2.4 KiB
TypeScript
75 lines
2.4 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Link } from 'wouter';
|
|
import { api, NodeStatusV2, fmtBytes } from '../lib/api';
|
|
import { StatTile } from '../components/StatTile';
|
|
|
|
interface Props {
|
|
name: string;
|
|
}
|
|
|
|
export function NodeDetail({ name }: Props) {
|
|
const [status, setStatus] = useState<NodeStatusV2 | null>(null);
|
|
const [err, setErr] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
api
|
|
.nodeStatus(name)
|
|
.then((n) => {
|
|
setStatus(n);
|
|
setErr(null);
|
|
})
|
|
.catch((e) => setErr(String(e)));
|
|
}, [name]);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<Link href="/">
|
|
<a className="text-sm text-slate-500 hover:text-slate-300">
|
|
← fleet
|
|
</a>
|
|
</Link>
|
|
<h1 className="text-2xl font-semibold text-slate-100 mt-2">
|
|
{name}
|
|
</h1>
|
|
</div>
|
|
|
|
{err && (
|
|
<div className="rounded border border-amber-800 bg-amber-950/40 p-3 text-amber-300 text-sm">
|
|
{err}
|
|
<div className="mt-2 text-xs text-slate-400">
|
|
Cross-node lookup lands in a follow-on PR. Until then this
|
|
page shows detail only when you're already viewing the
|
|
dashboard hosted by {name}. Try opening{' '}
|
|
<span className="font-mono">http://{name}:7700/v2/#/nodes/{name}</span>{' '}
|
|
directly.
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{status && (
|
|
<>
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
|
|
<StatTile label="blobs" value={status.blob_count.toLocaleString()} color="ok" />
|
|
<StatTile label="tags" value={status.tag_count} color="ok" />
|
|
<StatTile label="refs" value={status.ref_count} color="ok" />
|
|
<StatTile label="snapshots" value={status.snapshot_count} color="ok" />
|
|
<StatTile label="ref-tracking" value={status.ref_tracking_count} color="ok" />
|
|
<StatTile
|
|
label="store size"
|
|
value={fmtBytes(status.blob_store_bytes)}
|
|
color="ok"
|
|
/>
|
|
</div>
|
|
<div className="rounded border border-slate-800 bg-slate-900/40 p-4 text-sm">
|
|
<div className="text-slate-500 uppercase text-xs tracking-wider">
|
|
blob store root
|
|
</div>
|
|
<div className="font-mono mt-1">{status.blob_store_root ?? '—'}</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|