import { rowHTML, setRows, safe, showToast } 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 closeForSid(sid) { const entry = liveSockets.get(sid); if (!entry) return; try { entry.obs && entry.obs.close(); } catch {} liveSockets.delete(sid); } export function stopAllAdminWatches() { if (slowTimer) { clearInterval(slowTimer); slowTimer = null; } if (fastTimer) { clearInterval(fastTimer); fastTimer = null; } activeSids.clear(); for (const sid of Array.from(liveSockets.keys())) closeForSid(sid); } function flashDot(cell) { if (!cell) return; const nameWrap = cell.querySelector('.vm-name-wrap') || (() => { const w = document.createElement('span'); w.className = 'vm-name-wrap'; while (cell.firstChild) w.appendChild(cell.firstChild); cell.appendChild(w); return w; })(); const dot = document.createElement('span'); dot.className = 'pulse-dot'; nameWrap.appendChild(dot); setTimeout(() => { try { dot.remove(); } catch {} }, 4200); } function rebuildTargetSelect(selectEl, currentNode, nodes) { if (!selectEl) return; const html = nodes.map(n => `` ).join(''); selectEl.innerHTML = html; const idx = Array.from(selectEl.options).findIndex(o => !o.disabled); if (idx >= 0) selectEl.selectedIndex = idx; } export async function renderVMAdmin() { injectOnceCSS(); stopAllAdminWatches(); const data = await api.listAllVmct(); const arr = Array.isArray(data.all) ? data.all : []; const availableNodes = 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 nameCell = `${name}`; const actions = `
`; const sel = ``; const tools = `
`; // Kolumny: SID | TYPE | NAME | NODE | ACTIONS | TARGET | TOOLS return rowHTML([sid, type.toUpperCase(), nameCell, node, 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 nodeCell = tr.children[3]; const nameCell = tr.children[2]; const targetSel = tr.querySelector('.target-node'); const ensureWatchOn = () => { const existing = liveSockets.get(sid); if (existing && existing.obs && existing.obs.readyState <= 1) return; closeForSid(sid); const wsObs = new WebSocket(api.wsObserveURL(sid)); liveSockets.set(sid, { obs: wsObs }); wsObs.onmessage = (ev) => { try { const msg = JSON.parse(ev.data); if (msg.type === 'task-start') { activeSids.add(sid); flashDot(nameCell); } else if (msg.type === 'task') { activeSids.add(sid); flashDot(nameCell); } else if (msg.type === 'moved' && msg.new_node) { if (nodeCell && nodeCell.textContent.trim() !== msg.new_node) { nodeCell.textContent = msg.new_node; rebuildTargetSelect(targetSel, msg.new_node, window.__nodesCache || []); flashDot(nameCell); } activeSids.add(sid); } else if (msg.type === 'done') { setTimeout(() => activeSids.delete(sid), 4000); flashDot(nameCell); } } catch {} }; wsObs.onclose = () => { const e = liveSockets.get(sid); if (e && e.obs === wsObs) liveSockets.set(sid, { obs: null }); }; }; const doAction = async (action, withTarget=false) => { try { ensureWatchOn(); if (action !== 'unlock') activeSids.add(sid); const target = withTarget ? (targetSel?.value || '') : undefined; const resp = await api.vmAction(sid, action, target); if (!resp.ok) throw new Error(resp.error || 'unknown'); flashDot(nameCell); } catch (e) { showToast('Error', 'ERROR: ' + (e.message || e), 'danger'); activeSids.delete(sid); } }; tr.querySelector('.act-unlock')?.addEventListener('click', () => doAction('unlock')); 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-migrate')?.addEventListener('click', () => doAction('migrate', true)); tr.querySelector('.act-status')?.addEventListener('click', async () => { flashDot(nameCell); }); 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 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); } }); } catch {} }, 30000); // tylko aktywne – co 10 s 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 nameCell = tr.children[2]; const targetSel = tr.querySelector('.target-node'); const newNode = String(detail.node || (detail.meta && detail.meta.node) || '').trim(); if (newNode) { if (nodeCell && nodeCell.textContent.trim() !== newNode) { nodeCell.textContent = newNode; flashDot(nameCell); } rebuildTargetSelect(targetSel, newNode, window.__nodesCache || []); } setTimeout(() => activeSids.delete(sid), 4000); } } catch {} }, 10000); window.addEventListener('beforeunload', stopAllAdminWatches, { once: true }); }