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<number> so the value is number|undefined. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d1c63a329b
commit
bb29ef1c98
@@ -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
|
- The dashboard now shows, **per host** (local + each registered agent, online
|
||||||
dot / offline notice), a **resource overview bar** (CPU cores, memory
|
dot / offline notice), a **resource overview bar** (CPU cores, memory
|
||||||
used/total, disk used/total, containers, Docker version) and a
|
used/total, disk used/total, Docker volumes total, containers, Docker version)
|
||||||
**stacks-with-usage table** with CPU/memory meters and inline
|
and a **stacks-with-usage table** with CPU/memory meters and inline
|
||||||
start/stop/restart.
|
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
|
- New agent endpoint `/agent/stacks/stats` (proxied at
|
||||||
`/api/agents/{id}/stacks/stats`); `/agent/system` now also reports `cpu_cores`,
|
`/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
|
`mem_total`, `mem_used`, and `disk_total`/`disk_used` (disk of the host volume
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ def _map_docker(exc: DockerError):
|
|||||||
raise HTTPException(status_code=code, detail=exc.detail or exc.error)
|
raise HTTPException(status_code=code, detail=exc.detail or exc.error)
|
||||||
raise exc # falls through to the global 502 DockerError handler
|
raise exc # falls through to the global 502 DockerError handler
|
||||||
|
|
||||||
AGENT_VERSION = "0.21.0"
|
AGENT_VERSION = "0.21.1"
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
|
|||||||
+1
-1
@@ -55,7 +55,7 @@ async def lifespan(app: FastAPI):
|
|||||||
schedule_task.cancel()
|
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(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "stackpilot-frontend",
|
"name": "stackpilot-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.21.0",
|
"version": "0.21.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Square,
|
Square,
|
||||||
RotateCw,
|
RotateCw,
|
||||||
Server,
|
Server,
|
||||||
|
Database,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Card, Spinner, StatusDot, Badge } from "@/components/ui";
|
import { Card, Spinner, StatusDot, Badge } from "@/components/ui";
|
||||||
@@ -20,12 +21,16 @@ import { stacksApi } from "@/api/stacks";
|
|||||||
import { systemApi } from "@/api/system";
|
import { systemApi } from "@/api/system";
|
||||||
import { imagesApi } from "@/api/images";
|
import { imagesApi } from "@/api/images";
|
||||||
import { agentsApi } from "@/api/agents";
|
import { agentsApi } from "@/api/agents";
|
||||||
|
import { volumesApi } from "@/api/volumes";
|
||||||
import { apiErrorMessage } from "@/api/client";
|
import { apiErrorMessage } from "@/api/client";
|
||||||
import { formatBytes, relativeTime } from "@/lib/utils";
|
import { formatBytes, relativeTime } from "@/lib/utils";
|
||||||
import { useAuthStore } from "@/store/auth";
|
import { useAuthStore } from "@/store/auth";
|
||||||
import { useStackActions } from "@/hooks/useStackActions";
|
import { useStackActions } from "@/hooks/useStackActions";
|
||||||
import type { Agent, StackStats, StackSummary } from "@/types";
|
import type { Agent, StackStats, StackSummary } from "@/types";
|
||||||
|
|
||||||
|
const sumSizes = (m: Record<string, number | null>) =>
|
||||||
|
Object.values(m).reduce<number>((a, b) => a + (b ?? 0), 0);
|
||||||
|
|
||||||
export function Dashboard() {
|
export function Dashboard() {
|
||||||
const isAdmin = useAuthStore((s) => s.user?.role === "admin");
|
const isAdmin = useAuthStore((s) => s.user?.role === "admin");
|
||||||
const { busyId, start, stop, restart } = useStackActions();
|
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 audit = useQuery({ queryKey: ["audit"], queryFn: () => systemApi.audit(10), refetchInterval: 10000 });
|
||||||
const updates = useQuery({ queryKey: ["image-updates"], queryFn: () => imagesApi.updates(), refetchInterval: 60000 });
|
const updates = useQuery({ queryKey: ["image-updates"], queryFn: () => imagesApi.updates(), refetchInterval: 60000 });
|
||||||
const agents = useQuery({ queryKey: ["agents"], queryFn: () => agentsApi.list(), refetchInterval: 15000 });
|
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 updateCount = Object.values(updates.data ?? {}).filter((u) => u.update_available).length;
|
||||||
const hasAgents = (agents.data?.length ?? 0) > 0;
|
const hasAgents = (agents.data?.length ?? 0) > 0;
|
||||||
@@ -65,6 +76,7 @@ export function Dashboard() {
|
|||||||
memTotal={info.data?.ram.total ?? 0}
|
memTotal={info.data?.ram.total ?? 0}
|
||||||
diskUsed={info.data?.disk.used ?? 0}
|
diskUsed={info.data?.disk.used ?? 0}
|
||||||
diskTotal={info.data?.disk.total ?? 0}
|
diskTotal={info.data?.disk.total ?? 0}
|
||||||
|
volumesSize={volSize.data}
|
||||||
containersRunning={info.data?.containers_running ?? 0}
|
containersRunning={info.data?.containers_running ?? 0}
|
||||||
containersTotal={info.data?.containers_total ?? 0}
|
containersTotal={info.data?.containers_total ?? 0}
|
||||||
dockerVersion={info.data?.docker_version ?? ""}
|
dockerVersion={info.data?.docker_version ?? ""}
|
||||||
@@ -140,6 +152,12 @@ function AgentDashboardSection({ agent, isAdmin }: { agent: Agent; isAdmin: bool
|
|||||||
enabled: online,
|
enabled: online,
|
||||||
refetchInterval: 30000,
|
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) => {
|
const run = async (action: string, label: string, id: string) => {
|
||||||
setBusyId(id);
|
setBusyId(id);
|
||||||
@@ -173,6 +191,7 @@ function AgentDashboardSection({ agent, isAdmin }: { agent: Agent; isAdmin: bool
|
|||||||
memTotal={sys.data?.mem_total ?? 0}
|
memTotal={sys.data?.mem_total ?? 0}
|
||||||
diskUsed={sys.data?.disk_used ?? 0}
|
diskUsed={sys.data?.disk_used ?? 0}
|
||||||
diskTotal={sys.data?.disk_total ?? 0}
|
diskTotal={sys.data?.disk_total ?? 0}
|
||||||
|
volumesSize={volSize.data}
|
||||||
containersRunning={sys.data?.containers_running ?? 0}
|
containersRunning={sys.data?.containers_running ?? 0}
|
||||||
containersTotal={sys.data?.containers_total ?? 0}
|
containersTotal={sys.data?.containers_total ?? 0}
|
||||||
dockerVersion={sys.data?.docker_version ?? ""}
|
dockerVersion={sys.data?.docker_version ?? ""}
|
||||||
@@ -427,6 +446,7 @@ function ResourceBar({
|
|||||||
memTotal,
|
memTotal,
|
||||||
diskUsed,
|
diskUsed,
|
||||||
diskTotal,
|
diskTotal,
|
||||||
|
volumesSize,
|
||||||
containersRunning,
|
containersRunning,
|
||||||
containersTotal,
|
containersTotal,
|
||||||
dockerVersion,
|
dockerVersion,
|
||||||
@@ -436,12 +456,13 @@ function ResourceBar({
|
|||||||
memTotal: number;
|
memTotal: number;
|
||||||
diskUsed: number;
|
diskUsed: number;
|
||||||
diskTotal: number;
|
diskTotal: number;
|
||||||
|
volumesSize?: number;
|
||||||
containersRunning: number;
|
containersRunning: number;
|
||||||
containersTotal: number;
|
containersTotal: number;
|
||||||
dockerVersion: string;
|
dockerVersion: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-5">
|
<div className="mb-4 grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-6">
|
||||||
<Stat icon={<Cpu className="h-5 w-5" />} label="CPU cores" value={cpuCores || "—"} />
|
<Stat icon={<Cpu className="h-5 w-5" />} label="CPU cores" value={cpuCores || "—"} />
|
||||||
<Stat
|
<Stat
|
||||||
icon={<MemoryStick className="h-5 w-5" />}
|
icon={<MemoryStick className="h-5 w-5" />}
|
||||||
@@ -453,6 +474,11 @@ function ResourceBar({
|
|||||||
label="Disk"
|
label="Disk"
|
||||||
value={diskTotal ? `${formatBytes(diskUsed)} / ${formatBytes(diskTotal)}` : "—"}
|
value={diskTotal ? `${formatBytes(diskUsed)} / ${formatBytes(diskTotal)}` : "—"}
|
||||||
/>
|
/>
|
||||||
|
<Stat
|
||||||
|
icon={<Database className="h-5 w-5" />}
|
||||||
|
label="Volumes"
|
||||||
|
value={volumesSize === undefined ? "…" : formatBytes(volumesSize)}
|
||||||
|
/>
|
||||||
<Stat
|
<Stat
|
||||||
icon={<Container className="h-5 w-5" />}
|
icon={<Container className="h-5 w-5" />}
|
||||||
label="Containers"
|
label="Containers"
|
||||||
|
|||||||
Reference in New Issue
Block a user