diff --git a/controller/internal/web/backup_page_state_test.go b/controller/internal/web/backup_page_state_test.go
index d7d5118..768a018 100644
--- a/controller/internal/web/backup_page_state_test.go
+++ b/controller/internal/web/backup_page_state_test.go
@@ -191,12 +191,18 @@ func (u *unitProvider) GetStackComposePath(string) (string, bool) { return "comp
// ---------------------------------------------------------------------------
func renderBackups(t *testing.T, data map[string]interface{}) string {
+ return renderBackupPage(t, "backups", data)
+}
+
+// renderBackupPage renders one of the four backups sub-page templates (v0.124.0 IA split)
+// through the PRODUCTION template tree.
+func renderBackupPage(t *testing.T, page string, data map[string]interface{}) string {
t.Helper()
s := testServer(t)
s.loadTemplates()
var buf bytes.Buffer
- if err := s.tmpl.ExecuteTemplate(&buf, "backups", data); err != nil {
- t.Fatalf("render backups: %v", err)
+ if err := s.tmpl.ExecuteTemplate(&buf, page, data); err != nil {
+ t.Fatalf("render %s: %v", page, err)
}
return buf.String()
}
@@ -227,7 +233,7 @@ func TestBackupsTemplate_Tier3Live(t *testing.T) {
LastRun: "2026-07-11T02:00:00Z", EscrowState: "escrowed",
}
data["OffboxConfigured"] = true
- html := renderBackups(t, data)
+ html := renderBackupPage(t, "backups_apps", data)
for _, want := range []string{
"Sikeres", // active badge (LastStatus ok)
@@ -235,11 +241,19 @@ func TestBackupsTemplate_Tier3Live(t *testing.T) {
"Utolsó:", // a relative last-run time is shown
"Kikapcsolva", // radarr off row
"Bekapcsolás", // radarr enable link
- `href="#offbox-section"`, // the anchor jump target
- "Távoli mentés (3. mentés)", // the renamed section header
+ `href="/backups/remote#offbox-section"`, // the cross-page anchor jump target (IA split)
} {
if !strings.Contains(html, want) {
- t.Errorf("rendered page missing %q", want)
+ t.Errorf("rendered apps page missing %q", want)
+ }
+ }
+ remoteHTML := renderBackupPage(t, "backups_remote", data)
+ for _, want := range []string{
+ "Távoli mentés (3. mentés)", // the renamed section header
+ `id="offbox-section"`, // the anchor target the apps-page links jump to
+ } {
+ if !strings.Contains(remoteHTML, want) {
+ t.Errorf("rendered remote page missing %q", want)
}
}
for _, banned := range []string{
@@ -267,7 +281,7 @@ func TestBackupsTemplate_Tier3EscrowPending(t *testing.T) {
})
data["Offbox"] = &settings.OffboxTarget{Enabled: true, Host: "nas.local", LastStatus: "ok", EscrowState: "pending"}
data["OffboxConfigured"] = true
- html := renderBackups(t, data)
+ html := renderBackupPage(t, "backups_apps", data)
if !strings.Contains(html, "Kulcsletétre vár") {
t.Error("escrow-pending row must show 'Kulcsletétre vár'")
@@ -285,23 +299,22 @@ func TestBackupsTemplate_DBMessaging(t *testing.T) {
t.Run("embedded", func(t *testing.T) {
data := baseBackupData(nil)
data["DBSectionState"] = "embedded"
- html := renderBackups(t, data)
- for _, want := range []string{
- "beágyazott DB-k a kötetmentésben", // stat-card sublabel
- "beágyazott adatbázist használnak", // Adatbázisok empty-state explanation
- } {
- if !strings.Contains(html, want) {
- t.Errorf("embedded page missing %q", want)
- }
+ overviewHTML := renderBackups(t, data)
+ if !strings.Contains(overviewHTML, "beágyazott DB-k a kötetmentésben") { // stat-card sublabel
+ t.Error("embedded overview page missing the stat-card sublabel")
}
- if strings.Contains(html, "Nem található adatbázis mentés.") {
+ appsHTML := renderBackupPage(t, "backups_apps", data)
+ if !strings.Contains(appsHTML, "beágyazott adatbázist használnak") { // Adatbázisok empty state
+ t.Error("embedded apps page missing the Adatbázisok explanation")
+ }
+ if strings.Contains(appsHTML, "Nem található adatbázis mentés.") {
t.Error("embedded box must NOT show the old bare 'not found' message")
}
})
t.Run("pending", func(t *testing.T) {
data := baseBackupData(nil)
data["DBSectionState"] = "pending"
- html := renderBackups(t, data)
+ html := renderBackupPage(t, "backups_apps", data)
if !strings.Contains(html, "az első ütemezett mentés éjjel fut le") {
t.Error("pending page missing the 'first run tonight' message")
}
@@ -320,7 +333,7 @@ func TestBackupsTemplate_Tier2RelativeTime(t *testing.T) {
Tier2LastRun: "2026-07-10T03:30:00Z", Tier2LastStatus: "ok", Tier2StatusBadge: "Sikeres",
Tier3State: "unconfigured",
}})
- html := renderBackups(t, data)
+ html := renderBackupPage(t, "backups_apps", data)
// The visible "Utolsó:" label must be a relative time, never the raw RFC3339. (The raw
// timestamp legitimately survives in the restore confirm() dialog — precise there by design.)
if strings.Contains(html, "Utolsó: 2026-07-10T03:30:00Z") {
diff --git a/controller/internal/web/backups_split_test.go b/controller/internal/web/backups_split_test.go
new file mode 100644
index 0000000..e513238
--- /dev/null
+++ b/controller/internal/web/backups_split_test.go
@@ -0,0 +1,120 @@
+package web
+
+import (
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "gitea.dooplex.hu/admin/felhom-controller/internal/backup"
+ "gitea.dooplex.hu/admin/felhom-controller/internal/settings"
+)
+
+// v0.124.0 IA split (§10 nav/state): each backups sub-page renders ONLY its mapped sections —
+// a marker string per section, asserted present on its page and absent on every other page.
+
+func splitTestData() map[string]interface{} {
+ return map[string]interface{}{
+ "Page": "backups", "Title": "Biztonsági mentés",
+ "Backup": &backup.FullBackupStatus{
+ AppDataInfo: []backup.AppBackupInfo{{StackName: "calibre-web", DisplayName: "Calibre-Web"}},
+ },
+ "AppBackupRows": []AppBackupRow{
+ {StackName: "calibre-web", DisplayName: "Calibre-Web", OffboxEnabled: true, Tier3State: "active"},
+ },
+ "DBSectionState": "dumps",
+ "Offbox": &settings.OffboxTarget{
+ Enabled: true, Host: "nas.local", LastStatus: "ok", EscrowState: "escrowed",
+ },
+ "OffboxConfigured": true,
+ "OffboxApps": []OffboxAppRow{{Name: "calibre-web", DisplayName: "Calibre-Web", Enabled: true}},
+ "OffboxToggledCount": 1,
+ "OffboxQuotaPct": 0,
+ "GuestBackup": map[string]interface{}{"Available": false, "Note": "n/a"},
+ }
+}
+
+func TestBackupsSplit_SectionsOnExactlyOnePage(t *testing.T) {
+ pages := map[string]string{
+ "backups": renderBackupPage(t, "backups", splitTestData()),
+ "backups_remote": renderBackupPage(t, "backups_remote", splitTestData()),
+ "backups_apps": renderBackupPage(t, "backups_apps", splitTestData()),
+ "backups_restore": renderBackupPage(t, "backups_restore", splitTestData()),
+ }
+
+ // marker → the ONE page it belongs to
+ markers := map[string]string{
+ "Tárhely áttekintés": "backups", // Section 0
+ "Rendszermentés (teljes mentés)": "backups", // whole-guest
+ ">Adatmentés<": "backups", // stat cards (neutral branch)
+ `id="offbox-section"`: "backups_remote", // the offbox anchor target
+ "Távoli mentési cél beállítása": "backups_remote", // manual-target form
+ "Mely alkalmazások mentődnek": "backups_remote", // toggle list
+ "
Ütemezés ": "backups_apps", // schedule
+ "Adatbázisok ": "backups_apps", // databases
+ "Alkalmazások mentési állapota": "backups_apps", // per-app rows
+ `id="restore-app"`: "backups_restore", // restore panel
+ "Ellenőrző visszaállítás a távoli tárolóból": "backups_restore", // moved restore-to-verify
+ `action="/backup/offbox/restore"`: "backups_restore", // the MOVED form itself
+ }
+ for marker, home := range markers {
+ for page, html := range pages {
+ has := strings.Contains(html, marker)
+ if page == home && !has {
+ t.Errorf("%s: missing its own section marker %q", page, marker)
+ }
+ if page != home && has {
+ t.Errorf("%s: renders %q which belongs to %s only", page, marker, home)
+ }
+ }
+ }
+}
+
+// The tier-3 row actions must deep-link cross-page to the offbox section (the pre-split
+// #offbox-section anchors would be orphaned on the apps page).
+func TestBackupsSplit_CrossPageAnchors(t *testing.T) {
+ data := splitTestData()
+ data["AppBackupRows"] = []AppBackupRow{
+ {StackName: "radarr", DisplayName: "Radarr", Tier3State: "off"},
+ }
+ html := renderBackupPage(t, "backups_apps", data)
+ if !strings.Contains(html, `href="/backups/remote#offbox-section"`) {
+ t.Error("tier-3 'Bekapcsolás' must link cross-page to /backups/remote#offbox-section")
+ }
+ if strings.Contains(html, `href="#offbox-section"`) {
+ t.Error("orphaned same-page #offbox-section anchor survives on the apps page")
+ }
+}
+
+// Route → handler wiring: each sub-route sets its own .Page id, so the sidebar child renders
+// active on the right page (backupMgr nil → the empty state renders, the nav still does).
+func TestBackupsSplit_RoutesSetPageIDs(t *testing.T) {
+ s := testServer(t)
+ s.loadTemplates()
+
+ cases := []struct {
+ path string
+ active string
+ }{
+ {"/backups/remote", `href="/backups/remote" class="active"`},
+ {"/backups/apps", `href="/backups/apps" class="active"`},
+ {"/backups/restore", `href="/backups/restore" class="active"`},
+ }
+ for _, c := range cases {
+ rr := httptest.NewRecorder()
+ req := httptest.NewRequest("GET", c.path, nil)
+ switch c.path {
+ case "/backups/remote":
+ s.backupsRemoteHandler(rr, req)
+ case "/backups/apps":
+ s.backupsAppsHandler(rr, req)
+ case "/backups/restore":
+ s.backupsRestoreHandler(rr, req)
+ }
+ if rr.Code != 200 {
+ t.Fatalf("%s: status %d", c.path, rr.Code)
+ }
+ if !strings.Contains(rr.Body.String(), c.active) {
+ t.Errorf("%s: sidebar child not active (want %q)", c.path, c.active)
+ }
+ }
+}
diff --git a/controller/internal/web/handlers.go b/controller/internal/web/handlers.go
index f6604e2..566a9ba 100644
--- a/controller/internal/web/handlers.go
+++ b/controller/internal/web/handlers.go
@@ -612,8 +612,55 @@ func isPingConfigured(uuid string) bool {
return uuid != "" && !strings.HasPrefix(uuid, "CHANGEME")
}
+// backupsCommonData builds what every backups sub-page shares (v0.124.0 IA split): the page
+// chrome + the backup full-status with the redirect flash. Backup stays nil (empty-state) when
+// the manager is absent. Each page handler adds ONLY the data its sections render — no
+// duplicated computation across the four pages.
+func (s *Server) backupsCommonData(page, title string, r *http.Request) map[string]interface{} {
+ data := s.baseData(page, title)
+ if s.backupMgr == nil {
+ data["Backup"] = nil
+ return data
+ }
+ nextDBDump := scheduler.NextDailyRun(s.cfg.Backup.DBDumpSchedule)
+ fullStatus := s.backupMgr.GetFullStatus(nextDBDump)
+
+ // Pass flash messages from query params (set by redirect handlers)
+ if flash := r.URL.Query().Get("flash"); flash != "" {
+ fullStatus.FlashSuccess = flash
+ }
+ if flashErr := r.URL.Query().Get("flash_error"); flashErr != "" {
+ fullStatus.FlashError = flashErr
+ }
+ data["Backup"] = fullStatus
+ return data
+}
+
+// backupsOffboxData adds the offbox target + per-app toggle state (the remote, apps and restore
+// pages all render some of it: status card / toggle list / tier-3 rows / restore-to-verify).
+func (s *Server) backupsOffboxData(data map[string]interface{}) {
+ offboxTgt := s.settings.GetOffboxTarget()
+ data["Offbox"] = offboxTgt
+ data["OffboxConfigured"] = s.backupMgr != nil && s.backupMgr.OffboxConfigured()
+ offboxApps := s.buildOffboxApps()
+ data["OffboxApps"] = offboxApps
+ // Zero-toggle hint (take-two obs.): configured + escrowed but no app selected — nothing is
+ // actually covered by the offsite leg until the customer toggles at least one.
+ offboxToggled := 0
+ for _, a := range offboxApps {
+ if a.Enabled {
+ offboxToggled++
+ }
+ }
+ data["OffboxToggledCount"] = offboxToggled
+ // SLICE 4 soft-quota usage bar (rendered only when a quota is set — shared model).
+ data["OffboxQuotaPct"] = backup.OffboxQuotaPercent(offboxTgt)
+}
+
+// backupsHandler renders the Áttekintés page: storage overview, whole-guest Rendszermentés and
+// the status stat cards.
func (s *Server) backupsHandler(w http.ResponseWriter, r *http.Request) {
- data := s.baseData("backups", "Biztonsági mentés")
+ data := s.backupsCommonData("backups", "Biztonsági mentés", r)
// System info for storage overview bars
data["SystemInfo"] = system.GetInfo(s.primaryHDDPath(), s.cpuCollector)
@@ -622,18 +669,30 @@ func (s *Server) backupsHandler(w http.ResponseWriter, r *http.Request) {
// Whole-guest backup view (agent-sourced, read-only) for the "Rendszermentés" section.
data["GuestBackup"] = s.loadGuestBackup(r.Context())
- if s.backupMgr != nil {
- nextDBDump := scheduler.NextDailyRun(s.cfg.Backup.DBDumpSchedule)
- fullStatus := s.backupMgr.GetFullStatus(nextDBDump)
+ if fullStatus, ok := data["Backup"].(*backup.FullBackupStatus); ok && fullStatus != nil {
+ // DB-section state — honest messaging for embedded-DB-only boxes (SQLite etc.):
+ // "dumps" (real dumps) | "pending" (discovered, first run tonight) | "embedded".
+ data["DBSectionState"] = dbSectionState(len(fullStatus.DiscoveredDBs), len(fullStatus.DumpFiles))
+ }
- // Pass flash messages from query params (set by redirect handlers)
- if flash := r.URL.Query().Get("flash"); flash != "" {
- fullStatus.FlashSuccess = flash
- }
- if flashErr := r.URL.Query().Get("flash_error"); flashErr != "" {
- fullStatus.FlashError = flashErr
- }
+ s.executeTemplate(w, r, "backups", data)
+}
+// backupsRemoteHandler renders the Távoli mentés page: the Felhom-offsite status card, the
+// participation toggles and the manual-target form.
+func (s *Server) backupsRemoteHandler(w http.ResponseWriter, r *http.Request) {
+ data := s.backupsCommonData("backups-remote", "Biztonsági mentés — Távoli mentés", r)
+ s.backupsOffboxData(data)
+ s.executeTemplate(w, r, "backups_remote", data)
+}
+
+// backupsAppsHandler renders the Alkalmazások page: schedule, databases and the per-app
+// 1./2./3. tier rows.
+func (s *Server) backupsAppsHandler(w http.ResponseWriter, r *http.Request) {
+ data := s.backupsCommonData("backups-apps", "Biztonsági mentés — Alkalmazások", r)
+ s.backupsOffboxData(data) // the tier-3 rows render $.Offbox status
+
+ if fullStatus, ok := data["Backup"].(*backup.FullBackupStatus); ok && fullStatus != nil {
// Enrich AppDataInfo with storage labels
storagePaths := s.settings.GetStoragePaths()
for i := range fullStatus.AppDataInfo {
@@ -653,42 +712,18 @@ func (s *Server) backupsHandler(w http.ResponseWriter, r *http.Request) {
// Build unified per-app backup rows for the app-data backup UI.
// Disk-tier (cross-drive / restic) backup has moved to the host agent.
data["AppBackupRows"] = s.buildAppBackupRows(fullStatus)
-
- data["Backup"] = fullStatus
-
- // DB-section state — honest messaging for embedded-DB-only boxes (SQLite etc.):
- // "dumps" (real dumps) | "pending" (discovered, first run tonight) | "embedded".
data["DBSectionState"] = dbSectionState(len(fullStatus.DiscoveredDBs), len(fullStatus.DumpFiles))
-
- // DB dump total size
- var dbDumpTotalBytes int64
- for _, f := range fullStatus.DumpFiles {
- dbDumpTotalBytes += f.Size
- }
- data["DBDumpTotalBytes"] = dbDumpTotalBytes
-
- // Off-box (NAS) restic-SFTP backup (Part B): the target status + per-app off-box toggles.
- offboxTgt := s.settings.GetOffboxTarget()
- data["Offbox"] = offboxTgt
- data["OffboxConfigured"] = s.backupMgr.OffboxConfigured()
- offboxApps := s.buildOffboxApps()
- data["OffboxApps"] = offboxApps
- // Zero-toggle hint (take-two obs.): configured + escrowed but no app selected — nothing is
- // actually covered by the offsite leg until the customer toggles at least one.
- offboxToggled := 0
- for _, a := range offboxApps {
- if a.Enabled {
- offboxToggled++
- }
- }
- data["OffboxToggledCount"] = offboxToggled
- // SLICE 4 soft-quota usage bar (rendered only when a quota is set — shared model).
- data["OffboxQuotaPct"] = backup.OffboxQuotaPercent(offboxTgt)
- } else {
- data["Backup"] = nil
}
- s.executeTemplate(w, r, "backups", data)
+ s.executeTemplate(w, r, "backups_apps", data)
+}
+
+// backupsRestoreHandler renders the Visszaállítás page: the restore panel, the offbox
+// restore-to-verify list and the .fab export/import loop.
+func (s *Server) backupsRestoreHandler(w http.ResponseWriter, r *http.Request) {
+ data := s.backupsCommonData("backups-restore", "Biztonsági mentés — Visszaállítás", r)
+ s.backupsOffboxData(data) // restore-to-verify lists the offbox-toggled apps
+ s.executeTemplate(w, r, "backups_restore", data)
}
// OffboxAppRow is one deployed app's off-box toggle state for the backups page.
@@ -930,26 +965,26 @@ func (s *Server) backupRestoreHandler(w http.ResponseWriter, r *http.Request) {
}
if stackName == "" || snapshotID == "" {
- http.Redirect(w, r, "/backups?flash_error=Hi%C3%A1nyz%C3%B3+param%C3%A9terek", http.StatusFound)
+ http.Redirect(w, r, "/backups/restore?flash_error=Hi%C3%A1nyz%C3%B3+param%C3%A9terek", http.StatusFound)
return
}
// F2 (defense-in-depth): a stack name is a single segment, never a path. Reject traversal before any
// restore work — never let it reach RestoreFromRecoveryUnit.
if !validStackName(stackName) {
s.logger.Printf("[WARN] [web] restore rejected: invalid stack_name %q from %s", stackName, r.RemoteAddr)
- http.Redirect(w, r, "/backups?flash_error=%C3%89rv%C3%A9nytelen+alkalmaz%C3%A1sn%C3%A9v", http.StatusFound)
+ http.Redirect(w, r, "/backups/restore?flash_error=%C3%89rv%C3%A9nytelen+alkalmaz%C3%A1sn%C3%A9v", http.StatusFound)
return
}
if s.backupMgr == nil {
- http.Redirect(w, r, "/backups?flash_error=Ment%C3%A9s+nincs+be%C3%A1ll%C3%ADtva", http.StatusFound)
+ http.Redirect(w, r, "/backups/restore?flash_error=Ment%C3%A9s+nincs+be%C3%A1ll%C3%ADtva", http.StatusFound)
return
}
// Part B: restore is a long SYNCHRONOUS op (F4 — through cloudflared's hard 100s cap the customer
// got an error page while it silently succeeded). Fast-path refuse a concurrent op, then run it in
// a BACKGROUND goroutine (survives the request; the poll banner shows progress → result).
if s.backupMgr.IsRunning() {
- http.Redirect(w, r, "/backups?flash_error="+url.QueryEscape("Egy mentési/visszaállítási művelet már fut."), http.StatusFound)
+ http.Redirect(w, r, "/backups/restore?flash_error="+url.QueryEscape("Egy mentési/visszaállítási művelet már fut."), http.StatusFound)
return
}
s.logger.Printf("[WARN] [web] Restore requested (async): stack=%s, snapshot=%s from %s", stackName, snapshotID, r.RemoteAddr)
@@ -966,7 +1001,7 @@ func (s *Server) backupRestoreHandler(w http.ResponseWriter, r *http.Request) {
s.logger.Printf("[INFO] [web] Restore completed (async): stack=%s in %s", stackName, time.Since(start))
s.backupMgr.EndRestoreOp(true, stackName+" visszaállítva ("+snapshotID+").")
}()
- http.Redirect(w, r, "/backups?flash="+url.QueryEscape("Visszaállítás elindult — az állapot itt frissül."), http.StatusFound)
+ http.Redirect(w, r, "/backups/restore?flash="+url.QueryEscape("Visszaállítás elindult — az állapot itt frissül."), http.StatusFound)
}
// backupTier2RestoreHandler (C2, closes F2) restores an app's MISSING user files in place from its
@@ -977,22 +1012,22 @@ func (s *Server) backupTier2RestoreHandler(w http.ResponseWriter, r *http.Reques
stackName := r.FormValue("stack_name")
if stackName == "" {
- http.Redirect(w, r, "/backups?flash_error=Hi%C3%A1nyz%C3%B3+param%C3%A9terek", http.StatusFound)
+ http.Redirect(w, r, "/backups/apps?flash_error=Hi%C3%A1nyz%C3%B3+param%C3%A9terek", http.StatusFound)
return
}
// Same F2-defense as the unit restore: a stack name is a single segment, never a path.
if !validStackName(stackName) {
s.logger.Printf("[WARN] [web] Tier-2 file restore rejected: invalid stack_name %q from %s", stackName, r.RemoteAddr)
- http.Redirect(w, r, "/backups?flash_error=%C3%89rv%C3%A9nytelen+alkalmaz%C3%A1sn%C3%A9v", http.StatusFound)
+ http.Redirect(w, r, "/backups/apps?flash_error=%C3%89rv%C3%A9nytelen+alkalmaz%C3%A1sn%C3%A9v", http.StatusFound)
return
}
if s.backupMgr == nil {
- http.Redirect(w, r, "/backups?flash_error=Ment%C3%A9s+nincs+be%C3%A1ll%C3%ADtva", http.StatusFound)
+ http.Redirect(w, r, "/backups/apps?flash_error=Ment%C3%A9s+nincs+be%C3%A1ll%C3%ADtva", http.StatusFound)
return
}
// Part B (same async shape as backupRestoreHandler): fast-path refuse, then background goroutine.
if s.backupMgr.IsRunning() {
- http.Redirect(w, r, "/backups?flash_error="+url.QueryEscape("Egy mentési/visszaállítási művelet már fut."), http.StatusFound)
+ http.Redirect(w, r, "/backups/apps?flash_error="+url.QueryEscape("Egy mentési/visszaállítási művelet már fut."), http.StatusFound)
return
}
s.logger.Printf("[WARN] [web] Tier-2 file restore requested (async): stack=%s from %s", stackName, r.RemoteAddr)
@@ -1011,7 +1046,7 @@ func (s *Server) backupTier2RestoreHandler(w http.ResponseWriter, r *http.Reques
s.logger.Printf("[INFO] [web] Tier-2 file restore completed (async): stack=%s (%d files)", stackName, n)
s.backupMgr.EndRestoreOp(true, msg)
}()
- http.Redirect(w, r, "/backups?flash="+url.QueryEscape("Fájl-visszaállítás elindult — az állapot itt frissül."), http.StatusFound)
+ http.Redirect(w, r, "/backups/apps?flash="+url.QueryEscape("Fájl-visszaállítás elindult — az állapot itt frissül."), http.StatusFound)
}
// settingsBaseData is the shared identity block used by every settings-family subpage
diff --git a/controller/internal/web/offbox_handlers.go b/controller/internal/web/offbox_handlers.go
index c98d799..cae061d 100644
--- a/controller/internal/web/offbox_handlers.go
+++ b/controller/internal/web/offbox_handlers.go
@@ -17,13 +17,19 @@ import (
// The SSH private key + known-host line are provided out-of-band by the operator (textareas) and written
// to 0600/0644 files by the backup Manager; they are NEVER echoed back, logged, or stored in settings.
-// offboxRedirect sends the operator back to the backups page with a flash (success or error) message.
+// offboxRedirect sends the customer back to the Távoli mentés page with a flash (success or
+// error) message (v0.124.0 IA split: the offbox controls live on /backups/remote; the
+// restore-to-verify flow redirects to /backups/restore via offboxRedirectTo).
func offboxRedirect(w http.ResponseWriter, r *http.Request, msg string, isErr bool) {
+ offboxRedirectTo(w, r, "/backups/remote", msg, isErr)
+}
+
+func offboxRedirectTo(w http.ResponseWriter, r *http.Request, page, msg string, isErr bool) {
q := "flash"
if isErr {
q = "flash_error"
}
- http.Redirect(w, r, "/backups?"+q+"="+url.QueryEscape(msg), http.StatusFound)
+ http.Redirect(w, r, page+"?"+q+"="+url.QueryEscape(msg), http.StatusFound)
}
// offboxConfigHandler saves the off-box target + (out-of-band) SSH key + known_hosts.
@@ -211,20 +217,20 @@ func (s *Server) offboxRunHandler(w http.ResponseWriter, r *http.Request) {
// overwrite live data; the operator inspects the restored files).
func (s *Server) offboxRestoreHandler(w http.ResponseWriter, r *http.Request) {
if s.backupMgr == nil || !s.backupMgr.OffboxConfigured() {
- offboxRedirect(w, r, "A távoli mentési cél nincs beállítva.", true)
+ offboxRedirectTo(w, r, "/backups/restore", "A távoli mentési cél nincs beállítva.", true)
return
}
_ = r.ParseForm()
app := strings.TrimSpace(r.FormValue("app"))
if app == "" {
- offboxRedirect(w, r, "Hiányzó alkalmazás.", true)
+ offboxRedirectTo(w, r, "/backups/restore", "Hiányzó alkalmazás.", true)
return
}
// Part B: fast-path refuse a concurrent op, then run async on a BACKGROUND context. The old code
// bounded on r.Context()+30m — a proxy read-timeout then CANCELED the SFTP restore mid-flight
// (worse than F4: not just an error page, an aborted restore). Background ctx fixes that.
if s.backupMgr.IsRunning() {
- offboxRedirect(w, r, "Egy mentési/visszaállítási művelet már fut.", true)
+ offboxRedirectTo(w, r, "/backups/restore", "Egy mentési/visszaállítási művelet már fut.", true)
return
}
dest := filepath.Join(s.cfg.Paths.DataDir, "offbox-restore", app)
@@ -240,5 +246,5 @@ func (s *Server) offboxRestoreHandler(w http.ResponseWriter, r *http.Request) {
s.logger.Printf("[INFO] [web] off-box restore %s completed (async) → %s", app, dest)
s.backupMgr.EndRestoreOp(true, "A(z) "+app+" visszaállítva ide (ellenőrzésre): "+dest)
}()
- offboxRedirect(w, r, "A távoli visszaállítás elindult — az állapot itt frissül.", false)
+ offboxRedirectTo(w, r, "/backups/restore", "A távoli visszaállítás elindult — az állapot itt frissül.", false)
}
diff --git a/controller/internal/web/server.go b/controller/internal/web/server.go
index 09f3d65..7e9a08a 100644
--- a/controller/internal/web/server.go
+++ b/controller/internal/web/server.go
@@ -293,6 +293,14 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.stacksHandler(w, r)
case path == "/backups":
s.backupsHandler(w, r)
+ // v0.124.0 IA split: the backups page's four sub-pages (old /backups deep links keep working —
+ // /backups itself is the Áttekintés page).
+ case path == "/backups/remote":
+ s.backupsRemoteHandler(w, r)
+ case path == "/backups/apps":
+ s.backupsAppsHandler(w, r)
+ case path == "/backups/restore":
+ s.backupsRestoreHandler(w, r)
case path == "/monitoring":
s.monitoringHandler(w, r)
case path == "/settings":
diff --git a/controller/internal/web/templates/backups.html b/controller/internal/web/templates/backups.html
index 71a2225..9b9bfad 100644
--- a/controller/internal/web/templates/backups.html
+++ b/controller/internal/web/templates/backups.html
@@ -6,27 +6,13 @@
{{.Domain}}
-{{if .Backup}}{{if .Backup.FlashSuccess}}
-{{.Backup.FlashSuccess}}
-{{end}}{{end}}
-{{if .Backup}}{{if .Backup.FlashError}}
-{{.Backup.FlashError}}
-{{end}}{{end}}
+{{template "backups_flash" .}}
{{if .Backup}}{{if .Backup.SingleCopyWarning}}
{{.Backup.SingleCopyWarning}}
{{end}}{{end}}
-
-
-
{{if not .Backup}}
-
-
🛡
-
Biztonsági mentés nincs beállítva
-
A biztonsági mentés funkció nem aktív.
- Kérjük, vegye fel a kapcsolatot a Felhom csapattal a beállításhoz.
-
+{{template "backups_empty" .}}
{{else}}
@@ -123,107 +109,6 @@
{{end}}
-{{if .Backup}}
-
-Távoli mentés (3. mentés) — titkosított, offsite
-Az alkalmazás-mentések titkosított másolata egy távoli tárolóra — saját NAS vagy Felhom offsite tárhely — restic + SFTP kapcsolaton. A tároló csak titkosított adatot lát. Ez a 3-2-1 szabály „1 off-site" lába — független a helyi másodpéldánytól és a teljes rendszermentéstől.
-
- {{if .Offbox}}
-
-
-
{{if eq .Offbox.LastStatus "ok"}}✓ Rendben{{else if eq .Offbox.LastStatus "error"}}✗ Hiba{{else if eq .Offbox.LastStatus "running"}}Fut…{{else}}–{{end}}
-
Utolsó távoli mentés{{if .Offbox.LastRun}}{{timeAgoStr .Offbox.LastRun}} {{end}}
-
-
-
{{if .Offbox.RepoSizeHuman}}{{.Offbox.RepoSizeHuman}}{{else}}–{{end}}
-
Tároló méret · {{.Offbox.SnapshotCount}} pillanatkép
-
-
-
{{if .Offbox.Enabled}}{{.Offbox.User}}@{{.Offbox.Host}}{{else}}Kikapcsolva{{end}}
-
{{if .Offbox.Enabled}}{{.Offbox.RepoPath}}{{else}}A távoli mentés ki van kapcsolva{{end}}
-
-
- {{if and .Offbox.Enabled (gt .Offbox.QuotaGB 0)}}
-
-
-
Tárhelykeret: {{if .Offbox.RepoSizeHuman}}{{.Offbox.RepoSizeHuman}}{{else}}0{{end}} / {{.Offbox.QuotaGB}} GB ({{.OffboxQuotaPct}}%)
-
-
- {{end}}
- {{if .Offbox.LastError}}
Utolsó hiba: {{.Offbox.LastError}}
{{end}}
- {{if .Offbox.LastWarning}}
{{.Offbox.LastWarning}}
{{end}}
- {{if and .OffboxConfigured (ne .Offbox.EscrowState "escrowed")}}
-
-
A távoli mentés a kulcs letétbe helyezésére vár — a mentés addig nem fut (így nem keletkezik visszaállíthatatlan másolat). Futtasd a letéti szertartást, majd erősítsd meg.
-
-
- {{end}}
- {{if .OffboxConfigured}}
-
-
-
-
Mely alkalmazások mentődnek a távoli tárolóra?
- {{if and .OffboxApps (eq .Offbox.EscrowState "escrowed") (eq .OffboxToggledCount 0)}}
-
Nincs távoli mentésre jelölt alkalmazás — jelölj ki legalább egyet.
- {{end}}
- {{if .OffboxApps}}
-
- {{range .OffboxApps}}
-
-
-
- {{end}}
-
- {{else}}
Nincs telepített alkalmazás.
{{end}}
- {{end}}
- {{else}}
-
Még nincs beállítva távoli mentési cél.
- {{end}}
-
-
- Távoli mentési cél beállítása
-
-
-
-{{end}}
-
-Alkalmazás-mentések (adatbázis + konfiguráció)
-Az egyes alkalmazások részletes, granulált mentése — adatbázis-kiírások, beállítások és alkalmazás-fájlok. A fenti teljes mentéstől függetlenül, alkalmazásonként visszaállítható.
-
{{if .Backup.LastDBDump}}
@@ -270,409 +155,9 @@
-
-
-
Ütemezés
-
-
- Adatbázis mentés
- {{.Backup.DBDumpSchedule}}
- Következő: {{nextRunLabel .Backup.NextDBDump}}
-
-
-
- {{if .Backup.LastDBDump}}
-
- Utolsó adatbázis mentés:
- {{fmtTime .Backup.LastDBDump.LastRun}} ({{timeAgo .Backup.LastDBDump.LastRun}})
-
- {{else}}
-
- Utolsó adatbázis mentés:
- Még nem futott
-
- {{end}}
-
-
-
- {{if .Backup.Running}}Mentés folyamatban...{{else}}Mentés most{{end}}
-
-
-
-
-
-
-
Adatbázisok
- {{if or .Backup.DumpFiles .Backup.DiscoveredDBs}}
-
-
-
-
- Alkalmazás
- Típus
- Méret
- Utolsó
- Érvényesítés
- Állapot
-
-
-
- {{if .Backup.LastDBDump}}
- {{range .Backup.LastDBDump.Results}}
-
- {{.DB.StackName}}
- {{dbTypeLabel .DB.DBType}}
- {{if .Error}}–{{else}}{{fmtBytes .Size}}{{end}}
- {{if .Error}}–{{else}}{{fmtTimeShort $.Backup.LastDBDump.LastRun}}{{end}}
-
- {{if .Error}}
- –
- {{else if .Validation.Valid}}
- {{.Validation.TableCount}} tábla
- {{else if .Validation.Error}}
- Hiba
- {{else}}
- –
- {{end}}
-
-
- {{if .Error}}
- Hiba
- {{else}}
- OK
- {{end}}
-
-
- {{end}}
- {{else}}
- {{range .Backup.DumpFiles}}
-
- {{.StackName}}
- {{dbTypeLabel .DBType}}
- {{fmtBytes .Size}}
- {{fmtTimeShort .ModTime}}
-
- {{if .Validation.Valid}}
- {{.Validation.TableCount}} tábla
- {{else if .Validation.Error}}
- Hiba
- {{else}}
- –
- {{end}}
-
- OK
-
- {{end}}
- {{end}}
-
-
-
- {{else if eq .DBSectionState "pending"}}
-
Még nem készült adatbázis-mentés — az első ütemezett mentés éjjel fut le.
- {{else}}
-
A telepített alkalmazások beágyazott adatbázist használnak (pl. SQLite) — adatbázisuk a fájl- és kötetmentés része, külön adatbázis-kiírás nem szükséges.
- {{end}}
-
-
-
-{{if .Backup.AppDataInfo}}
-
-
Alkalmazások mentési állapota
-
- {{if .NoUserDataBackupWarning}}
-
-
Felhasználói adatokról nincs biztonsági mentés.
- A szerveren tárolt fotók, dokumentumok és egyéb fájlok jelenleg csak egy példányban léteznek.
- Külső meghajtó csatlakoztatásával biztonsági másolat készíthető a 3-2-1 szabály szerint.
-
Meghajtó beállítása →
-
- {{end}}
-
- {{range .AppBackupRows}}
-
-
-
-
-
-
- 1. mentés
- Auto
- helyi
- {{if .Tier1LastRun}}
- Utolsó: {{timeAgoStr .Tier1LastRun}}
- {{if eq .Tier1LastStatus "ok"}}
- {{else if eq .Tier1LastStatus "error"}} {{end}}
-
- {{end}}
- {{.BackupContents}}
- {{if and .HasDB (eq .Tier1DBStatus "error")}}
- DB dump hiba
- {{end}}
-
-
-
-
2. mentés
- {{if .Tier2UserDisabled}}
-
2. mentés kikapcsolva
-
- {{else if and .Tier2Configured .Tier2DestDisconnected}}
-
rsync
-
→ {{.Tier2Dest}}
-
Cél meghajtó leválasztva
- {{if .Tier2LastRun}}
-
Utolsó: {{timeAgoStr .Tier2LastRun}}
- {{end}}
-
{{.BackupContents}}
-
- {{else if and .Tier2Configured .Tier2DestInactive}}
-
rsync
-
→ {{.Tier2Dest}}
-
Cél meghajtó inaktív
- {{if .Tier2LastRun}}
-
Utolsó: {{timeAgoStr .Tier2LastRun}}
- {{end}}
-
{{.BackupContents}}
-
- {{else if .Tier2Configured}}
-
rsync
-
→ {{.Tier2Dest}}
-
{{.Tier2Schedule}}
- {{if .Tier2LastRun}}
-
Utolsó: {{timeAgoStr .Tier2LastRun}}
-
- {{.Tier2StatusBadge}}
-
-
- {{end}}
- {{if .Tier2SizeHuman}}
{{.Tier2SizeHuman}} {{end}}
-
{{.BackupContents}}
-
-
- {{if .Tier2LastRun}}
-
{{$.CSRFField}}
-
- Fájlok visszaállítása
-
- {{end}}
-
Beállítás
-
- {{else}}
-
1. mentés auto
-
Nincs 2. (off-drive) másolat
- {{if .Tier2LastError}}
-
{{.Tier2LastError}}
- {{end}}
-
- {{end}}
-
-
- {{if eq .Tier3State "active"}}
-
- 3. mentés
- {{if eq $.Offbox.LastStatus "ok"}}Sikeres{{else if eq $.Offbox.LastStatus "error"}}Hiba{{else if eq $.Offbox.LastStatus "running"}}Fut...{{else}}—{{end}}
- restic → {{$.Offbox.Host}}
- {{if $.Offbox.LastRun}}Utolsó: {{timeAgoStr $.Offbox.LastRun}} {{end}}
- Helyreállítási egység, titkosítva
-
- {{else if eq .Tier3State "escrow_pending"}}
-
- 3. mentés
- Kulcsletétre vár
- A távoli mentés a titkosítási kulcs letétbe helyezéséig szünetel
-
- {{else if eq .Tier3State "off"}}
-
-
3. mentés
-
Kikapcsolva
-
Ez az alkalmazás nincs kijelölve távoli mentésre
-
-
- {{else}}
-
-
3. mentés
-
Nincs beállítva
-
távoli (offsite)
-
Nincs távoli mentési cél beállítva
-
-
- {{end}}
-
- {{if .Warnings}}
-
- {{range .Warnings}}
-
{{.}}
- {{end}}
-
- {{end}}
-
-
- {{end}}
-
-
-{{end}}
-
-
-{{if .Backup.AppDataInfo}}
-
-
Visszaállítás
-
-
- Alkalmazás:
-
- — Válasszon —
- {{range .Backup.AppDataInfo}}
- {{.DisplayName}}
- {{end}}
-
-
-
- Pillanatkép:
-
- — Válasszon alkalmazást —
-
-
-
-
-
- Még nincs mentés felhasználói adattal.
-
-
- A visszaállítás felülírja az alkalmazás jelenlegi adatait a kiválasztott mentés állapotával.
- Az alkalmazás a folyamat során automatikusan leáll és újraindul.
-
-
-
-
- Megértettem, visszaállítás indítása.
-
-
-
- Visszaállítás indítása
-
-
-
-
-{{end}}
-
{{end}}
{{template "layout_end" .}}
diff --git a/controller/internal/web/templates/backups_apps.html b/controller/internal/web/templates/backups_apps.html
new file mode 100644
index 0000000..4022416
--- /dev/null
+++ b/controller/internal/web/templates/backups_apps.html
@@ -0,0 +1,348 @@
+{{define "backups_apps"}}
+{{template "layout_start" .}}
+
+
+
+{{template "backups_flash" .}}
+{{template "restore_banner" .}}
+
+{{if not .Backup}}
+{{template "backups_empty" .}}
+{{else}}
+
+Alkalmazás-mentések (adatbázis + konfiguráció)
+Az egyes alkalmazások részletes, granulált mentése — adatbázis-kiírások, beállítások és alkalmazás-fájlok. A fenti teljes mentéstől függetlenül, alkalmazásonként visszaállítható.
+
+
+
+
Ütemezés
+
+
+ Adatbázis mentés
+ {{.Backup.DBDumpSchedule}}
+ Következő: {{nextRunLabel .Backup.NextDBDump}}
+
+
+
+ {{if .Backup.LastDBDump}}
+
+ Utolsó adatbázis mentés:
+ {{fmtTime .Backup.LastDBDump.LastRun}} ({{timeAgo .Backup.LastDBDump.LastRun}})
+
+ {{else}}
+
+ Utolsó adatbázis mentés:
+ Még nem futott
+
+ {{end}}
+
+
+
+ {{if .Backup.Running}}Mentés folyamatban...{{else}}Mentés most{{end}}
+
+
+
+
+
+
+
Adatbázisok
+ {{if or .Backup.DumpFiles .Backup.DiscoveredDBs}}
+
+
+
+
+ Alkalmazás
+ Típus
+ Méret
+ Utolsó
+ Érvényesítés
+ Állapot
+
+
+
+ {{if .Backup.LastDBDump}}
+ {{range .Backup.LastDBDump.Results}}
+
+ {{.DB.StackName}}
+ {{dbTypeLabel .DB.DBType}}
+ {{if .Error}}–{{else}}{{fmtBytes .Size}}{{end}}
+ {{if .Error}}–{{else}}{{fmtTimeShort $.Backup.LastDBDump.LastRun}}{{end}}
+
+ {{if .Error}}
+ –
+ {{else if .Validation.Valid}}
+ {{.Validation.TableCount}} tábla
+ {{else if .Validation.Error}}
+ Hiba
+ {{else}}
+ –
+ {{end}}
+
+
+ {{if .Error}}
+ Hiba
+ {{else}}
+ OK
+ {{end}}
+
+
+ {{end}}
+ {{else}}
+ {{range .Backup.DumpFiles}}
+
+ {{.StackName}}
+ {{dbTypeLabel .DBType}}
+ {{fmtBytes .Size}}
+ {{fmtTimeShort .ModTime}}
+
+ {{if .Validation.Valid}}
+ {{.Validation.TableCount}} tábla
+ {{else if .Validation.Error}}
+ Hiba
+ {{else}}
+ –
+ {{end}}
+
+ OK
+
+ {{end}}
+ {{end}}
+
+
+
+ {{else if eq .DBSectionState "pending"}}
+
Még nem készült adatbázis-mentés — az első ütemezett mentés éjjel fut le.
+ {{else}}
+
A telepített alkalmazások beágyazott adatbázist használnak (pl. SQLite) — adatbázisuk a fájl- és kötetmentés része, külön adatbázis-kiírás nem szükséges.
+ {{end}}
+
+
+
+{{if .Backup.AppDataInfo}}
+
+
Alkalmazások mentési állapota
+
+ {{if .NoUserDataBackupWarning}}
+
+
Felhasználói adatokról nincs biztonsági mentés.
+ A szerveren tárolt fotók, dokumentumok és egyéb fájlok jelenleg csak egy példányban léteznek.
+ Külső meghajtó csatlakoztatásával biztonsági másolat készíthető a 3-2-1 szabály szerint.
+
Meghajtó beállítása →
+
+ {{end}}
+
+ {{range .AppBackupRows}}
+
+
+
+
+
+
+ 1. mentés
+ Auto
+ helyi
+ {{if .Tier1LastRun}}
+ Utolsó: {{timeAgoStr .Tier1LastRun}}
+ {{if eq .Tier1LastStatus "ok"}}
+ {{else if eq .Tier1LastStatus "error"}} {{end}}
+
+ {{end}}
+ {{.BackupContents}}
+ {{if and .HasDB (eq .Tier1DBStatus "error")}}
+ DB dump hiba
+ {{end}}
+
+
+
+
2. mentés
+ {{if .Tier2UserDisabled}}
+
2. mentés kikapcsolva
+
+ {{else if and .Tier2Configured .Tier2DestDisconnected}}
+
rsync
+
→ {{.Tier2Dest}}
+
Cél meghajtó leválasztva
+ {{if .Tier2LastRun}}
+
Utolsó: {{timeAgoStr .Tier2LastRun}}
+ {{end}}
+
{{.BackupContents}}
+
+ {{else if and .Tier2Configured .Tier2DestInactive}}
+
rsync
+
→ {{.Tier2Dest}}
+
Cél meghajtó inaktív
+ {{if .Tier2LastRun}}
+
Utolsó: {{timeAgoStr .Tier2LastRun}}
+ {{end}}
+
{{.BackupContents}}
+
+ {{else if .Tier2Configured}}
+
rsync
+
→ {{.Tier2Dest}}
+
{{.Tier2Schedule}}
+ {{if .Tier2LastRun}}
+
Utolsó: {{timeAgoStr .Tier2LastRun}}
+
+ {{.Tier2StatusBadge}}
+
+
+ {{end}}
+ {{if .Tier2SizeHuman}}
{{.Tier2SizeHuman}} {{end}}
+
{{.BackupContents}}
+
+
+ {{if .Tier2LastRun}}
+
{{$.CSRFField}}
+
+ Fájlok visszaállítása
+
+ {{end}}
+
Beállítás
+
+ {{else}}
+
1. mentés auto
+
Nincs 2. (off-drive) másolat
+ {{if .Tier2LastError}}
+
{{.Tier2LastError}}
+ {{end}}
+
+ {{end}}
+
+
+ {{if eq .Tier3State "active"}}
+
+ 3. mentés
+ {{if eq $.Offbox.LastStatus "ok"}}Sikeres{{else if eq $.Offbox.LastStatus "error"}}Hiba{{else if eq $.Offbox.LastStatus "running"}}Fut...{{else}}—{{end}}
+ restic → {{$.Offbox.Host}}
+ {{if $.Offbox.LastRun}}Utolsó: {{timeAgoStr $.Offbox.LastRun}} {{end}}
+ Helyreállítási egység, titkosítva
+
+ {{else if eq .Tier3State "escrow_pending"}}
+
+ 3. mentés
+ Kulcsletétre vár
+ A távoli mentés a titkosítási kulcs letétbe helyezéséig szünetel
+
+ {{else if eq .Tier3State "off"}}
+
+
3. mentés
+
Kikapcsolva
+
Ez az alkalmazás nincs kijelölve távoli mentésre
+
+
+ {{else}}
+
+
3. mentés
+
Nincs beállítva
+
távoli (offsite)
+
Nincs távoli mentési cél beállítva
+
+
+ {{end}}
+
+ {{if .Warnings}}
+
+ {{range .Warnings}}
+
{{.}}
+ {{end}}
+
+ {{end}}
+
+
+ {{end}}
+
+
+{{end}}
+
+{{end}}
+
+
+
+{{template "layout_end" .}}
+{{end}}
diff --git a/controller/internal/web/templates/backups_remote.html b/controller/internal/web/templates/backups_remote.html
new file mode 100644
index 0000000..07865a4
--- /dev/null
+++ b/controller/internal/web/templates/backups_remote.html
@@ -0,0 +1,107 @@
+{{define "backups_remote"}}
+{{template "layout_start" .}}
+
+
+
+{{template "backups_flash" .}}
+
+{{if not .Backup}}
+{{template "backups_empty" .}}
+{{else}}
+
+
+Távoli mentés (3. mentés) — titkosított, offsite
+Az alkalmazás-mentések titkosított másolata egy távoli tárolóra — saját NAS vagy Felhom offsite tárhely — restic + SFTP kapcsolaton. A tároló csak titkosított adatot lát. Ez a 3-2-1 szabály „1 off-site" lába — független a helyi másodpéldánytól és a teljes rendszermentéstől.
+
+ {{if .Offbox}}
+
+
+
{{if eq .Offbox.LastStatus "ok"}}✓ Rendben{{else if eq .Offbox.LastStatus "error"}}✗ Hiba{{else if eq .Offbox.LastStatus "running"}}Fut…{{else}}–{{end}}
+
Utolsó távoli mentés{{if .Offbox.LastRun}}{{timeAgoStr .Offbox.LastRun}} {{end}}
+
+
+
{{if .Offbox.RepoSizeHuman}}{{.Offbox.RepoSizeHuman}}{{else}}–{{end}}
+
Tároló méret · {{.Offbox.SnapshotCount}} pillanatkép
+
+
+
{{if .Offbox.Enabled}}{{.Offbox.User}}@{{.Offbox.Host}}{{else}}Kikapcsolva{{end}}
+
{{if .Offbox.Enabled}}{{.Offbox.RepoPath}}{{else}}A távoli mentés ki van kapcsolva{{end}}
+
+
+ {{if and .Offbox.Enabled (gt .Offbox.QuotaGB 0)}}
+
+
+
Tárhelykeret: {{if .Offbox.RepoSizeHuman}}{{.Offbox.RepoSizeHuman}}{{else}}0{{end}} / {{.Offbox.QuotaGB}} GB ({{.OffboxQuotaPct}}%)
+
+
+ {{end}}
+ {{if .Offbox.LastError}}
Utolsó hiba: {{.Offbox.LastError}}
{{end}}
+ {{if .Offbox.LastWarning}}
{{.Offbox.LastWarning}}
{{end}}
+ {{if and .OffboxConfigured (ne .Offbox.EscrowState "escrowed")}}
+
+
A távoli mentés a kulcs letétbe helyezésére vár — a mentés addig nem fut (így nem keletkezik visszaállíthatatlan másolat). Futtasd a letéti szertartást, majd erősítsd meg.
+
{{.CSRFField}}
+ Letét megerősítése
+
+
+ {{end}}
+ {{if .OffboxConfigured}}
+
+
{{.CSRFField}}
+ Távoli mentés most
+
+
+
Mely alkalmazások mentődnek a távoli tárolóra?
+ {{if and .OffboxApps (eq .Offbox.EscrowState "escrowed") (eq .OffboxToggledCount 0)}}
+
Nincs távoli mentésre jelölt alkalmazás — jelölj ki legalább egyet.
+ {{end}}
+ {{if .OffboxApps}}
+
+ {{range .OffboxApps}}
+
+
+
+ {{end}}
+
+ {{else}}
Nincs telepített alkalmazás.
{{end}}
+ {{end}}
+ {{else}}
+
Még nincs beállítva távoli mentési cél.
+ {{end}}
+
+
+ Távoli mentési cél beállítása
+ {{.CSRFField}}
+ Cél címe (IP vagy hosztnév — NAS vagy SFTP-kiszolgáló)
+ SSH port
+ Felhasználó
+ Tároló útvonala a célgépen
+ Napi automatikus távoli mentés engedélyezve
+ SSH privát kulcs {{if .OffboxConfigured}}(üresen hagyva változatlan){{end}}
+
+ A kulcsot 0600-as fájlba írjuk; sosem naplózzuk és nem tároljuk a beállításokban.
+ A célgép ismert-host sora (known_hosts) {{if .OffboxConfigured}}(üresen hagyva változatlan){{end}}
+
+ A host-kulcs rögzítése (no blind TOFU). Lekérdezhető: ssh-keyscan -p <port> <host>
+ Mentés
+
+
+
+{{end}}
+
+{{template "layout_end" .}}
+{{end}}
diff --git a/controller/internal/web/templates/backups_restore.html b/controller/internal/web/templates/backups_restore.html
new file mode 100644
index 0000000..3367ddc
--- /dev/null
+++ b/controller/internal/web/templates/backups_restore.html
@@ -0,0 +1,227 @@
+{{define "backups_restore"}}
+{{template "layout_start" .}}
+
+
+
+{{template "backups_flash" .}}
+{{template "restore_banner" .}}
+
+{{if not .Backup}}
+{{template "backups_empty" .}}
+{{else}}
+
+
+{{if .Backup.AppDataInfo}}
+
+
Visszaállítás
+
+
+ Alkalmazás:
+
+ — Válasszon —
+ {{range .Backup.AppDataInfo}}
+ {{.DisplayName}}
+ {{end}}
+
+
+
+ Pillanatkép:
+
+ — Válasszon alkalmazást —
+
+
+
+
+
+ Még nincs mentés felhasználói adattal.
+
+
+ A visszaállítás felülírja az alkalmazás jelenlegi adatait a kiválasztott mentés állapotával.
+ Az alkalmazás a folyamat során automatikusan leáll és újraindul.
+
+
+
+
+ Megértettem, visszaállítás indítása.
+
+
+
+ Visszaállítás indítása
+
+
+
+
+{{end}}
+
+
+{{if .OffboxConfigured}}
+
+
Ellenőrző visszaállítás a távoli tárolóból
+
A távoli mentésre kijelölt alkalmazások legutóbbi pillanatképe egy külön ellenőrző mappába állítható vissza — a meglévő adatok nem változnak.
+ {{if .OffboxToggledCount}}
+
+ {{range .OffboxApps}}
+ {{if .Enabled}}
+
+
+
+ {{end}}
+ {{end}}
+
+ {{else}}
+
Nincs távoli mentésre jelölt alkalmazás — a kijelölés a Távoli mentés oldalon történik.
+ {{end}}
+
+{{end}}
+
+{{end}}
+
+
+
+{{template "layout_end" .}}
+{{end}}
diff --git a/controller/internal/web/templates/backups_shared.html b/controller/internal/web/templates/backups_shared.html
new file mode 100644
index 0000000..f6f5dba
--- /dev/null
+++ b/controller/internal/web/templates/backups_shared.html
@@ -0,0 +1,63 @@
+{{define "backups_flash"}}
+{{if .Backup}}{{if .Backup.FlashSuccess}}
+{{.Backup.FlashSuccess}}
+{{end}}{{end}}
+{{if .Backup}}{{if .Backup.FlashError}}
+{{.Backup.FlashError}}
+{{end}}{{end}}
+{{end}}
+
+{{define "backups_empty"}}
+
+
🛡
+
Biztonsági mentés nincs beállítva
+
A biztonsági mentés funkció nem aktív.
+ Kérjük, vegye fel a kapcsolatot a Felhom csapattal a beállításhoz.
+
+{{end}}
+
+{{define "restore_banner"}}
+
+
+{{end}}
+
+{{define "restore_banner_js"}}
+// Part B: restore-progress banner. Polls the async restore op-status every 3s. Shows a neutral
+// "in progress" while running (including on a fresh page load mid-op), success on completion, and the
+// error state ONLY on failure. Stops polling when idle after a terminal result was shown.
+(function(){
+ var banner = document.getElementById('restore-banner');
+ if (!banner) return;
+ var sawRunning = false;
+ function opLabel(op){ return op === 'tier2-restore' ? 'Fájl-visszaállítás'
+ : op === 'offbox-restore' ? 'Távoli visszaállítás' : 'Visszaállítás'; }
+ function render(st){
+ if (st.running) {
+ sawRunning = true;
+ banner.className = 'flash';
+ banner.style.display = 'block';
+ banner.textContent = opLabel(st.op) + ' folyamatban' + (st.stack ? ': ' + st.stack : '') + '…';
+ return;
+ }
+ if (st.last && sawRunning) {
+ banner.style.display = 'block';
+ if (st.last.ok) {
+ banner.className = 'flash flash-success';
+ banner.textContent = st.last.message || (opLabel(st.last.op) + ' kész.');
+ } else {
+ banner.className = 'flash flash-error';
+ banner.textContent = st.last.message || (opLabel(st.last.op) + ' sikertelen.');
+ }
+ }
+ }
+ function poll(){
+ fetch('/api/backup/restore-status', {headers: {'Accept':'application/json'}})
+ .then(function(r){ return r.json(); })
+ .then(function(j){ if (j && j.data) render(j.data); })
+ .catch(function(){});
+ }
+ poll();
+ setInterval(poll, 3000);
+})();
+{{end}}
diff --git a/controller/internal/web/templates/layout.html b/controller/internal/web/templates/layout.html
index fa2fc0f..08af8bf 100644
--- a/controller/internal/web/templates/layout.html
+++ b/controller/internal/web/templates/layout.html
@@ -56,7 +56,14 @@
Hálózati tárhely
- Biztonsági mentés
+ Biztonsági mentés
+
+
Rendszermonitor
Debug
diff --git a/controller/internal/web/templates/tier2_config.html b/controller/internal/web/templates/tier2_config.html
index 16fb215..b8cdda2 100644
--- a/controller/internal/web/templates/tier2_config.html
+++ b/controller/internal/web/templates/tier2_config.html
@@ -3,7 +3,7 @@
{{if .Flash}}{{.Flash}}
{{end}}
diff --git a/controller/scripts/backups_split_move_check.py b/controller/scripts/backups_split_move_check.py
new file mode 100644
index 0000000..299ef8e
--- /dev/null
+++ b/controller/scripts/backups_split_move_check.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+"""Backups IA-split move check (v0.124.0, one-shot) — the split MOVED the v0.123.0 sections,
+it did not rewrite them: every moved block from the pre-split backups.html (commit df7ad37)
+must appear, whitespace-normalized, on EXACTLY its mapped page. Allowed divergences are encoded
+explicitly (the tier-3 anchor retarget; the restore-to-verify form's relocation).
+
+Run from controller/: python scripts/backups_split_move_check.py
+"""
+import io, os, re, subprocess, sys
+
+BASELINE = "df7ad37"
+OLD_PATH = "controller/internal/web/templates/backups.html"
+TPL = os.path.join("internal", "web", "templates")
+
+# (name, 1-indexed start, end inclusive, target template file, normalizer applied to the NEW page)
+BLOCKS = [
+ ("storage-overview", 32, 77, "backups.html", None),
+ ("whole-guest", 79, 124, "backups.html", None),
+ ("stat-cards", 227, 271, "backups.html", None),
+ ("offbox-pre", 127, 186, "backups_remote.html", None),
+ ("offbox-post", 193, 221, "backups_remote.html", None),
+ ("apps-divider", 224, 225, "backups_apps.html", None),
+ ("schedule", 273, 302, "backups_apps.html", None),
+ ("databases", 304, 376, "backups_apps.html", None),
+ # the tier-3 action anchors were retargeted cross-page — normalize them back before comparing
+ ("per-app-rows", 378, 541, "backups_apps.html",
+ lambda s: s.replace('href="/backups/remote#offbox-section"', 'href="#offbox-section"')),
+ ("restore-panel", 543, 586, "backups_restore.html", None),
+ ("offbox-verify-form", 188, 191, "backups_restore.html", None),
+ ("banner-js", 591, 627, "backups_shared.html", None),
+ ("apps-js", 628, 674, "backups_apps.html", None),
+ ("guest-js", 676, 712, "backups.html", None),
+ ("restore-js", 714, 841, "backups_restore.html", None),
+]
+
+PAGES = ["backups.html", "backups_remote.html", "backups_apps.html", "backups_restore.html",
+ "backups_shared.html"]
+
+
+def norm(s):
+ return re.sub(r"\s+", " ", s).strip()
+
+
+def main():
+ old = subprocess.run(["git", "show", BASELINE + ":" + OLD_PATH],
+ capture_output=True, text=True, encoding="utf-8")
+ if old.returncode != 0:
+ print("cannot read baseline %s:%s — %s" % (BASELINE, OLD_PATH, old.stderr.strip()))
+ sys.exit(2)
+ old_lines = old.stdout.split("\n")
+
+ pages = {}
+ for fn in PAGES:
+ pages[fn] = io.open(os.path.join(TPL, fn), encoding="utf-8").read()
+
+ failed = 0
+ for name, a, b, target, fix in BLOCKS:
+ block = norm("\n".join(old_lines[a - 1:b]))
+ hits = []
+ for fn in PAGES:
+ content = pages[fn]
+ if fix:
+ content = fix(content)
+ if block in norm(content):
+ hits.append(fn)
+ if hits != [target]:
+ failed += 1
+ print("MOVED-BLOCK MISMATCH %-18s (old L%d-%d): expected [%s], found %s"
+ % (name, a, b, target, hits))
+ if failed:
+ print("MOVE CHECK FAILED: %d block(s) rewritten, duplicated or lost" % failed)
+ sys.exit(1)
+ print("move check OK — all %d v0.123.0 blocks moved verbatim to their mapped page" % len(BLOCKS))
+
+
+if __name__ == "__main__":
+ main()