Dashboard: per-host resource overview bar (0.20.0)
The dashboard resource bar (CPU cores, memory used/total, containers, Docker version) is now rendered per host instead of once for the local host — each host section (local + each agent) shows its own bar above its stacks table. - agent_app.py: _system_info() now also returns mem_used (from meminfo available), alongside the cpu_cores/mem_total added in 0.19.0. - Frontend: extracted a ResourceBar component used by the local section and each AgentDashboardSection; AgentSystem type gained mem_used. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8fbacd200a
commit
eb9ffd0b0d
@@ -150,12 +150,14 @@ as intuitive as Dockge, as capable as Portainer for Compose workflows.
|
|||||||
|
|
||||||
### Phase 17 — Multi-host dashboard
|
### Phase 17 — Multi-host dashboard
|
||||||
|
|
||||||
- The dashboard now shows a **stacks-with-usage table per host** — the local host
|
- The dashboard now shows, **per host** (local + each registered agent, online
|
||||||
plus a section for each registered agent (online dot, offline notice), with the
|
dot / offline notice), a **resource overview bar** (CPU cores, memory
|
||||||
same CPU/memory meters and inline start/stop/restart as the local list.
|
used/total, containers, Docker version) and a **stacks-with-usage table** with
|
||||||
|
the same 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`,
|
||||||
and `mem_total` so remote meters have a host reference.
|
`mem_total` and `mem_used` so the remote resource bar and meters have a host
|
||||||
|
reference.
|
||||||
|
|
||||||
### Phase 16 — Volumes page (multi-host)
|
### Phase 16 — Volumes page (multi-host)
|
||||||
|
|
||||||
|
|||||||
+17
-6
@@ -61,7 +61,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.19.0"
|
AGENT_VERSION = "0.20.0"
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
@@ -159,16 +159,25 @@ def _hostname() -> str:
|
|||||||
return os.uname().nodename
|
return os.uname().nodename
|
||||||
|
|
||||||
|
|
||||||
def _mem_total() -> int:
|
def _mem_info() -> tuple[int, int]:
|
||||||
|
"""Return (total_bytes, used_bytes) from meminfo (used = total - available)."""
|
||||||
for base in (settings.HOST_PROC_PATH, "/proc"):
|
for base in (settings.HOST_PROC_PATH, "/proc"):
|
||||||
try:
|
try:
|
||||||
|
vals: dict[str, int] = {}
|
||||||
with open(os.path.join(base, "meminfo"), "r", encoding="utf-8") as fh:
|
with open(os.path.join(base, "meminfo"), "r", encoding="utf-8") as fh:
|
||||||
for line in fh:
|
for line in fh:
|
||||||
if line.startswith("MemTotal:"):
|
parts = line.split(":")
|
||||||
return int(line.split()[1]) * 1024 # kB -> bytes
|
if len(parts) == 2 and parts[0] in ("MemTotal", "MemAvailable", "MemFree"):
|
||||||
|
try:
|
||||||
|
vals[parts[0]] = int(parts[1].split()[0]) * 1024 # kB -> bytes
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
total = vals.get("MemTotal", 0)
|
||||||
|
available = vals.get("MemAvailable", vals.get("MemFree", 0))
|
||||||
|
return total, max(total - available, 0)
|
||||||
except OSError:
|
except OSError:
|
||||||
continue
|
continue
|
||||||
return 0
|
return 0, 0
|
||||||
|
|
||||||
|
|
||||||
def _system_info() -> dict:
|
def _system_info() -> dict:
|
||||||
@@ -184,12 +193,14 @@ def _system_info() -> dict:
|
|||||||
total = info.get("Containers", 0)
|
total = info.get("Containers", 0)
|
||||||
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()
|
||||||
return {
|
return {
|
||||||
"hostname": _hostname(),
|
"hostname": _hostname(),
|
||||||
"docker_version": docker_version,
|
"docker_version": docker_version,
|
||||||
"host_os": host_os,
|
"host_os": host_os,
|
||||||
"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,
|
||||||
"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.19.0", lifespan=lifespan)
|
app = FastAPI(title="StackPilot", version="0.20.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.19.0",
|
"version": "0.20.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface AgentSystem {
|
|||||||
host_os: string;
|
host_os: string;
|
||||||
cpu_cores: number;
|
cpu_cores: number;
|
||||||
mem_total: number;
|
mem_total: number;
|
||||||
|
mem_used: number;
|
||||||
containers_running: number;
|
containers_running: number;
|
||||||
containers_total: number;
|
containers_total: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,33 +51,21 @@ export function Dashboard() {
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Local host resource bar */}
|
{/* Local host */}
|
||||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
|
||||||
<Stat icon={<Cpu className="h-5 w-5" />} label="CPU cores" value={info.data?.cpu_cores ?? "—"} />
|
|
||||||
<Stat
|
|
||||||
icon={<MemoryStick className="h-5 w-5" />}
|
|
||||||
label="Memory"
|
|
||||||
value={
|
|
||||||
info.data
|
|
||||||
? `${formatBytes(info.data.ram.used)} / ${formatBytes(info.data.ram.total)}`
|
|
||||||
: "—"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Stat
|
|
||||||
icon={<Container className="h-5 w-5" />}
|
|
||||||
label="Containers"
|
|
||||||
value={info.data ? `${info.data.containers_running} / ${info.data.containers_total}` : "—"}
|
|
||||||
/>
|
|
||||||
<Stat icon={<HardDrive className="h-5 w-5" />} label="Docker" value={info.data?.docker_version ?? "—"} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Local stacks */}
|
|
||||||
<section>
|
<section>
|
||||||
{hasAgents ? (
|
{hasAgents ? (
|
||||||
<HostHeader />
|
<HostHeader />
|
||||||
) : (
|
) : (
|
||||||
<h2 className="mb-3 text-sm font-semibold uppercase tracking-wide text-slate-500">Stacks</h2>
|
<h2 className="mb-3 text-sm font-semibold uppercase tracking-wide text-slate-500">This host</h2>
|
||||||
)}
|
)}
|
||||||
|
<ResourceBar
|
||||||
|
cpuCores={info.data?.cpu_cores ?? 0}
|
||||||
|
memUsed={info.data?.ram.used ?? 0}
|
||||||
|
memTotal={info.data?.ram.total ?? 0}
|
||||||
|
containersRunning={info.data?.containers_running ?? 0}
|
||||||
|
containersTotal={info.data?.containers_total ?? 0}
|
||||||
|
dockerVersion={info.data?.docker_version ?? ""}
|
||||||
|
/>
|
||||||
<StacksTable
|
<StacksTable
|
||||||
stacks={stacks.data}
|
stacks={stacks.data}
|
||||||
stats={stats.data}
|
stats={stats.data}
|
||||||
@@ -175,6 +163,15 @@ function AgentDashboardSection({ agent, isAdmin }: { agent: Agent; isAdmin: bool
|
|||||||
</p>
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
|
<ResourceBar
|
||||||
|
cpuCores={sys.data?.cpu_cores ?? 0}
|
||||||
|
memUsed={sys.data?.mem_used ?? 0}
|
||||||
|
memTotal={sys.data?.mem_total ?? 0}
|
||||||
|
containersRunning={sys.data?.containers_running ?? 0}
|
||||||
|
containersTotal={sys.data?.containers_total ?? 0}
|
||||||
|
dockerVersion={sys.data?.docker_version ?? ""}
|
||||||
|
/>
|
||||||
<StacksTable
|
<StacksTable
|
||||||
stacks={stacks.data}
|
stacks={stacks.data}
|
||||||
stats={stats.data}
|
stats={stats.data}
|
||||||
@@ -189,6 +186,7 @@ function AgentDashboardSection({ agent, isAdmin }: { agent: Agent; isAdmin: bool
|
|||||||
onRestart={(id) => run("restart", "Restarting", id)}
|
onRestart={(id) => run("restart", "Restarting", id)}
|
||||||
emptyText="No stacks on this host."
|
emptyText="No stacks on this host."
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@@ -418,6 +416,39 @@ function Meter({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ResourceBar({
|
||||||
|
cpuCores,
|
||||||
|
memUsed,
|
||||||
|
memTotal,
|
||||||
|
containersRunning,
|
||||||
|
containersTotal,
|
||||||
|
dockerVersion,
|
||||||
|
}: {
|
||||||
|
cpuCores: number;
|
||||||
|
memUsed: number;
|
||||||
|
memTotal: number;
|
||||||
|
containersRunning: number;
|
||||||
|
containersTotal: number;
|
||||||
|
dockerVersion: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="mb-4 grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||||
|
<Stat icon={<Cpu className="h-5 w-5" />} label="CPU cores" value={cpuCores || "—"} />
|
||||||
|
<Stat
|
||||||
|
icon={<MemoryStick className="h-5 w-5" />}
|
||||||
|
label="Memory"
|
||||||
|
value={memTotal ? `${formatBytes(memUsed)} / ${formatBytes(memTotal)}` : "—"}
|
||||||
|
/>
|
||||||
|
<Stat
|
||||||
|
icon={<Container className="h-5 w-5" />}
|
||||||
|
label="Containers"
|
||||||
|
value={`${containersRunning} / ${containersTotal}`}
|
||||||
|
/>
|
||||||
|
<Stat icon={<HardDrive className="h-5 w-5" />} label="Docker" value={dockerVersion || "—"} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function Stat({
|
function Stat({
|
||||||
icon,
|
icon,
|
||||||
label,
|
label,
|
||||||
|
|||||||
Reference in New Issue
Block a user