From f2d6b5af76ce8b9d20d88ec00e8454b67e8a585a Mon Sep 17 00:00:00 2001 From: Lance Pioch Date: Fri, 6 Feb 2026 02:32:21 -0500 Subject: [PATCH] Allow schedule cron inputs to use a user-selected timezone (#1228) Add a timezone dropdown (defaulting to the user's profile timezone) to the schedule form so cron inputs are interpreted in the chosen timezone. The selected timezone is used to compute next_run_at in UTC at save time with no database schema changes required. Resolves #1228 --- .../Schedules/Pages/CreateSchedule.php | 6 +++++- .../Resources/Schedules/Pages/EditSchedule.php | 6 +++++- .../Resources/Schedules/ScheduleResource.php | 17 +++++++++++++---- app/Helpers/Utilities.php | 4 ++-- .../Api/Client/Servers/ScheduleController.php | 9 +++++---- lang/en/server/schedule.php | 3 ++- 6 files changed, 32 insertions(+), 13 deletions(-) diff --git a/app/Filament/Server/Resources/Schedules/Pages/CreateSchedule.php b/app/Filament/Server/Resources/Schedules/Pages/CreateSchedule.php index 10f00e406..53e50726c 100644 --- a/app/Filament/Server/Resources/Schedules/Pages/CreateSchedule.php +++ b/app/Filament/Server/Resources/Schedules/Pages/CreateSchedule.php @@ -39,13 +39,17 @@ class CreateSchedule extends CreateRecord $data['server_id'] = $server->id; } + $timezone = $data['timezone'] ?? user()->timezone ?? 'UTC'; + unset($data['timezone']); + if (!isset($data['next_run_at'])) { $data['next_run_at'] = ScheduleResource::getNextRun( $data['cron_minute'], $data['cron_hour'], $data['cron_day_of_month'], $data['cron_month'], - $data['cron_day_of_week'] + $data['cron_day_of_week'], + $timezone ); } diff --git a/app/Filament/Server/Resources/Schedules/Pages/EditSchedule.php b/app/Filament/Server/Resources/Schedules/Pages/EditSchedule.php index eff631ff9..cb8dab2d1 100644 --- a/app/Filament/Server/Resources/Schedules/Pages/EditSchedule.php +++ b/app/Filament/Server/Resources/Schedules/Pages/EditSchedule.php @@ -37,12 +37,16 @@ class EditSchedule extends EditRecord protected function mutateFormDataBeforeSave(array $data): array { + $timezone = $data['timezone'] ?? user()->timezone ?? 'UTC'; + unset($data['timezone']); + $data['next_run_at'] = ScheduleResource::getNextRun( $data['cron_minute'], $data['cron_hour'], $data['cron_day_of_month'], $data['cron_month'], - $data['cron_day_of_week'] + $data['cron_day_of_week'], + $timezone ); return $data; diff --git a/app/Filament/Server/Resources/Schedules/ScheduleResource.php b/app/Filament/Server/Resources/Schedules/ScheduleResource.php index a9ed0e4cf..89207a14d 100644 --- a/app/Filament/Server/Resources/Schedules/ScheduleResource.php +++ b/app/Filament/Server/Resources/Schedules/ScheduleResource.php @@ -100,13 +100,15 @@ class ScheduleResource extends Resource Section::make('Cron') ->label(trans('server/schedule.cron')) ->description(function (Get $get) { + $timezone = $get('timezone') ?? user()->timezone ?? 'UTC'; + try { - $nextRun = Utilities::getScheduleNextRunDate($get('cron_minute'), $get('cron_hour'), $get('cron_day_of_month'), $get('cron_month'), $get('cron_day_of_week'))->timezone(user()->timezone ?? 'UTC'); + $nextRun = Utilities::getScheduleNextRunDate($get('cron_minute'), $get('cron_hour'), $get('cron_day_of_month'), $get('cron_month'), $get('cron_day_of_week'), $timezone)->timezone($timezone); } catch (Exception) { $nextRun = trans('server/schedule.invalid'); } - return new HtmlString(trans('server/schedule.cron_body') . '
' . trans('server/schedule.cron_timezone', ['timezone' => user()->timezone ?? 'UTC', 'next_run' => $nextRun])); + return new HtmlString(trans('server/schedule.cron_body', ['timezone' => $timezone]) . '
' . trans('server/schedule.cron_timezone', ['timezone' => $timezone, 'next_run' => $nextRun])); }) ->schema([ Actions::make([ @@ -295,6 +297,13 @@ class ScheduleResource extends Resource 'default' => 4, 'lg' => 5, ]), + Select::make('timezone') + ->label(trans('server/schedule.timezone')) + ->options(fn () => array_combine(timezone_identifiers_list(), timezone_identifiers_list())) + ->default(user()->timezone ?? 'UTC') + ->searchable() + ->live() + ->hiddenOn('view'), ]) ->columnSpanFull(), ]); @@ -379,10 +388,10 @@ class ScheduleResource extends Resource ]; } - public static function getNextRun(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek): Carbon + public static function getNextRun(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek, string $timezone = 'UTC'): Carbon { try { - return Utilities::getScheduleNextRunDate($minute, $hour, $dayOfMonth, $month, $dayOfWeek); + return Utilities::getScheduleNextRunDate($minute, $hour, $dayOfMonth, $month, $dayOfWeek, $timezone); } catch (Exception) { Notification::make() ->title(trans('server/schedule.notification_invalid_cron')) diff --git a/app/Helpers/Utilities.php b/app/Helpers/Utilities.php index cef0ccff2..df745e7b5 100644 --- a/app/Helpers/Utilities.php +++ b/app/Helpers/Utilities.php @@ -38,11 +38,11 @@ class Utilities * * @throws Exception */ - public static function getScheduleNextRunDate(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek): Carbon + public static function getScheduleNextRunDate(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek, string $timezone = 'UTC'): Carbon { return Carbon::instance((new CronExpression( sprintf('%s %s %s %s %s', $minute, $hour, $dayOfMonth, $month, $dayOfWeek) - ))->getNextRunDate(now('UTC'))); + ))->getNextRunDate(now($timezone)))->setTimezone('UTC'); } public static function checked(string $name, mixed $default): string diff --git a/app/Http/Controllers/Api/Client/Servers/ScheduleController.php b/app/Http/Controllers/Api/Client/Servers/ScheduleController.php index 9308d4ae3..229289c49 100644 --- a/app/Http/Controllers/Api/Client/Servers/ScheduleController.php +++ b/app/Http/Controllers/Api/Client/Servers/ScheduleController.php @@ -75,7 +75,7 @@ class ScheduleController extends ClientApiController 'cron_minute' => $request->input('minute'), 'is_active' => (bool) $request->input('is_active'), 'only_when_online' => (bool) $request->input('only_when_online'), - 'next_run_at' => $this->getNextRunAt($request), + 'next_run_at' => $this->getNextRunAt($request, $request->user()->timezone ?? 'UTC'), ]); Activity::event('server:schedule.create') @@ -131,7 +131,7 @@ class ScheduleController extends ClientApiController 'cron_minute' => $request->input('minute'), 'is_active' => $active, 'only_when_online' => (bool) $request->input('only_when_online'), - 'next_run_at' => $this->getNextRunAt($request), + 'next_run_at' => $this->getNextRunAt($request, $request->user()->timezone ?? 'UTC'), ]; // Toggle the processing state of the scheduled task when it is enabled or disabled so that an @@ -188,7 +188,7 @@ class ScheduleController extends ClientApiController * * @throws DisplayException */ - protected function getNextRunAt(Request $request): Carbon + protected function getNextRunAt(Request $request, string $timezone = 'UTC'): Carbon { try { return Utilities::getScheduleNextRunDate( @@ -196,7 +196,8 @@ class ScheduleController extends ClientApiController $request->input('hour'), $request->input('day_of_month'), $request->input('month'), - $request->input('day_of_week') + $request->input('day_of_week'), + $timezone ); } catch (Exception) { throw new DisplayException('The cron data provided does not evaluate to a valid expression.'); diff --git a/lang/en/server/schedule.php b/lang/en/server/schedule.php index be0ae8d7b..1e2679f10 100644 --- a/lang/en/server/schedule.php +++ b/lang/en/server/schedule.php @@ -29,7 +29,8 @@ return [ 'enabled' => 'Enable Schedule?', 'enabled_hint' => 'This schedule will be executed automatically if enabled.', - 'cron_body' => 'Please keep in mind that the cron inputs below always assume UTC.', + 'timezone' => 'Timezone', + 'cron_body' => 'The cron inputs below use your timezone (:timezone).', 'cron_timezone' => 'Next run in your timezone (:timezone): :next_run ', 'invalid' => 'Invalid',