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]) }}
-
+
+ {% if webhook_enabled %} +

Webhook Setup

+

The scheduler is configured to use webhooks. To trigger jobs via webhook:

+
curl -X POST {{ grav.base_url_absolute }}/scheduler/webhook \
+  -H "Authorization: Bearer YOUR_TOKEN"
+

Make sure the scheduler-webhook plugin is installed and enabled.

+ +
+

Alternative: Cron Setup

+ {% endif %} +
{{- grav.scheduler.getCronCommand()|trim -}}

{{ "PLUGIN_ADMIN.SCHEDULER_POST_INSTRUCTIONS"|t([user])|raw }}

diff --git a/themes/grav/webfonts/fa-brands-400.ttf b/themes/grav/webfonts/fa-brands-400.ttf deleted file mode 100644 index 0f82a836..00000000 Binary files a/themes/grav/webfonts/fa-brands-400.ttf and /dev/null differ diff --git a/themes/grav/webfonts/fa-brands-400.woff2 b/themes/grav/webfonts/fa-brands-400.woff2 index 3c5cf97e..4bb9e620 100644 Binary files a/themes/grav/webfonts/fa-brands-400.woff2 and b/themes/grav/webfonts/fa-brands-400.woff2 differ diff --git a/themes/grav/webfonts/fa-regular-400.ttf b/themes/grav/webfonts/fa-regular-400.ttf deleted file mode 100644 index 9ee1919d..00000000 Binary files a/themes/grav/webfonts/fa-regular-400.ttf and /dev/null differ diff --git a/themes/grav/webfonts/fa-regular-400.woff2 b/themes/grav/webfonts/fa-regular-400.woff2 index 57d91796..644a834d 100644 Binary files a/themes/grav/webfonts/fa-regular-400.woff2 and b/themes/grav/webfonts/fa-regular-400.woff2 differ diff --git a/themes/grav/webfonts/fa-solid-900.ttf b/themes/grav/webfonts/fa-solid-900.ttf deleted file mode 100644 index 1c10972e..00000000 Binary files a/themes/grav/webfonts/fa-solid-900.ttf and /dev/null differ diff --git a/themes/grav/webfonts/fa-solid-900.woff2 b/themes/grav/webfonts/fa-solid-900.woff2 index 16721020..6b79def0 100644 Binary files a/themes/grav/webfonts/fa-solid-900.woff2 and b/themes/grav/webfonts/fa-solid-900.woff2 differ diff --git a/themes/grav/webfonts/fa-v4compatibility.ttf b/themes/grav/webfonts/fa-v4compatibility.ttf deleted file mode 100644 index 3bcb67ff..00000000 Binary files a/themes/grav/webfonts/fa-v4compatibility.ttf and /dev/null differ diff --git a/themes/grav/webfonts/fa-v4compatibility.woff2 b/themes/grav/webfonts/fa-v4compatibility.woff2 index fbafb222..c5eb3351 100644 Binary files a/themes/grav/webfonts/fa-v4compatibility.woff2 and b/themes/grav/webfonts/fa-v4compatibility.woff2 differ