Phase 3: Quick command enhancements with templates and history

Changes:
- Added command templates modal with 12 common system commands
- Added command history tracking (stored in localStorage)
- History saves last 50 commands with timestamp and worker name
- Template categories: system info, disk/memory, network, Docker, logs
- Click templates to auto-fill command field
- Click history items to reuse previous commands
- Terminal-themed modals with green/amber styling
- History persists across browser sessions

Templates included:
- System: uname, uptime, CPU info, processes
- Resources: df -h, free -h, memory usage
- Network: ip addr, active connections
- Docker: container list
- Logs: syslog tail, who is logged in, last logins

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-07 22:45:40 -05:00
parent bc3524e163
commit 8152a827e6

View File

@@ -759,18 +759,23 @@
<div id="quickcommand" class="tab-content"> <div id="quickcommand" class="tab-content">
<div class="card"> <div class="card">
<h3>⚡ Quick Command Execution</h3> <h3>⚡ Quick Command Execution</h3>
<p style="color: #666; margin-bottom: 20px;">Execute a command on selected workers instantly</p> <p style="color: var(--terminal-green); margin-bottom: 20px;">Execute a command on selected workers instantly</p>
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
<button onclick="showCommandTemplates()" style="flex: 0;">[ 📋 Templates ]</button>
<button onclick="showCommandHistory()" style="flex: 0;">[ 🕐 History ]</button>
</div>
<label style="display: block; margin-bottom: 10px; font-weight: 600;">Select Worker:</label> <label style="display: block; margin-bottom: 10px; font-weight: 600;">Select Worker:</label>
<select id="quickWorkerSelect"> <select id="quickWorkerSelect">
<option value="">Loading workers...</option> <option value="">Loading workers...</option>
</select> </select>
<label style="display: block; margin-bottom: 10px; margin-top: 20px; font-weight: 600;">Command:</label> <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> <textarea id="quickCommand" placeholder="Enter command to execute (e.g., 'uptime' or 'df -h')"></textarea>
<button onclick="executeQuickCommand()">▶️ Execute Command</button> <button onclick="executeQuickCommand()">[ ▶️ Execute Command ]</button>
<div id="quickCommandResult" style="margin-top: 20px;"></div> <div id="quickCommandResult" style="margin-top: 20px;"></div>
</div> </div>
</div> </div>
@@ -803,7 +808,25 @@
<div class="modal-content"> <div class="modal-content">
<h2>Execution Details</h2> <h2>Execution Details</h2>
<div id="executionDetails"></div> <div id="executionDetails"></div>
<button onclick="closeModal('viewExecutionModal')">Close</button> <button onclick="closeModal('viewExecutionModal')">[ Close ]</button>
</div>
</div>
<!-- Command Templates Modal -->
<div id="commandTemplatesModal" class="modal">
<div class="modal-content">
<h2>Command Templates</h2>
<div id="templateList" style="max-height: 400px; overflow-y: auto;"></div>
<button onclick="closeModal('commandTemplatesModal')">[ Close ]</button>
</div>
</div>
<!-- Command History Modal -->
<div id="commandHistoryModal" class="modal">
<div class="modal-content">
<h2>Command History</h2>
<div id="historyList" style="max-height: 400px; overflow-y: auto;"></div>
<button onclick="closeModal('commandHistoryModal')">[ Close ]</button>
</div> </div>
</div> </div>
@@ -1167,6 +1190,82 @@
} }
} }
// Command Templates
const commandTemplates = [
{ name: 'System Info', cmd: 'uname -a', desc: 'Show system information' },
{ name: 'Uptime', cmd: 'uptime', desc: 'Show system uptime and load' },
{ name: 'Disk Usage', cmd: 'df -h', desc: 'Show disk space usage' },
{ name: 'Memory Usage', cmd: 'free -h', desc: 'Show memory usage' },
{ name: 'CPU Info', cmd: 'lscpu', desc: 'Show CPU information' },
{ name: 'Running Processes', cmd: 'ps aux --sort=-%mem | head -20', desc: 'Top 20 processes by memory' },
{ name: 'Network Interfaces', cmd: 'ip addr show', desc: 'Show network interfaces' },
{ name: 'Active Connections', cmd: 'ss -tunap', desc: 'Show active network connections' },
{ name: 'Docker Containers', cmd: 'docker ps -a', desc: 'List all Docker containers' },
{ name: 'System Log Tail', cmd: 'tail -n 50 /var/log/syslog', desc: 'Last 50 lines of system log' },
{ name: 'Who is Logged In', cmd: 'w', desc: 'Show logged in users' },
{ name: 'Last Logins', cmd: 'last -n 20', desc: 'Show last 20 logins' }
];
function showCommandTemplates() {
const html = commandTemplates.map((template, index) => `
<div class="template-item" onclick="useTemplate(${index})" style="cursor: pointer; padding: 12px; margin: 8px 0; background: #001a00; border: 1px solid #003300; border-left: 3px solid var(--terminal-green);">
<div style="color: var(--terminal-green); font-weight: bold; margin-bottom: 4px;">${template.name}</div>
<div style="color: var(--terminal-amber); font-family: var(--font-mono); font-size: 0.9em; margin-bottom: 4px;"><code>${escapeHtml(template.cmd)}</code></div>
<div style="color: #666; font-size: 0.85em;">${template.desc}</div>
</div>
`).join('');
document.getElementById('templateList').innerHTML = html;
document.getElementById('commandTemplatesModal').classList.add('show');
}
function useTemplate(index) {
document.getElementById('quickCommand').value = commandTemplates[index].cmd;
closeModal('commandTemplatesModal');
}
function showCommandHistory() {
const history = JSON.parse(localStorage.getItem('commandHistory') || '[]');
if (history.length === 0) {
document.getElementById('historyList').innerHTML = '<div class="empty">No command history yet</div>';
} else {
const html = history.map((item, index) => `
<div class="history-item" onclick="useHistoryCommand(${index})" style="cursor: pointer; padding: 12px; margin: 8px 0; background: #001a00; border: 1px solid #003300; border-left: 3px solid var(--terminal-amber);">
<div style="color: var(--terminal-green); font-family: var(--font-mono); margin-bottom: 4px;"><code>${escapeHtml(item.command)}</code></div>
<div style="color: #666; font-size: 0.85em;">${new Date(item.timestamp).toLocaleString()} - ${item.worker}</div>
</div>
`).join('');
document.getElementById('historyList').innerHTML = html;
}
document.getElementById('commandHistoryModal').classList.add('show');
}
function useHistoryCommand(index) {
const history = JSON.parse(localStorage.getItem('commandHistory') || '[]');
document.getElementById('quickCommand').value = history[index].command;
closeModal('commandHistoryModal');
}
function addToCommandHistory(command, workerName) {
const history = JSON.parse(localStorage.getItem('commandHistory') || '[]');
// Add to beginning, limit to 50 items
history.unshift({
command: command,
worker: workerName,
timestamp: new Date().toISOString()
});
// Keep only last 50 commands
if (history.length > 50) {
history.splice(50);
}
localStorage.setItem('commandHistory', JSON.stringify(history));
}
async function deleteWorker(workerId, name) { async function deleteWorker(workerId, name) {
if (!confirm(`Delete worker: ${name}?`)) return; if (!confirm(`Delete worker: ${name}?`)) return;
@@ -1248,31 +1347,39 @@
async function executeQuickCommand() { async function executeQuickCommand() {
const workerId = document.getElementById('quickWorkerSelect').value; const workerId = document.getElementById('quickWorkerSelect').value;
const command = document.getElementById('quickCommand').value; const command = document.getElementById('quickCommand').value;
if (!workerId || !command) { if (!workerId || !command) {
alert('Please select a worker and enter a command'); alert('Please select a worker and enter a command');
return; 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'); const resultDiv = document.getElementById('quickCommandResult');
resultDiv.innerHTML = '<div class="loading">Executing command...</div>'; resultDiv.innerHTML = '<div class="loading">Executing command...</div>';
try { try {
const response = await fetch(`/api/workers/${workerId}/command`, { const response = await fetch(`/api/workers/${workerId}/command`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command }) body: JSON.stringify({ command })
}); });
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
// Add to command history
addToCommandHistory(command, workerName);
resultDiv.innerHTML = ` resultDiv.innerHTML = `
<div style="background: #f0fdf4; border: 2px solid #86efac; padding: 15px; border-radius: 5px;"> <div style="background: #001a00; border: 2px solid var(--terminal-green); padding: 15px;">
<strong style="color: #166534;">✓ Command sent successfully!</strong> <strong style="color: var(--terminal-green);">✓ Command sent successfully!</strong>
<div style="margin-top: 10px; font-family: monospace; font-size: 0.9em;"> <div style="margin-top: 10px; font-family: var(--font-mono); font-size: 0.9em; color: var(--terminal-green);">
Execution ID: ${data.execution_id} Execution ID: ${data.execution_id}
</div> </div>
<div style="margin-top: 10px; color: #166534;"> <div style="margin-top: 10px; color: var(--terminal-amber);">
Check the Executions tab to see the results Check the Executions tab to see the results
</div> </div>
</div> </div>