import { rowHTML, setRows, safe, showToast, badge } from './helpers.js'; import { api } from './api.js'; // helpers const q = (r, s) => (r || document).querySelector(s); const qq = (r, s) => Array.from((r || document).querySelectorAll(s)); const low = (x) => String(x ?? '').toLowerCase(); // --- dropdown: wybierz pierwszy inny node od bieżącego function rebuildTargetSelect(selectEl, currentNode, nodes) { if (!selectEl) return; const cur = String(currentNode || '').trim(); const all = (nodes || []).map(n => String(n.name || n.node || n).trim()).filter(Boolean); const others = all.filter(n => n && n !== cur); selectEl.innerHTML = others.map(n => ``).join(''); // domyślnie pierwszy sensowny inny node if (selectEl.options.length > 0) selectEl.selectedIndex = 0; } // --- statusy + przyciski function boolRunning(statusRaw) { return /running|online|started/i.test(statusRaw || ''); } function setButtonsByStatus(tr, statusRaw) { const running = boolRunning(statusRaw); // start tylko gdy nie działa const bStart = tr.querySelector('.act-start'); const bStop = tr.querySelector('.act-stop'); const bShutdown = tr.querySelector('.act-shutdown'); if (bStart) bStart.disabled = running; if (bStop) bStop.disabled = !running; if (bShutdown) bShutdown.disabled = !running; // MIGRATE: aktywny także dla offline (offline migrate) const bMig = tr.querySelector('.act-migrate'); const sel = tr.querySelector('.target-node'); const hasTarget = !!(sel && sel.value); // blokujemy tylko gdy nie ma targetu albo trwa „working/progress” const busy = /working|progress|busy/i.test(statusRaw || ''); if (bMig) bMig.disabled = !hasTarget || busy; } // --- render export async function renderVMAdmin() { const wrap = document.getElementById('vm-admin'); if (!wrap) return; const tbody = wrap.querySelector('tbody'); if (!tbody) return; // pobierz listy const [list, nodesSummary] = await Promise.all([api.listAllVmct(), api.nodesSummary()]); const all = Array.isArray(list.all) ? list.all : []; const nodes = Array.isArray(nodesSummary?.nodes) ? nodesSummary.nodes : []; // zbuduj wiersze (bez checkboxa) const rows = all.map(vm => { const status = safe(vm.status || vm.current?.status || vm.current?.qmpstatus || '—'); const isRun = boolRunning(status); return rowHTML([ safe(vm.sid), safe(vm.name || vm.vmid || vm.sid), safe(vm.node || '—'), badge(status, isRun ? 'ok' : 'dark'), `
`, `
` ], `data-sid="${safe(vm.sid)}"`); }); setRows(tbody, rows); // uzupełnij selecty + stany przycisków qq(tbody, 'tr[data-sid]').forEach(tr => { const nodeCell = tr.children[2]; const statusCell= tr.children[3]; const sel = tr.querySelector('.target-node'); rebuildTargetSelect(sel, nodeCell?.textContent?.trim(), nodes); setButtonsByStatus(tr, statusCell?.innerText || ''); }); // --- delegacja CLICK globalnie: łapie też elementy po rerenderach document.addEventListener('click', async (ev) => { const btn = ev.target.closest?.('.act-start,.act-stop,.act-shutdown,.act-unlock,.act-migrate'); if (!btn) return; // zapobiegamy submitom, bąbelkowaniu itp. ev.preventDefault(); ev.stopPropagation(); const tr = btn.closest('tr[data-sid]'); if (!tr) return; const sid = tr.getAttribute('data-sid'); const name = tr.children[1]?.textContent?.trim() || sid; // mapowanie akcji let action = ''; let target = undefined; if (btn.classList.contains('act-start')) action = 'start'; if (btn.classList.contains('act-stop')) action = 'stop'; if (btn.classList.contains('act-shutdown')) action = 'shutdown'; if (btn.classList.contains('act-unlock')) action = 'unlock'; if (btn.classList.contains('act-migrate')) { action = 'migrate'; target = tr.querySelector('.target-node')?.value; if (!target) { showToast('Migrate', 'Wybierz docelowy node inny niż bieżący', 'warning'); return; } } try { const res = await api.vmAction(sid, action, target); if (res?.ok) { showToast('OK', `${action.toUpperCase()} × ${name}`, 'success'); } else { showToast('Błąd', res?.error || `Nie udało się: ${action}`, 'danger'); } } catch (e) { showToast('Błąd', String(e?.message || e), 'danger'); } }, { capture: true }); // capture: pewność, że nasz handler zadziała przed innymi // prosty, lekki refresh co 30s (statusy i node) setInterval(async () => { try { const latest = await api.listAllVmct(); const bySid = new Map((latest?.all || []).map(x => [String(x.sid), x])); qq(tbody, 'tr[data-sid]').forEach(tr => { const sid = tr.getAttribute('data-sid'); const row = bySid.get(sid); if (!row) return; const nodeCell = tr.children[2]; const statusCell = tr.children[3]; const sel = tr.querySelector('.target-node'); const newNode = String(row.node || '').trim(); if (newNode && nodeCell?.textContent?.trim() !== newNode) { nodeCell.textContent = newNode; rebuildTargetSelect(sel, newNode, nodes); } const st = String(row.status || row.current?.status || row.current?.qmpstatus || '').trim(); if (st && statusCell?.innerText?.trim() !== st) { statusCell.innerHTML = badge(st, boolRunning(st) ? 'ok' : 'dark'); setButtonsByStatus(tr, st); } }); } catch { /* no-op */ } }, 30000); }