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() { ) : ( <> - - handleScheduleChange('frequency', value)} + data={[ + { value: 'daily', label: 'Daily' }, + { value: 'weekly', label: 'Weekly' }, + ]} + disabled={!schedule.enabled} + /> {schedule.frequency === 'weekly' && (