Files
pve-ha-web/static/js/admin.js
Mateusz Gruszczyński cdc13def49 refator_comm1
2025-10-18 23:11:25 +02:00

262 lines
10 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 { rowHTML, setRows, safe, showToast, badge } from './helpers.js';
import { api } from './api.js';
const liveSockets = new Map();
let slowTimer = null;
let fastTimer = null;
const activeSids = new Set();
function injectOnceCSS() {
if (document.getElementById('vmadmin-live-css')) return;
const style = document.createElement('style');
style.id = 'vmadmin-live-css';
style.textContent = `
.pulse-dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:#22c55e;margin-left:.5rem;opacity:.25;animation:pulse-vm 1s ease-in-out 4}
@keyframes pulse-vm {0%,100%{opacity:.25;transform:scale(1)}50%{opacity:1;transform:scale(1.25)}}
`;
document.head.appendChild(style);
}
function flashDot(cell) {
if (!cell) return;
const dot = document.createElement('span');
dot.className = 'pulse-dot';
cell.appendChild(dot);
setTimeout(() => dot.remove(), 1500);
}
function setBadgeCell(cell, textOrState) {
if (!cell) return;
let html = '';
const s = String(textOrState || '').toLowerCase();
if (/running|online|started/.test(s)) html = badge('running','ok');
else if (/stopp|shutdown|offline/.test(s)) html = badge('stopped','dark');
else if (/working|progress|busy/.test(s)) html = badge('working','info');
else html = badge(textOrState || '—','dark');
if (cell.innerHTML !== html) {
cell.innerHTML = html;
return true; // changed
}
return false;
}
function rebuildTargetSelect(selectEl, currentNode, nodes) {
if (!selectEl) return [];
const others = (nodes || [])
.map(n => String(n).trim())
.filter(Boolean)
.filter(n => n !== String(currentNode || '').trim());
selectEl.innerHTML = others.map(n => `<option value="${n}">${n}</option>`).join('');
if (selectEl.options.length > 0) {
selectEl.selectedIndex = 0;
}
return others; // return list to decide enable/disable of MIGRATE
}
function updateMigrateButton(tr, isRunning) {
const btn = tr?.querySelector('.act-migrate');
const targetSel = tr?.querySelector('.target-node');
if (!btn) return;
const hasTarget = targetSel && targetSel.options && targetSel.options.length > 0;
const enable = isRunning && hasTarget;
if (enable) {
btn.removeAttribute('disabled');
btn.classList.remove('disabled');
} else {
btn.setAttribute('disabled', '');
btn.classList.add('disabled');
}
}
function updateActionButtons(tr, isRunning) {
const bStart = tr?.querySelector('.act-start');
const bStop = tr?.querySelector('.act-stop');
const bShutdown = tr?.querySelector('.act-shutdown');
if (bStart) {
if (isRunning) { bStart.setAttribute('disabled',''); bStart.classList.add('disabled'); }
else { bStart.removeAttribute('disabled'); bStart.classList.remove('disabled'); }
}
if (bStop) {
if (isRunning) { bStop.removeAttribute('disabled'); bStop.classList.remove('disabled'); }
else { bStop.setAttribute('disabled',''); bStop.classList.add('disabled'); }
}
if (bShutdown) {
if (isRunning) { bShutdown.removeAttribute('disabled'); bShutdown.classList.remove('disabled'); }
else { bShutdown.setAttribute('disabled',''); bShutdown.classList.add('disabled'); }
}
}
? Array.from(new Set(ns.nodes.map(n => String(n.name || n.node || n).trim()).filter(Boolean)))
: (Array.isArray(ns) ? Array.from(new Set(ns.map(n => String(n.name || n.node || n).trim()).filter(Boolean))) : []);
setRows(tbody, []);
// initial table fill
try {
const latest = await api.listAllVmct();
const all = Array.isArray(latest.all) ? latest.all : [];
const nodesNow = Array.isArray(latest.nodes) ? latest.nodes : [];
window.__nodesCache = nodesNow.slice();
const rows = all.map(vm => ({
sid: safe(vm.sid),
name: safe(vm.name || vm.vmid || vm.sid),
node: safe(vm.node || '—'),
status: safe(vm.status || '—')
}));
const htmlRows = rows.map(r => rowHTML([
`<input type=\"checkbox\" class=\"row-check\">`,
safe(r.sid),
safe(r.name),
safe(r.node),
badge(safe(r.status), /running|online|started/i.test(r.status) ? 'ok' : 'dark'),
`<div class=\"btn-group\">
<button class=\"btn btn-sm btn-success act-start\">Start</button>
<button class=\"btn btn-sm btn-outline-secondary act-shutdown\">Shutdown</button>
<button class=\"btn btn-sm btn-outline-danger act-stop\">Stop</button>
<button class=\"btn btn-sm btn-warning act-unlock\">Unlock</button>
</div>`,
`<select class=\"form-select form-select-sm target-node\"></select>`,
`<button class=\"btn btn-sm btn-primary w-100 act-migrate\" disabled>MIGRATE</button>`
]));
setRows(tbody, htmlRows);
// wire node selects and actions
Array.from(tbody.querySelectorAll('tr[data-sid]')).forEach(tr => {
const nodeCell = tr.children[3];
const targetSel = tr.querySelector('.target-node');
const _others = rebuildTargetSelect(targetSel, nodeCell?.textContent.trim(), availableNodes);
updateMigrateButton(tr, /running|online|started/i.test(tr.children[4].innerText));
const sid = tr.getAttribute('data-sid');
const nameCell = tr.children[2];
async function doAction(kind, needsTarget) {
try {
const targetNode = needsTarget ? targetSel?.value : undefined;
activeSids.add(sid);
setBadgeCell(tr.children[4], 'working');
updateMigrateButton(tr, false);
const res = await api.vmAction(sid, kind, targetNode);
if (res?.ok) {
showToast(`Task ${kind} started for ${safe(nameCell.textContent)}`);
} else {
showToast(`Task ${kind} failed for ${safe(nameCell.textContent)}`, 'danger');
}
} catch (e) {
showToast(`Error: ${e?.message || e}`, 'danger');
}
}
tr.querySelector('.act-start')?.addEventListener('click', () => doAction('start'));
tr.querySelector('.act-stop')?.addEventListener('click', () => doAction('stop'));
tr.querySelector('.act-shutdown')?.addEventListener('click', () => doAction('shutdown'));
tr.querySelector('.act-unlock')?.addEventListener('click', () => doAction('unlock'));
tr.querySelector('.act-migrate')?.addEventListener('click', () => doAction('migrate', true));
ensureWatchOn();
});
window.__nodesCache = availableNodes.slice();
// full refresh every 30s
slowTimer = setInterval(async () => {
try {
const latest = await api.listAllVmct();
const all = Array.isArray(latest.all) ? latest.all : [];
const bySid = new Map(all.map(x => [String(x.sid), x]));
const nodesNow = Array.isArray(latest.nodes) ? latest.nodes : window.__nodesCache || [];
window.__nodesCache = nodesNow;
Array.from(tbody.querySelectorAll('tr[data-sid]')).forEach(tr => {
const sid = tr.getAttribute('data-sid');
const rowData = bySid.get(sid);
if (!rowData) return;
const nodeCell = tr.children[3];
const statusCell= tr.children[4];
const nameCell = tr.children[2];
const targetSel = tr.querySelector('.target-node');
const newNode = String(rowData.node || '').trim();
if (nodeCell && newNode && nodeCell.textContent.trim() !== newNode) {
nodeCell.textContent = newNode;
const _others = rebuildTargetSelect(targetSel, newNode, nodesNow);
updateMigrateButton(tr, /running|online|started/i.test(tr.children[4].innerText));
flashDot(nameCell);
}
// status from slow reconcile — only when not 'working' to avoid overruling WS
const currentTxt = (statusCell?.innerText || '').toLowerCase();
if (!/working/.test(currentTxt)) {
const stRaw = String(rowData.status || '').toLowerCase(); // fallback z /cluster/resources
if (stRaw) {
const changed = setBadgeCell(statusCell, stRaw);
const isRunning = /running|online|started/.test(stRaw);
updateMigrateButton(tr, isRunning);
updateActionButtons(tr, isRunning);
updateActionButtons(tr, isRunning);
if (changed) flashDot(nameCell);
}
}
});
} catch {}
}, 30000);
// active only every 10s (pull precise status + node)
fastTimer = setInterval(async () => {
try {
const sids = Array.from(activeSids);
if (!sids.length) return;
for (const sid of sids) {
const detail = await api.vmDetail(sid);
if (!detail || !detail.meta) continue;
const tr = tbody.querySelector(`tr[data-sid="${sid}"]`);
if (!tr) continue;
const nodeCell = tr.children[3];
const statusCell= tr.children[4];
const nameCell = tr.children[2];
const targetSel = tr.querySelector('.target-node');
const stRaw = String((detail.current && (detail.current.status || detail.current.qmpstatus)) || '').toLowerCase();
const changed = setBadgeCell(statusCell, stRaw);
const isRunning = /running|online|started/.test(stRaw);
updateMigrateButton(tr, isRunning);
updateActionButtons(tr, isRunning);
updateActionButtons(tr, isRunning);
if (changed) flashDot(nameCell);
const newNode = String(detail.node || (detail.meta && detail.meta.node) || '').trim();
if (newNode) {
if (nodeCell && nodeCell.textContent.trim() !== newNode) {
nodeCell.textContent = newNode;
rebuildTargetSelect(targetSel, newNode, window.__nodesCache || []);
flashDot(nameCell);
}
}
if (stRaw && /running|stopped|shutdown/.test(stRaw)) {
setTimeout(() => activeSids.delete(sid), 4000);
}
}
} catch {}
}, 10000);
window.addEventListener('beforeunload', stopAllAdminWatches, { once: true });
} catch (e) {
showToast(`Failed to load list: ${e?.message || e}`, 'danger');
}
}
// Entry point expected by main.js
export async function renderVMAdmin() {
try {
await startAdminWatches();
} catch (e) {
showToast(`VM Admin initialization error: ${e?.message || e}`, 'danger');
console.error(e);
}
}