diff --git a/internal/jean/ui/index.html b/internal/jean/ui/index.html
index e0ac5e5..822f22e 100644
--- a/internal/jean/ui/index.html
+++ b/internal/jean/ui/index.html
@@ -326,6 +326,9 @@ button:disabled{opacity:.5;cursor:not-allowed}
GPU
…
+ RAM
+ …
+
Config active
…
@@ -772,6 +775,16 @@ async function loadVram(){
'
'+(g.used/1024).toFixed(1)+' / '+(g.total/1024).toFixed(1)+' GiB · GPU '+g.util+'% · '+g.temp+'°C
';
}).join('') || '(pas de GPU)';
}
+async function loadRam(){
+ const m=await jget('/api/ram');
+ const box=document.getElementById('ram-details');
+ if(!m || !m.total){ if(box) box.style.display='none'; return; }
+ if(box) box.style.display='';
+ const pct=Math.round(m.used*100/m.total);
+ document.getElementById('ram').innerHTML =
+ ''+
+ '
'+(m.used/1024).toFixed(1)+' / '+(m.total/1024).toFixed(1)+' GiB · '+pct+'%
';
+}
async function loadCfg(){
const c=await jget('/api/config');
const keys=['BIN','MODEL','CTX','BATCH','UBATCH','NGL','PORT'];
@@ -998,7 +1011,7 @@ async function saveCrawlUrl(){
const url=document.getElementById('crawl-url').value.trim();
renderInternet(await jpost('/api/internet',{url}));
}
-async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadCfg(),loadPresets(),loadAgent(),loadInternet(),loadApiKey(),loadPrefs(),loadLlamacpp()]); }
+async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadAgent(),loadInternet(),loadApiKey(),loadPrefs(),loadLlamacpp()]); }
async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); }
function openBenchModal(){ document.getElementById('bench-modal').style.display = 'flex'; }
function closeBenchModal(){ document.getElementById('bench-modal').style.display = 'none'; }
@@ -1661,6 +1674,7 @@ async function send(){
loadAll();
setInterval(loadStatus, 5000);
setInterval(loadVram, 3000);
+setInterval(loadRam, 3000);
// Backend llama.cpp — la barre latérale sert UNIQUEMENT à installer les moteurs.
// ⚡ Version rapide = binaires officiels précompilés (aucune compilation)
// 🔧 Version optimisée = compilée pour la machine
diff --git a/internal/jean/ui/src/index.tmpl.html b/internal/jean/ui/src/index.tmpl.html
index ea8a685..34c15a9 100644
--- a/internal/jean/ui/src/index.tmpl.html
+++ b/internal/jean/ui/src/index.tmpl.html
@@ -29,6 +29,9 @@
GPU
…
+ RAM
+ …
+
Config active
…
diff --git a/internal/jean/ui/src/js/05-status.js b/internal/jean/ui/src/js/05-status.js
index 52c3f9d..8dff798 100644
--- a/internal/jean/ui/src/js/05-status.js
+++ b/internal/jean/ui/src/js/05-status.js
@@ -64,6 +64,16 @@ async function loadVram(){
''+(g.used/1024).toFixed(1)+' / '+(g.total/1024).toFixed(1)+' GiB · GPU '+g.util+'% · '+g.temp+'°C
';
}).join('') || '(pas de GPU)';
}
+async function loadRam(){
+ const m=await jget('/api/ram');
+ const box=document.getElementById('ram-details');
+ if(!m || !m.total){ if(box) box.style.display='none'; return; }
+ if(box) box.style.display='';
+ const pct=Math.round(m.used*100/m.total);
+ document.getElementById('ram').innerHTML =
+ ''+
+ '
'+(m.used/1024).toFixed(1)+' / '+(m.total/1024).toFixed(1)+' GiB · '+pct+'%
';
+}
async function loadCfg(){
const c=await jget('/api/config');
const keys=['BIN','MODEL','CTX','BATCH','UBATCH','NGL','PORT'];
diff --git a/internal/jean/ui/src/js/06-settings.js b/internal/jean/ui/src/js/06-settings.js
index 4fd803e..5bd1dcf 100644
--- a/internal/jean/ui/src/js/06-settings.js
+++ b/internal/jean/ui/src/js/06-settings.js
@@ -216,5 +216,5 @@ async function saveCrawlUrl(){
const url=document.getElementById('crawl-url').value.trim();
renderInternet(await jpost('/api/internet',{url}));
}
-async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadCfg(),loadPresets(),loadAgent(),loadInternet(),loadApiKey(),loadPrefs(),loadLlamacpp()]); }
+async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadAgent(),loadInternet(),loadApiKey(),loadPrefs(),loadLlamacpp()]); }
async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); }
diff --git a/internal/jean/ui/src/js/09-stream.js b/internal/jean/ui/src/js/09-stream.js
index 33be052..787df04 100644
--- a/internal/jean/ui/src/js/09-stream.js
+++ b/internal/jean/ui/src/js/09-stream.js
@@ -144,3 +144,4 @@ async function send(){
loadAll();
setInterval(loadStatus, 5000);
setInterval(loadVram, 3000);
+setInterval(loadRam, 3000);
diff --git a/internal/jean/web_api.go b/internal/jean/web_api.go
index 6eb3b25..c2d494e 100644
--- a/internal/jean/web_api.go
+++ b/internal/jean/web_api.go
@@ -70,6 +70,35 @@ func handleVram(w http.ResponseWriter, r *http.Request) {
sendJSON(w, 200, gpus)
}
+// handleRam renvoie la RAM système {used, total} en Mo (mêmes unités que
+// /api/vram → l'UI divise par 1024 pour des Gio). Lit /proc/meminfo (Linux, la
+// box de prod). Sur un OS sans /proc, renvoie total=0 → l'UI masque le bloc.
+func handleRam(w http.ResponseWriter, r *http.Request) {
+ out := map[string]any{"used": 0, "total": 0}
+ b, err := os.ReadFile("/proc/meminfo")
+ if err == nil {
+ var totalKB, availKB int
+ for _, line := range strings.Split(string(b), "\n") {
+ f := strings.Fields(line)
+ if len(f) < 2 {
+ continue
+ }
+ v, _ := strconv.Atoi(f[1]) // valeur en kB
+ switch f[0] {
+ case "MemTotal:":
+ totalKB = v
+ case "MemAvailable:":
+ availKB = v
+ }
+ }
+ if totalKB > 0 {
+ out["total"] = totalKB / 1024
+ out["used"] = (totalKB - availKB) / 1024
+ }
+ }
+ sendJSON(w, 200, out)
+}
+
func handleConfigEnv(w http.ResponseWriter, r *http.Request) {
sendJSON(w, 200, ReadConfig())
}
diff --git a/internal/jean/web_server.go b/internal/jean/web_server.go
index 2094c9b..badeb4f 100644
--- a/internal/jean/web_server.go
+++ b/internal/jean/web_server.go
@@ -79,6 +79,7 @@ func newWebMux() *http.ServeMux {
api("/api/ping", handlePing)
api("/api/status", handleStatus)
api("/api/vram", handleVram)
+ api("/api/ram", handleRam)
api("/api/config", handleConfigEnv)
api("/api/catalog", handleCatalog)
api("/api/update", handleUpdateCheck)