import { useCallback, useEffect, useState } from "react"; import { Link } from "wouter"; import { Copy, Plus, Trash2 } from "lucide-react"; import { api } from "@/api"; import type { System } from "@/types"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { cn } from "@/lib/utils"; import { t } from "@/i18n"; const TB = 2 ** 40; const GB = 2 ** 30; const MB = 2 ** 20; const KB = 2 ** 10; export function fmtBytes(b: number) { if (b >= TB) return `${(b / TB).toFixed(1)} TB`; if (b >= GB) return `${(b / GB).toFixed(1)} GB`; if (b >= MB) return `${(b / MB).toFixed(1)} MB`; if (b >= KB) return `${(b / KB).toFixed(0)} KB`; return `${Math.round(b)} B`; } export function fmtUptime(s: number) { const d = Math.floor(s / 86400); if (d > 0) return `${d}d ${Math.floor((s % 86400) / 3600)}h`; const h = Math.floor(s / 3600); if (h > 0) return `${h}h ${Math.floor((s % 3600) / 60)}m`; return `${Math.floor(s / 60)}m`; } export function Bar({ pct, warnAt = 90 }: { pct: number | undefined; warnAt?: number }) { return ( warnAt ? "bg-down" : "bg-up")} style={{ width: `${Math.min(pct ?? 0, 100)}%` }} /> ); } /** The one-liner agents run; shown on create and on the detail page. */ export function agentCmd(sys: System) { const base = location.origin; return `DASH_URL=${base} DASH_TOKEN=${sys.token} dash-agent`; } export function SystemsPage() { const [systems, setSystems] = useState([]); const [loaded, setLoaded] = useState(false); const [dlgOpen, setDlgOpen] = useState(false); const [name, setName] = useState(""); const [busy, setBusy] = useState(false); const [created, setCreated] = useState(null); const load = useCallback(async () => { const { data } = await api.GET("/api/systems"); if (data) setSystems(data); setLoaded(true); }, []); useEffect(() => { void load(); const t = setInterval(() => void load(), 15_000); return () => clearInterval(t); }, [load]); async function create() { if (!name.trim()) return; setBusy(true); const { data } = await api.POST("/api/systems", { body: { name: name.trim() } }); setBusy(false); if (data) { setCreated(data); setName(""); void load(); } } async function remove(id: string | undefined, n: string | undefined) { if (!id || !confirm(t("systems.deleteConfirm", { name: n ?? "" }))) return; await api.DELETE("/api/systems/{id}", { params: { path: { id } } }); await load(); } return (

{t("systems.title")}

{t("systems.subtitle")}

{loaded && systems.length === 0 ? (

{t("systems.empty")}

{t("systems.emptyHintPre")} dash-agent {t("systems.emptyHintPost")}

) : (
{systems.map((sys) => { const l = sys.latest; const memPct = l?.memTotal ? ((l.memUsed ?? 0) / l.memTotal) * 100 : undefined; const diskPct = l?.diskTotal ? ((l.diskUsed ?? 0) / l.diskTotal) * 100 : undefined; const rows: [string, number | undefined, string][] = [ ["cpu", l?.cpu, l?.cpu !== undefined ? `${l.cpu.toFixed(0)}%` : "—"], ["mem", memPct, l?.memTotal ? `${fmtBytes(l.memUsed ?? 0)} / ${fmtBytes(l.memTotal)}` : "—"], ["disk", diskPct, l?.diskTotal ? `${fmtBytes(l.diskUsed ?? 0)} / ${fmtBytes(l.diskTotal)}` : "—"], ]; return (
{sys.name}
{sys.host ? (

{sys.host} · {sys.cpuModel || t("systems.cores", { n: sys.cores ?? 0 })} {l?.uptimeS ? ` · up ${fmtUptime(l.uptimeS)}` : ""}

) : (

{t("systems.waiting")}

)}
{rows.map(([label, pct, txt]) => (
{label} {txt}
))}
); })}
)} {t("systems.newTitle")} {t("systems.newDesc")} {created ? (

{t("systems.agentCmdFor")} {created.name}:

{agentCmd(created)}

{t("systems.tokenWarning")}

) : (
setName(e.target.value)} placeholder={t("systems.namePlaceholder")} autoFocus onKeyDown={(e) => { if (e.key === "Enter") void create(); }} />
)} {created ? ( ) : ( <> )}
); }