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:
@@ -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>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user