Files
pve-ha-web/static/js/admin.js
Mateusz Gruszczyński 2ffbe9badf refator_comm1
2025-10-18 22:57:05 +02:00

283 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; // zmiana
}
return false;
}
function rebuildTargetSelect(selectEl, currentNode, nodes) {
if (!selectEl) return;
const html = nodes.map(n =>
`<option value="${n}" ${n === currentNode ? 'disabled selected' : ''}>${n}</option>`
).join('');
selectEl.innerHTML = html;
const idx = Array.from(selectEl.options).findIndex(o => o.disabled);
selectEl.selectedIndex = idx >= 0 ? idx : 0;
}
function setMigrateDisabled(tr, isRunning) {
const btn = tr?.querySelector('.act-migrate');
if (!btn) return;
if (isRunning) {
btn.removeAttribute('disabled');
btn.classList.remove('disabled');
} else {
btn.setAttribute('disabled', '');
btn.classList.add('disabled');
}
}
export function stopAllAdminWatches() {
liveSockets.forEach(ws => { try { ws.close(); } catch {} });
liveSockets.clear();
if (slowTimer) clearInterval(slowTimer);
if (fastTimer) clearInterval(fastTimer);
slowTimer = null;
fastTimer = null;
activeSids.clear();
}
function ensureWatchOn() {
const tbody = document.querySelector('#vm-admin tbody');
if (!tbody) return;
Array.from(tbody.querySelectorAll('tr[data-sid]')).forEach(tr => {
const sid = tr.getAttribute('data-sid');
if (!sid) return;
if (liveSockets.has(sid)) return;
try {
const ws = new WebSocket(`${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws/observe?sid=${encodeURIComponent(sid)}`);
liveSockets.set(sid, ws);
ws.onopen = () => {};
ws.onclose = () => { liveSockets.delete(sid); };
ws.onerror = () => {};
ws.onmessage = (ev) => {
try {
const msg = JSON.parse(ev.data || '{}');
if (!msg || !msg.type) return;
const tr = tbody.querySelector(`tr[data-sid="${sid}"]`);
if (!tr) return;
const statusCell = tr.children[4];
const nameCell = tr.children[2];
const nodeCell = tr.children[3];
const targetSel = tr.querySelector('.target-node');
if (msg.type === 'status') {
const stRaw = String(msg.status || '').toLowerCase();
const changed = setBadgeCell(statusCell, stRaw);
const isRunning = /running|online|started/.test(stRaw);
setMigrateDisabled(tr, isRunning);
if (changed) flashDot(nameCell);
if (stRaw && /running|stopped|shutdown/.test(stRaw)) {
setTimeout(() => activeSids.delete(sid), 3000);
}
}
if (msg.type === 'node' && msg.node) {
const newNode = String(msg.node).trim();
if (nodeCell && newNode && nodeCell.textContent.trim() !== newNode) {
nodeCell.textContent = newNode;
rebuildTargetSelect(targetSel, newNode, window.__nodesCache || []);
flashDot(nameCell);
}
}
} catch {}
};
} catch {}
});
}
export async function startAdminWatches() {
injectOnceCSS();
const tbody = document.querySelector('#vm-admin tbody');
if (!tbody) return;
const availableNodes = await api.listNodes();
setRows(tbody, []);
// inicjalne wypełnienie tabeli
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 html = 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'),
`<select class="form-select form-select-sm target-node"></select>`,
`<div class="btn-group">
<button class="btn btn-sm btn-primary act-migrate" disabled>MIGRATE</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>
</div>`
])).join('');
setRows(tbody, html);
// podłącz selecty z node'ami i akcje
Array.from(tbody.querySelectorAll('tr[data-sid]')).forEach(tr => {
const nodeCell = tr.children[3];
const targetSel = tr.querySelector('.target-node');
rebuildTargetSelect(targetSel, nodeCell?.textContent.trim(), availableNodes);
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');
setMigrateDisabled(tr, false);
const res = await api.vmAction(sid, kind, targetNode);
if (res?.ok) {
showToast(`Zadanie ${kind} wystartowało dla ${safe(nameCell.textContent)}`);
} else {
showToast(`Błąd zadania ${kind} dla ${safe(nameCell.textContent)}`, 'danger');
}
} catch (e) {
showToast(`Błąd: ${e?.message || e}`, 'danger');
}
}
tr.querySelector('.act-stop')?.addEventListener('click', () => doAction('stop'));
tr.querySelector('.act-shutdown')?.addEventListener('click', () => doAction('shutdown'));
tr.querySelector('.act-migrate')?.addEventListener('click', () => doAction('migrate', true));
ensureWatchOn();
});
window.__nodesCache = availableNodes.slice();
// pełna lista co 30 s
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;
rebuildTargetSelect(targetSel, newNode, nodesNow);
flashDot(nameCell);
}
// status z wolnego reconcile — tylko gdy brak „working”, żeby nie zagłuszać 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);
setMigrateDisabled(tr, isRunning);
if (changed) flashDot(nameCell);
}
}
});
} catch {}
}, 30000);
// tylko aktywne co 10 s (dociąga precyzyjny 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);
setMigrateDisabled(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(`Nie udało się załadować listy: ${e?.message || e}`, 'danger');
}
}