From 7766c0ed101f1b28996d18724a5528029875b4ed Mon Sep 17 00:00:00 2001 From: dartcafe Date: Sat, 18 Oct 2025 14:19:06 +0200 Subject: [PATCH 1/2] updated job control for admins Signed-off-by: dartcafe --- lib/Attributes/ManuallyRunnableCronJob.php | 18 ++ lib/Controller/AdminController.php | 21 ++ lib/Cron/AutoReminderCron.php | 14 +- lib/Cron/JanitorCron.php | 21 +- lib/Cron/NotificationCron.php | 16 +- lib/Cron/TimedCronJob.php | 43 +++ lib/Service/SystemService.php | 68 +++++ src/Api/modules/admin.ts | 31 +-- src/Api/modules/api.types.ts | 13 + src/adminSettings.ts | 4 +- .../Settings/AdminSettings/AdminJobs.vue | 252 +++++++++++------- src/dashboard.ts | 2 - src/userSettings.ts | 4 +- 13 files changed, 357 insertions(+), 150 deletions(-) create mode 100644 lib/Attributes/ManuallyRunnableCronJob.php create mode 100644 lib/Cron/TimedCronJob.php diff --git a/lib/Attributes/ManuallyRunnableCronJob.php b/lib/Attributes/ManuallyRunnableCronJob.php new file mode 100644 index 0000000000..ee6ab6f2eb --- /dev/null +++ b/lib/Attributes/ManuallyRunnableCronJob.php @@ -0,0 +1,18 @@ + $this->urlGenerator]); } + /** + * Get list of cron jobs + * @return JSONResponse A list of cron jobs + */ + #[FrontpageRoute(verb: 'GET', url: '/administration/jobs')] + public function getJobs(): JSONResponse { + return $this->response(fn () => ['jobs' => $this->systemService->getCronJobs()]); + } + + /** + * Run job + * @param class-string $job Job to run + */ + #[FrontpageRoute(verb: 'POST', url: '/administration/job/run')] + public function runJob($job): JSONResponse { + return $this->response(fn () => $this->systemService->runJob($job)); + } + /** * Get list of polls for administrative purposes */ diff --git a/lib/Cron/AutoReminderCron.php b/lib/Cron/AutoReminderCron.php index 6310ebc75d..df3e53c450 100644 --- a/lib/Cron/AutoReminderCron.php +++ b/lib/Cron/AutoReminderCron.php @@ -10,23 +10,24 @@ use Exception; use OCA\Polls\AppConstants; +use OCA\Polls\Attributes\ManuallyRunnableCronJob; use OCA\Polls\Service\MailService; use OCP\AppFramework\Utility\ITimeFactory; -use OCP\BackgroundJob\TimedJob; use OCP\ISession; use Psr\Log\LoggerInterface; /** * @psalm-api */ -class AutoReminderCron extends TimedJob { +#[ManuallyRunnableCronJob] +class AutoReminderCron extends TimedCronJob { public function __construct( protected ITimeFactory $time, + protected LoggerInterface $logger, private MailService $mailService, private ISession $session, - private LoggerInterface $logger, + protected bool $supportsManualRun = true, ) { - parent::__construct($time); parent::setInterval(30); // run every 30 minutes } @@ -39,7 +40,6 @@ protected function run($argument) { try { $this->mailService->sendAutoReminder(); } catch (Exception $e) { - $this->logger->error('AutoReminderCron: An error occurred while running the auto reminder cron:' . $e->getMessage()); return; } finally { $this->session->remove(AppConstants::SESSION_KEY_CRON_JOB); @@ -47,8 +47,8 @@ protected function run($argument) { } public function manuallyRun(): string { - $this->logger->info('AutoReminderCron manually run.'); + $this->logger->Info('AutoReminderCron will manually run.'); $this->run(null); - return 'AutoReminderCron manually run.'; + return 'AutoReminderCron was manually executed.'; } } diff --git a/lib/Cron/JanitorCron.php b/lib/Cron/JanitorCron.php index 8c43a91042..300879b45a 100644 --- a/lib/Cron/JanitorCron.php +++ b/lib/Cron/JanitorCron.php @@ -10,6 +10,7 @@ use Exception; use OCA\Polls\AppConstants; +use OCA\Polls\Attributes\ManuallyRunnableCronJob; use OCA\Polls\Db\CommentMapper; use OCA\Polls\Db\LogMapper; use OCA\Polls\Db\OptionMapper; @@ -17,34 +18,30 @@ use OCA\Polls\Db\ShareMapper; use OCA\Polls\Db\V5\TableManager; use OCA\Polls\Db\VoteMapper; -use OCA\Polls\Helper\Container; use OCA\Polls\Model\Settings\AppSettings; -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\BackgroundJob\TimedJob; use OCP\ISession; use Psr\Log\LoggerInterface; /** * @psalm-api */ -class JanitorCron extends TimedJob { - private AppSettings $appSettings; +#[ManuallyRunnableCronJob] +class JanitorCron extends TimedCronJob { + // private AppSettings $appSettings; public function __construct( - protected ITimeFactory $time, - private CommentMapper $commentMapper, + protected LoggerInterface $logger, private ISession $session, - private LoggerInterface $logger, + private CommentMapper $commentMapper, private LogMapper $logMapper, private OptionMapper $optionMapper, private PollMapper $pollMapper, private ShareMapper $shareMapper, private VoteMapper $voteMapper, private TableManager $tableManager, + private AppSettings $appSettings, + protected bool $supportsManualRun = true, ) { - parent::__construct($time); - parent::setInterval(86400); // run once a day - $this->appSettings = Container::queryClass(AppSettings::class); } /** @@ -69,6 +66,7 @@ protected function run($argument) { $this->tableManager->updateHashes(); // purge entries virtually deleted more than 12 hours ago + $deleted = []; $deleted['comments'] = $this->commentMapper->purgeDeletedComments(time() - 4320); $deleted['options'] = $this->optionMapper->purgeDeletedOptions(time() - 4320); $deleted['shares'] = $this->shareMapper->purgeDeletedShares(time() - 4320); @@ -104,6 +102,7 @@ protected function run($argument) { $autoArchiveOffset = $this->appSettings->getAutoArchiveOffsetDays(); if ($this->appSettings->getAutoArchiveEnabled() && $autoArchiveOffset > 0) { + $archived = []; $archived['poll'] = $this->pollMapper->archiveExpiredPolls( time() - ($autoArchiveOffset * 86400) ); diff --git a/lib/Cron/NotificationCron.php b/lib/Cron/NotificationCron.php index f18f8bb3fb..87115f14d5 100644 --- a/lib/Cron/NotificationCron.php +++ b/lib/Cron/NotificationCron.php @@ -10,24 +10,24 @@ use Exception; use OCA\Polls\AppConstants; +use OCA\Polls\Attributes\ManuallyRunnableCronJob; use OCA\Polls\Service\MailService; -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\BackgroundJob\TimedJob; use OCP\ISession; use Psr\Log\LoggerInterface; /** * @psalm-api */ -class NotificationCron extends TimedJob { +#[ManuallyRunnableCronJob] +class NotificationCron extends TimedCronJob { public function __construct( - protected ITimeFactory $time, - private MailService $mailService, + protected LoggerInterface $logger, private ISession $session, - private LoggerInterface $logger, + private MailService $mailService, + protected bool $supportsManualRun = true, ) { - parent::__construct($time); - parent::setInterval(5); // run every 5 minutes + // parent::__construct($time); + parent::setInterval(300); // run every 5 minutes } /** diff --git a/lib/Cron/TimedCronJob.php b/lib/Cron/TimedCronJob.php new file mode 100644 index 0000000000..5f48b42d5c --- /dev/null +++ b/lib/Cron/TimedCronJob.php @@ -0,0 +1,43 @@ +getName(); + + $this->logger->error('{job} does not support manual execution', ['job' => $jobClassName]); + throw new Exception('Job does not support manual execution'); + } + +} diff --git a/lib/Service/SystemService.php b/lib/Service/SystemService.php index d6be7a849a..a77350137f 100644 --- a/lib/Service/SystemService.php +++ b/lib/Service/SystemService.php @@ -10,6 +10,8 @@ use Exception; use OCA\Polls\AppConstants; +use OCA\Polls\Attributes\ManuallyRunnableCronJob; +use OCA\Polls\Cron\TimedCronJob; use OCA\Polls\Db\Share; use OCA\Polls\Db\ShareMapper; use OCA\Polls\Db\UserMapper; @@ -24,9 +26,12 @@ use OCA\Polls\Model\User\Contact; use OCA\Polls\Model\User\Email; use OCA\Polls\Model\User\User; +use OCP\BackgroundJob\IJob; +use OCP\BackgroundJob\IJobList; use OCP\Collaboration\Collaborators\ISearch; use OCP\L10N\IFactory; use OCP\Share\IShare; +use phpDocumentor\Reflection\Types\This; use Psr\Log\LoggerInterface; class SystemService { @@ -49,6 +54,7 @@ class SystemService { public function __construct( private IFactory $transFactory, private ISearch $userSearch, + private IJobList $jobList, private LoggerInterface $logger, private ShareMapper $shareMapper, private VoteMapper $voteMapper, @@ -260,6 +266,68 @@ public function validatePublicUsernameByToken(string $userName, string $token): return $this->validatePublicUsername($userName, $share); } + /** + * Run a cron job immediately + * @param class-string $className The class name of the cron job to run + * @throws Exception + */ + public function runJob($className): string { + $jobs = $this->jobList->getJobsIterator($className, null, 0); + + foreach ($jobs as $job) { + $jobClassName = get_class($job); + $reflection_class = new \ReflectionClass($jobClassName); + + if ($reflection_class->getNamespaceName() !== 'OCA\\Polls\\Cron') { + continue; + } + + if (get_class($job) === $className) { + /** @var TimedCronJob $job */ + $job->setLastRun(time()); + + if ( + !empty($reflection_class->getAttributes(ManuallyRunnableCronJob::class)) + && $reflection_class->hasMethod('manuallyRun') + ) { + return $job->manuallyRun(); + } else { + $this->logger->error('Job {job} does not support manual execution', ['job' => $jobClassName]); + throw new Exception('Job does not support manual execution'); + } + } + } + throw new Exception('Job not found'); + } + + /** + * Get a list of cron jobs within Polls' namespace + * @return array A list of cron jobs of Polls' namespace + */ + public function getCronJobs(): array { + $jobs = $this->jobList->getJobsIterator(null, 1000, 1); + $joblist = []; + foreach ($jobs as $job) { + $jobClassName = get_class($job); + $reflection_class = new \ReflectionClass($jobClassName); + + if ($reflection_class->getNamespaceName() !== 'OCA\\Polls\\Cron') { + continue; + } + + $joblist[] = [ + 'id' => $job->getId(), + 'className' => get_class($job), + 'lastRun' => $job->getLastRun(), + 'argument' => $job->getArgument(), + 'nameSpace' => $reflection_class->getNamespaceName(), + 'name' => $reflection_class->getShortName(), + 'manuallyRunnable' => !empty($reflection_class->getAttributes(ManuallyRunnableCronJob::class)), + ]; + } + return $joblist; + } + /** * Validate if the user name is reserved * return false, if the requested userId or displayName exists as a user or as diff --git a/src/Api/modules/admin.ts b/src/Api/modules/admin.ts index e02db978d0..3ccfda1249 100644 --- a/src/Api/modules/admin.ts +++ b/src/Api/modules/admin.ts @@ -3,36 +3,33 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +import { AxiosResponse } from 'axios' import { httpInstance, createCancelTokenHandler } from './HttpApi' +import type { Job, JobsList } from './api.types' const adminJobs = { - runAutoReminder() { + getJobsList(): Promise> { return httpInstance.request({ method: 'GET', - url: 'administration/autoreminder/run', + url: 'administration/jobs', cancelToken: cancelTokenHandlerObject[ - this.runAutoReminder.name + this.getJobsList.name ].handleRequestCancellation().token, }) }, - runJanitor() { - return httpInstance.request({ - method: 'GET', - url: 'administration/janitor/run', - cancelToken: - cancelTokenHandlerObject[ - this.runJanitor.name - ].handleRequestCancellation().token, - }) - }, - runNotification() { + + runJob(job: Job) { return httpInstance.request({ - method: 'GET', - url: 'administration/notification/run', + method: 'POST', + url: 'administration/job/run', + data: { + job: job.className, + }, + cancelToken: cancelTokenHandlerObject[ - this.runNotification.name + this.runJob.name ].handleRequestCancellation().token, }) }, diff --git a/src/Api/modules/api.types.ts b/src/Api/modules/api.types.ts index 0e470516d5..721ad1db23 100644 --- a/src/Api/modules/api.types.ts +++ b/src/Api/modules/api.types.ts @@ -9,6 +9,19 @@ import type { Poll } from '../../stores/poll.types' import type { Comment } from '../../stores/comments.types' import type { Share } from '../../stores/shares.types' +export type Job = { + id: string + className: string + lastRun: number + argument: string | null + nameSpace: string + name: string + manuallyRunnable: boolean +} +export type JobsList = { + jobs: Job[] +} + export type ApiEmailAdressList = { displayName: string emailAddress: string diff --git a/src/adminSettings.ts b/src/adminSettings.ts index eab7b30d78..9740545bf4 100644 --- a/src/adminSettings.ts +++ b/src/adminSettings.ts @@ -5,10 +5,12 @@ import { createApp } from 'vue' import { pinia } from './stores' +import { Settings } from 'luxon' +import { getLanguage } from '@nextcloud/l10n' import AdminSettingsPage from './views/AdminSettingsPage.vue' -// Vue.config.devtools = import.meta.env.MODE !== 'production' +Settings.defaultLocale = getLanguage() const Polls = createApp(AdminSettingsPage).use(pinia) Polls.mount('#content_polls') diff --git a/src/components/Settings/AdminSettings/AdminJobs.vue b/src/components/Settings/AdminSettings/AdminJobs.vue index c3d21714d1..5952894404 100644 --- a/src/components/Settings/AdminSettings/AdminJobs.vue +++ b/src/components/Settings/AdminSettings/AdminJobs.vue @@ -3,72 +3,119 @@ - SPDX-License-Identifier: AGPL-3.0-or-later --> - @@ -77,65 +124,64 @@ async function runNotificationJob() { diff --git a/src/dashboard.ts b/src/dashboard.ts index a269636b38..218c80d1ed 100644 --- a/src/dashboard.ts +++ b/src/dashboard.ts @@ -9,9 +9,7 @@ import { pinia } from './stores' import Dashboard from './views/Dashboard.vue' import './assets/scss/polls-icon.scss' -// Vue.config.devtools = import.meta.env.MODE !== 'production' /** global: OCA */ - document.addEventListener('DOMContentLoaded', () => { // @ts-expect-error: Name not found error OCA.Dashboard.register('polls', (el) => { diff --git a/src/userSettings.ts b/src/userSettings.ts index a8457a2d72..3ff574f202 100644 --- a/src/userSettings.ts +++ b/src/userSettings.ts @@ -5,10 +5,12 @@ import { createApp } from 'vue' import { pinia } from './stores' +import { Settings } from 'luxon' +import { getLanguage } from '@nextcloud/l10n' import UserSettingsPage from './views/UserSettingsPage.vue' -// Vue.config.devtools = import.meta.env.MODE !== 'production' +Settings.defaultLocale = getLanguage() const Polls = createApp(UserSettingsPage).use(pinia) Polls.mount('#content_polls') From a3e7beb81ef921a430a8a220f778c5dbd0043bde Mon Sep 17 00:00:00 2001 From: dartcafe Date: Sat, 18 Oct 2025 14:20:13 +0200 Subject: [PATCH 2/2] changelog Signed-off-by: dartcafe --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d0c275d1d..c672b14cd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ All notable changes to this project will be documented in this file. - Show locale date as tooltip for relative date time information - Add popover to counter to show voters by answer - Add forced display mode to poll configuration +### Changed + - Updated job control for the administration ## [8.5.0] - 2025-10-03 ### Fixed