Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Successful in 18s
Reworks the SPA around the new DashboardStatus fields. The landing is now a fleet-health dashboard aimed at a layperson — disk gauges, mount ✓/✗, cache hit rate, next scheduled job. No hex, no primitives. Nav restructure: * Primary: 'Fleet health' (just the landing). * 'View Advanced ▾' dropdown reveals: Blobs / Tags / Refs / Snapshots / Ref-tracking. Routes moved under /advanced/*. New components: * StorageBar — horizontal used/total bar with pinned/evictable split, health-color threshold at 60/85%. * NodeCard — traffic-light dot + disk + hot tier bars + mount state + cache hit rate + next-timer countdown. Whole card is a link into node detail. New CommandCenter: * Fleet-wide storage roll-up card (sum of every node's disk). * Grid of NodeCards. * 10s poll cadence retained from previous version. Existing StorageBrowser + RefTrackingPage moved behind /advanced/* routes; internal component code untouched.
62 lines
4.1 KiB
JavaScript
62 lines
4.1 KiB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
import { Link } from 'wouter';
|
|
import { StorageBar } from './StorageBar';
|
|
/// Human-oriented node card for the FleetHealth landing.
|
|
/// Shows: overall health traffic-light, storage bars, mount state,
|
|
/// cache hit rate, next scheduled job. No hex, no primitives.
|
|
export function NodeCard({ node }) {
|
|
const health = healthOf(node);
|
|
const border = {
|
|
ok: 'border-emerald-700 hover:border-emerald-500',
|
|
warn: 'border-amber-700 hover:border-amber-500',
|
|
err: 'border-red-700 hover:border-red-500',
|
|
idle: 'border-slate-700 hover:border-slate-500',
|
|
}[health];
|
|
const dot = {
|
|
ok: 'bg-emerald-400',
|
|
warn: 'bg-amber-400',
|
|
err: 'bg-red-400',
|
|
idle: 'bg-slate-500',
|
|
}[health];
|
|
return (_jsx(Link, { href: `/nodes/${node.node_name}`, children: _jsxs("a", { className: [
|
|
'block rounded-lg bg-slate-900 border transition-colors',
|
|
'p-5 space-y-4',
|
|
border,
|
|
].join(' '), children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: `inline-block w-2.5 h-2.5 rounded-full ${dot}` }), _jsx("span", { className: "text-lg font-semibold text-slate-100", children: node.node_name })] }), _jsx("span", { className: "text-xs text-slate-500 font-mono", children: node.zone || '—' })] }), !node.online && (_jsx("div", { className: "text-sm text-red-400 break-words", children: node.error ?? 'offline' })), node.online && (_jsxs(_Fragment, { children: [node.filesystem && (_jsx(StorageBar, { label: "disk", used: node.filesystem.used_bytes, total: node.filesystem.total_bytes })), node.hot && node.hot.max_bytes > 0 && (_jsx(StorageBar, { label: "hot tier", used: node.hot.used_bytes, total: node.hot.max_bytes, pinned: node.hot.pinned_bytes ?? undefined })), _jsxs("div", { className: "grid grid-cols-2 gap-y-1 text-sm", children: [_jsx("span", { className: "text-slate-500", children: "mount" }), _jsx("span", { className: "text-right font-mono text-xs", children: node.mount?.active ? (_jsx("span", { className: "text-emerald-300", children: "\u2713 mounted" })) : (_jsx("span", { className: "text-slate-500", children: "not mounted" })) }), _jsx("span", { className: "text-slate-500", children: "cache hit rate" }), _jsx("span", { className: "text-right font-mono text-xs", children: node.cache && node.cache.hits + node.cache.misses > 0
|
|
? `${Math.round(node.cache.hit_rate * 100)}%`
|
|
: _jsx("span", { className: "text-slate-500", children: "idle" }) }), _jsx("span", { className: "text-slate-500", children: "next scheduled job" }), _jsx("span", { className: "text-right font-mono text-xs", children: nextTimer(node) })] })] }))] }) }));
|
|
}
|
|
function healthOf(n) {
|
|
if (!n.online)
|
|
return 'err';
|
|
const fsPct = n.filesystem
|
|
? n.filesystem.used_bytes / Math.max(n.filesystem.total_bytes, 1)
|
|
: 0;
|
|
const anyFailed = n.timers.some((t) => t.last_result && t.last_result !== 'success');
|
|
if (fsPct > 0.9 || anyFailed)
|
|
return 'err';
|
|
if (fsPct > 0.75)
|
|
return 'warn';
|
|
if (n.mount && !n.mount.active)
|
|
return 'warn';
|
|
return 'ok';
|
|
}
|
|
function nextTimer(n) {
|
|
const next = n.timers
|
|
.filter((t) => t.next_fire_unix)
|
|
.sort((a, b) => (a.next_fire_unix ?? 0) - (b.next_fire_unix ?? 0))[0];
|
|
if (!next?.next_fire_unix)
|
|
return _jsx("span", { className: "text-slate-500", children: "\u2014" });
|
|
const label = next.unit
|
|
.replace(/^clawstor-/, '')
|
|
.replace(/\.timer$/, '');
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const diff = next.next_fire_unix - now;
|
|
const when = diff < 3600
|
|
? `in ${Math.max(0, Math.floor(diff / 60))}m`
|
|
: diff < 86400
|
|
? `in ${Math.floor(diff / 3600)}h`
|
|
: `in ${Math.floor(diff / 86400)}d`;
|
|
return (_jsxs("span", { children: [_jsx("span", { className: "text-slate-300", children: label }), ' ', _jsx("span", { className: "text-slate-500", children: when })] }));
|
|
}
|