diff --git a/apps/backups/scheduler.py b/apps/backups/scheduler.py index b0b37567..426d2c7e 100644 --- a/apps/backups/scheduler.py +++ b/apps/backups/scheduler.py @@ -127,6 +127,7 @@ def _sync_periodic_task() -> None: day_of_week=day_of_week, day_of_month=day_of_month, month_of_year=month_of_year, + timezone=CoreSettings.get_system_time_zone(), ) except Exception as e: logger.error(f"Invalid cron expression '{settings['cron_expression']}': {e}") @@ -137,6 +138,7 @@ def _sync_periodic_task() -> None: hour, minute = settings["time"].split(":") # Build crontab based on frequency + system_tz = CoreSettings.get_system_time_zone() if settings["frequency"] == "daily": crontab, _ = CrontabSchedule.objects.get_or_create( minute=minute, @@ -144,6 +146,7 @@ def _sync_periodic_task() -> None: day_of_week="*", day_of_month="*", month_of_year="*", + timezone=system_tz, ) else: # weekly crontab, _ = CrontabSchedule.objects.get_or_create( @@ -152,6 +155,7 @@ def _sync_periodic_task() -> None: day_of_week=str(settings["day_of_week"]), day_of_month="*", month_of_year="*", + timezone=system_tz, ) # Create or update the periodic task diff --git a/apps/backups/tests.py b/apps/backups/tests.py index fcf854d6..cded0ba4 100644 --- a/apps/backups/tests.py +++ b/apps/backups/tests.py @@ -738,6 +738,16 @@ class BackupAPITestCase(TestCase): class BackupSchedulerTestCase(TestCase): """Test cases for backup scheduler""" + databases = {'default'} + + @classmethod + def setUpClass(cls): + pass + + @classmethod + def tearDownClass(cls): + pass + def setUp(self): from core.models import CoreSettings # Clean up any existing settings @@ -949,6 +959,63 @@ class BackupSchedulerTestCase(TestCase): self.assertEqual(task.crontab.hour, '*/6') self.assertEqual(task.crontab.day_of_week, '*') + def test_periodic_task_uses_system_timezone(self): + """Test that CrontabSchedule is created with the system timezone""" + from . import scheduler + from django_celery_beat.models import PeriodicTask + from core.models import CoreSettings + + original_tz = CoreSettings.get_system_time_zone() + + try: + # Set a non-UTC timezone + CoreSettings.set_system_time_zone('America/New_York') + + scheduler.update_schedule_settings({ + 'enabled': True, + 'frequency': 'daily', + 'time': '03:00', + }) + + task = PeriodicTask.objects.get(name='backup-scheduled-task') + self.assertEqual(str(task.crontab.timezone), 'America/New_York') + finally: + scheduler.update_schedule_settings({'enabled': False}) + CoreSettings.set_system_time_zone(original_tz) + + def test_periodic_task_timezone_updates_with_schedule(self): + """Test that CrontabSchedule timezone is updated when schedule is modified""" + from . import scheduler + from django_celery_beat.models import PeriodicTask + from core.models import CoreSettings + + original_tz = CoreSettings.get_system_time_zone() + + try: + # Create initial schedule with one timezone + CoreSettings.set_system_time_zone('America/Los_Angeles') + scheduler.update_schedule_settings({ + 'enabled': True, + 'frequency': 'daily', + 'time': '02:00', + }) + + task = PeriodicTask.objects.get(name='backup-scheduled-task') + self.assertEqual(str(task.crontab.timezone), 'America/Los_Angeles') + + # Change system timezone and update schedule + CoreSettings.set_system_time_zone('Europe/London') + scheduler.update_schedule_settings({ + 'enabled': True, + 'time': '04:00', + }) + + task.refresh_from_db() + self.assertEqual(str(task.crontab.timezone), 'Europe/London') + finally: + scheduler.update_schedule_settings({'enabled': False}) + CoreSettings.set_system_time_zone(original_tz) + class BackupTasksTestCase(TestCase): """Test cases for backup Celery tasks""" diff --git a/frontend/src/components/backups/BackupManager.jsx b/frontend/src/components/backups/BackupManager.jsx index 5c25f60b..2376aa14 100644 --- a/frontend/src/components/backups/BackupManager.jsx +++ b/frontend/src/components/backups/BackupManager.jsx @@ -98,37 +98,6 @@ function to24Hour(time12, period) { return `${String(hours24).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; } -// Convert UTC time (HH:MM) to local time (HH:MM) -function utcToLocal(utcTime) { - if (!utcTime) return '00:00'; - const [hours, minutes] = utcTime.split(':').map(Number); - - // Create a date in UTC - const date = new Date(); - date.setUTCHours(hours, minutes, 0, 0); - - // Get local time components - const localHours = date.getHours(); - const localMinutes = date.getMinutes(); - - return `${String(localHours).padStart(2, '0')}:${String(localMinutes).padStart(2, '0')}`; -} - -// Convert local time (HH:MM) to UTC time (HH:MM) -function localToUtc(localTime) { - if (!localTime) return '00:00'; - const [hours, minutes] = localTime.split(':').map(Number); - - // Create a date in local time - const date = new Date(); - date.setHours(hours, minutes, 0, 0); - - // Get UTC time components - const utcHours = date.getUTCHours(); - const utcMinutes = date.getUTCMinutes(); - - return `${String(utcHours).padStart(2, '0')}:${String(utcMinutes).padStart(2, '0')}`; -} // Get default timezone (same as Settings page) function getDefaultTimeZone() { @@ -376,20 +345,15 @@ export default function BackupManager() { // 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); } + setSchedule(settings); + + // Initialize 12-hour display values + const { time, period } = to12Hour(settings.time); + setDisplayTime(time); + setTimePeriod(period); + setScheduleChanged(false); } catch (error) { // Ignore errors on initial load - settings may not exist yet @@ -447,27 +411,12 @@ export default function BackupManager() { const handleSaveSchedule = async () => { setScheduleSaving(true); try { - 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 scheduleToSave = advancedMode + ? schedule + : { ...schedule, cron_expression: '' }; const updated = await API.updateBackupSchedule(scheduleToSave); - - if (advancedMode) { - setSchedule(updated); - } else { - // Convert UTC time from backend response back to local time - const localTime = utcToLocal(updated.time); - setSchedule({ ...updated, time: localTime }); - } - + setSchedule(updated); setScheduleChanged(false); notifications.show({ @@ -691,14 +640,36 @@ export default function BackupManager() { )} {is12Hour ? ( - handleTimeChange12h(e.currentTarget.value, null)} - placeholder="3:00" + { + const hour = displayTime ? displayTime.split(':')[0] : '12'; + handleTimeChange12h(`${hour}:${value}`, null); + }} + data={Array.from({ length: 60 }, (_, i) => ({ + value: String(i).padStart(2, '0'), + label: String(i).padStart(2, '0'), + }))} + disabled={!schedule.enabled} + searchable + /> + { + const minute = schedule.time ? schedule.time.split(':')[1] : '00'; + handleTimeChange24h(`${value}:${minute}`); + }} + data={Array.from({ length: 24 }, (_, i) => ({ + value: String(i).padStart(2, '0'), + label: String(i).padStart(2, '0'), + }))} + disabled={!schedule.enabled} + searchable + /> +