v0.57.0: stable host-storage list + per-app Tier-2 config panel
Part A of the UI-fixes/storage-spike spec.
A1: enrichHostStorageTargets sorts /api/host-metrics storage_targets
server-side and attaches friendly Hungarian labels + purpose, fixing the
#host-storage-bars reorder-on-poll bug. Display labels only — PVE storage
ids are never renamed.
A2: new GET/POST /stacks/{name}/backup Tier-2 config panel; the "2. mentés"
Beállítás button is repointed there from the dead-end deploy page. Customer
can pin a target drive or disable Tier 2; preference is preserved across the
runner's status writes. Always visible (single-SSD + non-HDD apps included).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Per-app Tier-2 (off-drive copy) config panel — item 4.
|
||||
//
|
||||
// The "2. mentés" row on the backup page used to link its "Beállítás" button at the app's deploy
|
||||
// page, which has no backup-location setting (a dead end). This is the real surface: it shows the
|
||||
// current/auto off-drive target + last-run status, and lets the customer pin a different registered
|
||||
// drive or turn Tier 2 off. It is ALWAYS shown — even when only the internal SSD qualifies, or the
|
||||
// app's data lives on the rootfs (already in PBS) — with honest context rather than a hidden control.
|
||||
//
|
||||
// Routes (wired in server.go, behind RequireAuth + CsrfProtect):
|
||||
// GET /stacks/{name}/backup → tier2ConfigPageHandler
|
||||
// POST /stacks/{name}/backup → tier2ConfigSaveHandler
|
||||
|
||||
func (s *Server) tier2ConfigPageHandler(w http.ResponseWriter, r *http.Request, name string) {
|
||||
stack, ok := s.stackMgr.GetStack(name)
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if s.backupMgr == nil {
|
||||
http.Error(w, "A mentés nincs beállítva ezen a szerveren.", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
info := s.backupMgr.Tier2Info(name)
|
||||
|
||||
data := s.baseData("backups", "2. mentés beállítása — "+stack.Meta.DisplayName)
|
||||
data["StackName"] = name
|
||||
data["DisplayName"] = stack.Meta.DisplayName
|
||||
data["Tier2"] = info
|
||||
if flash := r.URL.Query().Get("flash"); flash != "" {
|
||||
data["Flash"] = flash
|
||||
}
|
||||
if flashErr := r.URL.Query().Get("flash_error"); flashErr != "" {
|
||||
data["FlashError"] = flashErr
|
||||
}
|
||||
s.executeTemplate(w, r, "tier2_config", data)
|
||||
}
|
||||
|
||||
func (s *Server) tier2ConfigSaveHandler(w http.ResponseWriter, r *http.Request, name string) {
|
||||
if _, ok := s.stackMgr.GetStack(name); !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if s.backupMgr == nil {
|
||||
http.Error(w, "A mentés nincs beállítva ezen a szerveren.", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
_ = r.ParseForm()
|
||||
|
||||
// "enabled" checkbox: present → Tier 2 on; absent → off (UserDisabled = !enabled).
|
||||
enabled := r.FormValue("enabled") == "on" || r.FormValue("enabled") == "true"
|
||||
target := r.FormValue("target") // "" = automatic; otherwise a registered drive path
|
||||
|
||||
// Validate the chosen target against the eligible alternatives (defence-in-depth: the runner
|
||||
// also re-validates off-disk at run time, but reject a bogus path here for a clean message).
|
||||
if target != "" {
|
||||
valid := false
|
||||
for _, opt := range s.backupMgr.Tier2Info(name).Alternatives {
|
||||
if opt.Path == target {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
s.redirectTier2(w, r, name, "", "A választott cél meghajtó nem érvényes.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.settings.SetTier2Preference(name, !enabled, target); err != nil {
|
||||
s.logger.Printf("[ERROR] [web] save Tier 2 preference for %s: %v", name, err)
|
||||
s.redirectTier2(w, r, name, "", "A beállítás mentése nem sikerült.")
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] Tier 2 preference saved for %s: enabled=%v target=%q", name, enabled, target)
|
||||
|
||||
// Apply immediately when enabled for an HDD app so the customer sees the result on return.
|
||||
if enabled && s.backupMgr.Tier2Info(name).IsHDDApp {
|
||||
go func() {
|
||||
if err := s.backupMgr.RunTier2(name); err != nil {
|
||||
s.logger.Printf("[WARN] [web] immediate Tier 2 run for %s failed: %v", name, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
s.redirectTier2(w, r, name, "A 2. mentés beállítása elmentve.", "")
|
||||
}
|
||||
|
||||
// redirectTier2 sends the customer back to the panel with a flash message.
|
||||
func (s *Server) redirectTier2(w http.ResponseWriter, r *http.Request, name, flash, flashErr string) {
|
||||
dest := "/stacks/" + url.PathEscape(name) + "/backup"
|
||||
q := url.Values{}
|
||||
if flash != "" {
|
||||
q.Set("flash", flash)
|
||||
}
|
||||
if flashErr != "" {
|
||||
q.Set("flash_error", flashErr)
|
||||
}
|
||||
if e := q.Encode(); e != "" {
|
||||
dest += "?" + e
|
||||
}
|
||||
http.Redirect(w, r, dest, http.StatusSeeOther)
|
||||
}
|
||||
Reference in New Issue
Block a user