diff --git a/classes/plugin/Admin.php b/classes/plugin/Admin.php
index df1c7358..56958e5c 100644
--- a/classes/plugin/Admin.php
+++ b/classes/plugin/Admin.php
@@ -2413,7 +2413,7 @@ class Admin
*/
public function getLogFiles()
{
- $logs = new GravData(['grav.log' => 'Grav System Log', 'email.log' => 'Email Log']);
+ $logs = new GravData(['grav.log' => 'Grav System Log', 'email.log' => 'Email Log', 'scheduler.log' => 'Scheduler Log']);
Grav::instance()->fireEvent('onAdminLogFiles', new Event(['logs' => &$logs]));
return $logs->toArray();
}
diff --git a/languages/en.yaml b/languages/en.yaml
index 5e2ea962..0b924f08 100644
--- a/languages/en.yaml
+++ b/languages/en.yaml
@@ -798,7 +798,7 @@ PLUGIN_ADMIN:
STRICT_TWIG_COMPAT_HELP: "Enables deprecated Twig autoescape setting. When disabled, |raw filter is required to output HTML as Twig will autoescape output"
SCHEDULER: "Scheduler"
SCHEDULER_INSTALL_INSTRUCTIONS: "Install Instructions"
- SCHEDULER_INSTALLED_READY: "Installed and Ready"
+ SCHEDULER_INSTALLED_READY: "Scheduler Ready"
SCHEDULER_CRON_NA: "Cron Not Available for user: %s"
SCHEDULER_NOT_ENABLED: "Not Enabled for user: %s"
SCHEDULER_SETUP: "Scheduler Setup"
@@ -814,7 +814,7 @@ PLUGIN_ADMIN:
SCHEDULER_OUTPUT_TYPE_HELP: "Either append to the same file each run, or overwrite the file with each run"
SCHEDULER_EMAIL: "Email"
SCHEDULER_EMAIL_HELP: "Email to send output to. NOTE: requires output file to be set"
- SCHEDULER_WARNING: "The scheduler uses your system's crontab system to execute commands. You should use this only if you are an advanced user and know what you are doing. Misconfiguration or abuse can lead to security vulnerabilities."
+ SCHEDULER_WARNING: "The scheduler can use either system crontab or webhook triggers to execute commands. Webhooks are recommended for cloud environments. Only advanced users should configure custom jobs. Misconfiguration or abuse can lead to security vulnerabilities."
SECURITY: "Security"
XSS_SECURITY: "XSS Security for Content"
XSS_WHITELIST_PERMISSIONS: "Whitelist Permissions"
diff --git a/themes/grav/js/scheduler-admin.js b/themes/grav/js/scheduler-admin.js
new file mode 100644
index 00000000..60df75fe
--- /dev/null
+++ b/themes/grav/js/scheduler-admin.js
@@ -0,0 +1,144 @@
+/**
+ * Scheduler Admin JavaScript
+ * Handles dynamic loading of scheduler status in admin panel
+ */
+
+(function() {
+ 'use strict';
+
+ // Wait for DOM to be ready
+ document.addEventListener('DOMContentLoaded', function() {
+ // Check if we're on the scheduler config page
+ const healthStatusEl = document.getElementById('scheduler-health-status');
+ const triggersEl = document.getElementById('scheduler-triggers');
+
+ if (!healthStatusEl && !triggersEl) {
+ return; // Not on scheduler page
+ }
+
+ // Load scheduler status
+ loadSchedulerStatus();
+
+ // Refresh every 30 seconds if page is visible
+ let refreshInterval = setInterval(function() {
+ if (!document.hidden) {
+ loadSchedulerStatus();
+ }
+ }, 30000);
+
+ // Clean up interval when leaving page
+ window.addEventListener('beforeunload', function() {
+ clearInterval(refreshInterval);
+ });
+ });
+
+ /**
+ * Load scheduler status via AJAX
+ */
+ function loadSchedulerStatus() {
+ const healthStatusEl = document.getElementById('scheduler-health-status');
+ const triggersEl = document.getElementById('scheduler-triggers');
+
+ // Get the admin base URL
+ const adminBase = GravAdmin ? GravAdmin.config.base_url_relative : '/admin';
+ const nonce = GravAdmin ? GravAdmin.config.admin_nonce : '';
+
+ // Make AJAX request
+ fetch(adminBase + '/scheduler/status', {
+ method: 'GET',
+ headers: {
+ 'Accept': 'application/json',
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'Admin-Nonce': nonce
+ },
+ credentials: 'same-origin'
+ })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+ return response.json();
+ })
+ .then(data => {
+ // Update health status
+ if (healthStatusEl && data.health) {
+ healthStatusEl.innerHTML = data.health;
+ healthStatusEl.classList.remove('text-muted');
+ }
+
+ // Update triggers
+ if (triggersEl && data.triggers) {
+ triggersEl.innerHTML = data.triggers;
+ triggersEl.classList.remove('text-muted');
+ }
+ })
+ .catch(error => {
+ console.error('Error loading scheduler status:', error);
+
+ // Show error message
+ if (healthStatusEl) {
+ healthStatusEl.innerHTML = '
Failed to load status
';
+ }
+ if (triggersEl) {
+ triggersEl.innerHTML = 'Failed to load triggers
';
+ }
+ });
+ }
+
+ /**
+ * Test scheduler webhook
+ */
+ window.testSchedulerWebhook = function() {
+ const token = document.querySelector('input[name="data[scheduler][modern][webhook][token]"]')?.value;
+
+ if (!token) {
+ alert('Please set a webhook token first');
+ return;
+ }
+
+ const siteUrl = window.location.origin;
+ const webhookUrl = siteUrl + '/scheduler/webhook';
+
+ fetch(webhookUrl, {
+ method: 'POST',
+ headers: {
+ 'Authorization': 'Bearer ' + token,
+ 'Accept': 'application/json'
+ }
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ alert('Webhook test successful! Jobs run: ' + (data.jobs_run || 0));
+ } else {
+ alert('Webhook test failed: ' + (data.message || 'Unknown error'));
+ }
+ })
+ .catch(error => {
+ alert('Webhook test error: ' + error.message);
+ });
+ };
+
+ /**
+ * Generate secure token
+ */
+ window.generateSchedulerToken = function() {
+ const tokenField = document.querySelector('input[name="data[scheduler][modern][webhook][token]"]');
+
+ if (!tokenField) {
+ return;
+ }
+
+ // Generate random token (32 bytes = 64 hex chars)
+ const array = new Uint8Array(32);
+ crypto.getRandomValues(array);
+ const token = Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
+
+ tokenField.value = token;
+
+ // Trigger change event
+ const event = new Event('change', { bubbles: true });
+ tokenField.dispatchEvent(event);
+ };
+
+})();
\ No newline at end of file
diff --git a/themes/grav/templates/forms/fields/webhook-status/webhook-status.html.twig b/themes/grav/templates/forms/fields/webhook-status/webhook-status.html.twig
new file mode 100644
index 00000000..6a9f4e74
--- /dev/null
+++ b/themes/grav/templates/forms/fields/webhook-status/webhook-status.html.twig
@@ -0,0 +1,33 @@
+{% extends "forms/field.html.twig" %}
+
+{% block field %}
+
+ {% set plugin_exists = config.plugins['scheduler-webhook'] is defined %}
+ {% set plugin_enabled = plugin_exists and config.plugins['scheduler-webhook'].enabled %}
+
+ {% if not plugin_exists %}
+ {# Plugin not installed #}
+
+
Webhook Plugin Required
+ The
scheduler-webhook plugin is required for webhook functionality.
+
+ Install Plugin Now
+
+
or run: bin/gpm install scheduler-webhook
+
+ {% elseif not plugin_enabled %}
+ {# Plugin installed but disabled #}
+
+
Webhook Plugin Installed
+ The scheduler-webhook plugin is installed but disabled.
+
Enable it in plugin settings to use webhook functionality.
+
+ {% else %}
+ {# Plugin installed and enabled #}
+
+ Webhook Plugin Ready!
+ The scheduler-webhook plugin is installed and active. Configure your webhook settings below.
+
+ {% endif %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/themes/grav/templates/partials/footer.html.twig b/themes/grav/templates/partials/footer.html.twig
index 468f7a80..19f5aa84 100644
--- a/themes/grav/templates/partials/footer.html.twig
+++ b/themes/grav/templates/partials/footer.html.twig
@@ -1,5 +1,5 @@
{% if custom_admin_footer %}
{{ custom_admin_footer|raw }}
{% else %}
- Grav v{{ constant('GRAV_VERSION') }} - Admin v{{ admin_version }} - {{ "PLUGIN_ADMIN.WAS_MADE_WITH"|t|lower }} {{ "PLUGIN_ADMIN.BY"|t|lower }} Trilby Media.
+ Grav v{{ constant('GRAV_VERSION') }} - Admin v{{ admin_version }} - {{ "PLUGIN_ADMIN.WAS_MADE_WITH"|t|lower }} {{ "PLUGIN_ADMIN.BY"|t|lower }} Trilby Media.
{% endif %}
diff --git a/themes/grav/templates/partials/tools-scheduler.html.twig b/themes/grav/templates/partials/tools-scheduler.html.twig
index 06ba1f18..820e1eaf 100644
--- a/themes/grav/templates/partials/tools-scheduler.html.twig
+++ b/themes/grav/templates/partials/tools-scheduler.html.twig
@@ -3,12 +3,30 @@
{% set data = admin.data('config/scheduler') %}
{% set cron_status = grav.scheduler.isCrontabSetup() %}
{% set user = grav.scheduler.whoami() %}
+ {% set webhook_enabled = grav.scheduler.isWebhookEnabled() %}
+ {% set active_triggers = grav.scheduler.getActiveTriggers() %}
- {% if cron_status == 1 %}
-
-
{{ "PLUGIN_ADMIN.SCHEDULER_INSTALL_INSTRUCTIONS"|t }}
-
{{ "PLUGIN_ADMIN.SCHEDULER_INSTALLED_READY"|t }}
-
+ {% if active_triggers|length > 0 %}
+ {# We have at least one active trigger method #}
+ {% if 'webhook' in active_triggers and 'cron' not in active_triggers %}
+ {# Webhook only mode #}
+
+
Webhook Active - Scheduler is ready to receive webhook triggers
+
{{ "PLUGIN_ADMIN.SCHEDULER_INSTALL_INSTRUCTIONS"|t }}
+
+ {% elseif 'cron' in active_triggers and 'webhook' in active_triggers %}
+ {# Both cron and webhook #}
+
+
Cron & Webhook Active - Scheduler is running via cron and accepts webhook triggers
+
{{ "PLUGIN_ADMIN.SCHEDULER_INSTALL_INSTRUCTIONS"|t }}
+
+ {% elseif 'cron' in active_triggers %}
+ {# Cron only #}
+
+
{{ "PLUGIN_ADMIN.SCHEDULER_INSTALLED_READY"|t }}
+
{{ "PLUGIN_ADMIN.SCHEDULER_INSTALL_INSTRUCTIONS"|t }}
+
+ {% endif %}
{% elseif cron_status == 2 %}
{{ "PLUGIN_ADMIN.SCHEDULER_CRON_NA"|t([user])|raw }}
{% else %}
@@ -17,7 +35,18 @@
{{ "PLUGIN_ADMIN.SCHEDULER_WARNING"|t([user]) }}
-