From 16a4c3e8789ab360da2bcdd9314174e3594249c4 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Sun, 14 Jun 2026 19:48:22 +0200 Subject: [PATCH] =?UTF-8?q?B1:=20migrate=20UI=20wiring=20=E2=80=94=20/api/?= =?UTF-8?q?storage/migrate{,-app,/status}=20+=20settings=20&=20app=20pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ServeStorageAPI gains POST /api/storage/migrate (whole-namespace), POST /api/storage/migrate-app (single app), GET /api/storage/migrate/status (poll). settings.html: the greyed migrate-all span becomes a real target-select + button + shared progress panel; app_info.html gains a per-app 'Áthelyezés másik tárhelyre' control. Both poll the shared status endpoint and render Hungarian phase progress. Co-Authored-By: Claude Opus 4.8 (1M context) --- controller/internal/web/handlers.go | 17 ++++++ controller/internal/web/storage_handlers.go | 50 ++++++++++++++++++ .../internal/web/templates/app_info.html | 49 +++++++++++++++++ .../internal/web/templates/settings.html | 52 ++++++++++++++++++- 4 files changed, 167 insertions(+), 1 deletion(-) diff --git a/controller/internal/web/handlers.go b/controller/internal/web/handlers.go index 2219e58..0cb4666 100644 --- a/controller/internal/web/handlers.go +++ b/controller/internal/web/handlers.go @@ -471,6 +471,23 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug s data["HasAppInfo"] = found.Meta.HasAppInfo() data["EffectiveSubdomain"] = effectiveSubdomain + // Per-app migration (B1): offer to move this app's data to another connected drive (≠ current). + if found.Deployed { + current := "" + if appCfg := s.stackMgr.LoadAppConfigByName(found.Name); appCfg != nil { + current = appCfg.Env["HDD_PATH"] + } + var targets []settings.StoragePath + for _, sp := range s.settings.GetStoragePaths() { + if sp.Path == current || sp.Decommissioned || sp.Disconnected || !sp.Schedulable { + continue + } + targets = append(targets, sp) + } + data["MigrateTargets"] = targets + data["MigrateCurrent"] = current + } + s.executeTemplate(w, r, "app_info", data) } diff --git a/controller/internal/web/storage_handlers.go b/controller/internal/web/storage_handlers.go index 0e11d12..0eaf0a5 100644 --- a/controller/internal/web/storage_handlers.go +++ b/controller/internal/web/storage_handlers.go @@ -245,11 +245,61 @@ func (s *Server) ServeStorageAPI(w http.ResponseWriter, r *http.Request) { s.handleStorageRegister(w, r) case r.URL.Path == "/api/storage/activate" && r.Method == http.MethodPost: s.handleStorageActivate(w, r) + case r.URL.Path == "/api/storage/migrate" && r.Method == http.MethodPost: + s.handleStorageMigrate(w, r) + case r.URL.Path == "/api/storage/migrate-app" && r.Method == http.MethodPost: + s.handleStorageMigrateApp(w, r) + case r.URL.Path == "/api/storage/migrate/status" && r.Method == http.MethodGet: + s.handleStorageMigrateStatus(w, r) default: http.NotFound(w, r) } } +// handleStorageMigrate starts a whole-namespace migration (all apps + non-app content) off the source +// drive onto the chosen target. Async: VALIDATE runs synchronously (a refusal is returned here and +// changes nothing); the copy/redeploy/cleanup run in the background and the UI polls migrate/status. +func (s *Server) handleStorageMigrate(w http.ResponseWriter, r *http.Request) { + var req struct { + Source string `json:"source"` + Target string `json:"target"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeDiskJSON(w, http.StatusBadRequest, false, "érvénytelen kérés", nil) + return + } + id, err := s.stackMgr.MigrateAll(r.Context(), strings.TrimSpace(req.Source), strings.TrimSpace(req.Target)) + if err != nil { + writeDiskJSON(w, http.StatusConflict, false, err.Error(), nil) + return + } + writeDiskJSON(w, http.StatusOK, true, "", map[string]any{"started": true, "id": id}) +} + +// handleStorageMigrateApp starts a single-app migration onto the chosen target. +func (s *Server) handleStorageMigrateApp(w http.ResponseWriter, r *http.Request) { + var req struct { + App string `json:"app"` + Target string `json:"target"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeDiskJSON(w, http.StatusBadRequest, false, "érvénytelen kérés", nil) + return + } + id, err := s.stackMgr.MigrateApp(r.Context(), strings.TrimSpace(req.App), strings.TrimSpace(req.Target)) + if err != nil { + writeDiskJSON(w, http.StatusConflict, false, err.Error(), nil) + return + } + writeDiskJSON(w, http.StatusOK, true, "", map[string]any{"started": true, "id": id}) +} + +// handleStorageMigrateStatus returns the live migration job (nil/idle when none is running) for the +// progress panel poll. +func (s *Server) handleStorageMigrateStatus(w http.ResponseWriter, r *http.Request) { + writeDiskJSON(w, http.StatusOK, true, "", map[string]any{"job": s.stackMgr.MigrationStatus()}) +} + type storageProvReq struct { Device string `json:"device"` FSType string `json:"fstype"` diff --git a/controller/internal/web/templates/app_info.html b/controller/internal/web/templates/app_info.html index f610566..96082c7 100644 --- a/controller/internal/web/templates/app_info.html +++ b/controller/internal/web/templates/app_info.html @@ -55,6 +55,55 @@ onerror="this.style.display='none'"> +{{if and .Stack.Deployed .MigrateTargets}} +
+

Áthelyezés másik tárhelyre

+

Ennek az alkalmazásnak az adatait másik csatlakoztatott tárhelyre helyezheted át. Az alkalmazás az áthelyezés alatt rövid időre leáll, az adatok pedig csak az ellenőrzés és a sikeres újraindítás után törlődnek a régi helyről.

+
+ + +
+ +
+ +{{end}} + {{if .HasAppInfo}}
{{if .AppInfo.UseCases}} diff --git a/controller/internal/web/templates/settings.html b/controller/internal/web/templates/settings.html index 96314b3..85a9594 100644 --- a/controller/internal/web/templates/settings.html +++ b/controller/internal/web/templates/settings.html @@ -334,13 +334,63 @@ function pollUntilBack() { {{end}} {{if and (gt .AppCount 0) .HasOtherPaths}} - 📦 Összes adat átköltöztetése + {{$src := .Path}} + + + + {{end}}
{{end}} {{end}} + + + {{else}}
Nincs regisztrált adattároló. Adjon hozzá egyet az alábbi űrlappal.