add Docker container stats endpoint + dashboard section
This commit is contained in:
+71
@@ -64,6 +64,19 @@
|
||||
.lxc-pct { font-family: 'Geist Mono', monospace; font-size: 10px; color: var(--gray-400); min-width: 32px; text-align: right; }
|
||||
.lxc-off { opacity: 0.4; }
|
||||
|
||||
/* Docker table (same as LXC, wider) */
|
||||
.docker-table { margin-top: 12px; border-top: 1px solid var(--gray-100); padding-top: 8px; }
|
||||
.docker-header { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px; color: var(--gray-400); display: grid; grid-template-columns: 1.6fr 0.5fr 0.6fr 1.2fr 1.2fr 0.3fr; gap: 6px; padding: 2px 0; }
|
||||
.docker-row { display: grid; grid-template-columns: 1.6fr 0.5fr 0.6fr 1.2fr 1.2fr 0.3fr; gap: 6px; align-items: center; font-size: 11px; padding: 3px 0; }
|
||||
.docker-row + .docker-row { border-top: 1px solid var(--gray-50); }
|
||||
.docker-name { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.docker-cpu { color: var(--gray-400); font-family: 'Geist Mono', monospace; font-size: 10px; }
|
||||
.docker-bar-wrap { display: flex; align-items: center; gap: 4px; }
|
||||
.docker-bar-bg { flex: 1; height: 5px; background: var(--gray-100); border-radius: 2px; overflow: hidden; }
|
||||
.docker-bar-fill { height: 100%; border-radius: 2px; }
|
||||
.docker-val { font-family: 'Geist Mono', monospace; font-size: 10px; color: var(--gray-400); }
|
||||
.docker-io { font-size: 10px; color: var(--gray-400); font-family: 'Geist Mono', monospace; }
|
||||
|
||||
/* Service cards */
|
||||
.grid-4 { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
|
||||
.svc-card { display: flex; align-items: flex-start; gap: 12px; padding: 14px 18px; background: var(--white); box-shadow: var(--ring); border-radius: 8px; text-decoration: none; color: inherit; cursor: pointer; transition: box-shadow 0.15s, transform 0.1s; }
|
||||
@@ -152,6 +165,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── DOCKER CONTAINERS ─── -->
|
||||
<div class="alt-bg">
|
||||
<div class="section">
|
||||
<div class="section-header"><h2>🐳 Docker Containers</h2><span class="badge" id="docker-count">0</span></div>
|
||||
<p style="font-size:13px;color:var(--gray-400);margin:-8px 0 20px;">CT 100 (docker LXC) on primary Proxmox — CPU, memory, network, disk I/O</p>
|
||||
<div class="host-card" id="docker-card">
|
||||
<span class="stats-error">Loading Docker stats...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── PUBLIC SERVICES ─── -->
|
||||
<div class="alt-bg">
|
||||
<div class="section">
|
||||
@@ -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 = '<span class="stats-error">No Docker containers found</span>';
|
||||
document.getElementById("docker-count").textContent = "0";
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `<div class="docker-table">
|
||||
<div class="docker-header">
|
||||
<span>Container</span><span>CPU</span><span>RAM</span><span>Network ↓ / ↑</span><span>Disk ↓ / ↑</span><span>PIDs</span>
|
||||
</div>`;
|
||||
|
||||
for (const c of containers) {
|
||||
const memPct = parseFloat(c.mem_pct) || 0;
|
||||
html += `<div class="docker-row">
|
||||
<span class="docker-name" title="${c.name}">${c.name}</span>
|
||||
<span class="docker-cpu">${c.cpu}%</span>
|
||||
<div class="docker-bar-wrap"><div class="docker-bar-bg"><div class="docker-bar-fill" style="width:${Math.min(memPct,100)}%;background:${barColor(memPct)}"></div></div><span class="docker-val">${c.mem_used}</span></div>
|
||||
<span class="docker-io">${c.net_in} / ${c.net_out}</span>
|
||||
<span class="docker-io">${c.blk_in} / ${c.blk_out}</span>
|
||||
<span class="docker-val">${c.pids}</span>
|
||||
</div>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
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 = `<span class="stats-error">${d.error}</span>`;
|
||||
return;
|
||||
}
|
||||
renderDocker(d.containers);
|
||||
} catch(e) {
|
||||
console.log("docker fetch failed", e);
|
||||
document.getElementById("docker-card").innerHTML = '<span class="stats-error">Docker API unreachable</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Init ─────────────────────────────────────────────────────────────
|
||||
renderServices();
|
||||
updateStatus();
|
||||
updateHosts();
|
||||
updateDocker();
|
||||
setInterval(updateStatus, 60000);
|
||||
setInterval(updateHosts, 30000);
|
||||
setInterval(updateDocker, 30000);
|
||||
|
||||
// ─── Dark Mode Toggle ──────────────────────────────────────────────────
|
||||
function toggleDark() {
|
||||
|
||||
@@ -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("<h1>404</h1>", 404)
|
||||
return HTMLResponse("<h1>404</h1>\n", 404)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user