diff --git a/static/js/admin.js b/static/js/admin.js index bc8985e..a0997f6 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -1,7 +1,27 @@ import { badge, rowHTML, setRows, safe, showToast } from './helpers.js'; import { api } from './api.js'; +/** Globalny rejestr gniazd: sid -> { obs: WebSocket|null, task: WebSocket|null } */ +const liveSockets = new Map(); + +/** Pomocniczo: zamknij WS-y dla sid */ +function closeForSid(sid) { + const entry = liveSockets.get(sid); + if (!entry) return; + try { entry.obs && entry.obs.close(); } catch {} + try { entry.task && entry.task.close(); } catch {} + liveSockets.delete(sid); +} + +/** Exportowany helper do ew. sprzątania z zewnątrz (opcjonalnie) */ +export function stopAllAdminWatches() { + for (const sid of liveSockets.keys()) closeForSid(sid); +} + export async function renderVMAdmin() { + // zanim przebudujemy tabelę – zamknij WS-y (unikamy „sierotek”) + stopAllAdminWatches(); + const data = await api.listAllVmct(); const arr = Array.isArray(data.all) ? data.all : []; const availableNodes = Array.isArray(data.nodes) ? data.nodes : []; @@ -19,12 +39,12 @@ export async function renderVMAdmin() { `; const sel = ``; const tools = `
- +
`; return rowHTML([sid, type.toUpperCase(), name, node, st, actions, sel, tools], `data-sid="${sid}"`); }); @@ -50,15 +70,6 @@ export async function renderVMAdmin() { const logPre = sub.querySelector('.mig-log'); const toggleSub = (show) => sub.classList.toggle('d-none', !show); - let wsObs = null; // observe websocket - let wsTask = null; // tail websocket - - const closeWS = () => { - try { wsObs && wsObs.close(); } catch {} - try { wsTask && wsTask.close(); } catch {} - wsObs = wsTask = null; - }; - const setRowBusy = (busy) => { const nameCell = tr.children[2]; let spin = nameCell.querySelector('.op-spin'); @@ -77,21 +88,26 @@ export async function renderVMAdmin() { const html = availableNodes.map(n => ``).join(''); targetSel.innerHTML = html; - // jeśli selected jest disabled, ustaw zaznaczenie na pierwszy dozwolony - if (targetSel.options.length) { - const idx = Array.from(targetSel.options).findIndex(o => !o.disabled); - if (idx >= 0) targetSel.selectedIndex = idx; - } + // ustaw focus na pierwszy dozwolony, jeśli selected jest disabled + const idx = Array.from(targetSel.options).findIndex(o => !o.disabled); + if (idx >= 0) targetSel.selectedIndex = idx; }; const getTarget = () => targetSel?.value || ''; + // ---- WS task tail (na bieżący UPID) ---- const openTaskWS = (upid, node) => { if (!upid) return; toggleSub(true); logPre.textContent = `UPID: ${upid} @ ${node}\n`; - try { wsTask && wsTask.close(); } catch {} - wsTask = new WebSocket(api.wsTaskURL(upid, node)); + + // zamknij ewentualnie poprzednie tail WS dla tej VM + const entry = liveSockets.get(sid) || {}; + try { entry.task && entry.task.close(); } catch {} + const wsTask = new WebSocket(api.wsTaskURL(upid, node)); + entry.task = wsTask; + liveSockets.set(sid, entry); + wsTask.onmessage = (ev) => { try { const msg = JSON.parse(ev.data); @@ -107,29 +123,42 @@ export async function renderVMAdmin() { const ok = !!msg.ok; if (badgeCell) badgeCell.innerHTML = ok ? badge('running','ok') : badge('error','err'); setRowBusy(false); - // nie chowam subpanelu natychmiast, żeby dać doczytać log setTimeout(() => toggleSub(false), 1500); try { document.getElementById('btnRefresh').click(); } catch {} } } catch {} }; wsTask.onerror = () => {}; + wsTask.onclose = () => { + // nie czyścimy entry.task, żeby móc rozróżnić zamknięcie „nasze” vs. serwera + }; }; + // ---- WS observe (domyślnie ON) ---- const ensureWatchOn = () => { - if (wsObs) return; - const url = api.wsObserveURL(sid); - wsObs = new WebSocket(url); + // jeśli już obserwujemy – nic nie rób + const existing = liveSockets.get(sid); + if (existing && existing.obs && existing.obs.readyState <= 1) return; + + // upewnij się, że stare gniazda zamknięte + closeForSid(sid); + + const wsObs = new WebSocket(api.wsObserveURL(sid)); + liveSockets.set(sid, { obs: wsObs, task: null }); + + // wizualnie: włączony if (watchBtn) { watchBtn.classList.remove('btn-outline-info'); watchBtn.classList.add('btn-info'); watchBtn.textContent = 'Watching 🔔'; } + wsObs.onmessage = (ev) => { try { const msg = JSON.parse(ev.data); if (msg.type === 'vm' && msg.current) { + // live status (działa także dla start/stop/shutdown bez UPID) const st = String(msg.current.status || msg.current.qmpstatus || '').toLowerCase(); const ok = /running|online|started/.test(st); if (badgeCell) { @@ -139,6 +168,7 @@ export async function renderVMAdmin() { } else if (msg.type === 'task-start' && msg.upid && msg.node) { + // jeżeli akcja wystartowała spoza panelu lub bez UPID od API — podepnij log openTaskWS(msg.upid, msg.node); } @@ -153,6 +183,7 @@ export async function renderVMAdmin() { } else if (msg.type === 'moved' && msg.new_node) { + // VM przeniesiona – od razu popraw wiersz if (nodeCell) nodeCell.textContent = msg.new_node; rebuildTargetSelect(msg.new_node); try { document.getElementById('btnRefresh').click(); } catch {} @@ -164,7 +195,19 @@ export async function renderVMAdmin() { } catch {} }; - wsObs.onclose = () => { wsObs = null; if (watchBtn) { watchBtn.classList.remove('btn-info'); watchBtn.classList.add('btn-outline-info'); watchBtn.textContent='Watch 🔔'; } }; + + wsObs.onclose = () => { + const e = liveSockets.get(sid); + if (e && e.obs === wsObs) { + // oznacz jako wyłączone + liveSockets.set(sid, { obs: null, task: e.task || null }); + if (watchBtn) { + watchBtn.classList.remove('btn-info'); + watchBtn.classList.add('btn-outline-info'); + watchBtn.textContent = 'Watch 🔔'; + } + } + }; wsObs.onerror = () => {}; }; @@ -173,20 +216,22 @@ export async function renderVMAdmin() { try { const target = withTarget ? getTarget() : undefined; + // ZAWSZE live – zapewnia status/log nawet jeśli API nie odda UPID ensureWatchOn(); - if (action !== 'unlock') toggleSub(true); + if (action !== 'unlock') toggleSub(true); // pokaż subpanel logów dla akcji z zadaniami const resp = await api.vmAction(sid, action, target); if (!resp.ok) throw new Error(resp.error || 'unknown'); if (!resp.upid) { - + // np. unlock albo środowisko nie oddało UPID – poczekaj na task-start z observe logPre.textContent = `Waiting for task… (${action})\n`; showToast('Info', `${action} zainicjowane`, 'info'); - setRowBusy(false); + setRowBusy(false); // spinner wyłącz, status/log dojadą z observe return; } + // mamy UPID – od razu podłącz tail openTaskWS(resp.upid, resp.source_node); } catch (e) { @@ -196,18 +241,22 @@ export async function renderVMAdmin() { } }; + // Akcje 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)); + // Status – pokaż/ukryj subpanel (bez WS) tr.querySelector('.act-status')?.addEventListener('click', () => toggleSub(sub.classList.contains('d-none'))); + // Watch 🔔 – manualny toggle (domyślnie jest ON) if (watchBtn) { watchBtn.addEventListener('click', () => { - if (wsObs) { - closeWS(); + const e = liveSockets.get(sid); + if (e && e.obs) { + closeForSid(sid); watchBtn.classList.remove('btn-info'); watchBtn.classList.add('btn-outline-info'); watchBtn.textContent = 'Watch 🔔'; } else { @@ -216,6 +265,10 @@ export async function renderVMAdmin() { }); } - rebuildTargetSelect(nodeCell?.textContent?.trim()); + // startowo: LIVE bez klikania + ensureWatchOn(); }); + + // sprzątanie przy zamknięciu karty + window.addEventListener('beforeunload', stopAllAdminWatches, { once: true }); }