From 766aac806965f75f5be2ecabd85fee6adab0a9dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 18 Oct 2025 23:56:30 +0200 Subject: [PATCH] refator_comm1 --- static/js/admin.js | 336 ++++++++++++++++++++++++++++++--------------- 1 file changed, 226 insertions(+), 110 deletions(-) diff --git a/static/js/admin.js b/static/js/admin.js index 666c460..ffc4aa3 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -1,155 +1,271 @@ import { rowHTML, setRows, safe, showToast, badge } from './helpers.js'; import { api } from './api.js'; -// helpers -const q = (r, s) => (r || document).querySelector(s); +// ==== 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 isRunning(st) { return /running|online|started/.test(low(st)); } +function isBusy(st) { return /working|progress|busy/.test(low(st)); } + +function setBadge(cell, statusRaw) { + if (!cell) return; + const s = String(statusRaw || '').trim() || '—'; + let hue = 'dark'; + if (isRunning(s)) hue = 'ok'; + else if (isBusy(s)) hue = 'info'; + cell.innerHTML = badge(s, hue); +} + +function extractNodes(nodesSummary) { + if (!nodesSummary) return []; + const arr = Array.isArray(nodesSummary.nodes) ? nodesSummary.nodes : nodesSummary; + return Array.from(new Set((arr || []).map(n => String(n?.name || n?.node || n || '').trim()).filter(Boolean))); +} + 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); + const cur = String(currentNode || '').trim(); + const list = (nodes || []).map(n => String(n).trim()).filter(Boolean); + const others = list.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 +function syncButtonsForRow(tr, statusRaw) { + const running = isRunning(statusRaw); + const busy = isBusy(statusRaw); const bStart = tr.querySelector('.act-start'); const bStop = tr.querySelector('.act-stop'); const bShutdown = tr.querySelector('.act-shutdown'); + const bUnlock = tr.querySelector('.act-unlock'); + const bMigrate = tr.querySelector('.act-migrate'); + const sel = tr.querySelector('.target-node'); - if (bStart) bStart.disabled = running; - if (bStop) bStop.disabled = !running; - if (bShutdown) bShutdown.disabled = !running; + if (bStart) bStart.disabled = running || busy; + if (bStop) bStop.disabled = !running || busy; + if (bShutdown) bShutdown.disabled = !running || busy; + if (bUnlock) bUnlock.disabled = busy; - // 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; + // MIGRATE włączony także dla offline; blokujemy tylko gdy brak targetu albo busy + if (bMigrate) bMigrate.disabled = !hasTarget || busy; } -// --- render +// ==== state: szybki refresh dla aktywnych SID-ów ==== +const activeSids = new Set(); +let slowTimer = null; +let fastTimer = null; +let cachedNodes = []; + +// ==== rendering ==== export async function renderVMAdmin() { - const wrap = document.getElementById('vm-admin'); - if (!wrap) return; - const tbody = wrap.querySelector('tbody'); + const table = document.getElementById('vm-admin'); + if (!table) return; + const tbody = table.querySelector('tbody'); if (!tbody) return; - // pobierz listy + // początkowe pobranie 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 : []; + const all = Array.isArray(list?.all) ? list.all : []; + cachedNodes = extractNodes(nodesSummary); - // zbuduj wiersze (bez checkboxa) + // zbuduj rzędy 1:1 z nagłówkiem (8 kolumn) const rows = all.map(vm => { + const sid = safe(vm.sid); + const type = safe(vm.type || vm.meta?.type || vm.kind || '—'); + const name = safe(vm.name || vm.vmid || vm.sid); + const node = safe(vm.node || vm.meta?.node || '—'); 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'), + sid, // SID + type, // Type + name, // Name + node, // Node + badge(status, isRunning(status) ? 'ok' : 'dark'), // Status + // Actions `
`, - `
- - -
` - ], `data-sid="${safe(vm.sid)}"`); + // Target + ``, + // Migrate + `` + ], `data-sid="${sid}"`); }); setRows(tbody, rows); - // uzupełnij selecty + stany przycisków + // wstępne wypełnienie selectów i ustawienie stanów 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 || ''); + const nodeCell = tr.children[3]; + const statusCell = tr.children[4]; + const sel = tr.querySelector('.target-node'); + rebuildTargetSelect(sel, nodeCell?.textContent?.trim(), cachedNodes); + syncButtonsForRow(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(); + // ==== delegacja kliknięć (capture, żeby nic nie przechwyciło) ==== + document.addEventListener('click', onClickAction, { capture: true }); - const tr = btn.closest('tr[data-sid]'); - if (!tr) return; - const sid = tr.getAttribute('data-sid'); - const name = tr.children[1]?.textContent?.trim() || sid; + // ==== odświeżanie ==== + if (slowTimer) clearInterval(slowTimer); + if (fastTimer) clearInterval(fastTimer); - // 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; - } - } + // cały listing co 30s + slowTimer = setInterval(refreshAllRows, 30000); - 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); + // aktywne SID-y co 10s + fastTimer = setInterval(refreshActiveRows, 10000); +} + +async function onClickAction(ev) { + const btn = ev.target.closest?.('.act-start,.act-stop,.act-shutdown,.act-unlock,.act-migrate'); + if (!btn) return; + ev.preventDefault(); + ev.stopPropagation(); + + const tr = btn.closest('tr[data-sid]'); + if (!tr) return; + const sid = tr.getAttribute('data-sid'); + const name = tr.children[2]?.textContent?.trim() || sid; + const statusCell = tr.children[4]; + const nodeCell = tr.children[3]; + + 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'; + const sel = tr.querySelector('.target-node'); + target = sel?.value; + const curNode = nodeCell?.textContent?.trim(); + if (!target || target === curNode) { + showToast('Migrate', 'Wybierz docelowy node inny niż bieżący', 'warning'); + return; + } + } + + try { + // status natychmiast na "working" + blokady + setBadge(statusCell, 'working'); + syncButtonsForRow(tr, 'working'); + activeSids.add(sid); + + 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'); + // pozwól wrócić przyciski do sensownych stanów po błędzie + await refreshOneRow(sid, tr); + activeSids.delete(sid); + } + } catch (e) { + showToast('Błąd', String(e?.message || e), 'danger'); + await refreshOneRow(sid, tr); + activeSids.delete(sid); + } +} + +async function refreshAllRows() { + const table = document.getElementById('vm-admin'); + if (!table) return; + const tbody = table.querySelector('tbody'); + if (!tbody) return; + + try { + const latest = await api.listAllVmct(); + const list = Array.isArray(latest?.all) ? latest.all : []; + const bySid = new Map(list.map(x => [String(x.sid), x])); + const nodes = Array.isArray(latest?.nodes) ? latest.nodes : null; + if (nodes) cachedNodes = extractNodes({ nodes }); + + qq(tbody, 'tr[data-sid]').forEach(tr => { + const sid = tr.getAttribute('data-sid'); + const row = bySid.get(sid); + if (!row) return; + + const typeCell = tr.children[1]; + const nameCell = tr.children[2]; + const nodeCell = tr.children[3]; + const statusCell = tr.children[4]; + const sel = tr.querySelector('.target-node'); + + // Node (i przebudowa selecta, jeśli się zmienił) + const newNode = String(row.node || row.meta?.node || '').trim(); + if (newNode && nodeCell?.textContent?.trim() !== newNode) { + nodeCell.textContent = newNode; + rebuildTargetSelect(sel, newNode, cachedNodes); + } + + // Type/Name + const newType = String(row.type || row.meta?.type || row.kind || '—'); + if (typeCell && typeCell.textContent !== newType) typeCell.textContent = newType; + + const newName = String(row.name || row.vmid || row.sid); + if (nameCell && nameCell.textContent !== newName) nameCell.textContent = newName; + + // Status – nie nadpisujemy "working" dla aktywnych SID-ów, + // żeby nie migało w trakcie taska; resztę aktualizujemy. + const st = String(row.status || row.current?.status || row.current?.qmpstatus || '').trim(); + if (!activeSids.has(sid) && st) { + setBadge(statusCell, st); + syncButtonsForRow(tr, st); + } else { + // ale jeśli working „utknął”, to podtrzymujemy blokady + syncButtonsForRow(tr, 'working'); + } + }); + } catch { /* cicho */ } +} + +async function refreshActiveRows() { + if (activeSids.size === 0) return; + const table = document.getElementById('vm-admin'); + if (!table) return; + const tbody = table.querySelector('tbody'); + if (!tbody) return; + + for (const sid of Array.from(activeSids)) { + const tr = tbody.querySelector(`tr[data-sid="${sid}"]`); + if (!tr) { activeSids.delete(sid); continue; } + await refreshOneRow(sid, tr); + } +} + +async function refreshOneRow(sid, tr) { + try { + const detail = await api.vmDetail(sid); + const nodeCell = tr.children[3]; + const statusCell = tr.children[4]; + const sel = tr.querySelector('.target-node'); + + // status z detail + const st = String(detail?.current?.status || detail?.current?.qmpstatus || detail?.status || '').trim(); + if (st) { + setBadge(statusCell, st); + syncButtonsForRow(tr, st); + // jeśli task się skończył, zdejmij z listy aktywnych + if (/running|stopped|shutdown|locked|error|failed|unknown|offline/.test(low(st))) { + activeSids.delete(sid); + } + } + + // node z detail + const newNode = String(detail?.node || detail?.meta?.node || '').trim(); + if (newNode && nodeCell?.textContent?.trim() !== newNode) { + nodeCell.textContent = newNode; + rebuildTargetSelect(sel, newNode, cachedNodes); + } + } catch { /* no-op */ } }