"use client"; // Fleet UI: live per-node health cards (Local hardware) + a fleet overview. Data // comes from the nodes registry (`GET /api/nodes`), polled every 3s. import { useCallback, useEffect, useState } from "react"; import { Cpu, HardDrive, MemoryStick, Network, Plus, Server, ShieldCheck, Terminal, Trash2 } from "lucide-react"; import { useQueryStates } from "nuqs"; import { useFetchJson } from "@/lib/api/use-fetch"; import { panelParsers } from "@/lib/url/panel-params"; import { ConnectHostWizard } from "../ConnectHostWizard"; const mono = "'Geist Mono', ui-monospace, monospace"; export interface NodeHealth { cpuPct: number; memTotal: number; memUsed: number; memPressure: number; swapUsed: number; diskTotal: number; diskFree: number; load1: number; load5: number; load15: number; containerCount: number; } export interface FleetNode { id: string; name: string; hostname: string | null; localIp: string | null; status: "pending" | "online" | "offline" | "draining"; agentVersion: string | null; tailscaleIp: string | null; lastSeen: number | null; createdAt: number; health: NodeHealth | null; } const STATUS_COLOR: Record = { online: "#5fd08a", offline: "#6a6a72", pending: "#e8b465", draining: "#ff8a7a", }; /** A node is "live" if the channel reports online OR its last heartbeat is fresh * (<15s) — so a transient status-column flip never shows a healthy node down. */ export function isLive(n: FleetNode): boolean { if (n.status === "pending" || n.status === "draining") return false; if (n.lastSeen != null && Date.now() / 1000 - n.lastSeen < 15) return true; return n.status === "online"; } /** The status to render: pending/draining pass through; otherwise online iff live. */ export function displayStatus(n: FleetNode): FleetNode["status"] { if (n.status === "pending" || n.status === "draining") return n.status; return isLive(n) ? "online" : "offline"; } function fmtBytes(b: number): string { if (b >= 1e12) return `${(b / 1e12).toFixed(1)} TB`; if (b >= 1e9) return `${(b / 1e9).toFixed(1)} GB`; if (b >= 1e6) return `${(b / 1e6).toFixed(0)} MB`; return `${b} B`; } /** Stream the workspace's nodes live over SSE (status updates push instantly, * no 3s poll lag). `refresh` does a one-shot GET for an immediate nudge after * pairing a new host. */ export function useNodes(): { nodes: FleetNode[]; refresh: () => void } { const [nodes, setNodes] = useState([]); const refresh = useCallback(() => { fetch("/api/nodes") .then((r) => (r.ok ? r.json() : null)) .then((d: { nodes: FleetNode[] } | null) => { if (d?.nodes) setNodes(d.nodes); }) .catch(() => {}); }, []); useEffect(() => { refresh(); const es = new EventSource("/api/nodes/live"); es.addEventListener("nodes", (e) => { try { setNodes(JSON.parse((e as MessageEvent).data) as FleetNode[]); } catch { /* ignore malformed frame */ } }); return () => es.close(); }, [refresh]); return { nodes, refresh }; } function Bar({ label, pct, detail, color }: { label: string; pct: number; detail: string; color: string }) { return (
{label} {detail}
); } export function NodeCard({ node, onRemoved }: { node: FleetNode; onRemoved: () => void }) { const h = node.health; const [, setParams] = useQueryStates(panelParsers, { shallow: true }); const [check, setCheck] = useState<{ ok: boolean; output: string } | null>(null); const [checking, setChecking] = useState(false); const runCheck = useCallback(() => { setChecking(true); setCheck(null); fetch(`/api/nodes/${node.id}/sandbox-check`, { method: "POST" }) .then((r) => r.json()) .then((d: { ok: boolean; output: string }) => setCheck(d)) .catch((e: Error) => setCheck({ ok: false, output: e.message })) .finally(() => setChecking(false)); }, [node.id]); const memPct = h && h.memTotal > 0 ? (h.memUsed / h.memTotal) * 100 : 0; const diskUsedPct = h && h.diskTotal > 0 ? ((h.diskTotal - h.diskFree) / h.diskTotal) * 100 : 0; const remove = useCallback(() => { if (!confirm(`Remove "${node.name}" from your fleet?`)) return; fetch(`/api/nodes/${node.id}`, { method: "DELETE" }).then(onRemoved); }, [node.id, node.name, onRemoved]); return (
{node.hostname ?? node.name}
{node.status.toUpperCase()} {node.localIp ? · {node.localIp} : null} {node.agentVersion ? · v{node.agentVersion} : null}
{node.status === "online" ? ( <> ) : null}
{h ? (
0.85 ? "#ff6f61" : "#c98af0"} />
load {h.load1.toFixed(2)} · {h.containerCount} containers {node.tailscaleIp ? ( ) : null}
) : (
{node.status === "pending" ? "waiting for the daemon to connect…" : "no health reported yet"}
)} {checking || check ? (
{checking ? "RUNNING SANDBOX CHECK…" : check?.ok ? "✓ SANDBOX READY" : "✗ CHECK FAILED"}
{check ? (
{check.output}
) : null}
) : null}
); } export function LocalHardware() { const { nodes, refresh } = useNodes(); const [wizard, setWizard] = useState(false); return (
LOCAL HARDWARE · {nodes.length} NODE{nodes.length === 1 ? "" : "S"}
Your machines
{nodes.length === 0 ? (
No nodes connected yet

Run agents on your own machines — install the lightweight daemon and it reports back here with live health.

) : (
{nodes.map((n) => ( ))}
)}
{wizard ? { setWizard(false); refresh(); }} /> : null}
); } /** Choose where new agent sandboxes provision (a connected node, or local). */ function PlacementSection() { const { nodes } = useNodes(); const { data, refresh } = useFetchJson<{ node: string | null }>("/api/fleet/placement"); const current = data?.node ?? "local"; const online = nodes.filter((n) => n.status === "online"); const set = useCallback( (node: string) => { fetch("/api/fleet/placement", { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ node }) }).then(refresh); }, [refresh], ); return (
Run agents on

New agent sandboxes provision on this host. Falls back to local automatically if the node goes offline. Existing agents are unaffected until their sandbox is next created.

); } interface TsDevice { name: string | null; addr: string | null; os: string | null; version: string | null; lastSeen: string | null; } function tsOnline(lastSeen: string | null): boolean { if (!lastSeen) return false; const t = Date.parse(lastSeen); return Number.isFinite(t) && Date.now() - t < 5 * 60 * 1000; } /** Connect a workspace's Tailscale (BYO) and show its tailnet device metrics. */ export function TailscaleSection() { const { data: status, refresh } = useFetchJson<{ connected: boolean; tailnet: string | null }>("/api/fleet/tailscale"); const { data: dev } = useFetchJson<{ connected: boolean; devices: TsDevice[]; error?: string }>(status?.connected ? "/api/fleet/tailscale/devices" : null); const [apiKey, setApiKey] = useState(""); const [tailnet, setTailnet] = useState(""); const [busy, setBusy] = useState(false); const connect = useCallback(() => { if (!apiKey.trim() || !tailnet.trim()) return; setBusy(true); fetch("/api/fleet/tailscale", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ apiKey, tailnet }) }) .then(() => { setApiKey(""); refresh(); }) .finally(() => setBusy(false)); }, [apiKey, tailnet, refresh]); const devices = dev?.devices ?? []; const online = devices.filter((d) => tsOnline(d.lastSeen)).length; return (
Tailscale network {status?.connected ? ( {online}/{devices.length} online · {status.tailnet} ) : null}
{!status?.connected ? (

Connect your tailnet to see network status here. Paste a Tailscale API key and your tailnet (e.g. example.com or your-org.ts.net).

setTailnet(e.target.value)} placeholder="tailnet" style={{ flex: "1 1 160px", padding: "9px 11px", borderRadius: 9, border: "1px solid rgba(255,255,255,.12)", background: "#08080a", color: "#f3f3f5", fontSize: 13 }} /> setApiKey(e.target.value)} placeholder="tskey-api-…" type="password" style={{ flex: "2 1 240px", padding: "9px 11px", borderRadius: 9, border: "1px solid rgba(255,255,255,.12)", background: "#08080a", color: "#f3f3f5", fontSize: 13, fontFamily: mono }} />
) : devices.length === 0 ? (
{dev?.error ? `Tailscale: ${dev.error}` : "No devices on this tailnet yet."}
) : (
{devices.map((d, i) => (
{d.name ?? "device"} {d.addr} {d.os ? {d.os} : null}
))}
)}
); } export function FleetOverview() { const { nodes } = useNodes(); const online = nodes.filter((n) => n.status === "online"); const cores = online.reduce((a, n) => a + (n.health?.containerCount ?? 0), 0); const totalMem = online.reduce((a, n) => a + (n.health?.memTotal ?? 0), 0); const totalDisk = online.reduce((a, n) => a + (n.health?.diskTotal ?? 0), 0); const stats = [ { label: "Nodes", value: String(nodes.length), sub: `${online.length} online`, icon: Server, tint: "#5ec8d8" }, { label: "Containers", value: String(cores), sub: "running", icon: Cpu, tint: "#7fc8ff" }, { label: "Memory", value: fmtBytes(totalMem), sub: "fleet total", icon: MemoryStick, tint: "#c98af0" }, { label: "Storage", value: fmtBytes(totalDisk), sub: "fleet total", icon: HardDrive, tint: "#5fd08a" }, ]; return (
FLEET
Overview

The machines connected to your fleet across all of your infrastructure. Connect Tailscale to see your network here too.

{stats.map((s) => (
{s.label.toUpperCase()}
{s.value}
{s.sub}
))}
{nodes.map((n) => (
{n.name} {n.health ? {n.health.cpuPct.toFixed(0)}% cpu : null}
))} {nodes.length === 0 ? No nodes yet — add one under Local hardware. : null}
); }