controller: backups IA split — four sub-pages (Áttekintés /backups, Távoli mentés /backups/remote, Alkalmazások /backups/apps, Visszaállítás /backups/restore); sections MOVED verbatim (backups_split_move_check.py enforces vs df7ad37); shared data builder extracted; offbox restore-to-verify relocated to the restore page; flash redirects + tier-3 anchors + tier2 back-link retargeted
Claude-Session: https://claude.ai/code/session_01GzammAMzsJTgpQHqxwM2bC
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user