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,95 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { api, RefTrackingItem, fmtAge } from '../lib/api';
|
||||
|
||||
export function RefTrackingPage() {
|
||||
const [rows, setRows] = useState<RefTrackingItem[]>([]);
|
||||
const [repoFilter, setRepoFilter] = useState('');
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.refTracking(repoFilter)
|
||||
.then(setRows)
|
||||
.catch((e) => setErr(String(e)));
|
||||
}, [repoFilter]);
|
||||
|
||||
// Group by repo for a cleaner display.
|
||||
const grouped = useMemo(() => {
|
||||
const g = new Map<string, RefTrackingItem[]>();
|
||||
for (const r of rows) {
|
||||
const arr = g.get(r.repo) ?? [];
|
||||
arr.push(r);
|
||||
g.set(r.repo, arr);
|
||||
}
|
||||
return [...g.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
||||
}, [rows]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-slate-100">Ref-tracking</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
Every cached fingerprint's producing <span className="font-mono">(repo, git-ref)</span>.
|
||||
Feeds the nightly <span className="font-mono">cluster-ref-sweep</span> that reaps
|
||||
fingerprints whose refs are gone from Gitea.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<input
|
||||
value={repoFilter}
|
||||
onChange={(e) => setRepoFilter(e.target.value)}
|
||||
placeholder="filter repo — e.g. clawverse/clawstor"
|
||||
className="w-full sm:w-96 rounded bg-slate-900 border border-slate-800 px-3 py-2 text-sm font-mono"
|
||||
/>
|
||||
|
||||
{err && (
|
||||
<div className="rounded border border-red-800 bg-red-950/40 p-3 text-red-300 text-sm">
|
||||
{err}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{grouped.length === 0 && (
|
||||
<div className="text-slate-500 text-sm py-6">no ref-tracking entries yet</div>
|
||||
)}
|
||||
|
||||
{grouped.map(([repo, items]) => (
|
||||
<div key={repo} className="rounded border border-slate-800 overflow-hidden">
|
||||
<div className="bg-slate-900/60 px-4 py-2 flex items-baseline justify-between">
|
||||
<span className="font-mono text-sm text-emerald-300">{repo}</span>
|
||||
<span className="text-xs text-slate-500">
|
||||
{items.length} fingerprint{items.length === 1 ? '' : 's'}
|
||||
</span>
|
||||
</div>
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-slate-500 uppercase text-xs tracking-wider">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-2 font-normal">fingerprint</th>
|
||||
<th className="text-left px-4 py-2 font-normal">refs</th>
|
||||
<th className="text-left px-4 py-2 font-normal">first seen</th>
|
||||
<th className="text-left px-4 py-2 font-normal">last seen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((it) => (
|
||||
<tr key={it.fingerprint_hex} className="border-t border-slate-900">
|
||||
<td className="px-4 py-2 font-mono text-xs">
|
||||
{it.fingerprint_hex.slice(0, 24)}…
|
||||
</td>
|
||||
<td className="px-4 py-2 font-mono text-xs">
|
||||
{it.refs.join(', ')}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-slate-400">
|
||||
{fmtAge(it.first_seen_unix)}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-slate-400">
|
||||
{fmtAge(it.last_seen_unix)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user