v0.12.4 — 15 bug fixes (CRITICAL/HIGH/MEDIUM)

CRITICAL:
- C1: SetAppBackupBulk data loss + nil map panic (settings.go)
- C2: UpdateStackConfig nil Env map panic (deploy.go)
- C3: ValidateDump missing scanner.Err() check (dbdump.go)

HIGH:
- H1: nextDailyRun DST bug — use time.Date(day+1) not Add(24h)
- H2: Cache Europe/Budapest timezone with sync.Once in scheduler
- H3: settings.save() leaks .tmp file on WriteFile failure
- H4: SetNotificationPrefs nil pointer panic
- H5: appDirSize + getDirSizeBytes ignore Sscanf return value
- H6: getDirSizeBytes has no timeout — add 30s context
- H7: dbdump.go tmpFile not using defer Close
- H8: UpdateCrossDriveStatus misleading comment

MEDIUM:
- M1: Replace custom containsBytes with strings.Contains
- M2: scheduler.Every() validates interval > 0
- M3: executeJob panic recovery now sets LastRun
- M4: logPostStartStatus copies env slice before goroutine
- M5: Cache timezone in web package via getTimezone() sync.Once

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 07:50:02 +01:00
parent 731cca15a8
commit d160c6c06d
11 changed files with 115 additions and 42 deletions
+11 -5
View File
@@ -136,6 +136,7 @@ func (s *Settings) save() error {
}
if err := os.WriteFile(tmpPath, data, 0644); err != nil {
os.Remove(tmpPath) // clean up partial file
return fmt.Errorf("writing tmp settings: %w", err)
}
@@ -215,6 +216,9 @@ func (s *Settings) GetNotificationPrefs() *NotificationPrefs {
// SetNotificationPrefs updates notification preferences and saves to disk.
// H17: Deep-copies prefs so caller mutations after the call don't affect stored state.
func (s *Settings) SetNotificationPrefs(prefs *NotificationPrefs) error {
if prefs == nil {
return fmt.Errorf("notification preferences cannot be nil")
}
s.mu.Lock()
defer s.mu.Unlock()
copy := *prefs
@@ -267,17 +271,18 @@ func (s *Settings) GetAppBackupMap() map[string]bool {
}
// SetAppBackupBulk updates backup prefs for all stacks at once and saves to disk.
// Preserves existing CrossDrive configs.
// Preserves existing CrossDrive configs and stacks not present in the input.
func (s *Settings) SetAppBackupBulk(prefs map[string]bool) error {
s.mu.Lock()
defer s.mu.Unlock()
newMap := make(map[string]AppBackupPrefs, len(prefs))
if s.AppBackup == nil {
s.AppBackup = make(map[string]AppBackupPrefs)
}
for name, enabled := range prefs {
existing := s.AppBackup[name] // preserves CrossDrive
existing.Enabled = enabled
newMap[name] = existing
s.AppBackup[name] = existing
}
s.AppBackup = newMap
return s.save()
}
@@ -321,7 +326,8 @@ func (s *Settings) SetCrossDriveConfig(stackName string, cfg *CrossDriveBackup)
}
// UpdateCrossDriveStatus updates runtime status fields for a cross-drive backup in-place.
// fn receives a pointer to the CrossDriveBackup (creates one if nil) and may mutate it.
// fn receives a pointer to the CrossDriveBackup and may mutate it.
// If no cross-drive config exists for the stack, does nothing and returns nil.
func (s *Settings) UpdateCrossDriveStatus(stackName string, fn func(*CrossDriveBackup)) error {
s.mu.Lock()
defer s.mu.Unlock()