Phase 9: Execution Diff View

Added powerful execution comparison and diff view:

- Compare Mode toggle button in executions tab
- Multi-select up to 5 executions for comparison
- Visual selection indicators with checkmarks
- Comparison modal with summary table (status, duration, timestamps)
- Side-by-side output view for all selected executions
- Line-by-line diff analysis for 2-execution comparisons
- Highlights identical vs. different lines
- Shows identical/different line counts
- Color-coded diff (green for exec 1, amber for exec 2)
- Perfect for comparing same command across workers
- Terminal-themed comparison UI

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-07 23:08:55 -05:00
parent 5c41afed85
commit 4e17fdbf8c

View File

@@ -842,6 +842,13 @@
<button onclick="refreshData()">[ 🔄 Refresh ]</button>
<button onclick="clearCompletedExecutions()" style="margin-left: 10px;">[ 🗑️ Clear Completed ]</button>
<button onclick="toggleCompareMode()" id="compareModeBtn" style="margin-left: 10px;">[ 📊 Compare Mode ]</button>
<button onclick="compareSelectedExecutions()" id="compareBtn" style="margin-left: 10px; display: none;">[ ⚖️ Compare Selected ]</button>
<div id="compareInstructions" style="display: none; background: rgba(255, 176, 0, 0.1); border: 2px solid var(--terminal-amber); padding: 12px; margin: 15px 0; color: var(--terminal-amber);">
Select 2-5 executions to compare their outputs. Click executions to toggle selection.
</div>
<div id="executionList"><div class="loading">Loading...</div></div>
</div>
</div>
@@ -947,11 +954,22 @@
</div>
</div>
<!-- Compare Executions Modal -->
<div id="compareExecutionsModal" class="modal">
<div class="modal-content" style="max-width: 90%; max-height: 90vh;">
<h2>⚖️ Execution Comparison</h2>
<div id="compareContent" style="max-height: 70vh; overflow-y: auto; padding: 20px;"></div>
<button onclick="closeModal('compareExecutionsModal')">[ Close ]</button>
</div>
</div>
<script>
let currentUser = null;
let ws = null;
let workers = [];
let allExecutions = []; // Store all loaded executions for filtering
let compareMode = false;
let selectedExecutions = [];
async function loadUser() {
try {
@@ -1195,16 +1213,23 @@
// Render filtered results
const fullHtml = filtered.length === 0 ?
'<div class="empty">No executions match your filters</div>' :
filtered.map(e => `
<div class="execution-item" onclick="viewExecution('${e.id}')">
<span class="status ${e.status}">${e.status}</span>
<strong>${e.workflow_name || '[Quick Command]'}</strong>
<div class="timestamp">
Started by ${e.started_by} at ${new Date(e.started_at).toLocaleString()}
${e.completed_at ? ` • Completed at ${new Date(e.completed_at).toLocaleString()}` : ''}
filtered.map(e => {
const isSelected = selectedExecutions.includes(e.id);
const clickHandler = compareMode ? `toggleExecutionSelection('${e.id}')` : `viewExecution('${e.id}')`;
const selectedStyle = isSelected ? 'background: rgba(255, 176, 0, 0.2); border-left-width: 5px; border-left-color: var(--terminal-amber);' : '';
return `
<div class="execution-item" onclick="${clickHandler}" style="${selectedStyle} cursor: pointer;">
${compareMode && isSelected ? '<span style="color: var(--terminal-amber); margin-right: 8px;">✓</span>' : ''}
<span class="status ${e.status}">${e.status}</span>
<strong>${e.workflow_name || '[Quick Command]'}</strong>
<div class="timestamp">
Started by ${e.started_by} at ${new Date(e.started_at).toLocaleString()}
${e.completed_at ? ` • Completed at ${new Date(e.completed_at).toLocaleString()}` : ''}
</div>
</div>
</div>
`).join('');
`;
}).join('');
document.getElementById('executionList').innerHTML = fullHtml;
}
@@ -1219,6 +1244,211 @@
renderFilteredExecutions();
}
function toggleCompareMode() {
compareMode = !compareMode;
selectedExecutions = [];
const btn = document.getElementById('compareModeBtn');
const compareBtn = document.getElementById('compareBtn');
const instructions = document.getElementById('compareInstructions');
if (compareMode) {
btn.textContent = '[ ✗ Exit Compare Mode ]';
btn.style.borderColor = 'var(--terminal-amber)';
btn.style.color = 'var(--terminal-amber)';
compareBtn.style.display = 'inline-block';
instructions.style.display = 'block';
} else {
btn.textContent = '[ 📊 Compare Mode ]';
btn.style.borderColor = '';
btn.style.color = '';
compareBtn.style.display = 'none';
instructions.style.display = 'none';
}
renderFilteredExecutions();
}
function toggleExecutionSelection(executionId) {
const index = selectedExecutions.indexOf(executionId);
if (index > -1) {
selectedExecutions.splice(index, 1);
} else {
if (selectedExecutions.length >= 5) {
showTerminalNotification('Maximum 5 executions can be compared', 'error');
return;
}
selectedExecutions.push(executionId);
}
renderFilteredExecutions();
// Update compare button text
const compareBtn = document.getElementById('compareBtn');
if (selectedExecutions.length >= 2) {
compareBtn.textContent = `[ ⚖️ Compare Selected (${selectedExecutions.length}) ]`;
} else {
compareBtn.textContent = '[ ⚖️ Compare Selected ]';
}
}
async function compareSelectedExecutions() {
if (selectedExecutions.length < 2) {
showTerminalNotification('Please select at least 2 executions to compare', 'error');
return;
}
// Fetch detailed data for all selected executions
const executionDetails = [];
for (const execId of selectedExecutions) {
try {
const response = await fetch(`/api/executions/${execId}`);
if (response.ok) {
executionDetails.push(await response.json());
}
} catch (error) {
console.error('Error fetching execution:', error);
}
}
if (executionDetails.length < 2) {
showTerminalNotification('Failed to load execution details', 'error');
return;
}
// Generate comparison view
let comparisonHtml = '<div style="display: grid; gap: 20px;">';
// Summary table
comparisonHtml += `
<div style="background: rgba(0, 255, 65, 0.05); border: 2px solid var(--terminal-green); padding: 15px;">
<h3 style="margin-top: 0; color: var(--terminal-amber);">Comparison Summary</h3>
<table style="width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 0.9em;">
<thead>
<tr style="border-bottom: 2px solid var(--terminal-green);">
<th style="text-align: left; padding: 8px; color: var(--terminal-amber);">Execution</th>
<th style="text-align: left; padding: 8px; color: var(--terminal-amber);">Status</th>
<th style="text-align: left; padding: 8px; color: var(--terminal-amber);">Started</th>
<th style="text-align: left; padding: 8px; color: var(--terminal-amber);">Duration</th>
</tr>
</thead>
<tbody>
${executionDetails.map((exec, idx) => {
const duration = exec.completed_at ?
Math.round((new Date(exec.completed_at) - new Date(exec.started_at)) / 1000) + 's' :
'Running...';
return `
<tr style="border-bottom: 1px solid #003300;">
<td style="padding: 8px; color: var(--terminal-green);">Execution ${idx + 1}</td>
<td style="padding: 8px;"><span class="status ${exec.status}" style="font-size: 0.85em;">${exec.status}</span></td>
<td style="padding: 8px; color: var(--terminal-green);">${new Date(exec.started_at).toLocaleString()}</td>
<td style="padding: 8px; color: var(--terminal-green);">${duration}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
`;
// Side-by-side output comparison
comparisonHtml += `
<div style="background: rgba(0, 255, 65, 0.05); border: 2px solid var(--terminal-green); padding: 15px;">
<h3 style="margin-top: 0; color: var(--terminal-amber);">Output Comparison</h3>
<div style="display: grid; grid-template-columns: repeat(${executionDetails.length}, 1fr); gap: 15px;">
${executionDetails.map((exec, idx) => {
const logs = typeof exec.logs === 'string' ? JSON.parse(exec.logs) : exec.logs;
const resultLog = logs.find(l => l.action === 'command_result');
const stdout = resultLog?.stdout || 'No output';
const stderr = resultLog?.stderr || '';
return `
<div style="border: 2px solid var(--terminal-green); background: #000;">
<div style="background: var(--bg-secondary); padding: 10px; border-bottom: 2px solid var(--terminal-green);">
<strong style="color: var(--terminal-amber);">Execution ${idx + 1}</strong>
<div style="font-size: 0.85em; color: var(--terminal-green);">
${exec.workflow_name || '[Quick Command]'}
</div>
</div>
<div style="padding: 12px;">
${stdout ? `
<div style="margin-bottom: 10px;">
<div style="color: var(--terminal-amber); font-weight: bold; margin-bottom: 5px;">STDOUT:</div>
<pre style="margin: 0; color: var(--terminal-green); font-size: 0.85em; max-height: 400px; overflow-y: auto; white-space: pre-wrap;">${escapeHtml(stdout)}</pre>
</div>
` : ''}
${stderr ? `
<div>
<div style="color: #ff4444; font-weight: bold; margin-bottom: 5px;">STDERR:</div>
<pre style="margin: 0; color: #ff4444; font-size: 0.85em; max-height: 200px; overflow-y: auto; white-space: pre-wrap;">${escapeHtml(stderr)}</pre>
</div>
` : ''}
</div>
</div>
`;
}).join('')}
</div>
</div>
`;
// Diff analysis (simple line-by-line comparison for 2 executions)
if (executionDetails.length === 2) {
const logs1 = typeof executionDetails[0].logs === 'string' ? JSON.parse(executionDetails[0].logs) : executionDetails[0].logs;
const logs2 = typeof executionDetails[1].logs === 'string' ? JSON.parse(executionDetails[1].logs) : executionDetails[1].logs;
const result1 = logs1.find(l => l.action === 'command_result');
const result2 = logs2.find(l => l.action === 'command_result');
const stdout1 = result1?.stdout || '';
const stdout2 = result2?.stdout || '';
const lines1 = stdout1.split('\n');
const lines2 = stdout2.split('\n');
const maxLines = Math.max(lines1.length, lines2.length);
let diffLines = [];
let identicalCount = 0;
let differentCount = 0;
for (let i = 0; i < maxLines; i++) {
const line1 = lines1[i] || '';
const line2 = lines2[i] || '';
if (line1 === line2) {
identicalCount++;
diffLines.push(`<div style="color: #666; padding: 2px;">${i+1}: ${escapeHtml(line1) || '(empty)'}</div>`);
} else {
differentCount++;
diffLines.push(`
<div style="background: rgba(255, 176, 0, 0.1); border-left: 3px solid var(--terminal-amber); padding: 2px; margin: 2px 0;">
<div style="color: var(--terminal-green);">${i+1} [Exec 1]: ${escapeHtml(line1) || '(empty)'}</div>
<div style="color: var(--terminal-amber);">${i+1} [Exec 2]: ${escapeHtml(line2) || '(empty)'}</div>
</div>
`);
}
}
comparisonHtml += `
<div style="background: rgba(0, 255, 65, 0.05); border: 2px solid var(--terminal-green); padding: 15px;">
<h3 style="margin-top: 0; color: var(--terminal-amber);">Diff Analysis</h3>
<div style="margin-bottom: 10px; font-family: var(--font-mono); font-size: 0.9em;">
<span style="color: var(--terminal-green);">✓ Identical lines: ${identicalCount}</span> |
<span style="color: var(--terminal-amber);">≠ Different lines: ${differentCount}</span>
</div>
<div style="background: #000; border: 2px solid var(--terminal-green); padding: 10px; max-height: 400px; overflow-y: auto; font-family: var(--font-mono); font-size: 0.85em;">
${diffLines.join('')}
</div>
</div>
`;
}
comparisonHtml += '</div>';
document.getElementById('compareContent').innerHTML = comparisonHtml;
document.getElementById('compareExecutionsModal').classList.add('show');
}
async function loadMoreExecutions() {
executionOffset += executionLimit;
await loadExecutions(true);