From e99067ca60433e0444eda67cc107a3365b3014fe Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Mon, 23 Feb 2026 09:28:29 +0100 Subject: [PATCH] =?UTF-8?q?v0.27.2=20=E2=80=94=20copyable=20error=20popups?= =?UTF-8?q?,=20Tier2=20hub=20reporting,=20memory=20bar=20fixes,=20new=20la?= =?UTF-8?q?bels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace native alert() with custom showAlert() modal (text selectable) - Manual Tier2 backup now pushes infra backup to Hub - CommittedMemory() excludes stopped/exited apps - Pre-start memory check blocks start if insufficient RAM - Add hungarian_ui metadata field + "Magyar felület" badge - Add "USB" badge on storage cards in settings page Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 16 ++++++++++ controller/cmd/controller/main.go | 4 +++ controller/internal/api/router.go | 29 +++++++++++++++++++ controller/internal/stacks/manager.go | 17 ++++++++++- controller/internal/stacks/metadata.go | 1 + .../internal/web/templates/app_info.html | 1 + controller/internal/web/templates/deploy.html | 25 ++++++++-------- controller/internal/web/templates/layout.html | 19 +++++++----- .../internal/web/templates/settings.html | 1 + controller/internal/web/templates/stacks.html | 1 + 10 files changed, 93 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c003c8b..99a9426 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ ## Changelog +### v0.27.2 — Comprehensive Fixes and New Labels (2026-02-23) + +#### Fixed +- **Deploy error popups now copyable** — Replaced all native `alert()` calls with a custom modal (`showAlert()` in layout.html) using a `
` block with `user-select:text`. Error messages can now be selected and copied. Applied across deploy.html and layout.html.
+- **Manual Tier2 backup now reports to Hub** — Added `OnCrossDriveComplete` callback to `Router` (`internal/api/router.go`). Both `triggerCrossBackup` (single-app) and `triggerAllCrossBackups` (run-all) now call `pushInfraBackup()` + `writeLocalInfraBackup()` after completion, matching the automatic scheduled path.
+- **Memory bar excludes stopped apps** — `CommittedMemory()` in `internal/stacks/manager.go` now skips apps with `StateStopped` or `StateExited`. Only running/starting/unhealthy apps count toward committed memory.
+- **Pre-start memory check** — `actionStack("start")` in `internal/api/router.go` now validates available memory before starting a stopped app. Returns 409 Conflict with a descriptive Hungarian error if insufficient.
+
+#### Added
+- **`hungarian_ui` metadata field** — New `HungarianUI bool` field in `ResourceHints` (`internal/stacks/metadata.go`). Shows "Magyar felület" green badge on deploy, stacks, and app info pages when `hungarian_ui: true` in `.felhom.yml`.
+- **USB badge on storage cards** — Settings page storage cards now show an orange "USB" badge next to Aktív/Alapértelmezett when the drive is USB-attached (using existing `IsUSB` sysfs detection).
+- **`StackMemoryMB()` helper** — New method on `Manager` to get a specific stack's memory request.
+
+#### App Catalog (app-catalog-felhom.eu)
+- **AdventureLog** — Fixed image tags from `v0.12.0` (non-existent) to `v0.11.0` for both backend and frontend.
+
 ### v0.27.1 — Fix FileBrowser Mount Sync (2026-02-22)
 
 #### Fixed
diff --git a/controller/cmd/controller/main.go b/controller/cmd/controller/main.go
index 84e9609..b7d7dab 100644
--- a/controller/cmd/controller/main.go
+++ b/controller/cmd/controller/main.go
@@ -580,6 +580,10 @@ func main() {
 		apiRouter.OnConfigApplied = func() {
 			pushInfraBackup(cfg, sett, stackProv, hubPusher, logger)
 		}
+		apiRouter.OnCrossDriveComplete = func() {
+			pushInfraBackup(cfg, sett, stackProv, hubPusher, logger)
+			writeLocalInfraBackup(cfg, sett, stackProv, logger)
+		}
 	}
 	if assetsSyncer != nil {
 		apiRouter.SetAssetsSyncer(assetsSyncer)
diff --git a/controller/internal/api/router.go b/controller/internal/api/router.go
index 6fa15a5..b7ce56a 100644
--- a/controller/internal/api/router.go
+++ b/controller/internal/api/router.go
@@ -43,6 +43,9 @@ type Router struct {
 	// OnConfigApplied is called after a successful config apply (e.g., to push infra backup).
 	OnConfigApplied func()
 
+	// OnCrossDriveComplete is called after a manual cross-drive backup completes (to push infra backup to Hub).
+	OnCrossDriveComplete func()
+
 	// Asset syncer for on-demand Hub asset sync
 	assetsSyncer *assets.Syncer
 
@@ -327,6 +330,26 @@ func (r *Router) actionStack(w http.ResponseWriter, action, name string) {
 		return
 	}
 
+	// Memory check before starting a stopped app
+	if action == "start" {
+		stackMemMB := r.stackMgr.StackMemoryMB(name)
+		if stackMemMB > 0 {
+			if totalMB, memErr := system.GetTotalMemoryMB(); memErr == nil {
+				reservedMB := r.cfg.System.ReservedMemoryMB
+				usableMB := totalMB - reservedMB
+				committedReqMB, _ := r.stackMgr.CommittedMemory()
+				afterMB := committedReqMB + 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),
+					})
+					return
+				}
+			}
+		}
+	}
+
 	var err error
 	switch action {
 	case "start":
@@ -807,6 +830,9 @@ func (r *Router) triggerCrossBackup(w http.ResponseWriter, req *http.Request, na
 		if err := r.crossDriveRunner.RunAppBackup(context.Background(), name); err != nil {
 			r.logger.Printf("[API] Cross-drive backup failed for %s: %v", name, err)
 		}
+		if r.OnCrossDriveComplete != nil {
+			r.OnCrossDriveComplete()
+		}
 	}()
 
 	writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: "Mentés elindítva"})
@@ -849,6 +875,9 @@ func (r *Router) triggerAllCrossBackups(w http.ResponseWriter, _ *http.Request)
 		if err := r.crossDriveRunner.RunAllScheduled(ctx, "manual"); err != nil {
 			r.logger.Printf("[API] Cross-drive run-all manual error: %v", err)
 		}
+		if r.OnCrossDriveComplete != nil {
+			r.OnCrossDriveComplete()
+		}
 	}()
 	writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: "Összes mentés elindítva"})
 }
diff --git a/controller/internal/stacks/manager.go b/controller/internal/stacks/manager.go
index 2184652..34d86cc 100644
--- a/controller/internal/stacks/manager.go
+++ b/controller/internal/stacks/manager.go
@@ -756,7 +756,9 @@ func ParseMemoryMB(s string) int {
 	return int(val)
 }
 
-// CommittedMemory returns the sum of mem_request and mem_limit across all deployed stacks.
+// CommittedMemory returns the sum of mem_request and mem_limit across all
+// deployed stacks that are currently running (or starting/unhealthy/restarting).
+// Stopped and exited apps are excluded since they do not consume memory.
 func (m *Manager) CommittedMemory() (requestMB int, limitMB int) {
 	m.mu.RLock()
 	defer m.mu.RUnlock()
@@ -765,12 +767,25 @@ func (m *Manager) CommittedMemory() (requestMB int, limitMB int) {
 		if !s.Deployed {
 			continue
 		}
+		if s.State == StateStopped || s.State == StateExited {
+			continue
+		}
 		requestMB += ParseMemoryMB(s.Meta.Resources.MemRequest)
 		limitMB += ParseMemoryMB(s.Meta.Resources.MemLimit)
 	}
 	return
 }
 
+// StackMemoryMB returns the mem_request for a specific stack.
+func (m *Manager) StackMemoryMB(name string) int {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+	if s, ok := m.stacks[name]; ok {
+		return ParseMemoryMB(s.Meta.Resources.MemRequest)
+	}
+	return 0
+}
+
 // getCatalogTemplateSlugs reads the synced catalog cache and returns a set of
 // template slugs (directory names) that have a docker-compose.yml.
 func (m *Manager) getCatalogTemplateSlugs() map[string]bool {
diff --git a/controller/internal/stacks/metadata.go b/controller/internal/stacks/metadata.go
index 854dfcc..99f3aef 100644
--- a/controller/internal/stacks/metadata.go
+++ b/controller/internal/stacks/metadata.go
@@ -54,6 +54,7 @@ type ResourceHints struct {
 	MemLimit      string `yaml:"mem_limit" json:"mem_limit"`
 	PiCompatible  bool   `yaml:"pi_compatible" json:"pi_compatible"`
 	NeedsHDD      bool   `yaml:"needs_hdd" json:"needs_hdd"`
+	HungarianUI   bool   `yaml:"hungarian_ui" json:"hungarian_ui"`
 }
 
 // DeployField defines one configuration field shown during first deployment.
diff --git a/controller/internal/web/templates/app_info.html b/controller/internal/web/templates/app_info.html
index cee54b3..86c6ffc 100644
--- a/controller/internal/web/templates/app_info.html
+++ b/controller/internal/web/templates/app_info.html
@@ -39,6 +39,7 @@
             {{.Meta.Category}}
             {{if .Meta.Resources.NeedsHDD}}HDD szükséges{{end}}
             {{if .Meta.Resources.PiCompatible}}Pi kompatibilis{{else}}Csak x86{{end}}
+            {{if .Meta.Resources.HungarianUI}}Magyar felület{{end}}
         
     
 
diff --git a/controller/internal/web/templates/deploy.html b/controller/internal/web/templates/deploy.html
index 5acfc0e..86b31db 100644
--- a/controller/internal/web/templates/deploy.html
+++ b/controller/internal/web/templates/deploy.html
@@ -21,6 +21,7 @@
                 {{if .Meta.Resources.MemRequest}}~{{.Meta.Resources.MemRequest}}{{end}}
                 {{if .Meta.Resources.PiCompatible}}Pi kompatibilis{{end}}
                 {{if .Meta.Resources.NeedsHDD}}HDD szükséges{{end}}
+                {{if .Meta.Resources.HungarianUI}}Magyar felület{{end}}
             
             
                 Részletes leírás, képernyőképek
@@ -402,7 +403,7 @@ function triggerCrossDriveBackup(stackName, btn) {
     .then(function(r) { return r.json(); })
     .then(function(d) {
         if (!d.ok) {
-            alert('Hiba: ' + (d.error || 'Ismeretlen hiba'));
+            showAlert('Hiba: ' + (d.error || 'Ismeretlen hiba'));
             btn.disabled = false;
             btn.textContent = 'Mentés most';
             return;
@@ -421,7 +422,7 @@ function triggerCrossDriveBackup(stackName, btn) {
                         btn.textContent = 'Mentés kész';
                     } else {
                         btn.textContent = 'Hiba';
-                        alert('Hiba: ' + (s.data.last_error || 'Ismeretlen hiba'));
+                        showAlert('Hiba: ' + (s.data.last_error || 'Ismeretlen hiba'));
                     }
                     setTimeout(function() { location.reload(); }, 2000);
                 }
@@ -429,7 +430,7 @@ function triggerCrossDriveBackup(stackName, btn) {
         }, 3000);
     })
     .catch(function(e) {
-        alert('Hálózati hiba: ' + e.message);
+        showAlert('Hálózati hiba: ' + e.message);
         btn.disabled = false;
         btn.textContent = 'Mentés most';
     });
@@ -485,7 +486,7 @@ function deleteStaleData(stackName, stalePath, btn) {
     .then(function(r) { return r.json(); })
     .then(function(data) {
         if (!data.ok) {
-            alert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
+            showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
             btn.disabled = false;
             btn.textContent = 'Korábbi adatok törlése';
             return;
@@ -494,7 +495,7 @@ function deleteStaleData(stackName, stalePath, btn) {
         if (data.errors && data.errors.length > 0) {
             msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
         }
-        alert(msg);
+        showAlert(msg);
         // Remove the stale data card from DOM
         var item = btn.closest('.stale-data-item');
         if (item) item.remove();
@@ -505,9 +506,9 @@ function deleteStaleData(stackName, stalePath, btn) {
         }
     })
     .catch(function(e) {
-        alert('Hálózati hiba: ' + e.message);
+        showAlert('Hálózati hiba: ' + e.message);
         btn.disabled = false;
-        btn.textContent = '🗑️ Korábbi adatok törlése';
+        btn.textContent = 'Korábbi adatok törlése';
     });
 }
 
@@ -519,7 +520,7 @@ document.getElementById('deploy-form').addEventListener('submit', async function
     for (const pf of passwordFields) {
         if (!pf.disabled && pf.value.trim() === '') {
             const label = pf.closest('.form-group').querySelector('label').textContent.trim();
-            alert('Kötelező mező: ' + label + '\nHasználja a Generálás gombot vagy írjon be egy jelszót.');
+            showAlert('Kötelező mező: ' + label + '\nHasználja a Generálás gombot vagy írjon be egy jelszót.');
             pf.focus();
             return;
         }
@@ -530,7 +531,7 @@ document.getElementById('deploy-form').addEventListener('submit', async function
     if (subdomainField && !subdomainField.disabled) {
         const sd = subdomainField.value.trim().toLowerCase();
         if (!sd || !/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(sd)) {
-            alert('Az aldomain csak kisbetűket, számokat és kötőjelet tartalmazhat, és nem kezdődhet/végződhet kötőjellel.');
+            showAlert('Az aldomain csak kisbetűket, számokat és kötőjelet tartalmazhat, és nem kezdődhet/végződhet kötőjellel.');
             subdomainField.focus();
             return;
         }
@@ -541,7 +542,7 @@ document.getElementById('deploy-form').addEventListener('submit', async function
     for (const rf of requiredFields) {
         if (!rf.disabled && rf.value.trim() === '') {
             const label = rf.closest('.form-group').querySelector('label').textContent.trim();
-            alert('Kötelező mező: ' + label);
+            showAlert('Kötelező mező: ' + label);
             rf.focus();
             return;
         }
@@ -592,7 +593,7 @@ document.getElementById('deploy-form').addEventListener('submit', async function
         });
         var data = await resp.json();
         if (!data.ok) {
-            alert('Hiba: ' + data.error);
+            showAlert('Hiba: ' + data.error);
             btn.textContent = 'Telepítés indítása';
             btn.disabled = false;
             return;
@@ -669,7 +670,7 @@ document.getElementById('deploy-form').addEventListener('submit', async function
         }, 3000);
 
     } catch (err) {
-        alert('Hálózati hiba: ' + err.message);
+        showAlert('Hálózati hiba: ' + err.message);
         btn.textContent = 'Telepítés indítása';
         btn.disabled = false;
     }
diff --git a/controller/internal/web/templates/layout.html b/controller/internal/web/templates/layout.html
index a11bad0..b695e09 100644
--- a/controller/internal/web/templates/layout.html
+++ b/controller/internal/web/templates/layout.html
@@ -7,7 +7,10 @@
     {{.Title}} — Felhom.eu
     
     
-    
+