Files
pve-ha-web/static/js/admin.js
Mateusz Gruszczyński e3b3ff235b refator_comm1
2025-10-18 21:28:42 +02:00

194 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { badge, rowHTML, setRows, safe, showToast } from './helpers.js';
import { api } from './api.js';
export async function renderVMAdmin() {
const data = await api.listAllVmct();
const arr = Array.isArray(data.all) ? data.all : [];
const nodes = Array.isArray(data.nodes) ? data.nodes : [];
const tbody = document.querySelector('#vm-admin tbody');
if (!arr.length) { setRows(tbody, [rowHTML(['Brak VM/CT'])]); return; }
const rows = arr.map(x => {
const sid = safe(x.sid), type = safe(x.type), name = safe(x.name), node = safe(x.node);
const st = /running/i.test(x.status || '') ? badge(x.status,'ok') : badge(x.status || '—','dark');
const actions = `
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-secondary act-unlock">Unlock</button>
<button class="btn btn-outline-success act-start">Start</button>
<button class="btn btn-outline-warning act-shutdown">Shutdown</button>
<button class="btn btn-outline-danger act-stop">Stop</button>
</div>`;
const sel = `<select class="form-select form-select-sm target-node" style="min-width:140px">
${nodes.map(n => `<option value="${n}" ${n === x.node ? 'disabled' : ''}>${n}${n === x.node ? ' (src)' : ''}</option>`).join('')}
</select>`;
const tools = `<div class="d-flex align-items-center gap-2">
<button class="btn btn-outline-primary btn-sm act-migrate">Migrate (offline)</button>
<button class="btn btn-outline-secondary btn-sm act-status">Status</button>
<button class="btn btn-outline-info btn-sm act-watch" title="Live observe">Watch 🔔</button>
</div>`;
return rowHTML([sid, type.toUpperCase(), name, node, st, actions, sel, tools], `data-sid="${sid}"`);
});
setRows(tbody, rows);
Array.from(tbody.querySelectorAll('tr[data-sid]')).forEach(tr => {
const sid = tr.getAttribute('data-sid');
const colSpan = tr.children.length;
const nodeCell = tr.children[3]; // Node
const badgeCell = tr.children[4]; // Status
// subpanel (log)
let sub = tr.nextElementSibling;
if (!sub || !sub.classList.contains('mig-row')) {
sub = document.createElement('tr'); sub.className = 'mig-row d-none';
const td = document.createElement('td'); td.colSpan = colSpan;
td.innerHTML = '<div class="p-3 border rounded bg-body-tertiary"><div class="fw-semibold mb-1">Task status</div><pre class="small mb-0 mig-log">—</pre></div>';
sub.appendChild(td); tr.parentNode.insertBefore(sub, tr.nextSibling);
}
const logPre = sub.querySelector('.mig-log');
const toggleSub = (show) => sub.classList.toggle('d-none', !show);
let wsObs = null; // observe websocket
let wsTask = null; // tail websocket (auto z observe)
const closeWS = () => {
try { wsObs && wsObs.close(); } catch {}
try { wsTask && wsTask.close(); } catch {}
wsObs = wsTask = null;
};
const setRowBusy = (busy) => {
const nameCell = tr.children[2];
let spin = nameCell.querySelector('.op-spin');
if (busy && !spin) {
const span = document.createElement('span');
span.className = 'op-spin spinner-border spinner-border-sm align-middle ms-2';
span.setAttribute('role','status'); span.setAttribute('aria-hidden','true');
nameCell.appendChild(span);
}
if (!busy && spin) spin.remove();
};
const getTarget = () => tr.querySelector('.target-node')?.value || '';
const openTaskWS = (upid, node) => {
if (!upid) return;
toggleSub(true);
logPre.textContent = `UPID: ${upid} @ ${node}\n`;
wsTask = new WebSocket(api.wsTaskURL(upid, node));
wsTask.onmessage = (ev) => {
try {
const msg = JSON.parse(ev.data);
if (msg.type === 'log' && msg.line) {
logPre.textContent += msg.line + '\n';
logPre.scrollTop = logPre.scrollHeight;
} else if (msg.type === 'status' && msg.status) {
const ok = String(msg.status.exitstatus||'').toUpperCase() === 'OK';
const stopped = String(msg.status.status||'').toLowerCase() === 'stopped';
if (badgeCell) badgeCell.innerHTML = ok ? badge('running','ok') : (stopped ? badge('stopped','dark') : badge('working','info'));
} else if (msg.type === 'done') {
const ok = !!msg.ok;
if (badgeCell) badgeCell.innerHTML = ok ? badge('running','ok') : badge('error','err');
setRowBusy(false);
setTimeout(() => toggleSub(false), 1200);
try { document.getElementById('btnRefresh').click(); } catch {}
}
} catch {}
};
wsTask.onerror = () => {};
};
const openObserveWS = () => {
if (wsObs) return;
const url = api.wsObserveURL(sid);
wsObs = new WebSocket(url);
wsObs.onmessage = (ev) => {
try {
const msg = JSON.parse(ev.data);
if (msg.type === 'vm' && msg.current) {
const st = String(msg.current.status || msg.current.qmpstatus || '').toLowerCase();
const ok = /running|online|started/.test(st);
if (badgeCell) {
badgeCell.innerHTML = ok ? badge('running','ok') :
(/stopp|shutdown|offline/.test(st) ? badge('stopped','dark') : badge(st||'—','info'));
}
}
else if (msg.type === 'task-start' && msg.upid && msg.node) {
openTaskWS(msg.upid, msg.node);
}
else if (msg.type === 'task' && msg.upid && msg.status) {
const stopped = String(msg.status||'').toLowerCase() === 'stopped';
if (stopped && typeof msg.exitstatus !== 'undefined') {
const ok = String(msg.exitstatus||'').toUpperCase() === 'OK';
if (badgeCell) badgeCell.innerHTML = ok ? badge('running','ok') : badge('error','err');
} else {
if (badgeCell) badgeCell.innerHTML = badge('working','info');
}
}
else if (msg.type === 'moved' && msg.new_node) {
if (nodeCell) nodeCell.textContent = msg.new_node;
try { document.getElementById('btnRefresh').click(); } catch {}
}
else if (msg.type === 'done' && msg.upid) {
if (typeof msg.ok === 'boolean') {
if (badgeCell) badgeCell.innerHTML = msg.ok ? badge('running','ok') : badge('error','err');
}
}
} catch {}
};
wsObs.onclose = () => { wsObs = null; };
wsObs.onerror = () => {};
};
const doAction = async (action, withTarget=false) => {
setRowBusy(true);
try {
const target = withTarget ? getTarget() : undefined;
const resp = await api.vmAction(sid, action, target);
if (!resp.ok) throw new Error(resp.error || 'unknown');
// unlock brak UPID
if (!resp.upid) {
showToast('Success', `${action} executed for ${sid}`, 'success');
setRowBusy(false); toggleSub(false);
try { document.getElementById('btnRefresh').click(); } catch {}
return;
}
// UPID → WS tail
openTaskWS(resp.upid, resp.source_node);
} catch (e) {
showToast('Error', 'ERROR: ' + (e.message || e), 'danger');
setRowBusy(false);
}
};
tr.querySelector('.act-unlock')?.addEventListener('click', () => doAction('unlock'));
tr.querySelector('.act-start')?.addEventListener('click', () => { toggleSub(true); doAction('start'); });
tr.querySelector('.act-stop')?.addEventListener('click', () => { toggleSub(true); doAction('stop'); });
tr.querySelector('.act-shutdown')?.addEventListener('click', () => { toggleSub(true); doAction('shutdown'); });
tr.querySelector('.act-migrate')?.addEventListener('click', () => { toggleSub(true); doAction('migrate', true); });
tr.querySelector('.act-status')?.addEventListener('click', () => toggleSub(sub.classList.contains('d-none')));
const watchBtn = tr.querySelector('.act-watch');
if (watchBtn) {
watchBtn.addEventListener('click', () => {
if (wsObs) {
closeWS();
watchBtn.classList.remove('btn-info'); watchBtn.classList.add('btn-outline-info');
watchBtn.textContent = 'Watch 🔔';
} else {
openObserveWS();
watchBtn.classList.remove('btn-outline-info'); watchBtn.classList.add('btn-info');
watchBtn.textContent = 'Watching 🔔';
}
});
}
});
}