v0.168.0: customer-configurable backup window (Mentési időablak)

ONE setting (window start W) drives every nightly leg at fixed, never-stored
offsets: DB dump at W, tier-2 at W+60m, off-box at W+105m (wrap-safe). Precedence
settings > controller.yaml db_dump_schedule > 02:30.

- scheduler.UpdateDaily: retime a daily job at runtime (no restart) via a per-job
  buffered resched chan + a select case in runDailyJob.
- new pure package internal/backupwindow (LegTimes/GateWindow/EffectiveWindow).
- quiesce disk-tier window gate: scheduled cycles run only inside [W+2h,W+6h) with a
  safety valve (age>cadence+24h runs regardless); manual TriggerNow never gated.
  Backend.Due now also returns the backup age (from the agent's own /backup/due).
- backup page: Mentési időablak card (time input + derived leg/gate rows); POST
  /backups/window validates -> saves -> UpdateDaily x3 -> flash.

Tests: 5 groups, all red-proofed. Agent/cadence//backup/due untouched.
This commit is contained in:
2026-07-24 20:55:44 +02:00
parent e33c1aeabc
commit 82c67e32e1
19 changed files with 831 additions and 31 deletions
+27 -2
View File
@@ -61,6 +61,11 @@ type Settings struct {
// Per-app backup preferences
AppBackup map[string]AppBackupPrefs `json:"app_backup,omitempty"`
// Customer-configurable backup-window start "HH:MM" (v0.168.0). "" = use controller.yaml
// db_dump_schedule (then the "02:30" default). Every nightly leg derives from this at fixed
// offsets; overrides yaml when a valid value is present (mirrors PasswordHash precedence).
BackupWindowStart string `json:"backup_window_start,omitempty"`
// Storage paths registry
StoragePaths []StoragePath `json:"storage_paths,omitempty"`
@@ -146,7 +151,7 @@ type AppBackupPrefs struct {
type OffboxTarget struct {
Enabled bool `json:"enabled"`
Host string `json:"host"`
Port int `json:"port"` // default 22
Port int `json:"port"` // default 22
User string `json:"user"`
RepoPath string `json:"repo_path"` // absolute path on the NAS, e.g. /volume1/felhom-backup/repo
Schedule string `json:"schedule"` // "daily" | "manual"
@@ -217,7 +222,7 @@ type CrossDriveBackup struct {
LastRun string `json:"last_run,omitempty"` // RFC3339
LastStatus string `json:"last_status,omitempty"` // "ok", "error", "running"
LastError string `json:"last_error,omitempty"`
LastWarning string `json:"last_warning,omitempty"` // Tier-2 3b: capture-gap / state-only notice (Hungarian)
LastWarning string `json:"last_warning,omitempty"` // Tier-2 3b: capture-gap / state-only notice (Hungarian)
LastDuration string `json:"last_duration,omitempty"` // "2m34s"
LastSizeHuman string `json:"last_size_human,omitempty"` // "1.2 GB"
@@ -492,6 +497,26 @@ func (s *Settings) SetPasswordHash(hash string) error {
// ── Guest launcher share (v0.165.0) ──────────────────────────────────────────────
// ── Backup window (v0.168.0) ─────────────────────────────────────────────────────
// GetBackupWindowStart returns the customer-set backup-window start "HH:MM" ("" = fall back to
// controller.yaml, then the default — resolve via backupwindow.EffectiveWindow, never in isolation).
func (s *Settings) GetBackupWindowStart() string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.BackupWindowStart
}
// SetBackupWindowStart stores (or clears, on "") the backup-window start and saves. The caller
// validates the HH:MM format first (the scheduler/backupwindow gate) and fans the change out to the
// three daily legs via UpdateDaily — this only persists the single source-of-truth value.
func (s *Settings) SetBackupWindowStart(start string) error {
s.mu.Lock()
defer s.mu.Unlock()
s.BackupWindowStart = start
return s.save()
}
// GetLauncherShareToken returns the guest-launcher capability token ("" = sharing disabled).
func (s *Settings) GetLauncherShareToken() string {
s.mu.RLock()