/* ===================================================================== PULSE — Quick Command page (WP-F) Owns DOM id prefix `qc-` and action namespace `qc:*`. ===================================================================== */ 'use strict'; (function () { const esc = Pulse.esc; const HISTORY_KEY = 'commandHistory'; const HISTORY_LIMIT = 50; const RERUN_KEY = 'pulse_rerun'; /* Copied verbatim from the pre-redesign public/index.html commandTemplates array (12 templates) — kept static here since pageConfig only carries isAdmin from server.js (WP-A), not a templates list. */ const TEMPLATES = [ { 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' }, ]; let _workers = []; /* ------------------------------------------------------------------- History (localStorage, verbatim key/shape from the old UI) ------------------------------------------------------------------- */ function loadHistory() { return Pulse.util.storage.get(HISTORY_KEY, []) || []; } function addToHistory(command, workerLabel) { const history = loadHistory(); history.unshift({ command: command, worker: workerLabel, timestamp: new Date().toISOString() }); if (history.length > HISTORY_LIMIT) history.splice(HISTORY_LIMIT); Pulse.util.storage.set(HISTORY_KEY, history); } /* ------------------------------------------------------------------- Worker select / checkbox list ------------------------------------------------------------------- */ function currentMode() { const checked = document.querySelector('input[name="qc-exec-mode"]:checked'); return checked ? checked.value : 'single'; } function applyMode() { const mode = currentMode(); const single = document.getElementById('qc-single-mode'); const multi = document.getElementById('qc-multi-mode'); if (single) single.hidden = mode !== 'single'; if (multi) multi.hidden = mode !== 'multi'; } function renderWorkerSelect(workers, preserveId) { const select = document.getElementById('qc-worker'); if (!select) return; if (workers.length === 0) { select.innerHTML = ''; return; } select.innerHTML = workers.map(w => '' ).join(''); if (preserveId && workers.some(w => w.id === preserveId)) select.value = preserveId; } function renderWorkerCheckboxes(workers, preserveIds) { const wrap = document.getElementById('qc-worker-list'); if (!wrap) return; if (workers.length === 0) { wrap.innerHTML = '
No workers available
'; return; } const keep = preserveIds || new Set(); wrap.innerHTML = workers.map(w => { const checked = keep.has(w.id) ? ' checked' : ''; return ( '' ); }).join(''); } function selectedWorkerCheckboxIds() { return Array.from(document.querySelectorAll('input[name="qc-worker-cb"]:checked')).map(cb => cb.value); } async function loadWorkers() { const select = document.getElementById('qc-worker'); const preserveSingle = select ? select.value : ''; const preserveMulti = new Set(selectedWorkerCheckboxIds()); try { _workers = await Pulse.api.get('/api/workers') || []; } catch (e) { _workers = []; console.error('[Pulse:quick] failed to load workers', e); } renderWorkerSelect(_workers, preserveSingle); renderWorkerCheckboxes(_workers, preserveMulti); return _workers; } /* ------------------------------------------------------------------- Templates modal ------------------------------------------------------------------- */ function renderTemplates() { const list = document.getElementById('qc-templates-list'); if (!list) return; list.innerHTML = TEMPLATES.map((t, i) => '
' + '
' + esc(t.name) + '
' + '
' + esc(t.cmd) + '
' + '
' + esc(t.desc) + '
' + '
' ).join(''); } function renderHistory() { const list = document.getElementById('qc-history-list'); if (!list) return; const history = loadHistory(); if (history.length === 0) { list.innerHTML = '
No command history yet
'; return; } list.innerHTML = history.map((item, i) => '
' + '
' + esc(item.command) + '
' + '
' + esc(Pulse.fmt.dateTime(item.timestamp)) + ' — ' + esc(item.worker) + '
' + '
' ).join(''); } /* ------------------------------------------------------------------- Result rendering ------------------------------------------------------------------- */ function renderSingleSuccess(executionId) { const wrap = document.getElementById('qc-result'); if (!wrap) return; wrap.innerHTML = '
' + '' + '
' + '
Command sent successfully
' + '
Execution ID: ' + esc(executionId) + ' — ' + 'view in Executions' + '
' + '
' + '
'; } function renderSingleFailure(message) { const wrap = document.getElementById('qc-result'); if (!wrap) return; wrap.innerHTML = '
' + '' + '
' + '
Command failed
' + '
' + esc(message) + '
' + '
' + '
'; } function renderMultiResult(results, successCount, failCount) { const wrap = document.getElementById('qc-result'); if (!wrap) return; const rows = results.map(r => '' + '' + esc(r.worker) + '' + '' + (r.success ? '✓ Sent (' + esc(String(r.executionId || '').slice(0, 8)) + '…)' : '✕ ' + esc(r.error) + '') + '' + '' ).join(''); wrap.innerHTML = '
' + '
' + '
Multi-worker execution complete
' + '
Success: ' + successCount + '  |  Failed: ' + failCount + '
' + '
' + '
' + '' + '' + '' + rows + '' + '
WorkerResult
'; } function renderExecuting(count) { const wrap = document.getElementById('qc-result'); if (!wrap) return; wrap.innerHTML = '
Executing' + (count > 1 ? ' on ' + count + ' worker(s)' : '') + '…
'; } /* ------------------------------------------------------------------- Execute ------------------------------------------------------------------- */ async function execute() { const commandEl = document.getElementById('qc-command'); const command = commandEl ? commandEl.value.trim() : ''; if (!command) { Pulse.toast.error('Please enter a command'); return; } const mode = currentMode(); if (mode === 'single') { const select = document.getElementById('qc-worker'); const workerId = select ? select.value : ''; if (!workerId) { Pulse.toast.error('Please select a worker'); return; } const worker = _workers.find(w => w.id === workerId); const workerName = worker ? worker.name : 'Unknown'; renderExecuting(1); try { const data = await Pulse.api.post('/api/workers/' + encodeURIComponent(workerId) + '/command', { command }); addToHistory(command, workerName); renderSingleSuccess(data && data.execution_id); Pulse.beep('success'); } catch (e) { renderSingleFailure(e.message || 'Failed to execute command'); Pulse.beep('error'); } return; } const workerIds = selectedWorkerCheckboxIds(); if (workerIds.length === 0) { Pulse.toast.error('Please select at least one worker'); return; } renderExecuting(workerIds.length); const results = []; let successCount = 0; let failCount = 0; for (const workerId of workerIds) { const worker = _workers.find(w => w.id === workerId); const workerLabel = worker ? worker.name : workerId; try { const data = await Pulse.api.post('/api/workers/' + encodeURIComponent(workerId) + '/command', { command }); results.push({ worker: workerLabel, success: true, executionId: data && data.execution_id }); successCount++; } catch (e) { results.push({ worker: workerLabel, success: false, error: e.message || 'Failed to execute' }); failCount++; } } addToHistory(command, workerIds.length + ' workers'); renderMultiResult(results, successCount, failCount); if (failCount === 0) Pulse.beep('success'); else if (successCount > 0) Pulse.beep('info'); else Pulse.beep('error'); } /* ------------------------------------------------------------------- Cross-page re-run (from Executions page) ------------------------------------------------------------------- */ function consumeRerun() { let raw; try { raw = window.sessionStorage.getItem(RERUN_KEY); } catch (e) { return; } if (!raw) return; try { window.sessionStorage.removeItem(RERUN_KEY); } catch (e) { /* ignore */ } let payload; try { payload = JSON.parse(raw); } catch (e) { return; } if (!payload) return; const singleRadio = document.getElementById('qc-mode-single'); if (singleRadio) singleRadio.checked = true; applyMode(); if (payload.worker_id) { const select = document.getElementById('qc-worker'); const hasWorker = select && _workers.some(w => w.id === payload.worker_id); if (hasWorker) { select.value = payload.worker_id; } else { Pulse.toast.warning('Original worker is no longer available — select a worker to run this command.'); } } const commandEl = document.getElementById('qc-command'); if (commandEl) { commandEl.value = payload.command || ''; commandEl.focus(); } } /* ------------------------------------------------------------------- Data loading / refresh ------------------------------------------------------------------- */ async function refresh() { await loadWorkers(); } function init() { Pulse.actions.registerAll({ 'qc:mode': applyMode, 'qc:templates-open': () => { renderTemplates(); if (window.lt && lt.modal) lt.modal.open('qc-templates-modal'); }, 'qc:template-use': (el) => { const idx = Number(el.getAttribute('data-index')); const tpl = TEMPLATES[idx]; if (!tpl) return; const commandEl = document.getElementById('qc-command'); if (commandEl) commandEl.value = tpl.cmd; if (window.lt && lt.modal) lt.modal.close('qc-templates-modal'); }, 'qc:history-open': () => { renderHistory(); if (window.lt && lt.modal) lt.modal.open('qc-history-modal'); }, 'qc:history-use': (el) => { const idx = Number(el.getAttribute('data-index')); const history = loadHistory(); const item = history[idx]; if (!item) return; const commandEl = document.getElementById('qc-command'); if (commandEl) commandEl.value = item.command; if (window.lt && lt.modal) lt.modal.close('qc-history-modal'); }, 'qc:select-all': () => { document.querySelectorAll('input[name="qc-worker-cb"]').forEach(cb => { cb.checked = true; }); }, 'qc:select-online': () => { document.querySelectorAll('input[name="qc-worker-cb"]').forEach(cb => { cb.checked = cb.getAttribute('data-status') === 'online'; }); }, 'qc:clear-all': () => { document.querySelectorAll('input[name="qc-worker-cb"]').forEach(cb => { cb.checked = false; }); }, 'qc:execute': execute, }); if (window.lt && lt.keys && typeof lt.keys.on === 'function') { lt.keys.on('ctrl+enter', () => { execute(); }); } applyMode(); return loadWorkers().then(() => { consumeRerun(); }); } function onEvent(type) { if (type === 'worker_update') { loadWorkers(); return true; } return false; } Pulse.registerPage({ name: 'quick', init, refresh, onEvent }); })();