Tier-2 engine rework: class-driven legs, v2 layout, NAS-target exclusion (Task 3b, v0.135.0)

tier2_capture.go: classified apps get TierSecondary per-bind legs (paperless copy shrinks — export
drops); legacy apps keep the byte-identical resolver set. v2 relpath-mirroring layout
(backups/secondary/<stack>/{marker LAST, recovery-unit/, hdd/<rel>/, userdata/<rel>/}); N>1 native
(errTier2MultiDir/tier2AppDataName deleted). Migration=delete-and-rebuild + reconcile; all RemoveAll
via tier2SafeRemove (refuses outside backups/secondary/). SSD=state-only tier. selectTier2Target
never picks network storage (pinned+auto, F-6C-1). Restore reads v2 behind a marker gate.
Part 0: offbox_enlarge_blocked is a persisted one-time Load seed (opt-out sticks), not a getter
append. Part 0.5: offsite restore scratch prefers a local (non-network) path.
Full v2 test suite + all 10 §10 red-proofs verified. Destructive writes bounded to backups/secondary/.
This commit is contained in:
2026-07-15 10:10:20 +02:00
parent 1245a6c46e
commit 3603d1fc7f
20 changed files with 1014 additions and 484 deletions
+30 -5
View File
@@ -42,6 +42,11 @@ type Settings struct {
// Notification preferences (Phase 2 — define struct now, leave empty)
Notifications *NotificationPrefs `json:"notifications,omitempty"`
// OffboxEnlargeNoticeSeeded (v0.135.0) guards the ONE-TIME seed of offbox_enlarge_blocked into an
// existing customer's stored prefs. Persisted so a later opt-out sticks (the 3a-fix getter append
// re-enabled it on every read — this replaces it).
OffboxEnlargeNoticeSeeded bool `json:"offbox_enlarge_notice_seeded,omitempty"`
// Cached state
DBValidations map[string]DBValidationCache `json:"db_validations,omitempty"`
@@ -170,6 +175,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)
LastDuration string `json:"last_duration,omitempty"` // "2m34s"
LastSizeHuman string `json:"last_size_human,omitempty"` // "1.2 GB"
@@ -318,6 +324,7 @@ func Load(path string, logger *log.Logger) (*Settings, error) {
_ = os.WriteFile(path, bak, 0644) // best-effort promote
s2.LoadWarning = "settings.json volt sérült — visszaállítva biztonsági másolatból"
s2.migrateResticToRsync()
s2.seedOffboxEnlargeNotice()
return s2, nil
}
}
@@ -333,9 +340,28 @@ func Load(path string, logger *log.Logger) (*Settings, error) {
len(s.StoragePaths), len(s.Integrations), len(s.PendingEvents))
}
s.migrateResticToRsync()
s.seedOffboxEnlargeNotice()
return s, nil
}
// seedOffboxEnlargeNotice runs ONCE (guarded by OffboxEnlargeNoticeSeeded): an existing customer whose
// stored prefs predate offbox_enlarge_blocked (v0.135.0) gets it appended enabled — they could not have
// deliberately disabled a type that did not exist. Persisted, so a LATER opt-out sticks (unlike the
// 3a-fix getter append, which re-enabled it on every read). Fresh customers (nil prefs) get the type via
// DefaultEnabledEvents; the seed only touches customers with an explicit stored EnabledEvents list.
func (s *Settings) seedOffboxEnlargeNotice() {
if s.OffboxEnlargeNoticeSeeded {
return
}
s.OffboxEnlargeNoticeSeeded = true
if s.Notifications != nil && s.Notifications.EnabledEvents != nil {
s.Notifications.EnabledEvents = appendIfAbsent(s.Notifications.EnabledEvents, "offbox_enlarge_blocked")
}
if err := s.save(); err != nil && s.log != nil {
s.log.Printf("[ERROR] [settings] Failed to save offbox-enlarge-notice seed: %v", err)
}
}
// migrateResticToRsync converts any cross-drive backup configs using restic to rsync.
// Called once during Load() before the mutex is exposed.
func (s *Settings) migrateResticToRsync() {
@@ -520,13 +546,12 @@ func (s *Settings) GetNotificationPrefs() *NotificationPrefs {
if prefs.EnabledEvents == nil {
prefs.EnabledEvents = DefaultEnabledEvents
}
// Return a copy of the slice
// Return a copy of the slice verbatim. The offbox_enlarge_blocked seed is a ONE-TIME persisted
// migration (seedOffboxEnlargeNotice at Load), NOT a getter append — so a customer's later opt-out
// sticks instead of being re-enabled on every read.
events := make([]string, len(prefs.EnabledEvents))
copy(events, prefs.EnabledEvents)
// 3a-fix append-if-absent migration: an existing customer's stored prefs predate
// offbox_enlarge_blocked, so they cannot have deliberately disabled it — surface it enabled so the
// checkbox renders checked and the startup sync (main.go:782) carries it to the hub. Idempotent.
prefs.EnabledEvents = appendIfAbsent(events, "offbox_enlarge_blocked")
prefs.EnabledEvents = events
return &prefs
}