From ad4c005e01c21d783c300b6637003ebd3c1a3fb2 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Mon, 23 Feb 2026 10:06:03 +0100 Subject: [PATCH] v0.27.3: Use real system memory everywhere, add monitoring memory bar Deploy page, pre-start check, and deploy validation now use actual /proc/meminfo usage instead of declared mem_request sums. New GetMemoryMB() helper for lightweight real-time memory reads. Monitoring page gains a stacked memory distribution bar showing per-container usage, OS overhead, and free memory. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 10 +++ controller/README.md | 10 +-- controller/internal/api/router.go | 7 +- controller/internal/stacks/deploy.go | 20 +++--- controller/internal/system/info_linux.go | 10 +++ controller/internal/system/info_other.go | 5 ++ controller/internal/web/handlers.go | 29 ++++---- controller/internal/web/templates/deploy.html | 8 +-- .../internal/web/templates/monitoring.html | 67 +++++++++++++++++++ controller/internal/web/templates/style.css | 21 ++++++ 10 files changed, 151 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99a9426..34b5e29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ ## Changelog +### v0.27.3 — Real System Memory Everywhere (2026-02-23) + +#### Changed +- **Deploy page uses real system memory** — Memory bar now shows actual `/proc/meminfo` usage instead of declared `mem_request` sums. Labels changed from "Jelenlegi foglalás" to "Jelenlegi használat". `system.GetMemoryMB()` provides real-time total and used memory. +- **Pre-start memory check uses real memory** — `actionStack("start")` in `router.go` and `DeployStack()` in `deploy.go` now check real used memory (`usedMB + newReqMB > usableMB`) instead of declared committed sums. `CommittedMemory()` kept only for soft overcommit warnings. + +#### Added +- **`system.GetMemoryMB()` helper** — Lightweight function in `internal/system/info_linux.go` that returns real total and used memory from `/proc/meminfo` without the overhead of full `GetInfo()` (no disk/CPU/temp). Stub in `info_other.go` for non-Linux. +- **Monitoring page memory distribution bar** — New stacked bar on `/monitoring` showing per-container memory usage (colored segments), OS/system overhead (gray), and free memory. Built dynamically from container summary data + real-time `/api/system/info`. Color-coded legend with per-app labels. + ### v0.27.2 — Comprehensive Fixes and New Labels (2026-02-23) #### Fixed diff --git a/controller/README.md b/controller/README.md index 52636f6..3aae6ee 100644 --- a/controller/README.md +++ b/controller/README.md @@ -136,11 +136,12 @@ The app catalog lives in a separate Git repository. The controller: - User-configurable inputs (admin password, language, storage path) remain editable - Section header prompts the user to note down any passwords they need 3. `checkBeforeDeploy()` JS guard fetches live state first (prevents double-deploy from another tab) -4. **Memory validation** checks `mem_request` against available RAM: +4. **Memory validation** uses real system memory from `/proc/meminfo`: - `usable_memory = total_ram - reserved_memory_mb` (default 384MB reserved) - - `CommittedMemory()` only counts running/starting/unhealthy apps — stopped/exited apps are excluded (they don't consume RAM) - - Hard block if requests exceed usable memory - - Soft warning if limits exceed total RAM (overcommit OK) + - `system.GetMemoryMB()` returns real-time total and used memory (not declared reservations) + - Hard block if `used_mb + new_request > usable_memory` + - `CommittedMemory()` (declared sum) still used for soft overcommit warning only + - Deploy page shows real memory usage bar (not declared reservations) 5. Pre-generated secret values are submitted as hidden form inputs so the **same values** the user saw are saved to `app.yaml` (no silent re-generation on submit). Controller saves `app.yaml`, sets in-memory `Deployed` flag **before** `docker compose up -d` (avoids stale UI during slow image pulls), reverts on failure 6. 3-step progress panel polls `GET /api/stacks/{name}` every 3s: config saved → containers starting → health check passed 7. Post-deploy: locked fields (DB_PASSWORD, etc.) become read-only; the "Automatikusan generált értékek" section continues to show the saved values on the settings page @@ -560,6 +561,7 @@ Legacy pinger (`internal/monitor/pinger.go`) still runs for backward compatibili Full-page system monitor at `/monitoring`: - **System Overview**: hostname, OS, kernel, CPU model/cores, uptime - **System Metrics Charts**: 4 line charts (CPU, Memory, Temperature, Load) in 2x2 grid +- **Memory Distribution Bar**: stacked bar showing per-container memory usage, OS/system overhead, and free memory (real-time from `/proc/meminfo` + container stats) - **Container Resources**: horizontal bar charts (CPU% and Memory per container) - **Per-container Detail**: click-to-expand historical charts - **Hub Connection Status**: shows Hub URL, customer ID, connection state (connected/unreachable), last successful push, last error diff --git a/controller/internal/api/router.go b/controller/internal/api/router.go index b7ce56a..41c391d 100644 --- a/controller/internal/api/router.go +++ b/controller/internal/api/router.go @@ -334,15 +334,14 @@ func (r *Router) actionStack(w http.ResponseWriter, action, name string) { if action == "start" { stackMemMB := r.stackMgr.StackMemoryMB(name) if stackMemMB > 0 { - if totalMB, memErr := system.GetTotalMemoryMB(); memErr == nil { + if totalMB, usedMB, memErr := system.GetMemoryMB(); memErr == nil { reservedMB := r.cfg.System.ReservedMemoryMB usableMB := totalMB - reservedMB - committedReqMB, _ := r.stackMgr.CommittedMemory() - afterMB := committedReqMB + stackMemMB + afterMB := usedMB + stackMemMB if afterMB > usableMB { writeJSON(w, http.StatusConflict, apiResponse{ OK: false, - Error: fmt.Sprintf("Nincs elég memória az indításhoz. Szükséges: %d MB, elérhető: %d MB (foglalt: %d MB / használható: %d MB)", stackMemMB, usableMB-committedReqMB, committedReqMB, usableMB), + Error: fmt.Sprintf("Nincs elég memória az indításhoz. Szükséges: %d MB, elérhető: %d MB (használt: %d MB / használható: %d MB)", stackMemMB, usableMB-usedMB, usedMB, usableMB), }) return } diff --git a/controller/internal/stacks/deploy.go b/controller/internal/stacks/deploy.go index 947b91a..8c33fcd 100644 --- a/controller/internal/stacks/deploy.go +++ b/controller/internal/stacks/deploy.go @@ -123,33 +123,33 @@ func (m *Manager) DeployStack(req DeployRequest) (string, error) { // --- Memory validation --- var deployWarning string reservedMB := m.cfg.System.ReservedMemoryMB - totalMB, memErr := system.GetTotalMemoryMB() + totalMB, usedMB, memErr := system.GetMemoryMB() if memErr != nil { m.logger.Printf("[WARN] Cannot read system memory: %v — skipping memory check", memErr) } else { usableMB := totalMB - reservedMB - currentReqMB, currentLimitMB := m.CommittedMemory() newReqMB := ParseMemoryMB(meta.Resources.MemRequest) - newLimitMB := ParseMemoryMB(meta.Resources.MemLimit) - m.logger.Printf("[INFO] Memory check: total=%dMB, reserved=%dMB, usable=%dMB, committed_req=%dMB, new_req=%dMB, remaining=%dMB", - totalMB, reservedMB, usableMB, currentReqMB, newReqMB, usableMB-currentReqMB-newReqMB) + m.logger.Printf("[INFO] Memory check: total=%dMB, reserved=%dMB, usable=%dMB, real_used=%dMB, new_req=%dMB, remaining=%dMB", + totalMB, reservedMB, usableMB, usedMB, newReqMB, usableMB-usedMB-newReqMB) - // Hard block: requests exceed usable memory - if newReqMB > 0 && currentReqMB+newReqMB > usableMB { + // Hard block: real used + new request exceeds usable memory + if newReqMB > 0 && usedMB+newReqMB > usableMB { return "", fmt.Errorf( "Nincs elég memória az alkalmazás telepítéséhez. "+ "Szükséges: %d MB, Elérhető: %d MB "+ - "(összesen: %d MB, ebből %d MB már foglalt, %d MB rendszer számára fenntartva)", + "(összesen: %d MB, ebből %d MB használt, %d MB rendszer számára fenntartva)", newReqMB, - usableMB-currentReqMB, + usableMB-usedMB, totalMB, - currentReqMB, + usedMB, reservedMB, ) } // Soft warning: limits exceed total (overcommit) + _, currentLimitMB := m.CommittedMemory() + newLimitMB := ParseMemoryMB(meta.Resources.MemLimit) if newLimitMB > 0 && currentLimitMB+newLimitMB > totalMB { deployWarning = "Az alkalmazások csúcsterhelése meghaladhatja a rendelkezésre álló memóriát. " + "Normál használat mellett ez nem okoz problémát." diff --git a/controller/internal/system/info_linux.go b/controller/internal/system/info_linux.go index 4de1045..00be761 100644 --- a/controller/internal/system/info_linux.go +++ b/controller/internal/system/info_linux.go @@ -54,6 +54,16 @@ func GetTotalMemoryMB() (int, error) { return int(info.TotalMemMB), nil } +// GetMemoryMB returns total and used system memory in MB from /proc/meminfo. +func GetMemoryMB() (totalMB, usedMB int, err error) { + info := SystemInfo{} + readMemInfo(&info) + if info.TotalMemMB == 0 { + return 0, 0, fmt.Errorf("could not read MemTotal from /proc/meminfo") + } + return int(info.TotalMemMB), int(info.UsedMemMB), nil +} + func readMemInfo(info *SystemInfo) { f, err := os.Open("/proc/meminfo") if err != nil { diff --git a/controller/internal/system/info_other.go b/controller/internal/system/info_other.go index c8151eb..feccb79 100644 --- a/controller/internal/system/info_other.go +++ b/controller/internal/system/info_other.go @@ -13,3 +13,8 @@ func GetInfo(_ string, _ *CPUCollector) SystemInfo { func GetTotalMemoryMB() (int, error) { return 0, fmt.Errorf("/proc/meminfo not available on this platform") } + +// GetMemoryMB is not available on non-Linux platforms. +func GetMemoryMB() (totalMB, usedMB int, err error) { + return 0, 0, fmt.Errorf("/proc/meminfo not available on this platform") +} diff --git a/controller/internal/web/handlers.go b/controller/internal/web/handlers.go index feeb407..2cf6f90 100644 --- a/controller/internal/web/handlers.go +++ b/controller/internal/web/handlers.go @@ -354,35 +354,36 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri // Memory info for deploy page (only for non-deployed apps) if !alreadyDeployed { memInfo := map[string]interface{}{"Available": false} - totalMB, memErr := system.GetTotalMemoryMB() + totalMB, usedMB, memErr := system.GetMemoryMB() if memErr == nil { reservedMB := s.cfg.System.ReservedMemoryMB usableMB := totalMB - reservedMB - committedReqMB, committedLimitMB := s.stackMgr.CommittedMemory() newReqMB := stacks.ParseMemoryMB(meta.Resources.MemRequest) - newLimitMB := stacks.ParseMemoryMB(meta.Resources.MemLimit) - afterReqMB := committedReqMB + newReqMB - afterLimitMB := committedLimitMB + newLimitMB + afterMB := usedMB + newReqMB percent := 0 if usableMB > 0 { - percent = afterReqMB * 100 / usableMB + percent = afterMB * 100 / usableMB + } + usedPercent := 0 + if usableMB > 0 { + usedPercent = usedMB * 100 / usableMB } - committedPercent := 0 - if usableMB > 0 { - committedPercent = committedReqMB * 100 / usableMB - } + // Overcommit warning still uses declared limits + _, committedLimitMB := s.stackMgr.CommittedMemory() + newLimitMB := stacks.ParseMemoryMB(meta.Resources.MemLimit) + afterLimitMB := committedLimitMB + newLimitMB memInfo["Available"] = true memInfo["TotalMB"] = totalMB memInfo["ReservedMB"] = reservedMB memInfo["UsableMB"] = usableMB - memInfo["CommittedMB"] = committedReqMB + memInfo["UsedMB"] = usedMB memInfo["NewRequestMB"] = newReqMB - memInfo["AfterMB"] = afterReqMB + memInfo["AfterMB"] = afterMB memInfo["Percent"] = percent - memInfo["CommittedPercent"] = committedPercent - memInfo["Blocked"] = newReqMB > 0 && afterReqMB > usableMB + memInfo["UsedPercent"] = usedPercent + memInfo["Blocked"] = newReqMB > 0 && afterMB > usableMB memInfo["OvercommitWarn"] = newLimitMB > 0 && afterLimitMB > totalMB } data["MemoryInfo"] = memInfo diff --git a/controller/internal/web/templates/deploy.html b/controller/internal/web/templates/deploy.html index 86b31db..8d9e729 100644 --- a/controller/internal/web/templates/deploy.html +++ b/controller/internal/web/templates/deploy.html @@ -204,15 +204,15 @@ {{else}}
- Memória foglalás + Memória {{.AfterMB}} MB / {{.UsableMB}} MB ({{.Percent}}%)
-
-
+
+
- Jelenlegi foglalás ({{.CommittedMB}} MB) + Jelenlegi használat ({{.UsedMB}} MB) {{$.Meta.DisplayName}} (+{{.NewRequestMB}} MB)
{{if .OvercommitWarn}} diff --git a/controller/internal/web/templates/monitoring.html b/controller/internal/web/templates/monitoring.html index 8fb7b51..5b78bee 100644 --- a/controller/internal/web/templates/monitoring.html +++ b/controller/internal/web/templates/monitoring.html @@ -163,6 +163,14 @@ + + +

Alkalmazás erőforrások

@@ -506,11 +514,70 @@ chartContainerMem.data.labels = containerNames; chartContainerMem.data.datasets[0].data = memData; chartContainerMem.update('none'); + + buildMemoryDistributionBar(data); } catch(e) { console.error('Failed to load container summary:', e); } } + var memDistPalette = ['#238636','#0088cc','#d29922','#da3633','#8b5cf6','#ec6547','#2ea043','#1f6feb','#e3b341','#f47067']; + + async function buildMemoryDistributionBar(containers) { + var totalMB = {{.SystemInfo.TotalMemMB}}; + if (!totalMB) return; + + // Get real-time used memory from API + var usedMB = 0; + try { + var resp = await fetch('/api/system/info'); + var json = await resp.json(); + if (json.ok && json.data) usedMB = json.data.used_mem_mb || 0; + } catch(e) {} + if (!usedMB) return; + + var card = document.getElementById('memory-distribution-card'); + var bar = document.getElementById('mem-dist-bar'); + var legend = document.getElementById('mem-dist-legend'); + var header = document.getElementById('mem-dist-header'); + + // Sum container memory + var appTotal = 0; + containers.forEach(function(c) { appTotal += c.mem_usage_mb || 0; }); + var osMB = Math.max(0, usedMB - appTotal); + var freeMB = Math.max(0, totalMB - usedMB); + + function fmtMB(mb) { return mb >= 1024 ? (mb/1024).toFixed(1) + ' GB' : Math.round(mb) + ' MB'; } + + header.textContent = 'Használt: ' + fmtMB(usedMB) + ' / ' + fmtMB(totalMB) + ' (' + Math.round(usedMB/totalMB*100) + '%)'; + + // Build bar segments + var html = ''; + var legendHtml = ''; + containers.forEach(function(c, i) { + var mb = c.mem_usage_mb || 0; + if (mb < 1) return; + var pct = (mb / totalMB * 100).toFixed(2); + var color = memDistPalette[i % memDistPalette.length]; + html += '
'; + legendHtml += '
' + c.name + ' (' + fmtMB(mb) + ')
'; + }); + + // OS / system overhead + if (osMB > 10) { + var osPct = (osMB / totalMB * 100).toFixed(2); + html += '
'; + legendHtml += '
Rendszer (' + fmtMB(osMB) + ')
'; + } + + // Free space legend only + legendHtml += '
Szabad (' + fmtMB(freeMB) + ')
'; + + bar.innerHTML = html; + legend.innerHTML = legendHtml; + card.style.display = ''; + } + // ============================================= // CONTAINER DETAIL (per-container history) // ============================================= diff --git a/controller/internal/web/templates/style.css b/controller/internal/web/templates/style.css index 8fa21f7..c36d18a 100644 --- a/controller/internal/web/templates/style.css +++ b/controller/internal/web/templates/style.css @@ -840,6 +840,27 @@ select.form-control option { background: var(--bg-secondary); color: var(--text- background: rgba(35, 134, 54, 0.45); border: 1px solid #4edf72; } +.memory-dist-bar { + width: 100%; + height: 14px; + border-radius: 7px; + display: flex; + overflow: hidden; + background: var(--bg-secondary); + border: 1px solid var(--border-color); +} +.memory-dist-bar .memory-bar-segment:first-child { + border-radius: 6px 0 0 6px; +} +.memory-dist-bar .memory-bar-segment:last-child { + border-radius: 0 6px 6px 0; +} +.memory-dist-header { + font-size: .85rem; + color: var(--text-secondary); + margin-bottom: .5rem; + font-family: 'JetBrains Mono', monospace; +} /* Logs */ .logs-container {