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,44 @@
|
||||
// Thin fetch wrappers around /api/v2/*. Kept minimal — no state
|
||||
// library; each page owns its own useEffect + useState.
|
||||
// The backend runs on the same origin the SPA loads from, so
|
||||
// absolute URLs are unnecessary. Dev proxy handles the :5173 →
|
||||
// :7700 hop.
|
||||
async function get(path) {
|
||||
const resp = await fetch(path, { headers: { Accept: 'application/json' } });
|
||||
if (!resp.ok) {
|
||||
throw new Error(`${path} → ${resp.status} ${resp.statusText}`);
|
||||
}
|
||||
return resp.json();
|
||||
}
|
||||
export const api = {
|
||||
nodeStatus: (name = 'local') => get(`/api/v2/node/${name}/status`),
|
||||
blobs: (limit = 200, offset = 0) => get(`/api/v2/storage/blobs?limit=${limit}&offset=${offset}`),
|
||||
tags: (prefix = '') => get(`/api/v2/storage/tags?prefix=${encodeURIComponent(prefix)}`),
|
||||
refs: (limit = 200, offset = 0) => get(`/api/v2/storage/refs?limit=${limit}&offset=${offset}`),
|
||||
snapshots: () => get(`/api/v2/storage/snapshots`),
|
||||
refTracking: (repo = '') => get(`/api/v2/storage/ref-tracking?repo=${encodeURIComponent(repo)}`),
|
||||
};
|
||||
/** Format bytes as MB / GB / TB as needed. */
|
||||
export function fmtBytes(n) {
|
||||
if (n < 1024)
|
||||
return `${n} B`;
|
||||
if (n < 1024 * 1024)
|
||||
return `${(n / 1024).toFixed(1)} KiB`;
|
||||
if (n < 1024 * 1024 * 1024)
|
||||
return `${(n / (1024 * 1024)).toFixed(1)} MiB`;
|
||||
if (n < 1024 * 1024 * 1024 * 1024)
|
||||
return `${(n / (1024 * 1024 * 1024)).toFixed(1)} GiB`;
|
||||
return `${(n / (1024 * 1024 * 1024 * 1024)).toFixed(2)} TiB`;
|
||||
}
|
||||
/** Human-friendly relative time. */
|
||||
export function fmtAge(unix) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const diff = now - unix;
|
||||
if (diff < 60)
|
||||
return `${diff}s ago`;
|
||||
if (diff < 3600)
|
||||
return `${Math.floor(diff / 60)}m ago`;
|
||||
if (diff < 86400)
|
||||
return `${Math.floor(diff / 3600)}h ago`;
|
||||
return `${Math.floor(diff / 86400)}d ago`;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Thin fetch wrappers around /api/v2/*. Kept minimal — no state
|
||||
// library; each page owns its own useEffect + useState.
|
||||
|
||||
export interface NodeStatusV2 {
|
||||
node_name: string;
|
||||
blob_store_root: string | null;
|
||||
blob_count: number;
|
||||
tag_count: number;
|
||||
ref_count: number;
|
||||
snapshot_count: number;
|
||||
ref_tracking_count: number;
|
||||
blob_store_bytes: number;
|
||||
}
|
||||
|
||||
export interface BlobSummary {
|
||||
blob_id_hex: string;
|
||||
size_bytes: number;
|
||||
chunk_count: number;
|
||||
}
|
||||
|
||||
export interface TagSummary {
|
||||
key: string;
|
||||
value_hex: string;
|
||||
}
|
||||
|
||||
export interface RefSummary {
|
||||
fingerprint_hex: string;
|
||||
blob_id_hex: string;
|
||||
}
|
||||
|
||||
export interface SnapshotSummary {
|
||||
name: string;
|
||||
created_at_unix: number;
|
||||
blob_count: number;
|
||||
file_bytes: number;
|
||||
}
|
||||
|
||||
export interface RefTrackingItem {
|
||||
fingerprint_hex: string;
|
||||
repo: string;
|
||||
refs: string[];
|
||||
first_seen_unix: number;
|
||||
last_seen_unix: number;
|
||||
}
|
||||
|
||||
// The backend runs on the same origin the SPA loads from, so
|
||||
// absolute URLs are unnecessary. Dev proxy handles the :5173 →
|
||||
// :7700 hop.
|
||||
async function get<T>(path: string): Promise<T> {
|
||||
const resp = await fetch(path, { headers: { Accept: 'application/json' } });
|
||||
if (!resp.ok) {
|
||||
throw new Error(`${path} → ${resp.status} ${resp.statusText}`);
|
||||
}
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
export const api = {
|
||||
nodeStatus: (name = 'local') => get<NodeStatusV2>(`/api/v2/node/${name}/status`),
|
||||
blobs: (limit = 200, offset = 0) =>
|
||||
get<BlobSummary[]>(`/api/v2/storage/blobs?limit=${limit}&offset=${offset}`),
|
||||
tags: (prefix = '') =>
|
||||
get<TagSummary[]>(`/api/v2/storage/tags?prefix=${encodeURIComponent(prefix)}`),
|
||||
refs: (limit = 200, offset = 0) =>
|
||||
get<RefSummary[]>(`/api/v2/storage/refs?limit=${limit}&offset=${offset}`),
|
||||
snapshots: () => get<SnapshotSummary[]>(`/api/v2/storage/snapshots`),
|
||||
refTracking: (repo = '') =>
|
||||
get<RefTrackingItem[]>(`/api/v2/storage/ref-tracking?repo=${encodeURIComponent(repo)}`),
|
||||
};
|
||||
|
||||
/** Format bytes as MB / GB / TB as needed. */
|
||||
export function fmtBytes(n: number): string {
|
||||
if (n < 1024) return `${n} B`;
|
||||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KiB`;
|
||||
if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MiB`;
|
||||
if (n < 1024 * 1024 * 1024 * 1024) return `${(n / (1024 * 1024 * 1024)).toFixed(1)} GiB`;
|
||||
return `${(n / (1024 * 1024 * 1024 * 1024)).toFixed(2)} TiB`;
|
||||
}
|
||||
|
||||
/** Human-friendly relative time. */
|
||||
export function fmtAge(unix: number): string {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const diff = now - unix;
|
||||
if (diff < 60) return `${diff}s ago`;
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
||||
return `${Math.floor(diff / 86400)}d ago`;
|
||||
}
|
||||
Reference in New Issue
Block a user