diff --git a/system/blueprints/config/scheduler.yaml b/system/blueprints/config/scheduler.yaml index 6bccafbbd..8b829947a 100644 --- a/system/blueprints/config/scheduler.yaml +++ b/system/blueprints/config/scheduler.yaml @@ -80,8 +80,7 @@ form: content: | - modern_status: - type: conditional - condition: config.scheduler.modern.enabled - - fields: - modern_health: - type: display - label: Health Status - content: | -
-
Checking health...
-
- - markdown: false + // Refresh every 30 seconds + setInterval(loadHealthStatus, 30000); + })(); + + markdown: false - trigger_methods: - type: display - label: Active Triggers - content: | -
-
Checking triggers...
-
- - markdown: false + } + })(); + + markdown: false jobs_tab: type: tab @@ -501,233 +486,291 @@ form: modern_tab: type: tab - title: Modern Features + title: Advanced Features fields: - modern.enabled: - type: toggle - label: Enable Modern Scheduler - help: Enable enhanced scheduler features (job queue, retry, webhooks, monitoring) - highlight: 0 - default: 0 - options: - 1: PLUGIN_ADMIN.ENABLED - 0: PLUGIN_ADMIN.DISABLED - validate: - type: bool - - modern_features: - type: conditional - condition: config.scheduler.modern.enabled + workers_section: + type: section + title: Worker Configuration + underline: true fields: - workers_section: - type: section - title: Worker Configuration - underline: true + modern.workers: + type: number + label: Concurrent Workers + help: Number of jobs that can run simultaneously (1 = sequential) + default: 4 + size: x-small + append: workers + validate: + type: int + min: 1 + max: 10 + retry_section: + type: section + title: Retry Configuration + underline: true + + fields: + modern.retry.enabled: + type: toggle + label: Enable Job Retry + help: Automatically retry failed jobs + highlight: 1 + default: 1 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool + + modern.retry.max_attempts: + type: number + label: Maximum Retry Attempts + help: Maximum number of times to retry a failed job + default: 3 + size: x-small + append: retries + validate: + type: int + min: 1 + max: 10 + + modern.retry.backoff: + type: select + label: Retry Backoff Strategy + help: How to calculate delay between retries + default: exponential + options: + linear: Linear (fixed delay) + exponential: Exponential (increasing delay) + + queue_section: + type: section + title: Queue Configuration + underline: true + + fields: + modern.queue.path: + type: text + label: Queue Storage Path + help: Where to store queued jobs + default: 'user-data://scheduler/queue' + placeholder: 'user-data://scheduler/queue' + + modern.queue.max_size: + type: number + label: Maximum Queue Size + help: Maximum number of jobs that can be queued + default: 1000 + size: x-small + append: jobs + validate: + type: int + min: 100 + max: 10000 + + history_section: + type: section + title: Job History + underline: true + + fields: + modern.history.enabled: + type: toggle + label: Enable Job History + help: Track execution history for all jobs + highlight: 1 + default: 1 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool + + modern.history.retention_days: + type: number + label: History Retention (days) + help: How long to keep job history + default: 30 + size: x-small + append: days + validate: + type: int + min: 1 + max: 365 + + webhook_section: + type: section + title: Webhook Configuration + underline: true + + fields: + webhook_plugin_notice: + type: conditional + condition: config.plugins.scheduler-webhook.enabled == false + fields: - modern.workers: - type: number - label: Concurrent Workers - help: Number of jobs that can run simultaneously (1 = sequential) - default: 1 - size: x-small - append: workers - validate: - type: int - min: 1 - max: 10 - - retry_section: - type: section - title: Retry Configuration - underline: true - - fields: - modern.retry.enabled: - type: toggle - label: Enable Job Retry - help: Automatically retry failed jobs - highlight: 1 - default: 1 - options: - 1: PLUGIN_ADMIN.ENABLED - 0: PLUGIN_ADMIN.DISABLED - validate: - type: bool - - modern.retry.max_attempts: - type: number - label: Maximum Retry Attempts - help: Maximum number of times to retry a failed job - default: 3 - size: x-small - append: retries - validate: - type: int - min: 1 - max: 10 - - modern.retry.backoff: - type: select - label: Retry Backoff Strategy - help: How to calculate delay between retries - default: exponential - options: - linear: Linear (fixed delay) - exponential: Exponential (increasing delay) - - queue_section: - type: section - title: Queue Configuration - underline: true - - fields: - modern.queue.path: - type: text - label: Queue Storage Path - help: Where to store queued jobs - default: 'user-data://scheduler/queue' - placeholder: 'user-data://scheduler/queue' - - modern.queue.max_size: - type: number - label: Maximum Queue Size - help: Maximum number of jobs that can be queued - default: 1000 - size: x-small - append: jobs - validate: - type: int - min: 100 - max: 10000 - - history_section: - type: section - title: Job History - underline: true - - fields: - modern.history.enabled: - type: toggle - label: Enable Job History - help: Track execution history for all jobs - highlight: 1 - default: 1 - options: - 1: PLUGIN_ADMIN.ENABLED - 0: PLUGIN_ADMIN.DISABLED - validate: - type: bool - - modern.history.retention_days: - type: number - label: History Retention (days) - help: How long to keep job history - default: 30 - size: x-small - append: days - validate: - type: int - min: 1 - max: 365 - - webhook_section: - type: section - title: Webhook Configuration - underline: true - - fields: - webhook_plugin_notice: + webhook_install: type: display label: content: |
- Plugin Required: The scheduler-webhook plugin must be installed and enabled for webhook functionality to work. -

- Install with: bin/gpm install scheduler-webhook + Webhook Plugin Required
+ The scheduler-webhook plugin is required for webhook functionality.

+ + Install Plugin Now + + or run: bin/gpm install scheduler-webhook
markdown: false - modern.webhook.enabled: - type: toggle - label: Enable Webhook Triggers - help: Allow triggering scheduler via HTTP webhook - highlight: 0 - default: 0 - options: - 1: PLUGIN_ADMIN.ENABLED - 0: PLUGIN_ADMIN.DISABLED - validate: - type: bool - - modern.webhook.token: - type: text - label: Webhook Security Token - help: Secret token for authenticating webhook requests. Keep this secret! - placeholder: 'Click Generate to create a secure token' - webhook_token_generate: + webhook_plugin_ready: + type: conditional + condition: config.plugins.scheduler-webhook.enabled == true + + fields: + webhook_ready: type: display label: content: | - +
+ Webhook Plugin Ready!
+ The scheduler-webhook plugin is installed and active. Configure your webhook settings below. +
+ markdown: false + modern.webhook.enabled: + type: toggle + label: Enable Webhook Triggers + help: Allow triggering scheduler via HTTP webhook + highlight: 0 + default: 0 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool + + modern.webhook.token: + type: text + label: Webhook Security Token + help: Secret token for authenticating webhook requests. Keep this secret! + placeholder: 'Click Generate to create a secure token' + autocomplete: 'off' + + webhook_token_generate: + type: display + label: + content: | +
+ +
- markdown: false + markdown: false - modern.webhook.path: - type: text - label: Webhook Path - help: URL path for webhook endpoint - default: '/scheduler/webhook' - placeholder: '/scheduler/webhook' + modern.webhook.path: + type: text + label: Webhook Path + help: URL path for webhook endpoint + default: '/scheduler/webhook' + placeholder: '/scheduler/webhook' - health_section: - type: section - title: Health Check Configuration - underline: true + health_section: + type: section + title: Health Check Configuration + underline: true - fields: - modern.health.enabled: - type: toggle - label: Enable Health Check - help: Provide health status endpoint for monitoring - highlight: 1 - default: 1 - options: - 1: PLUGIN_ADMIN.ENABLED - 0: PLUGIN_ADMIN.DISABLED - validate: - type: bool + fields: + modern.health.enabled: + type: toggle + label: Enable Health Check + help: Provide health status endpoint for monitoring + highlight: 1 + default: 1 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool - modern.health.path: - type: text - label: Health Check Path - help: URL path for health check endpoint - default: '/scheduler/health' - placeholder: '/scheduler/health' + modern.health.path: + type: text + label: Health Check Path + help: URL path for health check endpoint + default: '/scheduler/health' + placeholder: '/scheduler/health' - webhook_usage: - type: section - title: Usage Examples - underline: true - - fields: - webhook_examples: - type: display - label: - content: | + webhook_usage: + type: section + title: Usage Examples + underline: true + + fields: + webhook_examples: + type: display + label: + content: |

How to use webhooks:

Trigger all due jobs (respects schedule):

@@ -747,7 +790,7 @@ form: curl -X POST ${{ secrets.SITE_URL }}/scheduler/webhook \ -H "Authorization: Bearer ${{ secrets.WEBHOOK_TOKEN }}"
- markdown: false + markdown: false diff --git a/system/src/Grav/Common/Scheduler/Scheduler.php b/system/src/Grav/Common/Scheduler/Scheduler.php index 410bfec1f..559c6dd30 100644 --- a/system/src/Grav/Common/Scheduler/Scheduler.php +++ b/system/src/Grav/Common/Scheduler/Scheduler.php @@ -18,6 +18,8 @@ use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\Process; use RocketTheme\Toolbox\File\YamlFile; use Symfony\Component\Yaml\Yaml; +use Monolog\Logger; +use Monolog\Handler\StreamHandler; use function is_callable; use function is_string; @@ -76,8 +78,8 @@ class Scheduler /** @var string */ protected $historyPath; - /** @var JobHistory|null */ - protected $jobHistory = null; + /** @var Logger|null */ + protected $logger = null; /** @var array */ protected $modernConfig = []; @@ -97,11 +99,10 @@ class Scheduler Folder::create($this->status_path); } - // Initialize modern features if enabled + // Initialize modern features (always enabled now) $this->modernConfig = $grav['config']->get('scheduler.modern', []); - if ($this->modernConfig['enabled'] ?? false) { - $this->initializeModernFeatures($locator); - } + // Always initialize modern features - they're now part of core + $this->initializeModernFeatures($locator); } /** @@ -249,16 +250,31 @@ class Scheduler $runTime = new DateTime('now'); } + // Log scheduler run + if ($this->logger) { + $jobCount = count($alljobs); + $forceStr = $force ? ' (forced)' : ''; + $this->logger->debug("Scheduler run started - {$jobCount} jobs available{$forceStr}", [ + 'time' => $runTime->format('Y-m-d H:i:s') + ]); + } + // Process jobs based on modern features if ($this->jobQueue && ($this->modernConfig['queue']['enabled'] ?? false)) { // Queue jobs for processing + $queuedCount = 0; foreach ($alljobs as $job) { if ($job->isDue($runTime) || $force) { // Add to queue for concurrent processing $this->jobQueue->push($job); + $queuedCount++; } } + if ($this->logger && $queuedCount > 0) { + $this->logger->debug("Queued {$queuedCount} job(s) for processing"); + } + // Process queue with workers $this->processJobsWithWorkers(); @@ -287,6 +303,31 @@ class Scheduler } } + // Log run summary + if ($this->logger) { + $successCount = 0; + $failureCount = 0; + $executedJobs = array_merge($this->executed_jobs, $this->jobs_run); + + foreach ($executedJobs as $job) { + if ($job->isSuccessful()) { + $successCount++; + } else { + $failureCount++; + } + } + + if (count($executedJobs) > 0) { + if ($failureCount > 0) { + $this->logger->warning("Scheduler completed: {$successCount} succeeded, {$failureCount} failed"); + } else { + $this->logger->info("Scheduler completed: {$successCount} job(s) succeeded"); + } + } else { + $this->logger->debug('Scheduler completed: no jobs were due'); + } + } + // Store run date file_put_contents("logs/lastcron.run", (new DateTime("now"))->format("Y-m-d H:i:s"), LOCK_EX); } @@ -474,15 +515,14 @@ class Scheduler Folder::create($this->historyPath); } - // Initialize job queue + // Initialize job queue (always enabled) $this->jobQueue = new JobQueue($this->queuePath); - // Initialize job history - $retentionDays = $this->modernConfig['history']['retention_days'] ?? 30; - $this->jobHistory = new JobHistory($this->historyPath, $retentionDays); + // Initialize scheduler logger + $this->initializeLogger($locator); - // Configure workers - $this->maxWorkers = $this->modernConfig['workers'] ?? 1; + // Configure workers (default to 4 for concurrent processing) + $this->maxWorkers = $this->modernConfig['workers'] ?? 4; // Configure webhook $this->webhookEnabled = $this->modernConfig['webhook']['enabled'] ?? false; @@ -503,13 +543,28 @@ class Scheduler } /** - * Get the job history + * Initialize the scheduler logger * - * @return JobHistory|null + * @param $locator + * @return void */ - public function getHistory(): ?JobHistory + protected function initializeLogger($locator): void { - return $this->jobHistory; + $this->logger = new Logger('scheduler'); + + // Single scheduler log file - all levels + $logFile = $locator->findResource('log://scheduler.log', true, true); + $this->logger->pushHandler(new StreamHandler($logFile, Logger::DEBUG)); + } + + /** + * Get the scheduler logger + * + * @return Logger|null + */ + public function getLogger(): ?Logger + { + return $this->logger; } /** @@ -678,13 +733,26 @@ class Scheduler $worker['job']->finalize(); $this->saveJobState($worker['job']); - // Log background job completion to history - if ($this->jobHistory) { - $metadata = [ - 'queue_id' => $worker['queueId'] ?? null, - 'background' => true - ]; - $this->jobHistory->logExecution($worker['job'], $metadata); + // Log background job completion + if ($this->logger) { + $job = $worker['job']; + $jobId = $job->getId(); + $command = is_string($job->getCommand()) ? $job->getCommand() : 'Closure'; + + if ($job->isSuccessful()) { + $execTime = method_exists($job, 'getExecutionTime') ? $job->getExecutionTime() : null; + $timeStr = $execTime ? sprintf(' (%.2fs)', $execTime) : ''; + $this->logger->info("Job '{$jobId}' completed successfully{$timeStr}", [ + 'command' => $command, + 'background' => true + ]); + } else { + $error = trim($job->getOutput()) ?: 'Unknown error'; + $this->logger->error("Job '{$jobId}' failed: {$error}", [ + 'command' => $command, + 'background' => true + ]); + } } } @@ -775,9 +843,22 @@ class Scheduler } // Log foreground jobs immediately - if (!$job->runInBackground() && $this->jobHistory) { - $metadata = ['queue_id' => $queueId]; - $this->jobHistory->logExecution($job, $metadata); + if (!$job->runInBackground() && $this->logger) { + $jobId = $job->getId(); + $command = is_string($job->getCommand()) ? $job->getCommand() : 'Closure'; + + if ($job->isSuccessful()) { + $execTime = method_exists($job, 'getExecutionTime') ? $job->getExecutionTime() : null; + $timeStr = $execTime ? sprintf(' (%.2fs)', $execTime) : ''; + $this->logger->info("Job '{$jobId}' completed successfully{$timeStr}", [ + 'command' => $command + ]); + } else { + $error = trim($job->getOutput()) ?: 'Unknown error'; + $this->logger->error("Job '{$jobId}' failed: {$error}", [ + 'command' => $command + ]); + } } }