diff --git a/public/index.html b/public/index.html index b601fb4..4c2ad2a 100644 --- a/public/index.html +++ b/public/index.html @@ -474,9 +474,9 @@ padding: 0; border: 3px double var(--terminal-green); border-radius: 0; - max-width: 600px; - width: 90%; - max-height: 85vh; + max-width: min(1100px, 96vw); + width: 96vw; + max-height: 90vh; overflow-y: auto; box-shadow: 0 0 30px rgba(0, 255, 65, 0.3); position: relative; @@ -702,6 +702,28 @@ border-color: #330000; } + /* parse_complete and route_taken log entries */ + .log-parse-table { + display: grid; + grid-template-columns: max-content 1fr; + gap: 2px 12px; + font-size: 0.82em; + margin-top: 6px; + } + .log-parse-key { color: var(--terminal-amber); opacity: .8; } + .log-parse-val { color: var(--terminal-green); word-break: break-all; } + .log-route-label { + color: var(--terminal-cyan); + font-size: 0.9em; + margin-top: 4px; + } + .log-route-goto { + color: var(--terminal-amber); + font-size: 0.82em; + opacity: .75; + margin-top: 2px; + } + .log-entry code { background: var(--bg-terminal); padding: 2px 6px; @@ -2136,6 +2158,31 @@ `; } + if (log.action === 'parse_complete') { + const pairs = log.parsed || {}; + const keys = Object.keys(pairs); + const tableRows = keys.map(k => + `
${escapeHtml(k)}
${escapeHtml(pairs[k])}
` + ).join(''); + return ` +
+
[${timestamp}]
+
⚙ Parsed ${keys.length} variable${keys.length !== 1 ? 's' : ''}
+ ${keys.length > 0 ? `
${tableRows}
` : ''} +
+ `; + } + + if (log.action === 'route_taken') { + return ` +
+
[${timestamp}]
+
⇒ Auto-route: Step ${log.step}
+ ${log.label ? `
${escapeHtml(log.label)}
${log.goto ? `
→ ${escapeHtml(log.goto)}
` : ''}
` : ''} +
+ `; + } + if (log.action === 'no_workers') { return `
diff --git a/server.js b/server.js index 96b0c83..3f5b927 100644 --- a/server.js +++ b/server.js @@ -855,6 +855,47 @@ async function executeWorkflowSteps(executionId, workflowId, definition, usernam timestamp: new Date().toISOString() }); await new Promise(r => setTimeout(r, ms)); + + } else if (step.type === 'parse') { + // Parse KEY=VALUE lines from last command output into execution state. + // Matches lines of the form: UPPER_KEY=value (key must start with uppercase letter) + const condOkParse = !step.condition || evalCondition(step.condition, execState?.state || {}, execState?.params || {}); + const parseState = condOkParse ? _executionState.get(executionId) : null; + if (parseState) { + const output = parseState.state._lastCommandOutput || ''; + const parsed = {}; + for (const line of output.split('\n')) { + const m = line.match(/^([A-Z][A-Z0-9_]*)=(.*)$/); + if (m) { + const k = m[1].toLowerCase(); + const v = m[2].trim(); + parsed[k] = v; + parseState.state[k] = v; + } + } + await addExecutionLog(executionId, { + step: currentIndex + 1, step_name: stepLabel, action: 'parse_complete', + parsed_count: Object.keys(parsed).length, parsed, + timestamp: new Date().toISOString() + }); + } + + } else if (step.type === 'route') { + // Evaluate conditions in order; jump to the first match (no user input needed). + const routeState = execState?.state || {}; + const routeParams = execState?.params || {}; + let routeTaken = null; + for (const cond of (step.conditions || [])) { + const matches = cond.default || evalCondition(cond.if || 'false', routeState, routeParams); + if (matches) { routeTaken = cond; gotoId = cond.goto || null; break; } + } + await addExecutionLog(executionId, { + step: currentIndex + 1, step_name: stepLabel, action: 'route_taken', + condition: routeTaken?.if || 'default', + label: routeTaken?.label || null, + goto: gotoId, + timestamp: new Date().toISOString() + }); } await addExecutionLog(executionId, { @@ -902,11 +943,15 @@ async function executeWorkflowSteps(executionId, workflowId, definition, usernam // Pause execution and wait for user to respond via POST /api/executions/:id/respond. // Resolves with the chosen option string, or null on 60-minute timeout. async function executePromptStep(executionId, step, stepNumber) { - const message = step.message || 'Please choose an option:'; + const execState = _executionState.get(executionId); + // Apply param substitution to the message so {{server_name}}, {{iface}} etc. work + let message = step.message || 'Please choose an option:'; + if (execState && Object.keys(execState.params).length > 0) { + message = applyParams(message, execState.params); + } const options = step.options || ['Continue']; // Include the last command output so the user can review results alongside the question - const execState = _executionState.get(executionId); const lastOutput = (execState?.state?._lastCommandOutput) || null; const logEntry = {