import { useState } from "react"; import { Link } from "react-router-dom"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Cpu, MemoryStick, HardDrive, Container, Clock, ArrowUpCircle, Play, Square, RotateCw, Server, Database, } from "lucide-react"; import { toast } from "sonner"; import { Card, Spinner, StatusDot, Badge } from "@/components/ui"; import { HostHeader } from "@/components/hosts/HostHeader"; import { stacksApi } from "@/api/stacks"; import { systemApi } from "@/api/system"; import { imagesApi } from "@/api/images"; import { agentsApi } from "@/api/agents"; import { volumesApi } from "@/api/volumes"; import { apiErrorMessage } from "@/api/client"; import { formatBytes, relativeTime } from "@/lib/utils"; import { useAuthStore } from "@/store/auth"; import { useStackActions } from "@/hooks/useStackActions"; import type { Agent, StackStats, StackSummary } from "@/types"; const sumSizes = (m: Record) => Object.values(m).reduce((a, b) => a + (b ?? 0), 0); export function Dashboard() { const isAdmin = useAuthStore((s) => s.user?.role === "admin"); const { busyId, start, stop, restart } = useStackActions(); const stacks = useQuery({ queryKey: ["stacks"], queryFn: stacksApi.list, refetchInterval: 5000 }); const stats = useQuery({ queryKey: ["stack-stats"], queryFn: stacksApi.stats, refetchInterval: 5000 }); const info = useQuery({ queryKey: ["system"], queryFn: systemApi.info, refetchInterval: 5000 }); const audit = useQuery({ queryKey: ["audit"], queryFn: () => systemApi.audit(10), refetchInterval: 10000 }); const updates = useQuery({ queryKey: ["image-updates"], queryFn: () => imagesApi.updates(), refetchInterval: 60000 }); const agents = useQuery({ queryKey: ["agents"], queryFn: () => agentsApi.list(), refetchInterval: 15000 }); // Volumes total is from `docker system df` (slow, cached ~60s server-side). const volSize = useQuery({ queryKey: ["volumes-size", "local"], queryFn: () => volumesApi.sizes().then(sumSizes), refetchInterval: 60000, }); const updateCount = Object.values(updates.data ?? {}).filter((u) => u.update_available).length; const hasAgents = (agents.data?.length ?? 0) > 0; return (
{updateCount > 0 && ( {updateCount} image update{updateCount > 1 ? "s" : ""} available — view on the Images page. )} {/* Local host */}
{hasAgents ? ( ) : (

This host

)}
{/* Remote hosts */} {agents.data?.map((agent) => ( ))} {/* Recent activity */}

Recent activity

{audit.data && audit.data.length > 0 ? (
    {audit.data.map((a) => (
  • {a.user}{" "} {a.action}{" "} {a.target} {relativeTime(a.timestamp)}
  • ))}
) : (

No activity yet.

)}
); } function AgentDashboardSection({ agent, isAdmin }: { agent: Agent; isAdmin: boolean }) { const qc = useQueryClient(); const online = agent.status === "online"; const [busyId, setBusyId] = useState(null); const stacks = useQuery({ queryKey: ["agent-stacks", agent.id], queryFn: () => agentsApi.stacks(agent.id), enabled: online, refetchInterval: 8000, }); const stats = useQuery({ queryKey: ["agent-stack-stats", agent.id], queryFn: () => agentsApi.stackStats(agent.id), enabled: online, refetchInterval: 5000, }); const sys = useQuery({ queryKey: ["agent-system", agent.id], queryFn: () => agentsApi.system(agent.id), enabled: online, refetchInterval: 30000, }); const volSize = useQuery({ queryKey: ["volumes-size", agent.id], queryFn: () => volumesApi.sizes(false, agent.id).then(sumSizes), enabled: online, refetchInterval: 60000, }); const run = async (action: string, label: string, id: string) => { setBusyId(id); const t = toast.loading(`${label} ${id} on ${agent.name}…`); try { await agentsApi.action(agent.id, id, action); toast.success(`${label} ${id} ✓`, { id: t }); qc.invalidateQueries({ queryKey: ["agent-stacks", agent.id] }); qc.invalidateQueries({ queryKey: ["agent-stack-stats", agent.id] }); } catch (e) { toast.error(apiErrorMessage(e), { id: t }); } finally { setBusyId(null); } }; return (
{!online ? (

Host is {agent.status}. Check it under Settings → Remote hosts.

) : ( <> run("start", "Starting", id)} onStop={(id) => run("stop", "Stopping", id)} onRestart={(id) => run("restart", "Restarting", id)} emptyText="No stacks on this host." /> )}
); } function StacksTable({ stacks, stats, hostCpus, hostMem, isAdmin, busyId, loading, linkBase = "/stacks", onStart, onStop, onRestart, emptyText, }: { stacks: StackSummary[] | undefined; stats: Record | undefined; hostCpus: number; hostMem: number; isAdmin: boolean; busyId: string | null; loading: boolean; linkBase?: string; onStart: (id: string) => void; onStop: (id: string) => void; onRestart: (id: string) => void; emptyText: string; }) { if (loading) return ; if (!stacks || stacks.length === 0) { return (

{emptyText}

); } return ( {isAdmin && } {stacks.map((s) => ( ))}
Stack CPU Memory
); } function StackRow({ stack, stats, hostCpus, hostMem, isAdmin, busy, linkBase, onStart, onStop, onRestart, }: { stack: StackSummary; stats?: StackStats; hostCpus: number; hostMem: number; isAdmin: boolean; busy: boolean; linkBase: string; onStart: (id: string) => void; onStop: (id: string) => void; onRestart: (id: string) => void; }) { const running = stack.running_count > 0; return ( {stack.name} {stack.status} {stack.running_count}/{stack.service_count} svc {running && stats ? ( ) : ( )} {running && stats ? ( ) : ( )} {isAdmin && (
{running ? ( <> onRestart(stack.id)} disabled={busy}> onStop(stack.id)} disabled={busy}> ) : ( onStart(stack.id)} disabled={busy}> )}
)} ); } function IconBtn({ title, onClick, disabled, children, }: { title: string; onClick: () => void; disabled?: boolean; children: React.ReactNode; }) { return ( ); } /** A compact usage bar. When a limit is set the bar fills toward the limit; * otherwise it fills toward the host total as a faint reference. */ function Meter({ used, limit, hostMax, label, }: { used: number; limit: number | null; hostMax: number; label: string; }) { const denom = limit ?? (hostMax || 0); const pct = denom > 0 ? Math.min((used / denom) * 100, 100) : 0; const over = limit != null && used > limit * 1.001; const bar = over || pct >= 90 ? "bg-red-500" : pct >= 75 ? "bg-amber-500" : limit != null ? "bg-sky-500" : "bg-slate-400"; return (
{label} {denom > 0 && ( {Math.round(pct)}% )}
); } function ResourceBar({ cpuCores, memUsed, memTotal, diskUsed, diskTotal, volumesSize, containersRunning, containersTotal, dockerVersion, }: { cpuCores: number; memUsed: number; memTotal: number; diskUsed: number; diskTotal: number; volumesSize?: number; containersRunning: number; containersTotal: number; dockerVersion: string; }) { return (
} label="CPU cores" value={cpuCores || "—"} /> } label="Memory" value={memTotal ? `${formatBytes(memUsed)} / ${formatBytes(memTotal)}` : "—"} /> } label="Disk" value={diskTotal ? `${formatBytes(diskUsed)} / ${formatBytes(diskTotal)}` : "—"} /> } label="Volumes" value={volumesSize === undefined ? "…" : formatBytes(volumesSize)} /> } label="Containers" value={`${containersRunning} / ${containersTotal}`} /> } label="Docker" value={dockerVersion || "—"} />
); } function Stat({ icon, label, value, }: { icon: React.ReactNode; label: string; value: React.ReactNode; }) { return (
{icon}

{label}

{value}

); }