From bb29ef1c9896d98a4b92b1008c3589c0d7979b01 Mon Sep 17 00:00:00 2001 From: menzelj Date: Mon, 8 Jun 2026 13:42:22 +0000 Subject: [PATCH] Dashboard: per-host Docker volumes total (0.21.1) The per-host resource bar gained a "Volumes" stat showing the total size of that host's Docker volumes. It reuses the existing cached /volumes/sizes lookup (docker system df, ~60s TTL) summed client-side, polled every 60s per host so the slow df walk never blocks the fast system-info poll. Frontend-only. Fixed sumSizes to reduce so the value is number|undefined. Co-Authored-By: Claude Opus 4.8 --- README.md | 7 ++++--- backend/agent_app.py | 2 +- backend/main.py | 2 +- frontend/package.json | 2 +- frontend/src/pages/Dashboard.tsx | 28 +++++++++++++++++++++++++++- 5 files changed, 34 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4563430..e587fdc 100644 --- a/README.md +++ b/README.md @@ -152,9 +152,10 @@ as intuitive as Dockge, as capable as Portainer for Compose workflows. - The dashboard now shows, **per host** (local + each registered agent, online dot / offline notice), a **resource overview bar** (CPU cores, memory - used/total, disk used/total, containers, Docker version) and a - **stacks-with-usage table** with CPU/memory meters and inline - start/stop/restart. + used/total, disk used/total, Docker volumes total, containers, Docker version) + and a **stacks-with-usage table** with CPU/memory meters and inline + start/stop/restart. The volumes total reuses the cached `/volumes/sizes` + lookup (`docker system df`), polled gently (~60s). - New agent endpoint `/agent/stacks/stats` (proxied at `/api/agents/{id}/stacks/stats`); `/agent/system` now also reports `cpu_cores`, `mem_total`, `mem_used`, and `disk_total`/`disk_used` (disk of the host volume diff --git a/backend/agent_app.py b/backend/agent_app.py index 5bf39ae..f0c9e8d 100644 --- a/backend/agent_app.py +++ b/backend/agent_app.py @@ -62,7 +62,7 @@ def _map_docker(exc: DockerError): raise HTTPException(status_code=code, detail=exc.detail or exc.error) raise exc # falls through to the global 502 DockerError handler -AGENT_VERSION = "0.21.0" +AGENT_VERSION = "0.21.1" # --------------------------------------------------------------------------- # diff --git a/backend/main.py b/backend/main.py index a871f00..6640b19 100644 --- a/backend/main.py +++ b/backend/main.py @@ -55,7 +55,7 @@ async def lifespan(app: FastAPI): schedule_task.cancel() -app = FastAPI(title="StackPilot", version="0.21.0", lifespan=lifespan) +app = FastAPI(title="StackPilot", version="0.21.1", lifespan=lifespan) app.add_middleware( CORSMiddleware, diff --git a/frontend/package.json b/frontend/package.json index b129b70..1de8c36 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "stackpilot-frontend", "private": true, - "version": "0.21.0", + "version": "0.21.1", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index a96f3ea..22a6aae 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -12,6 +12,7 @@ import { Square, RotateCw, Server, + Database, } from "lucide-react"; import { toast } from "sonner"; import { Card, Spinner, StatusDot, Badge } from "@/components/ui"; @@ -20,12 +21,16 @@ 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(); @@ -36,6 +41,12 @@ export function Dashboard() { 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; @@ -65,6 +76,7 @@ export function Dashboard() { memTotal={info.data?.ram.total ?? 0} diskUsed={info.data?.disk.used ?? 0} diskTotal={info.data?.disk.total ?? 0} + volumesSize={volSize.data} containersRunning={info.data?.containers_running ?? 0} containersTotal={info.data?.containers_total ?? 0} dockerVersion={info.data?.docker_version ?? ""} @@ -140,6 +152,12 @@ function AgentDashboardSection({ agent, isAdmin }: { agent: Agent; isAdmin: bool 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); @@ -173,6 +191,7 @@ function AgentDashboardSection({ agent, isAdmin }: { agent: Agent; isAdmin: bool memTotal={sys.data?.mem_total ?? 0} diskUsed={sys.data?.disk_used ?? 0} diskTotal={sys.data?.disk_total ?? 0} + volumesSize={volSize.data} containersRunning={sys.data?.containers_running ?? 0} containersTotal={sys.data?.containers_total ?? 0} dockerVersion={sys.data?.docker_version ?? ""} @@ -427,6 +446,7 @@ function ResourceBar({ memTotal, diskUsed, diskTotal, + volumesSize, containersRunning, containersTotal, dockerVersion, @@ -436,12 +456,13 @@ function ResourceBar({ memTotal: number; diskUsed: number; diskTotal: number; + volumesSize?: number; containersRunning: number; containersTotal: number; dockerVersion: string; }) { return ( -
+
} label="CPU cores" value={cpuCores || "—"} /> } @@ -453,6 +474,11 @@ function ResourceBar({ label="Disk" value={diskTotal ? `${formatBytes(diskUsed)} / ${formatBytes(diskTotal)}` : "—"} /> + } + label="Volumes" + value={volumesSize === undefined ? "…" : formatBytes(volumesSize)} + /> } label="Containers"