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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user