diff --git a/apps/backups/scheduler.py b/apps/backups/scheduler.py
index 52186e90..b0b37567 100644
--- a/apps/backups/scheduler.py
+++ b/apps/backups/scheduler.py
@@ -15,6 +15,7 @@ SETTING_KEYS = {
"time": "backup_schedule_time",
"day_of_week": "backup_schedule_day_of_week",
"retention_count": "backup_retention_count",
+ "cron_expression": "backup_schedule_cron_expression",
}
DEFAULTS = {
@@ -23,6 +24,7 @@ DEFAULTS = {
"time": "03:00",
"day_of_week": 0, # Sunday
"retention_count": 0,
+ "cron_expression": "",
}
@@ -60,6 +62,7 @@ def get_schedule_settings() -> dict:
"time": _get_setting("time"),
"day_of_week": _get_setting("day_of_week"),
"retention_count": _get_setting("retention_count"),
+ "cron_expression": _get_setting("cron_expression"),
}
@@ -88,7 +91,7 @@ def update_schedule_settings(data: dict) -> dict:
raise ValueError("retention_count must be >= 0")
# Update settings
- for key in ("enabled", "frequency", "time", "day_of_week", "retention_count"):
+ for key in ("enabled", "frequency", "time", "day_of_week", "retention_count", "cron_expression"):
if key in data:
_set_setting(key, data[key])
@@ -108,26 +111,48 @@ def _sync_periodic_task() -> None:
logger.info("Backup schedule disabled, removed periodic task")
return
- # Parse time
- hour, minute = settings["time"].split(":")
+ # Check if using cron expression (advanced mode)
+ if settings["cron_expression"]:
+ # Parse cron expression: "minute hour day month weekday"
+ try:
+ parts = settings["cron_expression"].split()
+ if len(parts) != 5:
+ raise ValueError("Cron expression must have 5 parts: minute hour day month weekday")
- # Build crontab based on frequency
- if settings["frequency"] == "daily":
- crontab, _ = CrontabSchedule.objects.get_or_create(
- minute=minute,
- hour=hour,
- day_of_week="*",
- day_of_month="*",
- month_of_year="*",
- )
- else: # weekly
- crontab, _ = CrontabSchedule.objects.get_or_create(
- minute=minute,
- hour=hour,
- day_of_week=str(settings["day_of_week"]),
- day_of_month="*",
- month_of_year="*",
- )
+ minute, hour, day_of_month, month_of_year, day_of_week = parts
+
+ crontab, _ = CrontabSchedule.objects.get_or_create(
+ minute=minute,
+ hour=hour,
+ day_of_week=day_of_week,
+ day_of_month=day_of_month,
+ month_of_year=month_of_year,
+ )
+ except Exception as e:
+ logger.error(f"Invalid cron expression '{settings['cron_expression']}': {e}")
+ raise ValueError(f"Invalid cron expression: {e}")
+ else:
+ # Use simple frequency-based scheduling
+ # Parse time
+ hour, minute = settings["time"].split(":")
+
+ # Build crontab based on frequency
+ if settings["frequency"] == "daily":
+ crontab, _ = CrontabSchedule.objects.get_or_create(
+ minute=minute,
+ hour=hour,
+ day_of_week="*",
+ day_of_month="*",
+ month_of_year="*",
+ )
+ else: # weekly
+ crontab, _ = CrontabSchedule.objects.get_or_create(
+ minute=minute,
+ hour=hour,
+ day_of_week=str(settings["day_of_week"]),
+ day_of_month="*",
+ month_of_year="*",
+ )
# Create or update the periodic task
task, created = PeriodicTask.objects.update_or_create(
diff --git a/frontend/src/components/backups/BackupManager.jsx b/frontend/src/components/backups/BackupManager.jsx
index 3732ae09..dd47f732 100644
--- a/frontend/src/components/backups/BackupManager.jsx
+++ b/frontend/src/components/backups/BackupManager.jsx
@@ -188,10 +188,12 @@ export default function BackupManager() {
time: '03:00',
day_of_week: 0,
retention_count: 0,
+ cron_expression: '',
});
const [scheduleLoading, setScheduleLoading] = useState(false);
const [scheduleSaving, setScheduleSaving] = useState(false);
const [scheduleChanged, setScheduleChanged] = useState(false);
+ const [advancedMode, setAdvancedMode] = useState(false);
// For 12-hour display mode
const [displayTime, setDisplayTime] = useState('3:00');
@@ -299,17 +301,24 @@ export default function BackupManager() {
try {
const settings = await API.getBackupSchedule();
- // Convert UTC time from backend to local time
- const localTime = utcToLocal(settings.time);
+ // Check if using cron expression (advanced mode)
+ if (settings.cron_expression) {
+ setAdvancedMode(true);
+ setSchedule(settings);
+ } else {
+ // Convert UTC time from backend to local time
+ const localTime = utcToLocal(settings.time);
+
+ // Store with local time for display
+ setSchedule({ ...settings, time: localTime });
+
+ // Initialize 12-hour display values from the local time
+ const { time, period } = to12Hour(localTime);
+ setDisplayTime(time);
+ setTimePeriod(period);
+ }
- // Store with local time for display
- setSchedule({ ...settings, time: localTime });
setScheduleChanged(false);
-
- // Initialize 12-hour display values from the local time
- const { time, period } = to12Hour(localTime);
- setDisplayTime(time);
- setTimePeriod(period);
} catch (error) {
// Ignore errors on initial load - settings may not exist yet
} finally {
@@ -350,15 +359,27 @@ export default function BackupManager() {
const handleSaveSchedule = async () => {
setScheduleSaving(true);
try {
- // Convert local time to UTC before sending to backend
- const utcTime = localToUtc(schedule.time);
- const scheduleToSave = { ...schedule, time: utcTime };
+ let scheduleToSave;
+
+ if (advancedMode) {
+ // In advanced mode, send cron expression as-is
+ scheduleToSave = schedule;
+ } else {
+ // Convert local time to UTC before sending to backend
+ const utcTime = localToUtc(schedule.time);
+ scheduleToSave = { ...schedule, time: utcTime, cron_expression: '' };
+ }
const updated = await API.updateBackupSchedule(scheduleToSave);
- // Convert UTC time from backend response back to local time
- const localTime = utcToLocal(updated.time);
- setSchedule({ ...updated, time: localTime });
+ if (advancedMode) {
+ setSchedule(updated);
+ } else {
+ // Convert UTC time from backend response back to local time
+ const localTime = utcToLocal(updated.time);
+ setSchedule({ ...updated, time: localTime });
+ }
+
setScheduleChanged(false);
notifications.show({
@@ -509,17 +530,65 @@ export default function BackupManager() {
) : (
<>
-
-
+ )}
+
+ {/* Timezone info - only show in simple mode */}
+ {!advancedMode && schedule.enabled && schedule.time && (
Timezone: {userTimezone} • Backup will run at {schedule.time}