Schedule backups in local time instead of UTC (0.66.0)
CI / check (push) Successful in 13m5s
CI / build-and-push (push) Successful in 3m3s

Schedule times were UTC, so a nightly 03:00 backup had to be entered as
01:00 or 02:00 depending on the season, and moved by an hour twice a year.

Each schedule now has an IANA time zone, preset in the form to the
browser's (e.g. Europe/Berlin), and its hour/minute/weekday are wall-clock
time there. compute_next_run builds each candidate day from its date in
that zone rather than adding 24 hours, so the time stays put across
daylight saving changes; a time skipped by the spring change runs an hour
later that day. next_run is still stored as naive UTC.

- Existing schedules default to UTC and keep their run times until edited.
- The API validates the zone and hour/minute/weekday ranges (400) instead
  of silently wrapping them.
- tzdata is a dependency: the slim base image has no system tz database.

Tests cover Berlin winter and summer time, both 2026 change days, the
skipped hour, a weekly run across the UTC date line, hourly at a +5:30
offset, the UTC fallback and the API checks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
menzeljandClaude Opus 5 committed 2026-09-18 12:50:46 +02:00
1 parent 8a0d673798
commit 9a357d2b92
10 files changed
+192 -27

No files matched your search

+7 -1
View File
@@ -30,9 +30,12 @@ class BackupSchedule(SQLModel, table=True):
all_stacks: bool = False
destination_id: int
frequency: str = "daily" # one of FREQUENCIES
hour: int = 3 # UTC, used for daily/weekly
hour: int = 3 # wall-clock time in ``timezone``, used for daily/weekly
minute: int = 0
weekday: int = 0 # 0=Mon .. 6=Sun, used for weekly
#: IANA zone the time is in ("Europe/Berlin"). Schedules from before
#: 0.66.0 were UTC and stay so until edited.
timezone: str = "UTC"
include_volumes: bool = True
stop_first: bool = True
keep: int = 7 # retention: keep newest N for this stack on the dest (0 = all)
@@ -88,6 +91,7 @@ class ScheduleCreate(SQLModel):
hour: int = 3
minute: int = 0
weekday: int = 0
timezone: str = "UTC"
include_volumes: bool = True
stop_first: bool = True
keep: int = 7
@@ -102,6 +106,7 @@ class ScheduleUpdate(SQLModel):
hour: Optional[int] = None
minute: Optional[int] = None
weekday: Optional[int] = None
timezone: Optional[str] = None
include_volumes: Optional[bool] = None
stop_first: Optional[bool] = None
keep: Optional[int] = None
@@ -120,6 +125,7 @@ class ScheduleRead(SQLModel):
hour: int
minute: int
weekday: int
timezone: str
include_volumes: bool
stop_first: bool
keep: int