diff --git a/app/main.py b/app/main.py index 5e4dea3..c3b1dff 100644 --- a/app/main.py +++ b/app/main.py @@ -1,5 +1,6 @@ import subprocess, json, time, re, os, http.client, socket as _socket from typing import Optional +from concurrent.futures import ThreadPoolExecutor import psutil import cpuinfo @@ -723,4 +724,45 @@ def api_dockernet(): return {"networks": result, "ts": int(now)} +# ─────────────────────────────────────────────────────── DOCKERMEM ── + +def _container_mem(c: dict) -> Optional[dict]: + """RAM réellement utilisée par un conteneur (usage - cache, comme `docker stats`).""" + cid = c.get("Id") + name = c.get("Names", [""])[0].lstrip("/") + try: + stats = _docker_api(f"/containers/{cid}/stats?stream=false") + mem = stats.get("memory_stats", {}) + usage = mem.get("usage", 0) + sub = mem.get("stats", {}).get("inactive_file", mem.get("stats", {}).get("cache", 0)) + used = max(0, usage - sub) + limit = mem.get("limit", 0) + return { + "name": name, + "mem_bytes": used, + "mem_limit": limit, + "mem_pct": (used / limit * 100) if limit else 0, + } + except Exception: + return None + + +@app.get("/api/dockermem") +def api_dockermem(): + try: + containers = _docker_api("/containers/json") + except Exception: + return {"containers": [], "ts": int(time.time())} + + # Docker échantillonne cpu/precpu à ~1s d'intervalle même avec stream=false + # (le endpoint stats renvoie toujours les deux) : avec 60+ conteneurs il faut + # tout lancer en parallèle (pas de petit pool) pour rester sous quelques secondes. + with ThreadPoolExecutor(max_workers=max(1, len(containers))) as ex: + results = list(ex.map(_container_mem, containers)) + + result = [r for r in results if r] + result.sort(key=lambda x: x["mem_bytes"], reverse=True) + return {"containers": result, "ts": int(time.time())} + + app.mount("/", StaticFiles(directory="/app/static", html=True), name="static") diff --git a/static/index.html b/static/index.html index 42b4a74..5262b8b 100644 --- a/static/index.html +++ b/static/index.html @@ -125,6 +125,11 @@ .netmap-status { font-size: 12px; color: var(--muted); } .netmap-name-input { background: #21262d; border: 1px solid var(--border); color: var(--text); border-radius: 5px; padding: 3px 8px; font-size: 12px; width: 130px; transition: border-color .15s; } .netmap-name-input:focus { outline: none; border-color: var(--accent); } + + /* DOCKERMEM */ + .dm-toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; font-size: 13px; } + .dm-toolbar input[type="number"] { background: #21262d; border: 1px solid var(--border); color: var(--text); border-radius: 5px; padding: 3px 8px; font-size: 12px; width: 70px; } + .dm-toolbar input[type="number"]:focus { outline: none; border-color: var(--accent); } .netmap-save-btn { background: none; border: 1px solid var(--border); color: var(--accent); border-radius: 5px; padding: 3px 10px; font-size: 11px; cursor: pointer; transition: background .15s; } .netmap-save-btn:hover { background: #21262d; } .netmap-flash { font-size: 11px; color: var(--green); width: 14px; display: inline-block; } @@ -404,6 +409,15 @@ function build_ui(os, hw, raid) { '