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:
@@ -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,8 +1213,14 @@
|
||||
// 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}')">
|
||||
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">
|
||||
@@ -1204,7 +1228,8 @@
|
||||
${e.completed_at ? ` • Completed at ${new Date(e.completed_at).toLocaleString()}` : ''}
|
||||
</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);
|
||||
|
||||
Reference in New Issue
Block a user