From 67ba0fe759e56ed2fd34406547850a227358ed01 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Sat, 14 Feb 2026 11:44:06 +0100 Subject: [PATCH] Added memory limits and system info for memory --- CONTEXT.md | 29 +++++-- controller/README.md | 14 +++- controller/go.mod | 2 +- controller/go.sum | 6 ++ controller/internal/api/router.go | 10 +-- controller/internal/config/config.go | 1 + controller/internal/stacks/metadata.go | 1 + controller/internal/system/info.go | 20 +++++ controller/internal/system/info_linux.go | 100 +++++++++++++++++++++++ controller/internal/system/info_other.go | 8 ++ controller/internal/web/server.go | 32 ++++++++ controller/internal/web/templates.go | 92 ++++++++++++++++++++- 12 files changed, 295 insertions(+), 20 deletions(-) create mode 100644 controller/go.sum create mode 100644 controller/internal/system/info.go create mode 100644 controller/internal/system/info_linux.go create mode 100644 controller/internal/system/info_other.go diff --git a/CONTEXT.md b/CONTEXT.md index 965e043..f7a78c6 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -7,7 +7,7 @@ > > Ask Claude Code: "Please update CONTEXT.md with what we did today" -Last updated: 2026-02-14 +Last updated: 2026-02-14 (evening) --- @@ -28,7 +28,20 @@ Last updated: 2026-02-14 - **Running on:** demo-felhom (N100 mini PC) at 192.168.0.162:8080 - **All Phase 1 features working:** deploy, start/stop/restart/update, logs, health-aware states, auth -### What was just completed (2026-02-13/14) +### What was just completed (2026-02-14) +- **System info bar on Vezérlőpult dashboard**: RAM, SSD, and optional HDD usage + - Progress bars with color coding (green < 70%, yellow 70-85%, red > 85%) + - New `internal/system` package reads `/proc/meminfo` + `syscall.Statfs` + - Platform-specific: Linux impl + non-Linux stub (build tags) + - Hungarian labels: "Memória", "SSD tárhely", "Külső HDD" +- **Docker Compose memory limits** on paperless-ngx template: + - paperless-webserver: 768M, postgres: 256M, redis: 128M + - Added `mem_limit` field to `.felhom.yml` ResourceHints (total: 1152M) +- **`/api/system/info` endpoint** now returns live system metrics (was customer info) +- **Config**: Added `paths.hdd_path` for external HDD monitoring +- Controller image builds via build.sh, pushes to Gitea container registry + +### Previously completed (2026-02-13) - Built the entire felhom-controller from scratch (Go, no frameworks) - Debugged and fixed 7 issues during first real deployment: 1. Password validation (empty passwords accepted) @@ -38,14 +51,14 @@ Last updated: 2026-02-14 5. "Részletek" button redirect for deployed apps 6. Paperless OCR language installation (LANGUAGES vs LANGUAGE env var) 7. Documentation: restart vs up -d for image updates -- Controller image builds via build.sh, pushes to Gitea container registry ### What's next (priorities) 1. Deploy a second app (e.g., Immich, Jellyfin) to validate the template system 2. Test on Raspberry Pi (pi-customer-1) -3. Phase 2: Monitoring & Healthchecks integration -4. Phase 3: Backup system (DB dumps + restic) -5. Dashboard dark theme (align with felhom.eu website) +3. Add `paths.hdd_path` to demo-felhom controller.yaml to enable HDD bar +4. Phase 2 continued: CPU/temperature metrics, Healthchecks.io pings +5. Phase 3: Backup system (DB dumps + restic) +6. Add memory limits to other app catalog templates (Immich, Jellyfin, etc.) ## Architecture decisions @@ -59,6 +72,8 @@ Last updated: 2026-02-14 | app.yaml per stack | Separates deploy config from compose files, survives git pulls | | Password fields require explicit input | Prevents accidental empty-password deployments | | Health-aware state from Docker Status field | Docker's State says "running" even for unhealthy containers | +| Memory limits via deploy.resources.limits | Prevents runaway containers; ~50% headroom over expected usage | +| System info from /proc/meminfo + statfs | No external dependencies, cheap to read on each page load | ## Key file locations on demo-felhom @@ -89,7 +104,7 @@ Last updated: 2026-02-14 | Repository | Status | Notes | |------------|--------|-------| | deploy-felhom-compose | Active | This repo. Controller code + deploy scripts | -| app-catalog-felhom.eu | Active | 49 app templates, needs PAPERLESS_OCR_LANGUAGES fix | +| app-catalog-felhom.eu | Active | 49 app templates, paperless-ngx has memory limits | | felhom.eu | Stable | Website live, SEO indexed, email working | | homelab-manifests | Stable | k3s cluster running (dooplex.hu services) | | misc-scripts | Utility | collect-repo.sh, backup helpers | diff --git a/controller/README.md b/controller/README.md index 1385a56..f5c53b0 100644 --- a/controller/README.md +++ b/controller/README.md @@ -36,6 +36,8 @@ Current version: **v0.2.1** - Manual rescan endpoint (`POST /api/stacks/rescan`) - Alphabetically sorted stack display (consistent card ordering) - Protected stacks (traefik, cloudflared, felhom-controller) can't be stopped +- System info bar on dashboard: RAM, SSD, and HDD usage with progress bars +- Docker Compose memory limits enforced via `deploy.resources.limits.memory` ### Known issues / next priorities - Cloudflare Tunnel + Traefik TLS: paperless.demo-felhom.eu works locally but shows "Not secure" (certificate chain not fully validated through tunnel) @@ -88,6 +90,10 @@ controller/ │ │ ├── metadata.go # Parse .felhom.yml app metadata │ │ └── deploy.go # First-deploy flow: secret gen, app.yaml, compose up │ ├── api/router.go # REST API endpoints +│ ├── system/ +│ │ ├── info.go # SystemInfo struct +│ │ ├── info_linux.go # Linux: /proc/meminfo + statfs +│ │ └── info_other.go # Non-Linux stub │ └── web/ │ ├── server.go # HTTP server, auth, page handlers, asset serving │ └── templates.go # Embedded HTML templates + CSS (Hungarian UI) @@ -110,6 +116,7 @@ controller/ | **Config** | `internal/config/` | ✅ Done | Load & validate controller.yaml, env overrides | | **Stacks** | `internal/stacks/` | ✅ Done | Compose operations, scanning, metadata, deploy flow | | **API** | `internal/api/` | ✅ Done | REST endpoints (stacks, deploy, rescan, system info, health) | +| **System** | `internal/system/` | ✅ Done | System resource info (RAM, disk usage) for dashboard & API | | **Web** | `internal/web/` | ✅ Done | Hungarian dashboard, auth, deploy pages, asset serving | | **Backup** | `internal/backup/` | 📲 Phase 3 | DB dumps, restic snapshots, restore | | **Monitor** | `internal/monitor/` | 📲 Phase 2 | Health checks, Healthchecks pings, system metrics | @@ -277,7 +284,7 @@ docker compose up -d | POST | `/api/stacks/{name}/update` | Yes | Pull images + recreate | | GET | `/api/stacks/{name}/logs` | Yes | Container logs | | POST | `/api/stacks/rescan` | Yes | Trigger manual stack discovery | -| GET | `/api/system/info` | Yes | Customer/domain info | +| GET | `/api/system/info` | Yes | System resource usage (RAM, disk, HDD) | ## Status & Roadmap @@ -305,9 +312,10 @@ docker compose up -d - [x] Deploy page doubles as read-only config viewer for deployed apps ### Phase 2 — Monitoring & Health -- [ ] System metrics collection (CPU, RAM, disk, temperature) +- [x] System metrics on dashboard (RAM, SSD, HDD usage bars) +- [x] `/api/system/info` endpoint with live resource data +- [ ] CPU and temperature metrics - [ ] Healthchecks.io ping integration -- [ ] Dashboard system health panel - [ ] Customer notifications (email/Telegram) ### Phase 3 — Backups diff --git a/controller/go.mod b/controller/go.mod index 604e27e..b1af698 100644 --- a/controller/go.mod +++ b/controller/go.mod @@ -3,6 +3,6 @@ module gitea.dooplex.hu/admin/felhom-controller go 1.22 require ( - gopkg.in/yaml.v3 v3.0.1 golang.org/x/crypto v0.31.0 + gopkg.in/yaml.v3 v3.0.1 ) diff --git a/controller/go.sum b/controller/go.sum new file mode 100644 index 0000000..570f3cb --- /dev/null +++ b/controller/go.sum @@ -0,0 +1,6 @@ +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/controller/internal/api/router.go b/controller/internal/api/router.go index ded56ec..6595f9c 100644 --- a/controller/internal/api/router.go +++ b/controller/internal/api/router.go @@ -10,6 +10,7 @@ import ( "gitea.dooplex.hu/admin/felhom-controller/internal/config" "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" + "gitea.dooplex.hu/admin/felhom-controller/internal/system" ) // Router handles all /api/* requests. @@ -215,13 +216,8 @@ func (r *Router) getStackLogs(w http.ResponseWriter, req *http.Request, name str } func (r *Router) systemInfo(w http.ResponseWriter, _ *http.Request) { - writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: map[string]interface{}{ - "customer_id": r.cfg.Customer.ID, - "customer_name": r.cfg.Customer.Name, - "domain": r.cfg.Customer.Domain, - "backup_enabled": r.cfg.Backup.Enabled, - "monitor_enabled": r.cfg.Monitoring.Enabled, - }}) + info := system.GetInfo(r.cfg.Paths.HDDPath) + writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: info}) } // --- Helpers --- diff --git a/controller/internal/config/config.go b/controller/internal/config/config.go index fada409..b3f29d8 100644 --- a/controller/internal/config/config.go +++ b/controller/internal/config/config.go @@ -44,6 +44,7 @@ type PathsConfig struct { DataDir string `yaml:"data_dir"` BackupDir string `yaml:"backup_dir"` DBDumpDir string `yaml:"db_dump_dir"` + HDDPath string `yaml:"hdd_path"` } type WebConfig struct { diff --git a/controller/internal/stacks/metadata.go b/controller/internal/stacks/metadata.go index d9a3e26..0c84962 100644 --- a/controller/internal/stacks/metadata.go +++ b/controller/internal/stacks/metadata.go @@ -22,6 +22,7 @@ type Metadata struct { // ResourceHints describe what the app needs. type ResourceHints struct { RAM string `yaml:"ram" json:"ram"` + MemLimit string `yaml:"mem_limit" json:"mem_limit"` PiCompatible bool `yaml:"pi_compatible" json:"pi_compatible"` NeedsHDD bool `yaml:"needs_hdd" json:"needs_hdd"` } diff --git a/controller/internal/system/info.go b/controller/internal/system/info.go new file mode 100644 index 0000000..9d47500 --- /dev/null +++ b/controller/internal/system/info.go @@ -0,0 +1,20 @@ +package system + +// SystemInfo holds system resource usage information. +type SystemInfo struct { + TotalMemMB uint64 `json:"total_mem_mb"` + UsedMemMB uint64 `json:"used_mem_mb"` + AvailMemMB uint64 `json:"avail_mem_mb"` + MemPercent float64 `json:"mem_percent"` + + DiskTotalGB float64 `json:"disk_total_gb"` + DiskUsedGB float64 `json:"disk_used_gb"` + DiskAvailGB float64 `json:"disk_avail_gb"` + DiskPercent float64 `json:"disk_percent"` + + HDDTotalGB float64 `json:"hdd_total_gb,omitempty"` + HDDUsedGB float64 `json:"hdd_used_gb,omitempty"` + HDDAvailGB float64 `json:"hdd_avail_gb,omitempty"` + HDDPercent float64 `json:"hdd_percent,omitempty"` + HDDConfigured bool `json:"hdd_configured"` +} diff --git a/controller/internal/system/info_linux.go b/controller/internal/system/info_linux.go new file mode 100644 index 0000000..05f676f --- /dev/null +++ b/controller/internal/system/info_linux.go @@ -0,0 +1,100 @@ +//go:build linux + +package system + +import ( + "bufio" + "os" + "strings" + "syscall" +) + +// GetInfo reads system memory and disk usage. +// hddPath is the mount path for external HDD; if empty, HDD info is skipped. +func GetInfo(hddPath string) SystemInfo { + info := SystemInfo{} + + // --- Memory from /proc/meminfo --- + readMemInfo(&info) + + // --- Root filesystem disk usage --- + readDiskUsage("/", &info.DiskTotalGB, &info.DiskUsedGB, &info.DiskAvailGB, &info.DiskPercent) + + // --- HDD disk usage (if configured) --- + if hddPath != "" { + info.HDDConfigured = true + readDiskUsage(hddPath, &info.HDDTotalGB, &info.HDDUsedGB, &info.HDDAvailGB, &info.HDDPercent) + } + + return info +} + +func readMemInfo(info *SystemInfo) { + f, err := os.Open("/proc/meminfo") + if err != nil { + return + } + defer f.Close() + + var totalKB, availKB uint64 + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + switch { + case strings.HasPrefix(line, "MemTotal:"): + totalKB = parseMemLine(line) + case strings.HasPrefix(line, "MemAvailable:"): + availKB = parseMemLine(line) + } + if totalKB > 0 && availKB > 0 { + break + } + } + + if totalKB > 0 { + info.TotalMemMB = totalKB / 1024 + info.AvailMemMB = availKB / 1024 + info.UsedMemMB = info.TotalMemMB - info.AvailMemMB + info.MemPercent = float64(info.UsedMemMB) / float64(info.TotalMemMB) * 100 + } +} + +// parseMemLine extracts the kB value from a /proc/meminfo line like "MemTotal: 16384000 kB" +func parseMemLine(line string) uint64 { + // Remove label prefix up to ':' + parts := strings.SplitN(line, ":", 2) + if len(parts) < 2 { + return 0 + } + valStr := strings.TrimSpace(parts[1]) + valStr = strings.TrimSuffix(valStr, " kB") + valStr = strings.TrimSpace(valStr) + + var val uint64 + for _, c := range valStr { + if c >= '0' && c <= '9' { + val = val*10 + uint64(c-'0') + } + } + return val +} + +func readDiskUsage(path string, totalGB, usedGB, availGB *float64, percent *float64) { + var stat syscall.Statfs_t + if err := syscall.Statfs(path, &stat); err != nil { + return + } + + bsize := uint64(stat.Bsize) + total := stat.Blocks * bsize + avail := stat.Bavail * bsize + used := total - (stat.Bfree * bsize) // Bfree includes reserved blocks + + const gb = 1024 * 1024 * 1024 + *totalGB = float64(total) / gb + *usedGB = float64(used) / gb + *availGB = float64(avail) / gb + if total > 0 { + *percent = float64(used) / float64(total) * 100 + } +} diff --git a/controller/internal/system/info_other.go b/controller/internal/system/info_other.go new file mode 100644 index 0000000..edb7b94 --- /dev/null +++ b/controller/internal/system/info_other.go @@ -0,0 +1,8 @@ +//go:build !linux + +package system + +// GetInfo returns empty system info on non-Linux platforms. +func GetInfo(_ string) SystemInfo { + return SystemInfo{} +} diff --git a/controller/internal/web/server.go b/controller/internal/web/server.go index 030bf24..fdb3c5a 100644 --- a/controller/internal/web/server.go +++ b/controller/internal/web/server.go @@ -16,6 +16,7 @@ import ( "gitea.dooplex.hu/admin/felhom-controller/internal/config" "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" + "gitea.dooplex.hu/admin/felhom-controller/internal/system" "golang.org/x/crypto/bcrypt" ) @@ -129,6 +130,34 @@ func (s *Server) loadTemplates() { "appPageURL": func(slug string) string { return s.cfg.AppPageURL(slug) }, + "usageColor": func(percent float64) string { + if percent >= 85 { + return "red" + } + if percent >= 70 { + return "yellow" + } + return "green" + }, + "fmtMB": func(mb uint64) string { + if mb >= 1024 { + gb := float64(mb) / 1024.0 + if gb >= 10 { + return fmt.Sprintf("%.0f GB", gb) + } + return fmt.Sprintf("%.1f GB", gb) + } + return fmt.Sprintf("%d MB", mb) + }, + "fmtGB": func(gb float64) string { + if gb >= 100 { + return fmt.Sprintf("%.0f GB", gb) + } + if gb >= 10 { + return fmt.Sprintf("%.1f GB", gb) + } + return fmt.Sprintf("%.2f GB", gb) + }, } s.tmpl = template.Must(template.New("").Funcs(funcMap).Parse(allTemplates)) @@ -315,11 +344,14 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, _ *http.Request) { } } + sysInfo := system.GetInfo(s.cfg.Paths.HDDPath) + data := s.baseData("dashboard", "Vezérlőpult") data["Stacks"] = stackList data["RunningCount"] = running data["StoppedCount"] = stopped data["TotalCount"] = len(stackList) + data["SystemInfo"] = sysInfo s.render(w, "dashboard", data) } diff --git a/controller/internal/web/templates.go b/controller/internal/web/templates.go index 67bd8e3..9fade81 100644 --- a/controller/internal/web/templates.go +++ b/controller/internal/web/templates.go @@ -94,6 +94,42 @@ const dashboardTmpl = ` +{{if .SystemInfo.TotalMemMB}} +
+
+
+
+ Memória + {{fmtMB .SystemInfo.UsedMemMB}} / {{fmtMB .SystemInfo.TotalMemMB}} ({{printf "%.0f" .SystemInfo.MemPercent}}%) +
+
+
+
+
+
+
+ SSD tárhely + {{fmtGB .SystemInfo.DiskUsedGB}} / {{fmtGB .SystemInfo.DiskTotalGB}} ({{printf "%.0f" .SystemInfo.DiskPercent}}%) +
+
+
+
+
+ {{if .SystemInfo.HDDConfigured}} +
+
+ Külső HDD + {{fmtGB .SystemInfo.HDDUsedGB}} / {{fmtGB .SystemInfo.HDDTotalGB}} ({{printf "%.0f" .SystemInfo.HDDPercent}}%) +
+
+
+
+
+ {{end}} +
+
+{{end}} +

Alkalmazások állapota

@@ -195,9 +231,9 @@ const stacksTmpl = ` {{if isOperational .State}} - + {{else}} - + {{end}} Naplók Részletek @@ -635,6 +671,57 @@ h3 { margin-top: .25rem; } +/* System info bar */ +.system-info-card { + background: var(--bg-card); + border-radius: var(--radius); + padding: 1rem 1.25rem; + border: 1px solid var(--border-color); + margin-bottom: 2rem; +} +.system-info-items { + display: flex; + gap: 2rem; + flex-wrap: wrap; +} +.system-info-item { + flex: 1; + min-width: 200px; +} +.system-info-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: .4rem; +} +.system-info-label { + font-size: .8rem; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: .5px; +} +.system-info-value { + font-size: .8rem; + color: var(--text-muted); + font-family: 'JetBrains Mono', monospace; +} +.system-bar { + width: 100%; + height: 6px; + background: var(--bg-primary); + border-radius: 3px; + overflow: hidden; +} +.system-bar-fill { + height: 100%; + border-radius: 3px; + transition: width 0.3s ease; +} +.system-bar-green { background: var(--green); } +.system-bar-yellow { background: var(--yellow); } +.system-bar-red { background: var(--red); } + /* Stack list (dashboard) */ .stack-list { display: flex; @@ -1039,5 +1126,6 @@ select.form-control option { background: var(--bg-secondary); color: var(--text- .stack-grid { grid-template-columns: 1fr; } .stats-grid { grid-template-columns: repeat(3, 1fr); } .deploy-info { flex-direction: column; } + .system-info-items { flex-direction: column; gap: 1rem; } } ` \ No newline at end of file