diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4017c96..8e2d1b0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
## Changelog
+### What was just completed (2026-02-17 session 34)
+- **v0.11.7 — Stale Data Cleanup + FileBrowser Sync + UI Title Fix:**
+ - **Feature: Stale data cleanup** — After app data migration, the deploy/settings page now shows leftover data on previous storage paths with size info and a delete button. Two-step confirmation required before deletion. Protected paths (storage root, media, Dokumentumok, appdata) cannot be deleted. Also available immediately after migration on the migration-done page.
+ - **Fix: FileBrowser sync after migration** — `syncFileBrowserMounts()` now called after successful data migration, ensuring FileBrowser mounts reflect the current storage layout.
+ - **Fix: Deploy page title** — Already-deployed apps now show "Beállítások" (Settings) instead of "Telepítés" (Deploy) in both the browser page title and the `
` heading.
+ - **Internal: Exported `ProtectedHDDPaths()`** from stacks package for reuse in web handlers.
+ - **Files modified (7):** `internal/stacks/delete.go`, `internal/web/handlers.go`, `internal/web/storage_handlers.go`, `internal/web/templates/deploy.html`, `internal/web/templates/migrate.html`, `internal/web/templates/style.css`
+
### What was just completed (2026-02-17 session 33)
- **v0.11.6 — FileBrowser Auto-Mount Sync + UI Polish (3 fixes):**
- **Feature: FileBrowser auto-mount sync** — Added `syncFileBrowserMounts()` and `generateFileBrowserCompose()` to `handlers.go`. After a storage path is added (via storage init wizard) or removed, the controller regenerates `/opt/docker/stacks/filebrowser/docker-compose.yml` with volume mounts for all registered paths (`/mnt/hdd_1:/srv/hdd_1` etc.), then recreates the FileBrowser container. Domain is read from FileBrowser's `.env`. If FileBrowser isn't deployed, the function silently returns. The generated compose is self-contained (no env vars).
diff --git a/controller/internal/stacks/delete.go b/controller/internal/stacks/delete.go
index f9ef8d3..3ace0fe 100644
--- a/controller/internal/stacks/delete.go
+++ b/controller/internal/stacks/delete.go
@@ -33,8 +33,8 @@ type HDDPath struct {
Exists bool `json:"exists"`
}
-// protectedHDDPaths returns the set of top-level HDD directories that must never be deleted.
-func protectedHDDPaths(hddPath string) map[string]bool {
+// ProtectedHDDPaths returns the set of top-level HDD directories that must never be deleted.
+func ProtectedHDDPaths(hddPath string) map[string]bool {
if hddPath == "" {
return nil
}
@@ -100,7 +100,7 @@ func (m *Manager) DeleteStack(name string, removeHDDData bool) (*DeleteResponse,
}
// Step 4: Handle HDD data
- protected := protectedHDDPaths(hddPath)
+ protected := ProtectedHDDPaths(hddPath)
for _, mount := range hddMounts {
// Safety: never delete protected top-level dirs
cleanPath := filepath.Clean(mount)
@@ -165,7 +165,7 @@ func (m *Manager) GetStackHDDData(name string) (*HDDDataResponse, error) {
}
mounts := ParseComposeHDDMounts(stack.ComposePath, hddPath)
- protected := protectedHDDPaths(hddPath)
+ protected := ProtectedHDDPaths(hddPath)
for _, mount := range mounts {
cleanPath := filepath.Clean(mount)
diff --git a/controller/internal/web/handlers.go b/controller/internal/web/handlers.go
index 3aac977..7855b37 100644
--- a/controller/internal/web/handlers.go
+++ b/controller/internal/web/handlers.go
@@ -164,7 +164,11 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
stack, _ := s.stackMgr.GetStack(name)
alreadyDeployed := appCfg != nil && appCfg.Deployed
- data := s.baseData("deploy", meta.DisplayName+" — Telepítés")
+ pageTitle := meta.DisplayName + " — Telepítés"
+ if alreadyDeployed {
+ pageTitle = meta.DisplayName + " — Beállítások"
+ }
+ data := s.baseData("deploy", pageTitle)
data["Stack"] = stack
data["Meta"] = meta
data["AppConfig"] = appCfg
@@ -195,6 +199,11 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
data["StorageInfo"] = storageInfo
data["OtherStoragePaths"] = s.otherStoragePathsForStack(name)
}
+ // Stale data from previous migrations (only for deployed apps with HDD data)
+ staleData := s.findStaleStorageData(name)
+ if len(staleData) > 0 {
+ data["StaleData"] = staleData
+ }
}
// Memory info for deploy page (only for non-deployed apps)
diff --git a/controller/internal/web/storage_handlers.go b/controller/internal/web/storage_handlers.go
index ed3af1a..3166c28 100644
--- a/controller/internal/web/storage_handlers.go
+++ b/controller/internal/web/storage_handlers.go
@@ -140,6 +140,8 @@ func (s *Server) storageAPIHandler(w http.ResponseWriter, r *http.Request) {
s.storageMigrateAPIHandler(w, r)
case path == "/api/storage/migrate/status" && r.Method == http.MethodGet:
s.storageMigrateStatusAPIHandler(w, r)
+ case path == "/api/storage/stale-cleanup" && r.Method == http.MethodPost:
+ s.staleDataCleanupHandler(w, r)
default:
http.NotFound(w, r)
}
@@ -452,6 +454,8 @@ func (s *Server) storageMigrateAPIHandler(w http.ResponseWriter, r *http.Request
s.logger.Printf("[ERROR] Migration failed: stack=%s: %v", req.StackName, err)
} else {
s.logger.Printf("[INFO] Migration complete: stack=%s → %s", req.StackName, req.TargetPath)
+ // Sync FileBrowser mounts (storage paths may now have new app data)
+ go s.syncFileBrowserMounts()
}
close(progressCh)
}()
@@ -619,3 +623,189 @@ func (s *Server) storageLabelForPath(path string) string {
}
return strings.TrimPrefix(path, "/mnt/")
}
+
+// StaleStorageData describes leftover data on a non-active storage path.
+type StaleStorageData struct {
+ Path string // e.g., "/mnt/hdd_placeholder"
+ Label string // e.g., "Külső tárhely (hdd_placeholder)"
+ Mounts []string // host-side paths with data
+ SizeHuman string // e.g., "48 MB"
+ SizeBytes int64
+}
+
+// findStaleStorageData detects leftover app data on non-active storage paths.
+// This happens after migration: the old data stays on the previous storage path.
+func (s *Server) findStaleStorageData(stackName string) []StaleStorageData {
+ appCfg := s.stackMgr.LoadAppConfigByName(stackName)
+ if appCfg == nil {
+ return nil
+ }
+ currentHDDPath := appCfg.Env["HDD_PATH"]
+ if currentHDDPath == "" {
+ return nil
+ }
+
+ stack, ok := s.stackMgr.GetStack(stackName)
+ if !ok {
+ return nil
+ }
+
+ var result []StaleStorageData
+
+ // Check all registered storage paths except the current one
+ for _, sp := range s.settings.GetStoragePaths() {
+ if sp.Path == currentHDDPath {
+ continue
+ }
+
+ // Use ParseComposeHDDMounts to find what dirs WOULD exist on this path
+ mounts := stacks.ParseComposeHDDMounts(stack.ComposePath, sp.Path)
+ if len(mounts) == 0 {
+ continue
+ }
+
+ // Check which mounts actually have data
+ var existingMounts []string
+ var totalSize int64
+ for _, m := range mounts {
+ info, err := os.Stat(m)
+ if err != nil || !info.IsDir() {
+ continue
+ }
+ size := dirSizeInt64(m)
+ if size > 0 {
+ existingMounts = append(existingMounts, m)
+ totalSize += size
+ }
+ }
+
+ if len(existingMounts) == 0 {
+ continue
+ }
+
+ label := sp.Label
+ if label == "" {
+ label = settings.InferStorageLabel(sp.Path)
+ }
+
+ result = append(result, StaleStorageData{
+ Path: sp.Path,
+ Label: label,
+ Mounts: existingMounts,
+ SizeHuman: dirSizeBytesHuman(totalSize),
+ SizeBytes: totalSize,
+ })
+ }
+
+ return result
+}
+
+// staleDataCleanupHandler handles POST /api/storage/stale-cleanup.
+// Deletes leftover app data from a previous storage path after migration.
+func (s *Server) staleDataCleanupHandler(w http.ResponseWriter, r *http.Request) {
+ var req struct {
+ StackName string `json:"stack_name"`
+ StalePath string `json:"stale_path"` // the old storage root, e.g., "/mnt/hdd_placeholder"
+ }
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ jsonError(w, "Érvénytelen kérés", http.StatusBadRequest)
+ return
+ }
+
+ if req.StackName == "" || req.StalePath == "" {
+ jsonError(w, "Hiányos paraméterek", http.StatusBadRequest)
+ return
+ }
+
+ // Verify the app exists and is deployed
+ stack, ok := s.stackMgr.GetStack(req.StackName)
+ if !ok {
+ jsonError(w, "Alkalmazás nem található: "+req.StackName, http.StatusNotFound)
+ return
+ }
+
+ appCfg := s.stackMgr.LoadAppConfigByName(req.StackName)
+ if appCfg == nil || !appCfg.Deployed {
+ jsonError(w, "Az alkalmazás nincs telepítve", http.StatusBadRequest)
+ return
+ }
+
+ currentHDDPath := appCfg.Env["HDD_PATH"]
+ if currentHDDPath == "" {
+ jsonError(w, "Az alkalmazásnak nincs HDD_PATH beállítva", http.StatusBadRequest)
+ return
+ }
+
+ // SAFETY: StalePath must NOT be the current HDD_PATH
+ if req.StalePath == currentHDDPath {
+ jsonError(w, "Az aktív tárhely adatai nem törölhetők! Ez az alkalmazás aktuális adattárolója.", http.StatusForbidden)
+ return
+ }
+
+ // SAFETY: StalePath must be a registered storage path
+ found := false
+ for _, sp := range s.settings.GetStoragePaths() {
+ if sp.Path == req.StalePath {
+ found = true
+ break
+ }
+ }
+ if !found {
+ jsonError(w, "A megadott útvonal nem regisztrált adattároló", http.StatusBadRequest)
+ return
+ }
+
+ // Find mounts to delete
+ mounts := stacks.ParseComposeHDDMounts(stack.ComposePath, req.StalePath)
+ if len(mounts) == 0 {
+ jsonError(w, "Nem találhatók törlendő adatok", http.StatusNotFound)
+ return
+ }
+
+ // Protected paths check
+ protected := stacks.ProtectedHDDPaths(req.StalePath)
+
+ var deleted []string
+ var errors []string
+ var totalFreed int64
+
+ for _, mountPath := range mounts {
+ cleanPath := filepath.Clean(mountPath)
+
+ // Safety: never delete protected top-level dirs
+ if protected != nil && protected[cleanPath] {
+ s.logger.Printf("[WARN] Refusing to delete protected HDD path: %s", cleanPath)
+ errors = append(errors, fmt.Sprintf("Védett útvonal, nem törölhető: %s", cleanPath))
+ continue
+ }
+
+ // Verify it actually exists and has data
+ info, err := os.Stat(cleanPath)
+ if err != nil || !info.IsDir() {
+ continue
+ }
+
+ size := dirSizeInt64(cleanPath)
+
+ if err := os.RemoveAll(cleanPath); err != nil {
+ s.logger.Printf("[ERROR] Failed to remove stale data %s: %v", cleanPath, err)
+ errors = append(errors, fmt.Sprintf("Törlés sikertelen: %s — %v", cleanPath, err))
+ } else {
+ s.logger.Printf("[INFO] Removed stale data: %s (%s) for stack %s", cleanPath, dirSizeBytesHuman(size), req.StackName)
+ deleted = append(deleted, cleanPath)
+ totalFreed += size
+ }
+ }
+
+ if len(deleted) == 0 && len(errors) > 0 {
+ jsonError(w, "Törlés sikertelen: "+strings.Join(errors, "; "), http.StatusInternalServerError)
+ return
+ }
+
+ jsonResponse(w, map[string]interface{}{
+ "ok": true,
+ "deleted": deleted,
+ "freed_human": dirSizeBytesHuman(totalFreed),
+ "errors": errors,
+ })
+}
diff --git a/controller/internal/web/templates/deploy.html b/controller/internal/web/templates/deploy.html
index dccabe0..070411b 100644
--- a/controller/internal/web/templates/deploy.html
+++ b/controller/internal/web/templates/deploy.html
@@ -4,7 +4,7 @@