-
${s.name}
+
${escapeHtml(s.name || '')}
Command: ${escapeHtml(s.command)}
@@ -1529,7 +1529,7 @@
${e.status}
${e.workflow_name || '[Quick Command]'}
-
by ${e.started_by} at ${safeDate(e.started_at)?.toLocaleString() ?? 'N/A'}
+
by ${escapeHtml(e.started_by || '')} at ${safeDate(e.started_at)?.toLocaleString() ?? 'N/A'}
`).join('');
document.getElementById('dashExecutions').innerHTML = dashHtml;
@@ -1655,7 +1655,7 @@
${e.status}
${e.workflow_name || '[Quick Command]'}
- Started by ${e.started_by} at ${safeDate(e.started_at)?.toLocaleString() ?? 'N/A'}
+ Started by ${escapeHtml(e.started_by || '')} at ${safeDate(e.started_at)?.toLocaleString() ?? 'N/A'}
${e.completed_at ? ` • Completed at ${safeDate(e.completed_at)?.toLocaleString() ?? 'N/A'}` : elapsed}
@@ -1996,7 +1996,7 @@
Status: ${execution.status}
Started: ${safeDate(execution.started_at)?.toLocaleString() ?? 'N/A'}
${execution.completed_at ? `
Completed: ${safeDate(execution.completed_at)?.toLocaleString() ?? 'N/A'}
` : ''}
-
Started by: ${execution.started_by}
+
Started by: ${escapeHtml(execution.started_by || '')}
`;
if (execution.waiting_for_input && execution.prompt) {
diff --git a/server.js b/server.js
index 16a26ff..f4870ad 100644
--- a/server.js
+++ b/server.js
@@ -987,7 +987,7 @@ async function executeCommandStep(executionId, step, stepNumber, params = {}) {
}
// Send command to worker
- const commandId = crypto.randomUUID();
+ let commandId = crypto.randomUUID();
await addExecutionLog(executionId, {
step: stepNumber,
@@ -998,21 +998,56 @@ async function executeCommandStep(executionId, step, stepNumber, params = {}) {
timestamp: new Date().toISOString()
});
+ // Per-step timeout override: step.timeout (seconds) overrides global default
+ const stepTimeoutMs = step.timeout
+ ? Math.min(Math.max(parseInt(step.timeout, 10) * 1000, 5000), 600000)
+ : COMMAND_TIMEOUT_MS;
+
workerWs.send(JSON.stringify({
type: 'execute_command',
execution_id: executionId,
command_id: commandId,
command: command,
worker_id: workerId,
- timeout: COMMAND_TIMEOUT_MS
+ timeout: stepTimeoutMs
}));
- // Wait for command result (with timeout)
- const result = await waitForCommandResult(executionId, commandId, COMMAND_TIMEOUT_MS);
+ // Per-step retry: step.retries (default 0) with step.retryDelayMs (default 2000)
+ const maxRetries = Math.min(parseInt(step.retries || 0, 10), 5);
+ const retryDelayMs = Math.min(parseInt(step.retryDelayMs || 2000, 10), 30000);
+
+ let result;
+ let attempt = 0;
+ while (true) {
+ result = await waitForCommandResult(executionId, commandId, stepTimeoutMs);
+ if (result.success || attempt >= maxRetries) break;
+ attempt++;
+ await addExecutionLog(executionId, {
+ step: stepNumber,
+ action: 'command_retry',
+ worker_id: workerId,
+ attempt,
+ max_retries: maxRetries,
+ message: `Command failed, retrying (attempt ${attempt}/${maxRetries})...`,
+ timestamp: new Date().toISOString()
+ });
+ await new Promise(r => setTimeout(r, retryDelayMs));
+ // Re-send command for retry
+ const retryCommandId = crypto.randomUUID();
+ await addExecutionLog(executionId, {
+ step: stepNumber, action: 'command_sent', worker_id: workerId,
+ command: command, command_id: retryCommandId, timestamp: new Date().toISOString()
+ });
+ workerWs.send(JSON.stringify({
+ type: 'execute_command', execution_id: executionId, command_id: retryCommandId,
+ command: command, worker_id: workerId, timeout: stepTimeoutMs
+ }));
+ commandId = retryCommandId; // update for next waitForCommandResult call
+ }
results.push(result);
if (!result.success) {
- // Command failed, workflow should stop
+ // Command failed after all retries, workflow should stop
return false;
}
}