"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, Terminal, Trash2 } from "lucide-react"; import { useFetchJson } from "@/lib/api/use-fetch"; import { ConnectHostWizard } from "../ConnectHostWizard"; import { NodeTerminal } from "./NodeTerminal"; 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; 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", }; 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`; } /** Poll the workspace's nodes every 3s. */ export function useNodes(): { nodes: FleetNode[]; refresh: () => void } { const { data, refresh } = useFetchJson<{ nodes: FleetNode[] }>("/api/nodes"); useEffect(() => { const t = setInterval(refresh, 3000); return () => clearInterval(t); }, [refresh]); return { nodes: data?.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 [term, setTerm] = useState(false); 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.name}
{node.status.toUpperCase()} {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"}
)} {term ? setTerm(false)} /> : 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}
); } 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. */ 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}
); }