Add parse/route step types, wider modal, automated link troubleshooter
- Add 'parse' step type: reads KEY=VALUE lines from last command output into execution state
- Add 'route' step type: evaluates JS conditions list, auto-jumps to first match with no user input
- Support condition field on parse steps (skip when conditional execute was also skipped)
- Widen execution detail modal to min(1100px, 96vw) to eliminate horizontal scrolling
- Show last command output in prompt boxes so user has context before clicking
- Add formatLogEntry support for parse_complete and route_taken log actions
- Apply applyParams() substitution to prompt messages ({{server_name}}, {{iface}}, etc.)
- Fix prompt button onclick using data-opt attribute to avoid JSON double-quote breakage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -474,9 +474,9 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
border: 3px double var(--terminal-green);
|
border: 3px double var(--terminal-green);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
max-width: 600px;
|
max-width: min(1100px, 96vw);
|
||||||
width: 90%;
|
width: 96vw;
|
||||||
max-height: 85vh;
|
max-height: 90vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
box-shadow: 0 0 30px rgba(0, 255, 65, 0.3);
|
box-shadow: 0 0 30px rgba(0, 255, 65, 0.3);
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -702,6 +702,28 @@
|
|||||||
border-color: #330000;
|
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 {
|
.log-entry code {
|
||||||
background: var(--bg-terminal);
|
background: var(--bg-terminal);
|
||||||
padding: 2px 6px;
|
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 =>
|
||||||
|
`<div class="log-parse-key">${escapeHtml(k)}</div><div class="log-parse-val">${escapeHtml(pairs[k])}</div>`
|
||||||
|
).join('');
|
||||||
|
return `
|
||||||
|
<div class="log-entry" style="border-left-color: #444; opacity:.85;">
|
||||||
|
<div class="log-timestamp">[${timestamp}]</div>
|
||||||
|
<div class="log-title" style="color:#666;">⚙ Parsed ${keys.length} variable${keys.length !== 1 ? 's' : ''}</div>
|
||||||
|
${keys.length > 0 ? `<div class="log-details"><div class="log-parse-table">${tableRows}</div></div>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.action === 'route_taken') {
|
||||||
|
return `
|
||||||
|
<div class="log-entry" style="border-left-color: var(--terminal-cyan); opacity:.9;">
|
||||||
|
<div class="log-timestamp">[${timestamp}]</div>
|
||||||
|
<div class="log-title" style="color: var(--terminal-cyan);">⇒ Auto-route: Step ${log.step}</div>
|
||||||
|
${log.label ? `<div class="log-details"><div class="log-route-label">${escapeHtml(log.label)}</div>${log.goto ? `<div class="log-route-goto">→ ${escapeHtml(log.goto)}</div>` : ''}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
if (log.action === 'no_workers') {
|
if (log.action === 'no_workers') {
|
||||||
return `
|
return `
|
||||||
<div class="log-entry failed">
|
<div class="log-entry failed">
|
||||||
|
|||||||
49
server.js
49
server.js
@@ -855,6 +855,47 @@ async function executeWorkflowSteps(executionId, workflowId, definition, usernam
|
|||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
await new Promise(r => setTimeout(r, ms));
|
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, {
|
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.
|
// 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.
|
// Resolves with the chosen option string, or null on 60-minute timeout.
|
||||||
async function executePromptStep(executionId, step, stepNumber) {
|
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'];
|
const options = step.options || ['Continue'];
|
||||||
|
|
||||||
// Include the last command output so the user can review results alongside the question
|
// 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 lastOutput = (execState?.state?._lastCommandOutput) || null;
|
||||||
|
|
||||||
const logEntry = {
|
const logEntry = {
|
||||||
|
|||||||
Reference in New Issue
Block a user