gravtest/system/blueprints/config/scheduler.yaml
2026-01-16 14:41:07 +01:00

793 lines
56 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

title: PLUGIN_ADMIN.SCHEDULER
form:
validation: loose
fields:
scheduler_tabs:
type: tabs
active: 1
fields:
status_tab:
type: tab
title: PLUGIN_ADMIN.SCHEDULER_STATUS
fields:
status_title:
type: section
title: PLUGIN_ADMIN.SCHEDULER_STATUS
underline: true
status:
type: cronstatus
validate:
type: commalist
webhook_status_override:
type: display
label:
content: |
<script>
(function() {
function updateSchedulerStatus() {
// Find all notice bars
var notices = document.querySelectorAll('.notice');
var webhookStatusChecked = false;
// Check for modern scheduler and webhook settings
fetch(window.location.origin + '/grav-editor-pro/scheduler/health')
.then(response => response.json())
.then(data => {
if (data.webhook_enabled) {
notices.forEach(function(notice) {
if (notice.textContent.includes('Not Enabled for user:')) {
// This is the cron status notice - replace it
notice.className = 'notice info';
notice.innerHTML = '<i class="fa fa-fw fa-check-circle"></i> <strong>Webhook Active</strong> - Scheduler can be triggered via webhook. Cron is not configured.';
}
});
// Also update the main status if it exists
var statusDiv = document.querySelector('.cronstatus-status');
if (statusDiv && statusDiv.textContent.includes('Not Enabled')) {
statusDiv.className = 'cronstatus-status success';
statusDiv.innerHTML = '<i class="fa fa-fw fa-check"></i> Webhook Ready';
}
}
})
.catch(error => {
console.log('Webhook status check failed:', error);
});
}
// Run on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', updateSchedulerStatus);
} else {
updateSchedulerStatus();
}
// Also run after a short delay to catch any late-rendered elements
setTimeout(updateSchedulerStatus, 500);
})();
</script>
markdown: false
status_enhanced:
type: display
label:
content: |
<script>
document.addEventListener('DOMContentLoaded', function() {
// Check if webhook is enabled
var webhookEnabled = document.querySelector('[name="data[scheduler][modern][webhook][enabled]"]:checked');
var statusDiv = document.querySelector('.cronstatus-status');
// Also find the parent notice bar
var noticeBar = document.querySelector('.notice.alert');
if (statusDiv) {
var currentStatus = statusDiv.textContent || statusDiv.innerText;
var cronReady = currentStatus.includes('Ready');
var cronNotEnabled = currentStatus.includes('Not Enabled');
// Check if scheduler-webhook plugin exists
var webhookPluginInstalled = false;
fetch(window.location.origin + '/grav-editor-pro/scheduler/health')
.then(response => response.json())
.then(data => {
webhookPluginInstalled = true;
updateStatusDisplay(data);
})
.catch(error => {
updateStatusDisplay(null);
});
function updateStatusDisplay(healthData) {
var isWebhookEnabled = webhookEnabled && webhookEnabled.value == '1';
var isWebhookReady = webhookPluginInstalled && isWebhookEnabled && healthData && healthData.webhook_enabled;
// Update the main status text
var mainStatusText = '';
var mainStatusClass = '';
if (cronReady && isWebhookReady) {
mainStatusText = 'Cron and Webhook Ready';
mainStatusClass = 'success';
} else if (cronReady) {
mainStatusText = 'Cron Ready';
mainStatusClass = 'success';
} else if (isWebhookReady) {
mainStatusText = 'Webhook Ready (No Cron)';
mainStatusClass = 'success'; // Changed from warning to success
} else if (cronNotEnabled && !isWebhookReady) {
mainStatusText = 'Not Configured';
mainStatusClass = 'error';
} else {
mainStatusText = 'Configuration Pending';
mainStatusClass = 'warning';
}
// Update the notice bar if webhooks are ready
if (noticeBar && isWebhookReady) {
// Change from error (red) to success (green) or info (blue)
noticeBar.classList.remove('alert');
noticeBar.classList.add('info');
var noticeIcon = noticeBar.querySelector('i.fa');
if (noticeIcon) {
noticeIcon.classList.remove('fa-times-circle');
noticeIcon.classList.add('fa-check-circle');
}
var noticeText = noticeBar.querySelector('strong') || noticeBar;
var username = noticeText.textContent.match(/user:\s*(\w+)/);
if (username) {
noticeText.innerHTML = 'Webhook Ready for user: <b>' + username[1] + '</b> (Cron not configured)';
} else {
noticeText.innerHTML = mainStatusText;
}
}
// Update the main status div
if (statusDiv) {
statusDiv.innerHTML = '<i class="fa fa-fw fa-' +
(mainStatusClass === 'success' ? 'check' : mainStatusClass === 'warning' ? 'exclamation' : 'times') +
'"></i> ' + mainStatusText;
statusDiv.className = 'cronstatus-status ' + mainStatusClass;
}
// Update install instructions button/content
var installButton = document.querySelector('.cronstatus-install-button');
var installDiv = document.querySelector('.cronstatus-install');
if (installDiv) {
var installHtml = '<div class="alert alert-info">';
installHtml += '<h4>Setup Instructions:</h4>';
var hasInstructions = false;
// Cron setup
if (!cronReady) {
installHtml += '<p><strong>Option 1: Traditional Cron</strong><br>';
installHtml += 'Run: <code>bin/grav scheduler --install</code><br>';
installHtml += 'This will add a cron job that runs every minute.</p>';
hasInstructions = true;
}
// Webhook setup
if (!webhookPluginInstalled) {
installHtml += '<p><strong>Option 2: Webhook Support</strong><br>';
installHtml += '1. Install plugin: <code>bin/gpm install scheduler-webhook</code><br>';
installHtml += '2. Configure webhook token in Advanced Features tab<br>';
installHtml += '3. Use webhook URL in your CI/CD or cloud scheduler</p>';
hasInstructions = true;
} else if (!isWebhookEnabled) {
installHtml += '<p><strong>Webhook Plugin Installed</strong><br>';
installHtml += 'Enable webhooks in Advanced Features tab and set a secure token.</p>';
hasInstructions = true;
} else if (isWebhookReady) {
installHtml += '<p><strong>✅ Webhook is Active!</strong><br>';
installHtml += 'Trigger URL: <code>' + window.location.origin + '/grav-editor-pro/scheduler/webhook</code><br>';
installHtml += 'Use with Authorization header: <code>Bearer YOUR_TOKEN</code></p>';
if (!cronReady) {
installHtml += '<p class="text-muted"><small>Note: No cron job configured. Scheduler runs only via webhook triggers.</small></p>';
}
}
if (!hasInstructions && cronReady) {
installHtml += '<p><strong>✅ Cron is configured and ready!</strong><br>';
installHtml += 'The scheduler runs automatically every minute via system cron.</p>';
}
installHtml += '</div>';
installDiv.innerHTML = installHtml;
// Update button text based on status
if (installButton) {
if (cronReady && isWebhookReady) {
installButton.innerHTML = '<i class="fa fa-info-circle"></i> Configuration Details';
} else if (cronReady || isWebhookReady) {
installButton.innerHTML = '<i class="fa fa-plus-circle"></i> Add More Triggers';
} else {
installButton.innerHTML = '<i class="fa fa-exclamation-triangle"></i> Install Instructions';
}
}
}
}
}
});
</script>
modern_health:
type: display
label: Health Status
content: |
<div id="scheduler-health-status">
<div class="text-muted">Checking health...</div>
</div>
<script>
(function() {
function loadHealthStatus() {
fetch(window.location.origin + '/grav-editor-pro/scheduler/health')
.then(response => response.json())
.then(data => {
var statusEl = document.getElementById('scheduler-health-status');
if (!statusEl) return;
// Modern card-based layout
var statusColor = '#6c757d';
var statusLabel = data.status || 'unknown';
if (data.status === 'healthy') statusColor = '#28a745';
else if (data.status === 'warning') statusColor = '#ffc107';
else if (data.status === 'critical') statusColor = '#dc3545';
var html = '<div style="display: flex; flex-direction: column; gap: 1rem;">';
// Status card
html += '<div style="display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 1rem; background: linear-gradient(135deg, #f8f9fa 0%, #fff 100%); border-radius: 6px; border: 1px solid #e9ecef; box-shadow: 0 1px 3px rgba(0,0,0,0.05);">';
html += '<span style="font-weight: 500; color: #495057;">Status:</span>';
html += '<span style="background: ' + statusColor + '; color: white; padding: 0.375rem 0.75rem; font-size: 0.875rem; font-weight: 500; border-radius: 4px; text-transform: uppercase; letter-spacing: 0.025em;">' + statusLabel + '</span>';
html += '</div>';
// Info grid
html += '<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem;">';
// Last run card
html += '<div style="background: white; border: 1px solid #e9ecef; border-radius: 6px; padding: 0.75rem; box-shadow: 0 1px 2px rgba(0,0,0,0.03);">';
html += '<div style="color: #6c757d; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.25rem;">Last Run</div>';
if (data.last_run) {
var age = data.last_run_age;
var ageText = 'just now';
if (age > 86400) {
ageText = Math.floor(age / 86400) + ' day(s) ago';
} else if (age > 3600) {
ageText = Math.floor(age / 3600) + ' hour(s) ago';
} else if (age > 60) {
ageText = Math.floor(age / 60) + ' minute(s) ago';
} else if (age > 0) {
ageText = age + ' second(s) ago';
}
html += '<div style="font-size: 1rem; color: #212529; font-weight: 500;">' + ageText + '</div>';
} else {
html += '<div style="font-size: 1rem; color: #6c757d;">Never</div>';
}
html += '</div>';
// Jobs count card
html += '<div style="background: white; border: 1px solid #e9ecef; border-radius: 6px; padding: 0.75rem; box-shadow: 0 1px 2px rgba(0,0,0,0.03);">';
html += '<div style="color: #6c757d; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.25rem;">Scheduled Jobs</div>';
html += '<div style="font-size: 1rem; color: #212529; font-weight: 500;">' + (data.scheduled_jobs || 0) + '</div>';
html += '</div>';
html += '</div>'; // Close grid
// Additional info if available
if (data.modern_features && data.queue_size !== undefined) {
html += '<div style="background: white; border: 1px solid #e9ecef; border-radius: 6px; padding: 0.75rem; box-shadow: 0 1px 2px rgba(0,0,0,0.03);">';
html += '<span style="color: #6c757d; font-size: 0.875rem;">Queue Size: </span>';
html += '<span style="font-weight: 500;">' + data.queue_size + '</span>';
html += '</div>';
}
// Failed jobs warning
if (data.failed_jobs_24h > 0) {
html += '<div style="background: #fff5f5; border: 1px solid #feb2b2; border-radius: 6px; padding: 0.75rem; color: #c53030;">';
html += '<strong>⚠️ Failed Jobs (24h):</strong> ' + data.failed_jobs_24h;
html += '</div>';
}
html += '</div>'; // Close main container
statusEl.innerHTML = html;
})
.catch(error => {
var statusEl = document.getElementById('scheduler-health-status');
if (statusEl) {
statusEl.innerHTML = '<div class="alert alert-warning">Unable to fetch health status. Ensure scheduler-webhook plugin is installed.</div>';
}
});
}
// Load on page ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadHealthStatus);
} else {
loadHealthStatus();
}
// Refresh every 30 seconds
setInterval(loadHealthStatus, 30000);
})();
</script>
markdown: false
trigger_methods:
type: display
label: Active Triggers
content: |
<div id="scheduler-triggers">
<div class="text-muted">Checking triggers...</div>
</div>
<script>
(function() {
function loadTriggers() {
// Check cron status from the main status field
var cronReady = false;
var statusDiv = document.querySelector('.cronstatus-status');
if (statusDiv) {
var statusText = statusDiv.textContent || statusDiv.innerText;
cronReady = statusText.includes('Ready');
}
// Check webhook status
fetch(window.location.origin + '/grav-editor-pro/scheduler/health')
.then(response => response.json())
.then(data => {
var triggersEl = document.getElementById('scheduler-triggers');
if (!triggersEl) return;
var html = '<div style="display: flex; flex-direction: column; gap: 0.5rem;">';
// Cron trigger card
var cronIcon = cronReady ? '✅' : '❌';
var cronStatus = cronReady ? 'Active' : 'Not Configured';
var cronStatusColor = cronReady ? '#28a745' : '#6c757d';
var cardBg = cronReady ? '#f8f9fa' : '#fff';
html += '<div style="display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 1rem; background: ' + cardBg + '; border: 1px solid #e9ecef; border-radius: 4px;">';
html += '<div style="display: flex; align-items: center; gap: 0.75rem;">';
html += '<span style="font-size: 1.25rem; line-height: 1;">' + cronIcon + '</span>';
html += '<span style="font-weight: 500; color: #212529; font-size: 1rem;">Cron:</span>';
html += '</div>';
html += '<span style="background: ' + cronStatusColor + '; color: white; padding: 0.25rem 0.75rem; font-size: 0.875rem; font-weight: 500; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.025em;">' + cronStatus + '</span>';
html += '</div>';
// Webhook trigger card
if (data.webhook_enabled) {
html += '<div style="display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 1rem; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px;">';
html += '<div style="display: flex; align-items: center; gap: 0.75rem;">';
html += '<span style="font-size: 1.25rem; line-height: 1;">✅</span>';
html += '<span style="font-weight: 500; color: #212529; font-size: 1rem;">Webhook:</span>';
html += '</div>';
html += '<span style="background: #28a745; color: white; padding: 0.25rem 0.75rem; font-size: 0.875rem; font-weight: 500; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.025em;">ACTIVE</span>';
html += '</div>';
} else {
// Show webhook as not configured/disabled
html += '<div style="display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 1rem; background: #fff; border: 1px solid #e9ecef; border-radius: 4px;">';
html += '<div style="display: flex; align-items: center; gap: 0.75rem;">';
html += '<span style="font-size: 1.25rem; line-height: 1;">⚠️</span>';
html += '<span style="font-weight: 500; color: #212529; font-size: 1rem;">Webhook:</span>';
html += '</div>';
html += '<span style="background: #ffc107; color: #212529; padding: 0.25rem 0.75rem; font-size: 0.875rem; font-weight: 500; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.025em;">DISABLED</span>';
html += '</div>';
}
html += '</div>';
// Add warning if no triggers active
if (!cronReady && !data.webhook_enabled) {
html += '<div class="alert alert-warning" style="margin-top: 1rem;"><i class="fa fa-exclamation-triangle"></i> No triggers active! Configure cron or enable webhooks.</div>';
}
triggersEl.innerHTML = html;
})
.catch(error => {
var triggersEl = document.getElementById('scheduler-triggers');
if (triggersEl) {
// Show just cron status if health endpoint not available
var html = '<ul class="list-unstyled">';
if (cronReady) {
html += '<li>✅ <strong>Cron:</strong> <span class="badge badge-success">Active</span></li>';
} else {
html += '<li>❌ <strong>Cron:</strong> <span class="badge badge-secondary">Not Configured</span></li>';
}
html += '<li>⚠️ <strong>Webhook:</strong> <span class="badge badge-secondary">Plugin Not Installed</span></li>';
html += '</ul>';
triggersEl.innerHTML = html;
}
});
}
// Load on page ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadTriggers);
} else {
loadTriggers();
}
})();
</script>
markdown: false
jobs_tab:
type: tab
title: PLUGIN_ADMIN.SCHEDULER_JOBS
fields:
jobs_title:
type: section
title: PLUGIN_ADMIN.SCHEDULER_JOBS
underline: true
custom_jobs:
type: list
style: vertical
label:
classes: cron-job-list compact
key: id
fields:
.id:
type: key
label: ID
placeholder: 'process-name'
validate:
required: true
pattern: '[a-zа-я0-9_\-]+'
max: 20
message: 'ID must be lowercase with dashes/underscores only and less than 20 characters'
.command:
type: text
label: PLUGIN_ADMIN.COMMAND
placeholder: 'ls'
validate:
required: true
.args:
type: text
label: PLUGIN_ADMIN.EXTRA_ARGUMENTS
placeholder: '-lah'
.at:
type: text
wrapper_classes: cron-selector
label: PLUGIN_ADMIN.SCHEDULER_RUNAT
help: PLUGIN_ADMIN.SCHEDULER_RUNAT_HELP
placeholder: '* * * * *'
validate:
required: true
.output:
type: text
label: PLUGIN_ADMIN.SCHEDULER_OUTPUT
help: PLUGIN_ADMIN.SCHEDULER_OUTPUT_HELP
placeholder: 'logs/ls-cron.out'
.output_mode:
type: select
label: PLUGIN_ADMIN.SCHEDULER_OUTPUT_TYPE
help: PLUGIN_ADMIN.SCHEDULER_OUTPUT_TYPE_HELP
default: append
options:
append: Append
overwrite: Overwrite
.email:
type: text
label: PLUGIN_ADMIN.SCHEDULER_EMAIL
help: PLUGIN_ADMIN.SCHEDULER_EMAIL_HELP
placeholder: 'notifications@yoursite.com'
modern_tab:
type: tab
title: Advanced Features
fields:
workers_section:
type: section
title: Worker Configuration
underline: true
fields:
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_status:
type: webhook-status
label:
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: |
<div style="margin-top: -10px; margin-bottom: 15px;">
<button type="button" class="button button-primary" onclick="generateWebhookToken()">
<i class="fa fa-refresh"></i> Generate Token
</button>
</div>
<script>
function generateWebhookToken() {
try {
// Generate token
const array = new Uint8Array(32);
crypto.getRandomValues(array);
const token = Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
// Try multiple selectors to find the field
let field = document.querySelector('[name="data[scheduler][modern][webhook][token]"]');
if (!field) {
field = document.querySelector('input[name*="webhook][token"]');
}
if (!field) {
field = document.getElementById('scheduler-modern-webhook-token');
}
if (!field) {
// Look for any text input in the webhook section
const webhookSection = document.querySelector('.webhook_section');
if (webhookSection) {
const inputs = webhookSection.querySelectorAll('input[type="text"]');
// Find the token field by checking for the placeholder
for (let input of inputs) {
if (input.placeholder && input.placeholder.includes('Generate')) {
field = input;
break;
}
}
}
}
if (field) {
field.value = token;
field.dispatchEvent(new Event('change', { bubbles: true }));
field.dispatchEvent(new Event('input', { bubbles: true }));
// Flash the field to show it was updated
field.style.backgroundColor = '#d4edda';
setTimeout(function() {
field.style.backgroundColor = '';
}, 500);
// Also try to trigger Grav's form change detection
if (window.jQuery) {
jQuery(field).trigger('change');
}
} else {
// Log more debugging info
console.error('Token field not found. Looking for input fields...');
console.log('All inputs:', document.querySelectorAll('input[type="text"]'));
alert('Could not find the token field. Please ensure you are in the Advanced Features tab and the Webhook Configuration section is visible.');
}
} catch (e) {
console.error('Error generating token:', e);
alert('Error generating token: ' + e.message);
}
}
</script>
markdown: false
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
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'
webhook_usage:
type: section
title: Usage Examples
underline: true
fields:
webhook_examples:
type: display
label:
content: |
<script src="{{ url('plugin://admin/themes/grav/js/clipboard-helper.js') }}"></script>
<div class="webhook-examples">
<script>
// Initialize webhook commands when page loads
document.addEventListener('DOMContentLoaded', function() {
if (typeof GravClipboard !== 'undefined') {
GravClipboard.initWebhookCommands();
}
});
</script>
<div class="alert alert-info">
<h4>How to use webhooks:</h4>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.25rem; font-weight: 500;">Trigger all due jobs (respects schedule):</label>
<div class="form-input-wrapper form-input-addon-wrapper">
<textarea id="webhook-all-cmd" readonly rows="2" style="font-family: monospace; background: #f5f5f5; resize: none;">Loading...</textarea>
<div class="form-input-addon form-input-append" style="cursor: pointer;" onclick="GravClipboard.copy(this)"><i class="fa fa-copy"></i> Copy</div>
</div>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.25rem; font-weight: 500;">Force-run specific job (ignores schedule):</label>
<div class="form-input-wrapper form-input-addon-wrapper">
<textarea id="webhook-job-cmd" readonly rows="2" style="font-family: monospace; background: #f5f5f5; resize: none;">Loading...</textarea>
<div class="form-input-addon form-input-append" style="cursor: pointer;" onclick="GravClipboard.copy(this)"><i class="fa fa-copy"></i> Copy</div>
</div>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.25rem; font-weight: 500;">Check health status:</label>
<div class="form-input-wrapper form-input-addon-wrapper">
<input type="text" id="webhook-health-cmd" readonly value="Loading..." style="font-family: monospace; background: #f5f5f5;">
<div class="form-input-addon form-input-append" style="cursor: pointer;" onclick="GravClipboard.copy(this)"><i class="fa fa-copy"></i> Copy</div>
</div>
</div>
<div style="margin-top: 1rem;">
<p><strong>GitHub Actions example:</strong></p>
<pre>- name: Trigger Scheduler
run: |
curl -X POST ${{ secrets.SITE_URL }}/scheduler/webhook \
-H "Authorization: Bearer ${{ secrets.WEBHOOK_TOKEN }}"</pre>
</div>
</div>
</div>
markdown: false