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 = `
`; const sel = ``; const migrateBtn = `
`; return rowHTML([sid, type.toUpperCase(), name, node, st, actions, sel, migrateBtn], `data-sid="${sid}"`); }); setRows(tbody, rows); Array.from(tbody.querySelectorAll('tr[data-sid]')).forEach(tr => { const sid = tr.getAttribute('data-sid'); const getTarget = () => tr.querySelector('.target-node')?.value || ''; const colSpan = tr.children.length; const setRowBusy = (busy) => { const nameCell = tr.children[2]; if (!nameCell) return; let spin = nameCell.querySelector('.op-spin'); if (busy) { if (!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); } } else { if (spin) spin.remove(); } }; const ensureMigRow = () => { let nxt = tr.nextElementSibling; if (!nxt || !nxt.classList.contains('mig-row')) { nxt = document.createElement('tr'); nxt.className = 'mig-row d-none'; const td = document.createElement('td'); td.colSpan = colSpan; td.innerHTML = '
Migration status
'; nxt.appendChild(td); tr.parentNode.insertBefore(nxt, tr.nextSibling); } return nxt; }; const setMigRowVisible = (row, vis) => row.classList.toggle('d-none', !vis); const tailTaskLog = async (upid, node, preEl, stopSignal) => { let start = 0; const delay = (ms)=>new Promise(r=>setTimeout(r,ms)); const append = (txt) => { if (!preEl) return; preEl.textContent = (preEl.textContent?preEl.textContent+'\n':'') + txt; preEl.scrollTop = preEl.scrollHeight; }; while (!stopSignal.done) { try { const d = await api.taskLog(upid, node, start); if (d && d.ok) { const lines = Array.isArray(d.lines) ? d.lines : []; for (const ln of lines) if (ln && typeof ln.t === 'string') append(ln.t); start = d.next_start ?? start; } } catch {} await delay(1500); } }; const pollTask = async (upid, node, onUpdate, onDone) => { let lastSt = null; if (!upid || !node) return; const started = Date.now(); const maxMs = 5*60*1000; const delay = (ms)=>new Promise(r=>setTimeout(r,ms)); while (Date.now() - started < maxMs) { try { const d = await api.taskStatus(upid, node); if (d && d.ok) { const st = d.status || {}; lastSt = st; try { onUpdate && onUpdate(st); } catch {} const s = (st.status||'').toLowerCase(); if (s === 'stopped' || st.exitstatus) break; } } catch {} await delay(2000); } try { onDone && onDone(lastSt); } catch {} }; const bind = (selector, action, needsTarget=false) => { const btn = tr.querySelector(selector); if (!btn) return; btn.onclick = async () => { setRowBusy(true); btn.disabled = true; try { if (action === 'migrate') { const target = getTarget(); const resp = await api.vmAction(sid, action, target); const row = ensureMigRow(); setMigRowVisible(row, true); const log = row.querySelector('.mig-log'); const srcNode = resp.source_node; const upid = resp.upid; const stopSig = { done:false }; tailTaskLog(upid, srcNode, log, stopSig); log.textContent = `Starting offline migrate to ${target} (UPID: ${upid || '—'})...`; const badgeCell = tr.children[4]; if (badgeCell) badgeCell.innerHTML = badge('migrating','info'); await pollTask(upid, srcNode, (st) => { const keys = ['type','status','pid','starttime','user','node','endtime','exitstatus']; const lines = keys.filter(k=>st && st[k]!==undefined).map(k=>`${k}: ${st[k]}`); log.textContent = lines.join('\n') || '—'; }, async (finalSt) => { stopSig.done = true; const exit = (finalSt && finalSt.exitstatus) ? String(finalSt.exitstatus) : ''; const ok = exit.toUpperCase() === 'OK'; const badgeCell = tr.children[4]; if (badgeCell) badgeCell.innerHTML = ok ? badge('running','ok') : badge('migrate error','err'); log.textContent += (log.textContent ? '\n' : '') + (ok ? 'Migration finished successfully.' : ('Migration failed: ' + (exit || 'unknown error'))); setRowBusy(false); // odśwież minimalnie: tylko ten wiersz przez szybkie /api/list-all-vmct? (tu prosty full-refresh stanu) try { document.getElementById('btnRefresh').click(); } catch {} if (ok) setTimeout(()=>{ const row = tr.nextElementSibling; if (row && row.classList.contains('mig-row')) setMigRowVisible(row,false); }, 2000); }); } else { await api.vmAction(sid, action, needsTarget ? getTarget() : undefined); showToast('Success', `${action} executed for ${sid}`, 'success'); setRowBusy(false); try { document.getElementById('btnRefresh').click(); } catch {} } } catch (e) { showToast('Error', 'ERROR: ' + (e.message || e), 'danger'); setRowBusy(false); } btn.disabled = false; }; }; bind('.act-unlock','unlock'); bind('.act-start','start'); bind('.act-stop','stop'); bind('.act-shutdown','shutdown'); bind('.act-migrate','migrate', true); const statusBtn = tr.querySelector('.act-status'); if (statusBtn) statusBtn.onclick = () => { const row = tr.nextElementSibling && tr.nextElementSibling.classList.contains('mig-row') ? tr.nextElementSibling : null; if (!row) return; const vis = row.classList.contains('d-none'); row.classList.toggle('d-none', !vis); }; }); }