Files
stackpilot/frontend/src/api/schedules.ts
T
menzeljandClaude Opus 5 9a357d2b92
CI / check (push) Successful in 13m5s
CI / build-and-push (push) Successful in 3m3s
Schedule backups in local time instead of UTC (0.66.0)
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>
2026-09-18 12:50:46 +02:00

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),
};