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 ? `
` : ''}
+
+ `;
+ }
+
+ 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 = {