Phase 7: Multi-Worker Command Execution

Added ability to execute commands on multiple workers simultaneously:

- Added execution mode selector (Single/Multiple Workers)
- Multi-worker mode with checkbox list for worker selection
- Helper buttons: Select All, Online Only, Clear All
- Sequential execution across selected workers
- Results summary showing success/fail count per worker
- Updated command history to track multi-worker executions
- Terminal beep feedback based on overall success/failure
- Maintained backward compatibility with single worker mode

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-07 23:03:45 -05:00
parent 661c83a578
commit 4baecc54d3
2 changed files with 519 additions and 214 deletions

View File

@@ -826,10 +826,36 @@
<button onclick="showCommandHistory()" style="flex: 0;">[ 🕐 History ]</button>
</div>
<label style="display: block; margin-bottom: 10px; font-weight: 600;">Select Worker:</label>
<select id="quickWorkerSelect">
<option value="">Loading workers...</option>
</select>
<label style="display: block; margin-bottom: 10px; font-weight: 600;">Execution Mode:</label>
<div style="margin-bottom: 20px;">
<label style="display: inline-flex; align-items: center; margin-right: 20px; cursor: pointer;">
<input type="radio" name="execMode" value="single" checked onchange="toggleWorkerSelection()" style="width: auto; margin-right: 8px;">
<span>Single Worker</span>
</label>
<label style="display: inline-flex; align-items: center; cursor: pointer;">
<input type="radio" name="execMode" value="multi" onchange="toggleWorkerSelection()" style="width: auto; margin-right: 8px;">
<span>Multiple Workers</span>
</label>
</div>
<div id="singleWorkerMode">
<label style="display: block; margin-bottom: 10px; font-weight: 600;">Select Worker:</label>
<select id="quickWorkerSelect">
<option value="">Loading workers...</option>
</select>
</div>
<div id="multiWorkerMode" style="display: none;">
<label style="display: block; margin-bottom: 10px; font-weight: 600;">Select Workers:</label>
<div id="workerCheckboxList" style="background: var(--bg-primary); border: 2px solid var(--terminal-green); padding: 15px; margin-bottom: 15px; max-height: 200px; overflow-y: auto;">
<div class="loading">Loading workers...</div>
</div>
<div style="margin-bottom: 15px;">
<button onclick="selectAllWorkers()" class="small">[ Select All ]</button>
<button onclick="selectOnlineWorkers()" class="small">[ Online Only ]</button>
<button onclick="deselectAllWorkers()" class="small">[ Clear All ]</button>
</div>
</div>
<label style="display: block; margin-bottom: 10px; margin-top: 20px; font-weight: 600;">Command:</label>
<textarea id="quickCommand" placeholder="Enter command to execute (e.g., 'uptime' or 'df -h')"></textarea>
@@ -919,14 +945,28 @@
try {
const response = await fetch('/api/workers');
workers = await response.json();
// Update worker select in quick command
// Update worker select in quick command (single mode)
const select = document.getElementById('quickWorkerSelect');
if (select) {
select.innerHTML = workers.map(w =>
select.innerHTML = workers.map(w =>
`<option value="${w.id}">${w.name} (${w.status})</option>`
).join('');
}
// Update worker checkboxes (multi mode)
const checkboxList = document.getElementById('workerCheckboxList');
if (checkboxList) {
checkboxList.innerHTML = workers.length === 0 ?
'<div class="empty">No workers available</div>' :
workers.map(w => `
<label style="display: block; margin-bottom: 10px; cursor: pointer; padding: 8px; border: 1px solid var(--terminal-green); background: ${w.status === 'online' ? 'rgba(0, 255, 65, 0.05)' : 'transparent'};">
<input type="checkbox" name="workerCheckbox" value="${w.id}" data-status="${w.status}" style="width: auto; margin-right: 8px;">
<span class="status ${w.status}" style="padding: 2px 8px; font-size: 0.8em;">[${w.status === 'online' ? '●' : '○'}]</span>
<strong>${w.name}</strong>
</label>
`).join('');
}
// Dashboard view
const dashHtml = workers.length === 0 ?
@@ -1416,6 +1456,38 @@
localStorage.setItem('commandHistory', JSON.stringify(history));
}
function toggleWorkerSelection() {
const mode = document.querySelector('input[name="execMode"]:checked').value;
const singleMode = document.getElementById('singleWorkerMode');
const multiMode = document.getElementById('multiWorkerMode');
if (mode === 'single') {
singleMode.style.display = 'block';
multiMode.style.display = 'none';
} else {
singleMode.style.display = 'none';
multiMode.style.display = 'block';
}
}
function selectAllWorkers() {
document.querySelectorAll('input[name="workerCheckbox"]').forEach(cb => {
cb.checked = true;
});
}
function selectOnlineWorkers() {
document.querySelectorAll('input[name="workerCheckbox"]').forEach(cb => {
cb.checked = cb.getAttribute('data-status') === 'online';
});
}
function deselectAllWorkers() {
document.querySelectorAll('input[name="workerCheckbox"]').forEach(cb => {
cb.checked = false;
});
}
async function deleteWorker(workerId, name) {
if (!confirm(`Delete worker: ${name}?`)) return;
@@ -1495,51 +1567,149 @@
}
async function executeQuickCommand() {
const workerId = document.getElementById('quickWorkerSelect').value;
const command = document.getElementById('quickCommand').value;
const execMode = document.querySelector('input[name="execMode"]:checked').value;
if (!workerId || !command) {
alert('Please select a worker and enter a command');
if (!command) {
alert('Please enter a command');
return;
}
// Find worker name for history
const worker = workers.find(w => w.id === workerId);
const workerName = worker ? worker.name : 'Unknown';
const resultDiv = document.getElementById('quickCommandResult');
resultDiv.innerHTML = '<div class="loading">Executing command...</div>';
try {
const response = await fetch(`/api/workers/${workerId}/command`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command })
});
if (execMode === 'single') {
// Single worker execution
const workerId = document.getElementById('quickWorkerSelect').value;
if (response.ok) {
const data = await response.json();
// Add to command history
addToCommandHistory(command, workerName);
resultDiv.innerHTML = `
<div style="background: #001a00; border: 2px solid var(--terminal-green); padding: 15px;">
<strong style="color: var(--terminal-green);">✓ Command sent successfully!</strong>
<div style="margin-top: 10px; font-family: var(--font-mono); font-size: 0.9em; color: var(--terminal-green);">
Execution ID: ${data.execution_id}
</div>
<div style="margin-top: 10px; color: var(--terminal-amber);">
Check the Executions tab to see the results
</div>
</div>
`;
} else {
resultDiv.innerHTML = '<div style="color: #ef4444;">Failed to execute command</div>';
if (!workerId) {
alert('Please select a worker');
return;
}
const worker = workers.find(w => w.id === workerId);
const workerName = worker ? worker.name : 'Unknown';
resultDiv.innerHTML = '<div class="loading">Executing command...</div>';
try {
const response = await fetch(`/api/workers/${workerId}/command`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command })
});
if (response.ok) {
const data = await response.json();
addToCommandHistory(command, workerName);
resultDiv.innerHTML = `
<div style="background: #001a00; border: 2px solid var(--terminal-green); padding: 15px;">
<strong style="color: var(--terminal-green);">✓ Command sent successfully!</strong>
<div style="margin-top: 10px; font-family: var(--font-mono); font-size: 0.9em; color: var(--terminal-green);">
Execution ID: ${data.execution_id}
</div>
<div style="margin-top: 10px; color: var(--terminal-amber);">
Check the Executions tab to see the results
</div>
</div>
`;
terminalBeep('success');
} else {
resultDiv.innerHTML = '<div style="color: #ef4444;">Failed to execute command</div>';
terminalBeep('error');
}
} catch (error) {
console.error('Error executing command:', error);
resultDiv.innerHTML = '<div style="color: #ef4444;">Error: ' + error.message + '</div>';
terminalBeep('error');
}
} else {
// Multi-worker execution
const selectedCheckboxes = document.querySelectorAll('input[name="workerCheckbox"]:checked');
const selectedWorkerIds = Array.from(selectedCheckboxes).map(cb => cb.value);
if (selectedWorkerIds.length === 0) {
alert('Please select at least one worker');
return;
}
resultDiv.innerHTML = `<div class="loading">Executing command on ${selectedWorkerIds.length} worker(s)...</div>`;
const results = [];
let successCount = 0;
let failCount = 0;
for (const workerId of selectedWorkerIds) {
try {
const worker = workers.find(w => w.id === workerId);
const response = await fetch(`/api/workers/${workerId}/command`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command })
});
if (response.ok) {
const data = await response.json();
results.push({
worker: worker.name,
success: true,
executionId: data.execution_id
});
successCount++;
} else {
results.push({
worker: worker.name,
success: false,
error: 'Failed to execute'
});
failCount++;
}
} catch (error) {
const worker = workers.find(w => w.id === workerId);
results.push({
worker: worker ? worker.name : workerId,
success: false,
error: error.message
});
failCount++;
}
}
// Add to history with multi-worker notation
addToCommandHistory(command, `${selectedWorkerIds.length} workers`);
// Display results summary
resultDiv.innerHTML = `
<div style="background: #001a00; border: 2px solid var(--terminal-green); padding: 15px;">
<strong style="color: var(--terminal-amber);">Multi-Worker Execution Complete</strong>
<div style="margin-top: 10px; font-family: var(--font-mono); font-size: 0.9em;">
<span style="color: var(--terminal-green);">✓ Success: ${successCount}</span> |
<span style="color: #ef4444;">✗ Failed: ${failCount}</span>
</div>
<div style="margin-top: 15px; max-height: 300px; overflow-y: auto;">
${results.map(r => `
<div style="margin-bottom: 8px; padding: 8px; border-left: 3px solid ${r.success ? 'var(--terminal-green)' : '#ef4444'}; background: rgba(0, 0, 0, 0.5);">
<strong>${r.worker}</strong>:
${r.success ?
`<span style="color: var(--terminal-green);">✓ Sent (ID: ${r.executionId.substring(0, 8)}...)</span>` :
`<span style="color: #ef4444;">✗ ${r.error}</span>`
}
</div>
`).join('')}
</div>
<div style="margin-top: 15px; color: var(--terminal-amber);">
Check the Executions tab to see detailed results
</div>
</div>
`;
if (failCount === 0) {
terminalBeep('success');
} else if (successCount > 0) {
terminalBeep('info');
} else {
terminalBeep('error');
}
} catch (error) {
console.error('Error executing command:', error);
resultDiv.innerHTML = '<div style="color: #ef4444;">Error: ' + error.message + '</div>';
}
}