/* =====================================================================
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 = 'No workers available ';
return;
}
select.innerHTML = workers.map(w =>
'' + esc(w.name) + ' (' + esc(w.status) + ') '
).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 = '
';
return;
}
const keep = preserveIds || new Set();
wrap.innerHTML = workers.map(w => {
const checked = keep.has(w.id) ? ' checked' : '';
return (
'' +
' ' +
'' + (w.status === 'online' ? '●' : '○') + ' ' +
'' + esc(w.name) + ' ' +
' '
);
}).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 = '';
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
' +
'
' +
'
' +
'
';
}
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 + '
' +
'
' +
'
' +
'' +
'Worker Result ' +
'' + rows + ' ' +
'
';
}
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 });
})();