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) { '
Chargement…
' )); + // DockerMem + grid.appendChild(make_card('card-dockermem','col-12','💾','RAM par conteneur — live', + `
+ Seuil : Mo + — jusqu'à 20 conteneurs affichés, triés par RAM décroissante +
+
Chargement…
` + )); + // NetMap grid.appendChild(make_card('card-netmap','col-12','📡','Appareils réseau — 192.168.1.10–150', `
@@ -558,6 +572,49 @@ function dnRender(nets) { `; } +// ──────────────────────────────── dockermem ── +let dmThreshold = parseInt(localStorage.getItem('dm_threshold_mb') || '50', 10); +let _dmLastContainers = []; + +async function dmRefresh() { + try { + const data = await fetch('/api/dockermem').then(r => r.json()); + _dmLastContainers = data.containers || []; + dmRender(_dmLastContainers); + } catch { /* silencieux */ } +} + +function dmOnThresholdChange(input) { + const v = parseInt(input.value, 10); + dmThreshold = Number.isFinite(v) && v >= 0 ? v : 0; + localStorage.setItem('dm_threshold_mb', String(dmThreshold)); + dmRender(_dmLastContainers); +} + +function dmRender(containers) { + const el = document.getElementById('dockermem-body'); + if (!el) return; + const thresholdBytes = dmThreshold * 1024 * 1024; + const rows = containers.filter(c => c.mem_bytes >= thresholdBytes).slice(0, 20); + if (!rows.length) { + el.innerHTML = `Aucun conteneur au-dessus de ${dmThreshold} Mo`; + return; + } + const trs = rows.map(c => { + const pct = c.mem_pct || 0; + const pctColor = pct > 50 ? 'var(--red)' : (pct > 20 ? 'var(--yellow)' : 'var(--muted)'); + return ` + ${c.name} + ${fmt_bytes(c.mem_bytes)} + ${pct.toFixed(1)}% + `; + }).join(''); + el.innerHTML = ` + + ${trs} +
ConteneurRAM utilisée% limite
`; +} + // ──────────────────────────────── netconn ── let _ncNames = {}; @@ -693,6 +750,7 @@ async function init() { // Premiers chargements dnRefresh(); ncRefresh(); + dmRefresh(); setInterval(async () => { try { @@ -700,6 +758,7 @@ async function init() { update_live(live); dnRefresh(); ncRefresh(); + dmRefresh(); } catch { document.getElementById('refresh-dot').style.background = 'var(--red)'; }