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
+28 -7
View File
@@ -8,6 +8,23 @@ import (
"time"
)
var (
budapestLoc *time.Location
budapestLocOnce sync.Once
)
func getBudapestLocation() *time.Location {
budapestLocOnce.Do(func() {
loc, err := time.LoadLocation("Europe/Budapest")
if err != nil {
log.Printf("[ERROR] Cannot load Europe/Budapest timezone: %v — using UTC", err)
loc = time.UTC
}
budapestLoc = loc
})
return budapestLoc
}
// JobFunc is the function signature for scheduler jobs.
type JobFunc func(ctx context.Context) error
@@ -41,6 +58,11 @@ func New(logger *log.Logger) *Scheduler {
// Every registers a periodic job that runs every interval.
func (s *Scheduler) Every(name string, interval time.Duration, fn JobFunc) {
if interval <= 0 {
s.logger.Printf("[ERROR] Periodic job %s has invalid interval %s — job not registered", name, interval)
return
}
s.mu.Lock()
defer s.mu.Unlock()
@@ -187,6 +209,7 @@ func (s *Scheduler) executeJob(job *Job, quiet bool) {
if r := recover(); r != nil {
s.mu.Lock()
job.LastErr = fmt.Errorf("panic: %v", r)
job.LastRun = time.Now()
s.mu.Unlock()
s.logger.Printf("[ERROR] Job %s panicked: %v", job.Name, r)
}
@@ -238,18 +261,16 @@ func nextDailyRun(timeStr string) time.Time {
return time.Now().Add(24 * time.Hour)
}
loc, err := time.LoadLocation("Europe/Budapest")
if err != nil {
// Fallback to UTC if timezone not available
loc = time.UTC
}
loc := getBudapestLocation()
now := time.Now().In(loc)
next := time.Date(now.Year(), now.Month(), now.Day(), hour, min, 0, 0, loc)
// If the time has already passed today, schedule for tomorrow
// If the time has already passed today, schedule for tomorrow.
// Use time.Date with day+1 instead of Add(24h) to correctly handle DST transitions
// (spring forward/fall back in Europe/Budapest would shift by 1 hour with Add(24h)).
if !next.After(now) {
next = next.Add(24 * time.Hour)
next = time.Date(now.Year(), now.Month(), now.Day()+1, hour, min, 0, 0, loc)
}
return next