Add rate limiting, cron scheduling, webhooks, dry-run, execution filtering, and UX improvements
- Rate limiting: 300 req/15min general, 20 req/min on POST /api/executions - Cron schedule type support using cron-parser for full cron expressions - Webhook notifications: POST to workflow webhook_url on execution complete/failed - Dry-run mode: simulate workflow execution without running any commands - Global execution timeout via EXECUTION_MAX_MINUTES env var (default 60min) - Execution filtering: status, workflow_id, started_by, after, before, search - Event-driven command result delivery (replaces 500ms DB polling) - Atomic log appends via JSON_ARRAY_APPEND (no read-modify-write race) - Separate browserClients/workerClients sets (workers no longer receive broadcasts) - Stale execution cleanup on startup (mark running→failed after crash) - Scheduler overlap prevention (skip if same workflow already running) - Frontend: webhook_url field in create/edit workflow modals - Frontend: dry-run checkbox in workflow param modal - Frontend: ESC closes modals, ws.onerror handler added - Frontend: selectedExecutions changed from Array to Set (O(1) ops) - Frontend: XSS fixes via escapeHtml() on all user-controlled innerHTML - Frontend: param modal keydown listener deduplication fix - Remove unused npm packages (bcryptjs, body-parser, cors, js-yaml, jsonwebtoken) - Add express-rate-limit and cron-parser dependencies Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -975,6 +975,8 @@
|
||||
}
|
||||
]
|
||||
}</textarea>
|
||||
<label style="display:block;margin:12px 0 6px;font-weight:600;">Webhook URL (optional):</label>
|
||||
<input type="url" id="workflowWebhookUrl" placeholder="https://example.com/webhook">
|
||||
<button onclick="createWorkflow()">Create</button>
|
||||
<button onclick="closeModal('createWorkflowModal')">Cancel</button>
|
||||
</div>
|
||||
@@ -1022,6 +1024,10 @@
|
||||
<h2 style="color:var(--terminal-green);margin:0 0 6px;font-size:1.1em;letter-spacing:.05em;">▶ RUN WORKFLOW</h2>
|
||||
<p style="color:var(--terminal-amber);font-size:.75em;margin:0 0 20px;letter-spacing:.04em;">Fill in required parameters</p>
|
||||
<div id="paramModalForm"></div>
|
||||
<div style="margin-top:16px;display:flex;align-items:center;gap:10px;">
|
||||
<input type="checkbox" id="paramDryRun" style="accent-color:var(--terminal-amber);">
|
||||
<label for="paramDryRun" style="color:var(--terminal-amber);font-size:.85em;cursor:pointer;">Dry Run (simulate, no commands executed)</label>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px;margin-top:20px;">
|
||||
<button onclick="submitParamForm()"
|
||||
style="flex:1;padding:8px;background:rgba(0,255,65,.1);border:1px solid var(--terminal-green);color:var(--terminal-green);font-family:var(--font-mono);cursor:pointer;font-size:.9em;">
|
||||
@@ -1046,6 +1052,8 @@
|
||||
<textarea id="editWorkflowDescription" placeholder="Description" style="min-height:60px;"></textarea>
|
||||
<label style="display:block;margin:12px 0 6px;font-weight:600;">Definition (JSON):</label>
|
||||
<textarea id="editWorkflowDefinition" style="min-height: 320px; font-family: var(--font-mono); font-size: 0.85em;"></textarea>
|
||||
<label style="display:block;margin:12px 0 6px;font-weight:600;">Webhook URL (optional):</label>
|
||||
<input type="url" id="editWorkflowWebhookUrl" placeholder="https://example.com/webhook">
|
||||
<div id="editWorkflowError" style="color:var(--terminal-red);font-size:0.85em;margin-top:8px;display:none;"></div>
|
||||
<div style="margin-top:16px;display:flex;gap:10px;">
|
||||
<button onclick="saveWorkflow()">[ 💾 Save ]</button>
|
||||
@@ -1871,17 +1879,18 @@
|
||||
} else {
|
||||
const name = document.querySelector(`[onclick="executeWorkflow('${workflowId}')"]`)
|
||||
?.closest('.workflow-item')?.querySelector('.workflow-name')?.textContent || 'this workflow';
|
||||
if (!confirm(`Execute: ${name}?`)) return;
|
||||
await startExecution(workflowId, {});
|
||||
const choice = confirm(`Execute: ${name}?\n\nClick OK to run normally, or Cancel to abort.\n(Use the workflow's Run button with dry-run checkbox for a dry run.)`);
|
||||
if (!choice) return;
|
||||
await startExecution(workflowId, {}, false);
|
||||
}
|
||||
}
|
||||
|
||||
async function startExecution(workflowId, params) {
|
||||
async function startExecution(workflowId, params, dryRun = false) {
|
||||
try {
|
||||
const response = await fetch('/api/executions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ workflow_id: workflowId, params })
|
||||
body: JSON.stringify({ workflow_id: workflowId, params, dry_run: dryRun })
|
||||
});
|
||||
if (response.ok) {
|
||||
switchTab('executions');
|
||||
@@ -1919,6 +1928,7 @@
|
||||
|
||||
function closeParamModal() {
|
||||
document.getElementById('paramModal').style.display = 'none';
|
||||
document.getElementById('paramDryRun').checked = false;
|
||||
_pendingExecWorkflowId = null;
|
||||
}
|
||||
|
||||
@@ -1937,8 +1947,10 @@
|
||||
}
|
||||
if (val) params[p.name] = val;
|
||||
}
|
||||
const dryRun = document.getElementById('paramDryRun').checked;
|
||||
const wfId = _pendingExecWorkflowId;
|
||||
closeParamModal();
|
||||
await startExecution(_pendingExecWorkflowId, params);
|
||||
await startExecution(wfId, params, dryRun);
|
||||
}
|
||||
|
||||
async function viewExecution(executionId) {
|
||||
@@ -2446,6 +2458,7 @@
|
||||
document.getElementById('editWorkflowName').value = wf.name;
|
||||
document.getElementById('editWorkflowDescription').value = wf.description || '';
|
||||
document.getElementById('editWorkflowDefinition').value = JSON.stringify(wf.definition, null, 2);
|
||||
document.getElementById('editWorkflowWebhookUrl').value = wf.webhook_url || '';
|
||||
document.getElementById('editWorkflowError').style.display = 'none';
|
||||
document.getElementById('editWorkflowModal').classList.add('show');
|
||||
} catch (error) {
|
||||
@@ -2459,6 +2472,7 @@
|
||||
const name = document.getElementById('editWorkflowName').value.trim();
|
||||
const description = document.getElementById('editWorkflowDescription').value.trim();
|
||||
const definitionText = document.getElementById('editWorkflowDefinition').value;
|
||||
const webhook_url = document.getElementById('editWorkflowWebhookUrl').value.trim() || null;
|
||||
const errorEl = document.getElementById('editWorkflowError');
|
||||
|
||||
if (!name) {
|
||||
@@ -2481,7 +2495,7 @@
|
||||
const response = await fetch(`/api/workflows/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, description, definition })
|
||||
body: JSON.stringify({ name, description, definition, webhook_url })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -2507,12 +2521,13 @@
|
||||
const name = document.getElementById('workflowName').value;
|
||||
const description = document.getElementById('workflowDescription').value;
|
||||
const definitionText = document.getElementById('workflowDefinition').value;
|
||||
|
||||
const webhook_url = document.getElementById('workflowWebhookUrl').value.trim() || null;
|
||||
|
||||
if (!name || !definitionText) {
|
||||
alert('Name and definition are required');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let definition;
|
||||
try {
|
||||
definition = JSON.parse(definitionText);
|
||||
@@ -2525,7 +2540,7 @@
|
||||
const response = await fetch('/api/workflows', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, description, definition })
|
||||
body: JSON.stringify({ name, description, definition, webhook_url })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
|
||||
Reference in New Issue
Block a user