From 2ffbe9badf41b35519cbd643abe00038fb2993b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 18 Oct 2025 22:57:05 +0200 Subject: [PATCH] refator_comm1 --- static/js/admin.js | 398 +++++++++++++++++++++++---------------------- static/styles.css | 12 ++ 2 files changed, 218 insertions(+), 192 deletions(-) diff --git a/static/js/admin.js b/static/js/admin.js index 4c88b9c..cea6d22 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -17,32 +17,12 @@ function injectOnceCSS() { 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); + cell.appendChild(dot); + setTimeout(() => dot.remove(), 1500); } function setBadgeCell(cell, textOrState) { @@ -66,203 +46,237 @@ function rebuildTargetSelect(selectEl, currentNode, nodes) { `` ).join(''); selectEl.innerHTML = html; - const idx = Array.from(selectEl.options).findIndex(o => !o.disabled); - if (idx >= 0) selectEl.selectedIndex = idx; + const idx = Array.from(selectEl.options).findIndex(o => o.disabled); + selectEl.selectedIndex = idx >= 0 ? idx : 0; } -function setMigrateDisabled(tr, disabled) { - const btn = tr.querySelector('.act-migrate'); - if (btn) btn.disabled = !!disabled; +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 async function renderVMAdmin() { - injectOnceCSS(); - stopAllAdminWatches(); +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(); +} - const data = await api.listAllVmct(); - const arr = Array.isArray(data.all) ? data.all : []; - const availableNodes = Array.isArray(data.nodes) ? data.nodes : []; +function ensureWatchOn() { 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 statusCell = badge('—','dark'); - const actions = ` -
- - - - -
`; - const sel = ``; - const migrateBtn = ``; - // SID | TYPE | NAME | NODE | STATUS | ACTIONS | TARGET | MIGRATE - return rowHTML([sid, type.toUpperCase(), nameCell, node, statusCell, actions, sel, migrateBtn], `data-sid="${sid}"`); - }); - - setRows(tbody, rows); - + if (!tbody) return; Array.from(tbody.querySelectorAll('tr[data-sid]')).forEach(tr => { const sid = tr.getAttribute('data-sid'); - const nodeCell = tr.children[3]; - const statusCell= tr.children[4]; - const nameCell = tr.children[2]; - const targetSel = tr.querySelector('.target-node'); + if (!sid) return; + if (liveSockets.has(sid)) return; - 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 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); + 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 === 'vm' && msg.current) { - const st = String(msg.current.status || msg.current.qmpstatus || '').toLowerCase(); - const changed = setBadgeCell(statusCell, st); - const isRunning = /running|online|started/.test(st); + 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); + } } - else if (msg.type === 'task-start') { - setBadgeCell(statusCell, 'working'); - activeSids.add(sid); - flashDot(nameCell); - } - else if (msg.type === 'task') { - setBadgeCell(statusCell, 'working'); - activeSids.add(sid); - } - 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 || []); + + 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); } - activeSids.add(sid); - } - else if (msg.type === 'done') { - // status końcowy dociągnie kolejny pakiet "vm" lub szybki refresh - 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') { - setBadgeCell(statusCell, 'working'); - 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)); - - 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)) { - // brak statusu w liście — zostaw jak jest, dociągnie WS/fastTimer - } - }); } 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; +export async function startAdminWatches() { + injectOnceCSS(); - const nodeCell = tr.children[3]; - const statusCell= tr.children[4]; - const nameCell = tr.children[2]; - const targetSel = tr.querySelector('.target-node'); + const tbody = document.querySelector('#vm-admin tbody'); + if (!tbody) return; - 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 availableNodes = await api.listNodes(); + setRows(tbody, []); - const newNode = String(detail.node || (detail.meta && detail.meta.node) || '').trim(); - if (newNode) { - if (nodeCell && nodeCell.textContent.trim() !== newNode) { + // 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([ + ``, + safe(r.sid), + safe(r.name), + safe(r.node), + badge(safe(r.status), /running|online|started/i.test(r.status) ? 'ok' : 'dark'), + ``, + `
+ + + +
` + ])).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, window.__nodesCache || []); + rebuildTargetSelect(targetSel, newNode, nodesNow); flashDot(nameCell); } - } - if (stRaw && /running|stopped|shutdown/.test(stRaw)) { - setTimeout(() => activeSids.delete(sid), 4000); - } - } - } catch {} - }, 10000); + // 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); - window.addEventListener('beforeunload', stopAllAdminWatches, { once: true }); + // 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'); + } } diff --git a/static/styles.css b/static/styles.css index 4f00865..48b454c 100644 --- a/static/styles.css +++ b/static/styles.css @@ -142,4 +142,16 @@ footer.site-footer a:hover { 100% { background-position: 0 0; } +} + +#toast-container { + position: fixed; + right: max(env(safe-area-inset-right), 1rem); + bottom: max(env(safe-area-inset-bottom), 1rem); + z-index: 1080; + pointer-events: none; +} + +#toast-container .toast { + pointer-events: auto; } \ No newline at end of file