import { Link } from 'wouter'; import { NodeStatusV2 } from '../lib/api'; import { StorageBar } from './StorageBar'; interface Props { node: NodeStatusV2; } /// 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 }: Props) { 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 ( {/* Header */}
{node.node_name}
{node.zone || '—'}
{/* Error banner */} {!node.online && (
{node.error ?? 'offline'}
)} {node.online && ( <> {/* Storage bars */} {node.filesystem && ( )} {node.hot && node.hot.max_bytes > 0 && ( )} {/* One-liner facts */}
mount {node.mount?.active ? ( ✓ mounted ) : ( not mounted )} cache hit rate {node.cache && node.cache.hits + node.cache.misses > 0 ? `${Math.round(node.cache.hit_rate * 100)}%` : idle} next scheduled job {nextTimer(node)}
)}
); } function healthOf(n: NodeStatusV2): 'ok' | 'warn' | 'err' | 'idle' { 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: NodeStatusV2): React.ReactNode { 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 ; 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 ( {label}{' '} {when} ); }