feat: card RAM par conteneur — live, avec seuil configurable
Nouvel endpoint /api/dockermem (Docker API stats en parallèle sur tous les conteneurs, ~2s pour 68 conteneurs). Carte affichant jusqu'à 20 conteneurs triés par RAM utilisée décroissante, filtrés par un seuil en Mo réglable dans l'interface (persisté en localStorage).
This commit is contained in:
+42
@@ -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")
|
||||
|
||||
@@ -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) {
|
||||
'<div id="netconn-body"><span style="color:var(--muted)">Chargement…</span></div>'
|
||||
));
|
||||
|
||||
// DockerMem
|
||||
grid.appendChild(make_card('card-dockermem','col-12','💾','RAM par conteneur — live',
|
||||
`<div class="dm-toolbar">
|
||||
Seuil : <input type="number" id="dockermem-threshold" min="0" step="10" value="${dmThreshold}" oninput="dmOnThresholdChange(this)"> Mo
|
||||
<span style="color:var(--muted);font-size:12px">— jusqu'à 20 conteneurs affichés, triés par RAM décroissante</span>
|
||||
</div>
|
||||
<div id="dockermem-body"><span style="color:var(--muted)">Chargement…</span></div>`
|
||||
));
|
||||
|
||||
// NetMap
|
||||
grid.appendChild(make_card('card-netmap','col-12','📡','Appareils réseau — 192.168.1.10–150',
|
||||
`<div class="netmap-toolbar">
|
||||
@@ -558,6 +572,49 @@ function dnRender(nets) {
|
||||
</table>`;
|
||||
}
|
||||
|
||||
// ──────────────────────────────── 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 = `<span style="color:var(--muted)">Aucun conteneur au-dessus de ${dmThreshold} Mo</span>`;
|
||||
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 `<tr>
|
||||
<td class="nc-ip">${c.name}</td>
|
||||
<td class="nc-bps">${fmt_bytes(c.mem_bytes)}</td>
|
||||
<td style="color:${pctColor}">${pct.toFixed(1)}%</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
el.innerHTML = `<table class="nc-table">
|
||||
<thead><tr><th>Conteneur</th><th>RAM utilisée</th><th>% limite</th></tr></thead>
|
||||
<tbody>${trs}</tbody>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
// ──────────────────────────────── 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)';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user