dashboard-v2 hotfix: relative asset base so /clawstor mount works
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Failing after 2s
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Failing after 2s
Browser hit /clawstor/ then requested /v2/assets/index-XXX.css (absolute path baked in by vite base '/v2/') which 404'd because Tailscale Serve only mounted /clawstor and /clawstor/api. Switch vite base to './' — assets resolve relative to whatever URL the SPA loaded from. Works for local (:7700/v2/) + Tailscale (/clawstor) with no config coupling.
This commit is contained in:
@@ -1,23 +1,22 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api, fmtBytes } from '../lib/api';
|
||||
import { api, fmtAge, fmtBytes } from '../lib/api';
|
||||
import { NodeCard } from '../components/NodeCard';
|
||||
import { StatTile } from '../components/StatTile';
|
||||
// Fleet-wide command center. Reads `/api/v2/node/local/status` from
|
||||
// this node. Cross-node fan-out is server-side once PR 3 lands;
|
||||
// until then we assume the operator hits any node's dashboard and
|
||||
// sees that node's storage state plus quick nav to the others.
|
||||
// Fleet command center — the landing pane.
|
||||
//
|
||||
// Poll cadence: 10 s. Cheap: 5 filesystem walks.
|
||||
// Aggregator polls each peer via cluster RPC every 10 s. Numbers
|
||||
// are ground truth from the daemons themselves; no local
|
||||
// blob-store reads on the aggregator.
|
||||
export function CommandCenter() {
|
||||
const [me, setMe] = useState(null);
|
||||
const [fleet, setFleet] = useState(null);
|
||||
const [err, setErr] = useState(null);
|
||||
const [tick, setTick] = useState(0);
|
||||
useEffect(() => {
|
||||
api
|
||||
.nodeStatus('local')
|
||||
.then((n) => {
|
||||
setMe(n);
|
||||
.fleet()
|
||||
.then((f) => {
|
||||
setFleet(f);
|
||||
setErr(null);
|
||||
})
|
||||
.catch((e) => setErr(String(e)));
|
||||
@@ -26,16 +25,16 @@ export function CommandCenter() {
|
||||
const id = setInterval(() => setTick((t) => t + 1), 10_000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold text-slate-100", children: "Fleet" }), _jsxs("div", { className: "text-sm text-slate-500", children: ["this dashboard was served by", ' ', _jsx("span", { className: "font-mono text-slate-300", children: me?.node_name ?? '…' }), ' · click any node card to drill in'] })] }), err && (_jsx("div", { className: "rounded border border-red-800 bg-red-950/40 p-3 text-red-300 text-sm", children: err })), _jsxs("section", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4", children: [me && (_jsx(NodeCard, { node: me, loading: false, error: null })), ['tank', 'architect', 'morpheus']
|
||||
.filter((n) => n !== me?.node_name)
|
||||
.map((n) => (_jsx(NodeCard, { node: {
|
||||
node_name: n,
|
||||
blob_store_root: null,
|
||||
blob_count: 0,
|
||||
tag_count: 0,
|
||||
ref_count: 0,
|
||||
snapshot_count: 0,
|
||||
ref_tracking_count: 0,
|
||||
blob_store_bytes: 0,
|
||||
}, loading: true, error: null }, n)))] }), me && (_jsxs("section", { children: [_jsx("h2", { className: "text-lg font-semibold text-slate-100 mb-3", children: "This node" }), _jsxs("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3", children: [_jsx(StatTile, { label: "blobs", value: me.blob_count.toLocaleString(), color: "ok" }), _jsx(StatTile, { label: "tags", value: me.tag_count, color: me.tag_count > 0 ? 'ok' : 'idle' }), _jsx(StatTile, { label: "refs", value: me.ref_count, color: me.ref_count > 0 ? 'ok' : 'idle' }), _jsx(StatTile, { label: "snapshots", value: me.snapshot_count, color: "ok" }), _jsx(StatTile, { label: "ref-tracking", value: me.ref_tracking_count, color: me.ref_tracking_count > 0 ? 'ok' : 'idle' }), _jsx(StatTile, { label: "store size", value: fmtBytes(me.blob_store_bytes), hint: me.blob_store_root ?? undefined, color: "ok" })] })] }))] }));
|
||||
const totals = fleet
|
||||
? fleet.nodes.reduce((a, n) => ({
|
||||
blobs: a.blobs + n.blob_count,
|
||||
tags: a.tags + n.tag_count,
|
||||
refs: a.refs + n.ref_count,
|
||||
snapshots: a.snapshots + n.snapshot_count,
|
||||
bytes: a.bytes + n.blob_store_bytes,
|
||||
online: a.online + (n.online ? 1 : 0),
|
||||
}), { blobs: 0, tags: 0, refs: 0, snapshots: 0, bytes: 0, online: 0 })
|
||||
: null;
|
||||
return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold text-slate-100", children: "Fleet" }), _jsxs("div", { className: "text-sm text-slate-500", children: ["aggregated by", ' ', _jsx("span", { className: "font-mono text-slate-300", children: fleet?.aggregator_name ?? '…' }), fleet && (_jsxs(_Fragment, { children: [' · updated ', _jsx("span", { className: "font-mono", children: fmtAge(fleet.fetched_at_unix) }), ' · ', _jsx("span", { className: "text-emerald-300", children: totals?.online }), "/", fleet.nodes.length, " online"] }))] })] }), err && (_jsx("div", { className: "rounded border border-red-800 bg-red-950/40 p-3 text-red-300 text-sm", children: err })), totals && fleet && (_jsxs("section", { children: [_jsx("h2", { className: "text-lg font-semibold text-slate-100 mb-3", children: "Totals (fleet-wide)" }), _jsxs("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3", children: [_jsx(StatTile, { label: "online nodes", value: `${totals.online}/${fleet.nodes.length}`, color: totals.online === fleet.nodes.length ? 'ok' : 'warn' }), _jsx(StatTile, { label: "blobs", value: totals.blobs.toLocaleString(), color: "ok" }), _jsx(StatTile, { label: "tags", value: totals.tags, color: "ok" }), _jsx(StatTile, { label: "refs", value: totals.refs, color: "ok" }), _jsx(StatTile, { label: "total bytes", value: fmtBytes(totals.bytes), color: "ok" })] })] })), _jsxs("section", { children: [_jsx("h2", { className: "text-lg font-semibold text-slate-100 mb-3", children: "Nodes" }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4", children: [fleet?.nodes.map((n) => (_jsx(NodeCard, { node: n }, n.node_name))), !fleet &&
|
||||
[1, 2, 3].map((i) => (_jsx("div", { className: "rounded-lg border border-slate-800 bg-slate-900/40 p-5 h-40 animate-pulse" }, i)))] })] })] }));
|
||||
}
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api, NodeStatusV2, fmtBytes } from '../lib/api';
|
||||
import { api, FleetSnapshot, fmtAge, fmtBytes } from '../lib/api';
|
||||
import { NodeCard } from '../components/NodeCard';
|
||||
import { StatTile } from '../components/StatTile';
|
||||
|
||||
// Fleet-wide command center. Reads `/api/v2/node/local/status` from
|
||||
// this node. Cross-node fan-out is server-side once PR 3 lands;
|
||||
// until then we assume the operator hits any node's dashboard and
|
||||
// sees that node's storage state plus quick nav to the others.
|
||||
// Fleet command center — the landing pane.
|
||||
//
|
||||
// Poll cadence: 10 s. Cheap: 5 filesystem walks.
|
||||
// Aggregator polls each peer via cluster RPC every 10 s. Numbers
|
||||
// are ground truth from the daemons themselves; no local
|
||||
// blob-store reads on the aggregator.
|
||||
export function CommandCenter() {
|
||||
const [me, setMe] = useState<NodeStatusV2 | null>(null);
|
||||
const [fleet, setFleet] = useState<FleetSnapshot | null>(null);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
const [tick, setTick] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.nodeStatus('local')
|
||||
.then((n) => {
|
||||
setMe(n);
|
||||
.fleet()
|
||||
.then((f) => {
|
||||
setFleet(f);
|
||||
setErr(null);
|
||||
})
|
||||
.catch((e) => setErr(String(e)));
|
||||
@@ -29,16 +28,38 @@ export function CommandCenter() {
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
const totals = fleet
|
||||
? fleet.nodes.reduce(
|
||||
(a, n) => ({
|
||||
blobs: a.blobs + n.blob_count,
|
||||
tags: a.tags + n.tag_count,
|
||||
refs: a.refs + n.ref_count,
|
||||
snapshots: a.snapshots + n.snapshot_count,
|
||||
bytes: a.bytes + n.blob_store_bytes,
|
||||
online: a.online + (n.online ? 1 : 0),
|
||||
}),
|
||||
{ blobs: 0, tags: 0, refs: 0, snapshots: 0, bytes: 0, online: 0 }
|
||||
)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-slate-100">Fleet</h1>
|
||||
<div className="text-sm text-slate-500">
|
||||
this dashboard was served by{' '}
|
||||
aggregated by{' '}
|
||||
<span className="font-mono text-slate-300">
|
||||
{me?.node_name ?? '…'}
|
||||
{fleet?.aggregator_name ?? '…'}
|
||||
</span>
|
||||
{' · click any node card to drill in'}
|
||||
{fleet && (
|
||||
<>
|
||||
{' · updated '}
|
||||
<span className="font-mono">{fmtAge(fleet.fetched_at_unix)}</span>
|
||||
{' · '}
|
||||
<span className="text-emerald-300">{totals?.online}</span>
|
||||
/{fleet.nodes.length} online
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,67 +69,36 @@ export function CommandCenter() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{me && (
|
||||
<NodeCard
|
||||
node={me}
|
||||
loading={false}
|
||||
error={null}
|
||||
/>
|
||||
)}
|
||||
{['tank', 'architect', 'morpheus']
|
||||
.filter((n) => n !== me?.node_name)
|
||||
.map((n) => (
|
||||
<NodeCard
|
||||
key={n}
|
||||
node={{
|
||||
node_name: n,
|
||||
blob_store_root: null,
|
||||
blob_count: 0,
|
||||
tag_count: 0,
|
||||
ref_count: 0,
|
||||
snapshot_count: 0,
|
||||
ref_tracking_count: 0,
|
||||
blob_store_bytes: 0,
|
||||
}}
|
||||
loading
|
||||
error={null}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{me && (
|
||||
{totals && fleet && (
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold text-slate-100 mb-3">
|
||||
This node
|
||||
Totals (fleet-wide)
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
|
||||
<StatTile label="blobs" value={me.blob_count.toLocaleString()} color="ok" />
|
||||
<StatTile
|
||||
label="tags"
|
||||
value={me.tag_count}
|
||||
color={me.tag_count > 0 ? 'ok' : 'idle'}
|
||||
/>
|
||||
<StatTile
|
||||
label="refs"
|
||||
value={me.ref_count}
|
||||
color={me.ref_count > 0 ? 'ok' : 'idle'}
|
||||
/>
|
||||
<StatTile label="snapshots" value={me.snapshot_count} color="ok" />
|
||||
<StatTile
|
||||
label="ref-tracking"
|
||||
value={me.ref_tracking_count}
|
||||
color={me.ref_tracking_count > 0 ? 'ok' : 'idle'}
|
||||
/>
|
||||
<StatTile
|
||||
label="store size"
|
||||
value={fmtBytes(me.blob_store_bytes)}
|
||||
hint={me.blob_store_root ?? undefined}
|
||||
color="ok"
|
||||
/>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
|
||||
<StatTile label="online nodes" value={`${totals.online}/${fleet.nodes.length}`} color={totals.online === fleet.nodes.length ? 'ok' : 'warn'} />
|
||||
<StatTile label="blobs" value={totals.blobs.toLocaleString()} color="ok" />
|
||||
<StatTile label="tags" value={totals.tags} color="ok" />
|
||||
<StatTile label="refs" value={totals.refs} color="ok" />
|
||||
<StatTile label="total bytes" value={fmtBytes(totals.bytes)} color="ok" />
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold text-slate-100 mb-3">Nodes</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{fleet?.nodes.map((n) => (
|
||||
<NodeCard key={n.node_name} node={n} />
|
||||
))}
|
||||
{!fleet &&
|
||||
[1, 2, 3].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="rounded-lg border border-slate-800 bg-slate-900/40 p-5 h-40 animate-pulse"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user