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"
+
) : (
- handleTimeChange24h(e.currentTarget.value)}
- placeholder="03:00"
- disabled={!schedule.enabled}
- />
+
+ {
+ 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
+ />
+ {
+ const hour = schedule.time ? schedule.time.split(':')[0] : '00';
+ handleTimeChange24h(`${hour}:${value}`);
+ }}
+ data={Array.from({ length: 60 }, (_, i) => ({
+ value: String(i).padStart(2, '0'),
+ label: String(i).padStart(2, '0'),
+ }))}
+ disabled={!schedule.enabled}
+ searchable
+ />
+
)}
- Timezone: {userTimezone} • Backup will run at {schedule.time}
+ System Timezone: {userTimezone} • Backup will run at {schedule.time} {userTimezone}
)}
>