Dashboard: per-host disk usage (0.21.0)
The per-host resource bar gained a Disk stat (used / total). The agent now reports disk_total/disk_used via shutil.disk_usage on its stacks dir (a host bind-mount), alongside the existing cpu/mem/containers fields; the local host uses the existing system info disk data. - agent_app.py: _disk_info() + disk_total/disk_used in _system_info(). - Frontend: ResourceBar gained diskUsed/diskTotal (5-column grid); AgentSystem type gained disk_total/disk_used. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
eb9ffd0b0d
commit
d1c63a329b
@@ -152,11 +152,13 @@ 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, containers, Docker version) and a **stacks-with-usage table** with
|
used/total, disk used/total, containers, Docker version) and a
|
||||||
the same CPU/memory meters and inline start/stop/restart.
|
**stacks-with-usage table** with CPU/memory meters and inline
|
||||||
|
start/stop/restart.
|
||||||
- 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` and `mem_used` so the remote resource bar and meters have a host
|
`mem_total`, `mem_used`, and `disk_total`/`disk_used` (disk of the host volume
|
||||||
|
backing the stacks dir) so the remote resource bar and meters have a host
|
||||||
reference.
|
reference.
|
||||||
|
|
||||||
### Phase 16 — Volumes page (multi-host)
|
### Phase 16 — Volumes page (multi-host)
|
||||||
|
|||||||
+16
-1
@@ -12,6 +12,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
|
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -61,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.20.0"
|
AGENT_VERSION = "0.21.0"
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
@@ -180,6 +181,17 @@ def _mem_info() -> tuple[int, int]:
|
|||||||
return 0, 0
|
return 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
def _disk_info() -> tuple[int, int]:
|
||||||
|
"""Return (total_bytes, used_bytes) for the host disk backing the stacks dir."""
|
||||||
|
for path in (settings.STACKS_DIR, "/"):
|
||||||
|
try:
|
||||||
|
usage = shutil.disk_usage(path)
|
||||||
|
return usage.total, usage.used
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
return 0, 0
|
||||||
|
|
||||||
|
|
||||||
def _system_info() -> dict:
|
def _system_info() -> dict:
|
||||||
docker_version = ""
|
docker_version = ""
|
||||||
host_os = ""
|
host_os = ""
|
||||||
@@ -194,6 +206,7 @@ def _system_info() -> dict:
|
|||||||
except DockerError as exc:
|
except DockerError as exc:
|
||||||
docker_version = f"unavailable ({exc.error})"
|
docker_version = f"unavailable ({exc.error})"
|
||||||
mem_total, mem_used = _mem_info()
|
mem_total, mem_used = _mem_info()
|
||||||
|
disk_total, disk_used = _disk_info()
|
||||||
return {
|
return {
|
||||||
"hostname": _hostname(),
|
"hostname": _hostname(),
|
||||||
"docker_version": docker_version,
|
"docker_version": docker_version,
|
||||||
@@ -201,6 +214,8 @@ def _system_info() -> dict:
|
|||||||
"cpu_cores": os.cpu_count() or 0,
|
"cpu_cores": os.cpu_count() or 0,
|
||||||
"mem_total": mem_total,
|
"mem_total": mem_total,
|
||||||
"mem_used": mem_used,
|
"mem_used": mem_used,
|
||||||
|
"disk_total": disk_total,
|
||||||
|
"disk_used": disk_used,
|
||||||
"containers_running": running,
|
"containers_running": running,
|
||||||
"containers_total": total,
|
"containers_total": total,
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -55,7 +55,7 @@ async def lifespan(app: FastAPI):
|
|||||||
schedule_task.cancel()
|
schedule_task.cancel()
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(title="StackPilot", version="0.20.0", lifespan=lifespan)
|
app = FastAPI(title="StackPilot", version="0.21.0", 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.20.0",
|
"version": "0.21.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export interface AgentSystem {
|
|||||||
cpu_cores: number;
|
cpu_cores: number;
|
||||||
mem_total: number;
|
mem_total: number;
|
||||||
mem_used: number;
|
mem_used: number;
|
||||||
|
disk_total: number;
|
||||||
|
disk_used: number;
|
||||||
containers_running: number;
|
containers_running: number;
|
||||||
containers_total: number;
|
containers_total: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
Play,
|
Play,
|
||||||
Square,
|
Square,
|
||||||
RotateCw,
|
RotateCw,
|
||||||
|
Server,
|
||||||
} 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";
|
||||||
@@ -62,6 +63,8 @@ export function Dashboard() {
|
|||||||
cpuCores={info.data?.cpu_cores ?? 0}
|
cpuCores={info.data?.cpu_cores ?? 0}
|
||||||
memUsed={info.data?.ram.used ?? 0}
|
memUsed={info.data?.ram.used ?? 0}
|
||||||
memTotal={info.data?.ram.total ?? 0}
|
memTotal={info.data?.ram.total ?? 0}
|
||||||
|
diskUsed={info.data?.disk.used ?? 0}
|
||||||
|
diskTotal={info.data?.disk.total ?? 0}
|
||||||
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 ?? ""}
|
||||||
@@ -168,6 +171,8 @@ function AgentDashboardSection({ agent, isAdmin }: { agent: Agent; isAdmin: bool
|
|||||||
cpuCores={sys.data?.cpu_cores ?? 0}
|
cpuCores={sys.data?.cpu_cores ?? 0}
|
||||||
memUsed={sys.data?.mem_used ?? 0}
|
memUsed={sys.data?.mem_used ?? 0}
|
||||||
memTotal={sys.data?.mem_total ?? 0}
|
memTotal={sys.data?.mem_total ?? 0}
|
||||||
|
diskUsed={sys.data?.disk_used ?? 0}
|
||||||
|
diskTotal={sys.data?.disk_total ?? 0}
|
||||||
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 ?? ""}
|
||||||
@@ -420,6 +425,8 @@ function ResourceBar({
|
|||||||
cpuCores,
|
cpuCores,
|
||||||
memUsed,
|
memUsed,
|
||||||
memTotal,
|
memTotal,
|
||||||
|
diskUsed,
|
||||||
|
diskTotal,
|
||||||
containersRunning,
|
containersRunning,
|
||||||
containersTotal,
|
containersTotal,
|
||||||
dockerVersion,
|
dockerVersion,
|
||||||
@@ -427,24 +434,31 @@ function ResourceBar({
|
|||||||
cpuCores: number;
|
cpuCores: number;
|
||||||
memUsed: number;
|
memUsed: number;
|
||||||
memTotal: number;
|
memTotal: number;
|
||||||
|
diskUsed: number;
|
||||||
|
diskTotal: 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-4">
|
<div className="mb-4 grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-5">
|
||||||
<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" />}
|
||||||
label="Memory"
|
label="Memory"
|
||||||
value={memTotal ? `${formatBytes(memUsed)} / ${formatBytes(memTotal)}` : "—"}
|
value={memTotal ? `${formatBytes(memUsed)} / ${formatBytes(memTotal)}` : "—"}
|
||||||
/>
|
/>
|
||||||
|
<Stat
|
||||||
|
icon={<HardDrive className="h-5 w-5" />}
|
||||||
|
label="Disk"
|
||||||
|
value={diskTotal ? `${formatBytes(diskUsed)} / ${formatBytes(diskTotal)}` : "—"}
|
||||||
|
/>
|
||||||
<Stat
|
<Stat
|
||||||
icon={<Container className="h-5 w-5" />}
|
icon={<Container className="h-5 w-5" />}
|
||||||
label="Containers"
|
label="Containers"
|
||||||
value={`${containersRunning} / ${containersTotal}`}
|
value={`${containersRunning} / ${containersTotal}`}
|
||||||
/>
|
/>
|
||||||
<Stat icon={<HardDrive className="h-5 w-5" />} label="Docker" value={dockerVersion || "—"} />
|
<Stat icon={<Server className="h-5 w-5" />} label="Docker" value={dockerVersion || "—"} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user