diff --git a/packages/vitec/Classes/Command/SchedulerSetupCommand.php b/packages/vitec/Classes/Command/SchedulerSetupCommand.php
new file mode 100644
index 0000000..7a21f4d
--- /dev/null
+++ b/packages/vitec/Classes/Command/SchedulerSetupCommand.php
@@ -0,0 +1,136 @@
+ [cron expression, description] */
+ private const TASKS = [
+ 'vitec:ssr-health' => [
+ '*/5 * * * *',
+ 'SSR health check: samples /__health and /__stats of the SSR node into the health log (cockpit charts), '
+ . 'mails the alert recipients from Site settings › VITEC on down/up changes and falls back to the SPA '
+ . 'after repeated failures. Formerly crontab.',
+ ],
+ 'vitec:client-error-cleanup' => [
+ '23 4 * * *',
+ 'Deletes browser error reports (client errors from the React frontend) older than 30 days. Formerly crontab.',
+ ],
+ ];
+
+ public function __construct(
+ private readonly TaskService $taskService,
+ private readonly SchedulerTaskRepository $taskRepository,
+ private readonly ConnectionPool $connectionPool,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void
+ {
+ $this->addOption('disabled', null, InputOption::VALUE_NONE, 'Create new tasks switched off');
+ $this->addOption('enable', null, InputOption::VALUE_NONE, 'Switch on all VITEC tasks created by this command');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ Bootstrap::initializeBackendAuthentication();
+ $groupUid = $this->groupUid();
+ $output->writeln('Scheduler group "' . self::GROUP . '": uid ' . $groupUid);
+
+ foreach (self::TASKS as $command => [$cron, $description]) {
+ $existing = $this->findTask($command);
+ if ($existing !== null) {
+ if ($input->getOption('enable') && (int)$existing['disable'] === 1) {
+ $task = $this->taskRepository->findByUid((int)$existing['uid']);
+ $task->setDisabled(false);
+ $this->taskRepository->update($task);
+ $output->writeln(sprintf('%s: enabled (uid %d)', $command, $existing['uid']));
+ } else {
+ $output->writeln(sprintf('%s: exists (uid %d, %s)', $command, $existing['uid'],
+ (int)$existing['disable'] === 1 ? 'disabled' : 'enabled'));
+ }
+ continue;
+ }
+ if (!$this->taskService->isTaskTypeRegistered($command)) {
+ $output->writeln('' . $command . ': not available as scheduler task type');
+ continue;
+ }
+ $task = $this->taskService->createNewTask($command);
+ $task->setDescription($description);
+ $task->setTaskGroup($groupUid);
+ $task->setExecution(Execution::createRecurringExecution(time() - 60, 0, 0, false, $cron));
+ $task->setDisabled((bool)$input->getOption('disabled'));
+ if ($this->taskRepository->add($task)) {
+ $output->writeln(sprintf('%s: created (uid %d, %s, %s)', $command, $task->getTaskUid(), $cron,
+ $task->isDisabled() ? 'disabled' : 'enabled'));
+ } else {
+ $output->writeln('' . $command . ': could not be created');
+ }
+ }
+ return Command::SUCCESS;
+ }
+
+ private function groupUid(): int
+ {
+ $connection = $this->connectionPool->getConnectionForTable('tx_scheduler_task_group');
+ $uid = $connection->executeQuery(
+ 'SELECT uid FROM tx_scheduler_task_group WHERE groupName = ? AND deleted = 0 ORDER BY uid LIMIT 1',
+ [self::GROUP]
+ )->fetchOne();
+ if ($uid !== false) {
+ return (int)$uid;
+ }
+ $sorting = (int)$connection->executeQuery('SELECT MAX(sorting) FROM tx_scheduler_task_group')->fetchOne();
+ $connection->insert('tx_scheduler_task_group', [
+ 'pid' => 0,
+ 'tstamp' => time(),
+ 'crdate' => time(),
+ 'groupName' => self::GROUP,
+ 'color' => '#f47937',
+ 'description' => 'Recurring jobs of the VITEC website (SSR monitoring, cleanups)',
+ 'sorting' => $sorting + 256,
+ ], ['pid' => Connection::PARAM_INT, 'tstamp' => Connection::PARAM_INT, 'crdate' => Connection::PARAM_INT, 'sorting' => Connection::PARAM_INT]);
+ return (int)$connection->lastInsertId();
+ }
+
+ private function findTask(string $command): ?array
+ {
+ $row = $this->connectionPool->getConnectionForTable('tx_scheduler_task')->executeQuery(
+ 'SELECT uid, disable FROM tx_scheduler_task WHERE tasktype = ? AND deleted = 0 ORDER BY uid LIMIT 1',
+ [$command]
+ )->fetchAssociative();
+ return $row === false ? null : $row;
+ }
+}