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 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 10:06:03 +01:00
parent c33247abc1
commit ad4c005e01
10 changed files with 151 additions and 36 deletions
+6 -4
View File
@@ -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
+3 -4
View File
@@ -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
}
+10 -10
View File
@@ -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."
+10
View File
@@ -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 {
+5
View File
@@ -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")
}
+15 -14
View File
@@ -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
@@ -204,15 +204,15 @@
</div>
{{else}}
<div class="memory-summary-header">
<span class="memory-summary-label">Memória foglalás</span>
<span class="memory-summary-label">Memória</span>
<span class="memory-summary-value">{{.AfterMB}} MB / {{.UsableMB}} MB ({{.Percent}}%)</span>
</div>
<div class="memory-bar-stacked">
<div class="memory-bar-segment memory-bar-committed" style="width:{{.CommittedPercent}}%" title="Jelenlegi foglalás: {{.CommittedMB}} MB"></div>
<div class="memory-bar-segment memory-bar-new" style="width:{{subtract .Percent .CommittedPercent}}%" title="{{$.Meta.DisplayName}}: +{{.NewRequestMB}} MB"></div>
<div class="memory-bar-segment memory-bar-committed" style="width:{{.UsedPercent}}%" title="Jelenlegi használat: {{.UsedMB}} MB"></div>
<div class="memory-bar-segment memory-bar-new" style="width:{{subtract .Percent .UsedPercent}}%" title="{{$.Meta.DisplayName}}: +{{.NewRequestMB}} MB"></div>
</div>
<div class="memory-bar-legend">
<span class="memory-legend-item"><span class="memory-legend-dot memory-legend-committed"></span>Jelenlegi foglalás ({{.CommittedMB}} MB)</span>
<span class="memory-legend-item"><span class="memory-legend-dot memory-legend-committed"></span>Jelenlegi használat ({{.UsedMB}} MB)</span>
<span class="memory-legend-item"><span class="memory-legend-dot memory-legend-new"></span>{{$.Meta.DisplayName}} (+{{.NewRequestMB}} MB)</span>
</div>
{{if .OvercommitWarn}}
@@ -163,6 +163,14 @@
</div>
</div>
<!-- Section 3.5: Memory Distribution Bar -->
<div class="monitor-card" id="memory-distribution-card" style="display:none">
<h3>Memória eloszlás</h3>
<div id="mem-dist-header" class="memory-dist-header"></div>
<div class="memory-dist-bar" id="mem-dist-bar"></div>
<div class="memory-bar-legend" id="mem-dist-legend"></div>
</div>
<!-- Section 4: Container Resources -->
<div class="monitor-card">
<h3>Alkalmazás erőforrások</h3>
@@ -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 += '<div class="memory-bar-segment" style="width:' + pct + '%;background:' + color + '" title="' + c.name + ': ' + fmtMB(mb) + '"></div>';
legendHtml += '<div class="memory-legend-item"><span class="memory-legend-dot" style="background:' + color + '"></span>' + c.name + ' (' + fmtMB(mb) + ')</div>';
});
// OS / system overhead
if (osMB > 10) {
var osPct = (osMB / totalMB * 100).toFixed(2);
html += '<div class="memory-bar-segment" style="width:' + osPct + '%;background:var(--text-muted);opacity:0.5" title="Rendszer: ' + fmtMB(osMB) + '"></div>';
legendHtml += '<div class="memory-legend-item"><span class="memory-legend-dot" style="background:var(--text-muted);opacity:0.5"></span>Rendszer (' + fmtMB(osMB) + ')</div>';
}
// Free space legend only
legendHtml += '<div class="memory-legend-item"><span class="memory-legend-dot" style="background:var(--bg-secondary);border:1px solid var(--border-color)"></span>Szabad (' + fmtMB(freeMB) + ')</div>';
bar.innerHTML = html;
legend.innerHTML = legendHtml;
card.style.display = '';
}
// =============================================
// CONTAINER DETAIL (per-container history)
// =============================================
@@ -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 {