v0.11.8 — Per-App Cross-Drive Backup (3-2-1 rule)

New feature: backup app data to a secondary storage drive to satisfy
the "different media" requirement of the 3-2-1 backup rule.

- settings.go: CrossDriveBackup struct, AppBackupPrefs.CrossDrive field,
  getter/setter methods, GetOrCreateCrossDrivePassword, preserves
  cross-drive config when toggling nightly backup

- crossdrive.go (new): CrossDriveRunner with rsync and restic backends.
  Validates destination (mount point, writable), prevents source/dest
  overlap, per-app concurrency lock, persists last_run/status/size.

- main.go: wire CrossDriveRunner, register cross-drive-daily (03:30)
  and cross-drive-weekly (04:30 Sundays) scheduler jobs

- router.go: 4 new API endpoints — save config, trigger run, get status,
  run-all. Router now accepts Settings and CrossDriveRunner.

- server.go: Server struct accepts CrossDriveRunner, new web route
  POST /settings/cross-backup/{name}

- handlers.go: deployHandler populates CrossDriveConfig, BackupDestPaths,
  BackupDestWarning, AppBackupEnabled. settingsCrossBackupHandler saves
  config. backupsHandler builds CrossDriveSummary, UnconfiguredApps,
  CrossDriveWarnings for backup page.

- deploy.html: "Biztonsági mentés" card with destination/method/schedule
  dropdowns, last-run status, manual trigger button, flash messages.

- backups.html: "Másolatok másik meghajtóra" section with per-app
  status rows, unconfigured app warnings, "Összes futtatása most" button.

- style.css: margin-bottom fix for .deploy-stale-data, new cross-drive
  card and list styles.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 15:45:31 +01:00
parent d4da3e6ea2
commit 1a8036d055
12 changed files with 1126 additions and 44 deletions
+29 -24
View File
@@ -20,17 +20,18 @@ import (
)
type Server struct {
cfg *config.Config
stackMgr *stacks.Manager
cpuCollector *system.CPUCollector
backupMgr *backup.Manager
scheduler *scheduler.Scheduler
settings *settings.Settings
alertManager *AlertManager
notifier *notify.Notifier
logger *log.Logger
version string
tmpl *template.Template
cfg *config.Config
stackMgr *stacks.Manager
cpuCollector *system.CPUCollector
backupMgr *backup.Manager
crossDriveRunner *backup.CrossDriveRunner
scheduler *scheduler.Scheduler
settings *settings.Settings
alertManager *AlertManager
notifier *notify.Notifier
logger *log.Logger
version string
tmpl *template.Template
sessions map[string]*session
sessionsMu sync.RWMutex
@@ -41,20 +42,21 @@ type Server struct {
diskJob *activeDiskJob
}
func NewServer(cfg *config.Config, stackMgr *stacks.Manager, cpuCollector *system.CPUCollector, backupMgr *backup.Manager, sched *scheduler.Scheduler, sett *settings.Settings, alertMgr *AlertManager, notif *notify.Notifier, logger *log.Logger, version string) *Server {
func NewServer(cfg *config.Config, stackMgr *stacks.Manager, cpuCollector *system.CPUCollector, backupMgr *backup.Manager, crossDrive *backup.CrossDriveRunner, sched *scheduler.Scheduler, sett *settings.Settings, alertMgr *AlertManager, notif *notify.Notifier, logger *log.Logger, version string) *Server {
s := &Server{
cfg: cfg,
stackMgr: stackMgr,
cpuCollector: cpuCollector,
backupMgr: backupMgr,
scheduler: sched,
settings: sett,
alertManager: alertMgr,
notifier: notif,
logger: logger,
version: version,
sessions: make(map[string]*session),
done: make(chan struct{}),
cfg: cfg,
stackMgr: stackMgr,
cpuCollector: cpuCollector,
backupMgr: backupMgr,
crossDriveRunner: crossDrive,
scheduler: sched,
settings: sett,
alertManager: alertMgr,
notifier: notif,
logger: logger,
version: version,
sessions: make(map[string]*session),
done: make(chan struct{}),
}
s.loadTemplates()
go s.cleanupSessions()
@@ -110,6 +112,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.settingsStorageLabelHandler(w, r)
case path == "/settings/app-backup" && r.Method == http.MethodPost:
s.settingsAppBackupHandler(w, r)
case strings.HasPrefix(path, "/settings/cross-backup/") && r.Method == http.MethodPost:
name := strings.TrimPrefix(path, "/settings/cross-backup/")
s.settingsCrossBackupHandler(w, r, name)
case path == "/backup/restore" && r.Method == http.MethodPost:
s.backupRestoreHandler(w, r)
case path == "/settings/storage/init":