import { useState } from "react"; import { Link } from "react-router-dom"; import { useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; import { Cpu, MemoryStick, HardDrive, Container, Clock, ArrowUpCircle, RefreshCw, Server, Database, } from "lucide-react"; import { toast } from "sonner"; import { Card } from "@/components/ui"; import { HostHeader } from "@/components/hosts/HostHeader"; import { StacksTable } from "@/components/stacks/StacksTable"; import { FunnelChart } from "@/components/dashboard/FunnelChart"; import { UptimeChart } from "@/components/dashboard/UptimeChart"; import { OpsGrid } from "@/components/dashboard/OpsGrid"; import { AiPromptBar } from "@/components/dashboard/AiPromptBar"; 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 { dashboardApi } from "@/api/dashboard"; import { apiErrorMessage } from "@/api/client"; import { cn, formatBytes, relativeTime } from "@/lib/utils"; import { useAuthStore } from "@/store/auth"; import { useStackActions } from "@/hooks/useStackActions"; import type { Agent } from "@/types"; const sumSizes = (m: Record) => Object.values(m).reduce((a, b) => a + (b ?? 0), 0); type RangeDays = 7 | 30; export function Dashboard() { const isAdmin = useAuthStore((s) => s.user?.role === "admin"); const { busyId, start, stop, restart } = useStackActions(); const [range, setRange] = useState(30); const funnel = useQuery({ queryKey: ["dashboard-funnel"], queryFn: () => dashboardApi.funnel(), refetchInterval: 30000, }); const summary = useQuery({ queryKey: ["dashboard-summary"], queryFn: dashboardApi.summary, refetchInterval: 60000, }); 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 (
{/* ---- Overview header ---- */}

Overview

{([30, 7] as RangeDays[]).map((d) => ( ))}
{updateCount > 0 && ( {updateCount} image update{updateCount > 1 ? "s" : ""} available — view on the Images page. )} {/* ---- Analytics row: funnel + container count ---- */}
0 ? Math.round((funnel.data.healthy / funnel.data.running) * 100) : null } />
{/* ---- Bottom row: uptime + ops ---- */}
{/* ---- 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.

)}
); } /* ---------------------------------------------------------------------- */ /* Analytics cards */ /* ---------------------------------------------------------------------- */ const FUNNEL_STAGES = [ { key: "discovered", label: "Discovered" }, { key: "running", label: "Running" }, { key: "healthy", label: "Healthy" }, { key: "updated", label: "Up to date" }, { key: "monitored", label: "Monitored" }, ] as const; function StackHealthCard({ funnel, loading, }: { funnel?: import("@/api/dashboard").FunnelData; loading: boolean; }) { const qc = useQueryClient(); const [refreshing, setRefreshing] = useState(false); const [hovered, setHovered] = useState(null); const refresh = async () => { setRefreshing(true); try { const data = await dashboardApi.funnel(true); qc.setQueryData(["dashboard-funnel"], data); } catch (e) { toast.error(apiErrorMessage(e)); } finally { setRefreshing(false); } }; return (

Stack health

{loading || !funnel ? (
) : ( <>
{FUNNEL_STAGES.map(({ key, label }, i) => (

{label}

{funnel[key]}

))}
({ label, value: funnel[key] }))} onHover={setHovered} /> )}
); } function ContainerCountCard({ localRunning, loading, agents, healthyRate, }: { localRunning?: number; loading: boolean; agents: Agent[]; healthyRate: number | null; }) { const online = agents.filter((a) => a.status === "online"); const remote = useQueries({ queries: online.map((a) => ({ queryKey: ["agent-system", a.id], queryFn: () => agentsApi.system(a.id), refetchInterval: 30000, })), }); const hosts: { name: string; count: number }[] = [ { name: "local", count: localRunning ?? 0 }, ...online.map((a, i) => ({ name: a.name, count: remote[i].data?.containers_running ?? 0 })), ]; const total = hosts.reduce((s, h) => s + h.count, 0); const max = Math.max(...hosts.map((h) => h.count), 1); return (

Containers running

{loading ? (
) : ( <>

{total}

{hosts.map((h) => (
{h.name} {h.count}
))}
)}

{healthyRate === null ? "Insights appear once stacks are running." : `${healthyRate}% of running stacks are healthy.`}

); } function UptimeCard({ series, loading, range, }: { series?: { date: string; value: number | null }[]; loading: boolean; range: RangeDays; }) { const values = series?.map((p) => p.value) ?? []; const latest = [...values].reverse().find((v): v is number => v !== null); return (

Uptime

{latest === undefined ? "—" : `${latest}%`}

{range}d
{loading || !series ? (
) : ( )}

Daily share of compose containers running, this host.

); } function OpsCard({ series, peakDay, loading, range, }: { series?: { date: string; count: number }[]; peakDay: string | null; loading: boolean; range: RangeDays; }) { const total = series?.reduce((s, p) => s + p.count, 0) ?? 0; const max = Math.max(...(series?.map((p) => p.count) ?? []), 1); return (

Operations

{total}

{range}d
{loading || !series ? (
) : ( p.count / max)} cols={range} /> )}

{peakDay ? `Busiest day: ${peakDay}.` : "Audit-log actions per day."}

); } /* ---------------------------------------------------------------------- */ /* Host sections (pre-Phase-24, retained) */ /* ---------------------------------------------------------------------- */ 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 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}

); }