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: |
-
-
- markdown: false
+ // Refresh every 30 seconds
+ setInterval(loadHealthStatus, 30000);
+ })();
+
+ markdown: false
- trigger_methods:
- type: display
- label: Active Triggers
- content: |
-
-
- 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
+ ]);
+ }
}
}