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>
58 lines
1.7 KiB
TypeScript
58 lines
1.7 KiB
TypeScript
import api from "./client";
|
|
|
|
export interface BackupSchedule {
|
|
id: number;
|
|
/** The chosen stacks; empty when all_stacks is set. */
|
|
stack_ids: string[];
|
|
/** Every stack, including ones added later. */
|
|
all_stacks: boolean;
|
|
destination_id: number;
|
|
destination_name: string | null;
|
|
frequency: "hourly" | "daily" | "weekly";
|
|
hour: number;
|
|
minute: number;
|
|
weekday: number;
|
|
/** IANA zone the time is in, e.g. "Europe/Berlin". */
|
|
timezone: string;
|
|
include_volumes: boolean;
|
|
stop_first: boolean;
|
|
keep: number;
|
|
enabled: boolean;
|
|
last_run: string | null;
|
|
last_status: string | null;
|
|
/** Per stack, from the last run. */
|
|
stack_status: Record<string, { status: string; at: string }>;
|
|
/** A run is in progress. */
|
|
running: boolean;
|
|
next_run: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ScheduleInput {
|
|
stack_ids: string[];
|
|
all_stacks: boolean;
|
|
destination_id: number;
|
|
frequency: string;
|
|
hour: number;
|
|
minute: number;
|
|
weekday: number;
|
|
timezone: string;
|
|
include_volumes: boolean;
|
|
stop_first: boolean;
|
|
keep: number;
|
|
enabled: boolean;
|
|
}
|
|
|
|
export const schedulesApi = {
|
|
list: () => api.get<BackupSchedule[]>("/api/backups/schedules").then((r) => r.data),
|
|
create: (body: ScheduleInput) =>
|
|
api.post<BackupSchedule>("/api/backups/schedules", body).then((r) => r.data),
|
|
update: (id: number, body: Partial<ScheduleInput>) =>
|
|
api.put<BackupSchedule>(`/api/backups/schedules/${id}`, body).then((r) => r.data),
|
|
remove: (id: number) =>
|
|
api.delete(`/api/backups/schedules/${id}`).then((r) => r.data),
|
|
/** Starts the run in the background; the list then shows `running`. */
|
|
run: (id: number) =>
|
|
api.post<{ started: boolean }>(`/api/backups/schedules/${id}/run`).then((r) => r.data),
|
|
};
|