more scheduler improvements

Signed-off-by: Andy Miller <rhuk@mac.com>
This commit is contained in:
Andy Miller
2025-08-25 10:05:19 +01:00
parent 8e2a54f2c4
commit aec62290d4
14 changed files with 216 additions and 10 deletions

View File

@@ -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();
}

View File

@@ -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: <b>%s</b>"
SCHEDULER_NOT_ENABLED: "Not Enabled for user: <b>%s</b>"
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"

144
themes/grav/js/scheduler-admin.js vendored Normal file
View File

@@ -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 = '<div class="alert alert-danger">Failed to load status</div>';
}
if (triggersEl) {
triggersEl.innerHTML = '<div class="alert alert-danger">Failed to load triggers</div>';
}
});
}
/**
* 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);
};
})();

View File

@@ -0,0 +1,33 @@
{% extends "forms/field.html.twig" %}
{% block field %}
<div class="webhook-status-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 #}
<div class="alert alert-warning">
<strong>Webhook Plugin Required</strong><br>
The <code>scheduler-webhook</code> plugin is required for webhook functionality.<br><br>
<a class="button button-primary" href="{{ base_url_relative }}/plugins/install/scheduler-webhook">
<i class="fa fa-download"></i> Install Plugin Now
</a>
<span class="hint" style="margin-left: 10px;">or run: <code>bin/gpm install scheduler-webhook</code></span>
</div>
{% elseif not plugin_enabled %}
{# Plugin installed but disabled #}
<div class="alert alert-info">
<i class="fa fa-info-circle"></i> <strong>Webhook Plugin Installed</strong><br>
The scheduler-webhook plugin is installed but disabled.
<a href="{{ base_url_relative }}/plugins/scheduler-webhook">Enable it in plugin settings</a> to use webhook functionality.
</div>
{% else %}
{# Plugin installed and enabled #}
<div class="alert alert-success">
<i class="fa fa-check-circle"></i> <strong>Webhook Plugin Ready!</strong><br>
The scheduler-webhook plugin is installed and active. Configure your webhook settings below.
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -1,5 +1,5 @@
{% if custom_admin_footer %}
{{ custom_admin_footer|raw }}
{% else %}
<a href="https://getgrav.org" target="_blank" rel="noopener noreferrer">Grav</a> v<span class="grav-version">{{ constant('GRAV_VERSION') }}</span> - Admin v{{ admin_version }} - {{ "PLUGIN_ADMIN.WAS_MADE_WITH"|t|lower }} <i class="fa fa-heart-o pulse"></i> {{ "PLUGIN_ADMIN.BY"|t|lower }} <a href="https://trilby.media" target="_blank" rel="noopener noreferrer">Trilby Media</a>.
<a href="https://getgrav.org" target="_blank" rel="noopener noreferrer"><i class="fa fa-grav"></i> Grav</a> v<span class="grav-version">{{ constant('GRAV_VERSION') }}</span> - Admin v{{ admin_version }} - {{ "PLUGIN_ADMIN.WAS_MADE_WITH"|t|lower }} <i class="fa fa-heart-o pulse"></i> {{ "PLUGIN_ADMIN.BY"|t|lower }} <a href="https://trilby.media" target="_blank" rel="noopener noreferrer">Trilby Media</a>.
{% endif %}

View File

@@ -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 %}
<div class="alert notice secondary-accent">
<div id="show-instructions" class="button button-small"><i class="fa fa-clock-o"></i> {{ "PLUGIN_ADMIN.SCHEDULER_INSTALL_INSTRUCTIONS"|t }}</div>
<i class="fa fa-check"></i> {{ "PLUGIN_ADMIN.SCHEDULER_INSTALLED_READY"|t }}
</div>
{% 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 #}
<div class="alert notice">
<i class="fa fa-plug"></i> <strong>Webhook Active</strong> - Scheduler is ready to receive webhook triggers
<div id="show-instructions" class="button button-small button-outline float-right"><i class="fa fa-clock-o"></i> {{ "PLUGIN_ADMIN.SCHEDULER_INSTALL_INSTRUCTIONS"|t }}</div>
</div>
{% elseif 'cron' in active_triggers and 'webhook' in active_triggers %}
{# Both cron and webhook #}
<div class="alert notice secondary-accent">
<i class="fa fa-check"></i> <strong>Cron & Webhook Active</strong> - Scheduler is running via cron and accepts webhook triggers
<div id="show-instructions" class="button button-small button-outline float-right"><i class="fa fa-clock-o"></i> {{ "PLUGIN_ADMIN.SCHEDULER_INSTALL_INSTRUCTIONS"|t }}</div>
</div>
{% elseif 'cron' in active_triggers %}
{# Cron only #}
<div class="alert notice secondary-accent">
<i class="fa fa-check"></i> {{ "PLUGIN_ADMIN.SCHEDULER_INSTALLED_READY"|t }}
<div id="show-instructions" class="button button-small button-outline float-right"><i class="fa fa-clock-o"></i> {{ "PLUGIN_ADMIN.SCHEDULER_INSTALL_INSTRUCTIONS"|t }}</div>
</div>
{% endif %}
{% elseif cron_status == 2 %}
<div class="alert warning"> {{ "PLUGIN_ADMIN.SCHEDULER_CRON_NA"|t([user])|raw }}</div>
{% else %}
@@ -17,7 +35,18 @@
<div class="alert notice"><i class="fa fa-exclamation-circle"></i> {{ "PLUGIN_ADMIN.SCHEDULER_WARNING"|t([user]) }}</div>
<div id="cron-install" class="form-border overlay {{ cron_status == 1 ? 'hide' : ''}}">
<div id="cron-install" class="form-border overlay {{ (active_triggers|length > 0) ? 'hide' : ''}}">
{% if webhook_enabled %}
<h3>Webhook Setup</h3>
<p>The scheduler is configured to use webhooks. To trigger jobs via webhook:</p>
<pre><code>curl -X POST {{ grav.base_url_absolute }}/scheduler/webhook \
-H "Authorization: Bearer YOUR_TOKEN"</code></pre>
<p>Make sure the <strong>scheduler-webhook</strong> plugin is installed and enabled.</p>
<hr>
<h3>Alternative: Cron Setup</h3>
{% endif %}
<pre><code>{{- grav.scheduler.getCronCommand()|trim -}}</code></pre>
<p>{{ "PLUGIN_ADMIN.SCHEDULER_POST_INSTRUCTIONS"|t([user])|raw }}</p>