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

181 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 badgeCell = tr.children[4];
// 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) {
// aktualizuj badge na podstawie bieżącego statusu
const st = String(msg.current.status || msg.current.qmpstatus || '').toLowerCase();
const ok = /running|online|started/.test(st);
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) {
// automatycznie podłącz tail do nowo wykrytego taska
openTaskWS(msg.upid, msg.node);
} else if (msg.type === 'task' && msg.upid && msg.status) {
// szybkie mrugnięcie statusem
const stopped = String(msg.status||'').toLowerCase() === 'stopped';
if (stopped && typeof msg.exitstatus !== 'undefined') {
const ok = String(msg.exitstatus||'').toUpperCase() === 'OK';
badgeCell.innerHTML = ok ? badge('running','ok') : badge('error','err');
} else {
badgeCell.innerHTML = badge('working','info');
}
} else if (msg.type === 'done' && msg.upid) {
// koniec zewnętrznego zadania (bez naszego taila)
if (msg.ok) badgeCell.innerHTML = badge('running','ok');
else badgeCell.innerHTML = badge('error','err');
try { document.getElementById('btnRefresh').click(); } catch {}
}
} 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); });
// Status pokaz/ukryj subpanel (bez WS)
tr.querySelector('.act-status')?.addEventListener('click', () => toggleSub(sub.classList.contains('d-none')));
// NEW: Watch 🔔 włącz/wyłącz broadcast observe
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 🔔';
}
});
}
});
}