@@ -414,12 +438,59 @@ async function updateHosts() {
} catch(e) { console.log("proxmox fetch failed", e); }
}
+// ─── Docker Stats ──────────────────────────────────────────────────────
+function renderDocker(containers) {
+ const card = document.getElementById("docker-card");
+ if (!containers || containers.length === 0) {
+ card.innerHTML = '
No Docker containers found';
+ document.getElementById("docker-count").textContent = "0";
+ return;
+ }
+
+ let html = `
+ `;
+
+ for (const c of containers) {
+ const memPct = parseFloat(c.mem_pct) || 0;
+ html += `
+
${c.name}
+
${c.cpu}%
+
+
${c.net_in} / ${c.net_out}
+
${c.blk_in} / ${c.blk_out}
+
${c.pids}
+
`;
+ }
+ html += `
`;
+ card.innerHTML = html;
+ document.getElementById("docker-count").textContent = containers.length;
+}
+
+async function updateDocker() {
+ try {
+ const r = await fetch("/api/docker/stats");
+ const d = await r.json();
+ if (d.error && !d.containers) {
+ document.getElementById("docker-card").innerHTML = `
${d.error}`;
+ return;
+ }
+ renderDocker(d.containers);
+ } catch(e) {
+ console.log("docker fetch failed", e);
+ document.getElementById("docker-card").innerHTML = '
Docker API unreachable';
+ }
+}
+
// ─── Init ─────────────────────────────────────────────────────────────
renderServices();
updateStatus();
updateHosts();
+updateDocker();
setInterval(updateStatus, 60000);
setInterval(updateHosts, 30000);
+setInterval(updateDocker, 30000);
// ─── Dark Mode Toggle ──────────────────────────────────────────────────
function toggleDark() {
diff --git a/serve.py b/serve.py
index d974ff5..b4c8d4e 100755
--- a/serve.py
+++ b/serve.py
@@ -179,6 +179,68 @@ async def health():
return {"status": "ok"}
+@app.get("/api/docker/stats")
+async def docker_stats():
+ """Get Docker container stats from CT 100 via Proxmox SSH."""
+ import subprocess
+ try:
+ # Run docker stats on CT 100 via proxmox (use raw output to avoid Go template escaping)
+ result = subprocess.run(
+ ["sshpass", "-p", _PVE_PASS, "ssh", "-o", "StrictHostKeyChecking=no",
+ "root@192.168.0.144", "pct", "exec", "100", "--",
+ "docker", "stats", "--no-stream"],
+ capture_output=True, text=True, timeout=15
+ )
+ if result.returncode != 0:
+ return {"error": result.stderr.strip(), "containers": []}
+
+ containers = []
+ for line in result.stdout.strip().split("\n"):
+ if not line.strip() or line.startswith("CONTAINER"):
+ continue
+ # Parse default docker stats output:
+ # CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
+ parts = line.split()
+ if len(parts) < 8:
+ continue
+ # Account for container ID being first column
+ cid = parts[0]
+ name = parts[1]
+ cpu = parts[2].replace("%", "")
+ mem_usage = parts[3] # e.g., "311.2MiB"
+ mem_limit = parts[5] # e.g., "2GiB" (parts[4] is "/")
+ mem_pct = parts[6].replace("%", "")
+ # NET I/O: parts[7] and parts[9] with "/" at parts[8]
+ # BLOCK I/O: parts[10] and parts[12] with "/" at parts[11]
+ # PIDS: last element (could be at different positions depending on name length)
+ # More reliable: find from the right
+ pids = parts[-1]
+ # Block I/O is tricky because of spaces in column names;
+ # use a simpler approach: rest of line after NET I/O position
+ # Actually, docker stats columns are fixed-width, let's just grab what we can
+ net_in = parts[7] if len(parts) > 7 else "0"
+ net_out = parts[9] if len(parts) > 9 else "0"
+ blk_in = parts[10] if len(parts) > 10 else "0"
+ blk_out = parts[12] if len(parts) > 12 else "0"
+
+ containers.append({
+ "name": name,
+ "cpu": cpu,
+ "mem_pct": mem_pct,
+ "mem_used": mem_usage,
+ "mem_limit": mem_limit,
+ "net_in": net_in,
+ "net_out": net_out,
+ "blk_in": blk_in,
+ "blk_out": blk_out,
+ "pids": pids,
+ })
+
+ return {"containers": containers, "host": "CT 100 (docker)", "count": len(containers)}
+ except Exception as e:
+ return {"error": str(e)[:200], "containers": []}
+
+
@app.get("/{path:path}")
async def index(path: str = ""):
"""Serve static files, fallback to index.html."""
@@ -188,7 +250,7 @@ async def index(path: str = ""):
file_path = STATIC_DIR / "index.html"
if file_path.is_file():
return HTMLResponse(file_path.read_text())
- return HTMLResponse("
404
", 404)
+ return HTMLResponse("
404
\n", 404)
if __name__ == "__main__":