diff --git a/public/index.html b/public/index.html
index 049bf10..e57c7c5 100644
--- a/public/index.html
+++ b/public/index.html
@@ -991,6 +991,25 @@
+
+
@@ -1156,26 +1175,43 @@
}
}
+ // Workflow registry (id → definition) for param lookup
+ let _workflowRegistry = {};
+
async function loadWorkflows() {
try {
const response = await fetch('/api/workflows');
const workflows = await response.json();
+ // Cache definitions for param lookup at execute time
+ _workflowRegistry = {};
+ workflows.forEach(w => {
+ const def = typeof w.definition === 'string' ? JSON.parse(w.definition) : w.definition;
+ _workflowRegistry[w.id] = def;
+ });
+
+ const paramBadge = (def) => {
+ const ps = (def && def.params) || [];
+ return ps.length ? `
[${ps.length} param${ps.length > 1 ? 's' : ''}]` : '';
+ };
+
const html = workflows.length === 0 ?
'
No workflows defined yet
' :
- workflows.map(w => `
+ workflows.map(w => {
+ const def = _workflowRegistry[w.id] || {};
+ return `
-
${w.name}
+
${w.name}${paramBadge(def)}
${w.description || 'No description'}
Created by ${w.created_by || 'Unknown'} on ${new Date(w.created_at).toLocaleString()}
-
+
${currentUser && currentUser.isAdmin ?
``
: ''}
-
- `).join('');
+
`;
+ }).join('');
document.getElementById('workflowList').innerHTML = html;
} catch (error) {
console.error('Error loading workflows:', error);
@@ -1781,30 +1817,87 @@
}
}
- async function executeWorkflow(workflowId, name) {
- if (!confirm(`Execute workflow: ${name}?`)) return;
+ // ── Workflow execution with optional param modal ───────────────────
+ let _pendingExecWorkflowId = null;
+ async function executeWorkflow(workflowId) {
+ const def = _workflowRegistry[workflowId] || {};
+ const paramDefs = def.params || [];
+ if (paramDefs.length > 0) {
+ showParamModal(workflowId, paramDefs);
+ } 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, {});
+ }
+ }
+
+ async function startExecution(workflowId, params) {
try {
const response = await fetch('/api/executions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ workflow_id: workflowId })
+ body: JSON.stringify({ workflow_id: workflowId, params })
});
-
if (response.ok) {
- const data = await response.json();
- alert('Workflow execution started!');
switchTab('executions');
refreshData();
} else {
- alert('Failed to start workflow');
+ const err = await response.json().catch(() => ({}));
+ alert('Failed to start: ' + (err.error || response.status));
}
} catch (error) {
- console.error('Error executing workflow:', error);
- alert('Error executing workflow');
+ alert('Error starting workflow: ' + error.message);
}
}
+ function showParamModal(workflowId, paramDefs) {
+ _pendingExecWorkflowId = workflowId;
+ const form = document.getElementById('paramModalForm');
+ form.innerHTML = paramDefs.map(p => `
+
+
+
+
`).join('');
+ document.getElementById('paramModal').style.display = 'flex';
+ // Focus first input; Enter key submits
+ form.querySelectorAll('input').forEach(inp => {
+ inp.addEventListener('keydown', e => { if (e.key === 'Enter') submitParamForm(); });
+ });
+ const first = form.querySelector('input');
+ if (first) setTimeout(() => first.focus(), 50);
+ }
+
+ function closeParamModal() {
+ document.getElementById('paramModal').style.display = 'none';
+ _pendingExecWorkflowId = null;
+ }
+
+ async function submitParamForm() {
+ if (!_pendingExecWorkflowId) return;
+ const def = _workflowRegistry[_pendingExecWorkflowId] || {};
+ const paramDefs = def.params || [];
+ const params = {};
+ for (const p of paramDefs) {
+ const el = document.getElementById(`param_${p.name}`);
+ const val = el ? el.value.trim() : '';
+ if (p.required && !val) {
+ el.style.borderColor = 'var(--terminal-red)';
+ el.focus();
+ return;
+ }
+ if (val) params[p.name] = val;
+ }
+ closeParamModal();
+ await startExecution(_pendingExecWorkflowId, params);
+ }
+
async function viewExecution(executionId) {
try {
const response = await fetch(`/api/executions/${executionId}`);
diff --git a/server.js b/server.js
index 719cdb7..ebf48ba 100644
--- a/server.js
+++ b/server.js
@@ -494,7 +494,21 @@ function authenticateGandalf(req, res, next) {
}
// Workflow Execution Engine
-async function executeWorkflowSteps(executionId, workflowId, definition, username) {
+
+// Substitute {{param_name}} placeholders in a command string.
+// Only alphanumeric + safe punctuation allowed in substituted values.
+function applyParams(command, params) {
+ return command.replace(/\{\{(\w+)\}\}/g, (match, key) => {
+ if (!(key in params)) return match;
+ const val = String(params[key]).trim();
+ if (!/^[a-zA-Z0-9._:@\-\/]+$/.test(val)) {
+ throw new Error(`Unsafe value for workflow parameter "${key}"`);
+ }
+ return val;
+ });
+}
+
+async function executeWorkflowSteps(executionId, workflowId, definition, username, params = {}) {
try {
console.log(`[Workflow] Starting execution ${executionId} for workflow ${workflowId}`);
@@ -522,7 +536,7 @@ async function executeWorkflowSteps(executionId, workflowId, definition, usernam
if (step.type === 'execute') {
// Execute command step
- const success = await executeCommandStep(executionId, step, i + 1);
+ const success = await executeCommandStep(executionId, step, i + 1, params);
if (!success) {
allStepsSucceeded = false;
break; // Stop workflow on failure
@@ -580,9 +594,12 @@ async function executeWorkflowSteps(executionId, workflowId, definition, usernam
}
}
-async function executeCommandStep(executionId, step, stepNumber) {
+async function executeCommandStep(executionId, step, stepNumber, params = {}) {
try {
- const command = step.command;
+ let command = step.command;
+ if (Object.keys(params).length > 0) {
+ command = applyParams(command, params);
+ }
const targets = step.targets || ['all'];
// Determine which workers to target
@@ -817,7 +834,7 @@ app.post('/api/workers/heartbeat', async (req, res) => {
app.post('/api/executions', authenticateSSO, async (req, res) => {
try {
- const { workflow_id } = req.body;
+ const { workflow_id, params = {} } = req.body;
const id = generateUUID();
// Get workflow definition
@@ -829,16 +846,27 @@ app.post('/api/executions', authenticateSSO, async (req, res) => {
const workflow = workflows[0];
const definition = typeof workflow.definition === 'string' ? JSON.parse(workflow.definition) : workflow.definition;
+ // Validate required params
+ const paramDefs = definition.params || [];
+ for (const pd of paramDefs) {
+ if (pd.required && !params[pd.name]) {
+ return res.status(400).json({ error: `Missing required parameter: ${pd.label || pd.name}` });
+ }
+ }
+
// Create execution record
+ const initLogs = Object.keys(params).length > 0
+ ? [{ action: 'params', params, timestamp: new Date().toISOString() }]
+ : [];
await pool.query(
'INSERT INTO executions (id, workflow_id, status, started_by, started_at, logs) VALUES (?, ?, ?, ?, NOW(), ?)',
- [id, workflow_id, 'running', req.user.username, JSON.stringify([])]
+ [id, workflow_id, 'running', req.user.username, JSON.stringify(initLogs)]
);
broadcast({ type: 'execution_started', execution_id: id, workflow_id });
// Start workflow execution asynchronously
- executeWorkflowSteps(id, workflow_id, definition, req.user.username).catch(err => {
+ executeWorkflowSteps(id, workflow_id, definition, req.user.username, params).catch(err => {
console.error(`[Workflow] Execution ${id} failed:`, err);
});