From 6bb5e2715f4c30a93f1ebc34606f78e30e14e73c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 18 Oct 2025 21:59:24 +0200 Subject: [PATCH] refator_comm1 --- static/js/admin.js | 145 ++++++++++++++++++++----------------------- static/js/api.js | 1 - templates/index.html | 3 +- 3 files changed, 69 insertions(+), 80 deletions(-) diff --git a/static/js/admin.js b/static/js/admin.js index a0997f6..5ac9001 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -1,10 +1,9 @@ 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(); +let reconcileTimer = null; -/** Pomocniczo: zamknij WS-y dla sid */ function closeForSid(sid) { const entry = liveSockets.get(sid); if (!entry) return; @@ -13,13 +12,17 @@ function closeForSid(sid) { liveSockets.delete(sid); } -/** Exportowany helper do ew. sprzątania z zewnątrz (opcjonalnie) */ export function stopAllAdminWatches() { - for (const sid of liveSockets.keys()) closeForSid(sid); + if (reconcileTimer) { clearInterval(reconcileTimer); reconcileTimer = null; } + for (const sid of Array.from(liveSockets.keys())) closeForSid(sid); +} + +function setBadge(cell, val) { + if (!cell) return; + cell.innerHTML = val; } export async function renderVMAdmin() { - // zanim przebudujemy tabelę – zamknij WS-y (unikamy „sierotek”) stopAllAdminWatches(); const data = await api.listAllVmct(); @@ -44,7 +47,6 @@ export async function renderVMAdmin() { const tools = `
-
`; return rowHTML([sid, type.toUpperCase(), name, node, st, actions, sel, tools], `data-sid="${sid}"`); }); @@ -54,12 +56,10 @@ export async function renderVMAdmin() { Array.from(tbody.querySelectorAll('tr[data-sid]')).forEach(tr => { const sid = tr.getAttribute('data-sid'); const colSpan = tr.children.length; - const nodeCell = tr.children[3]; // kolumna Node - const badgeCell = tr.children[4]; // kolumna Status + const nodeCell = tr.children[3]; + const badgeCell = tr.children[4]; const targetSel = tr.querySelector('.target-node'); - const watchBtn = tr.querySelector('.act-watch'); - // subpanel (log) let sub = tr.nextElementSibling; if (!sub || !sub.classList.contains('mig-row')) { sub = document.createElement('tr'); sub.className = 'mig-row d-none'; @@ -88,20 +88,17 @@ export async function renderVMAdmin() { const html = availableNodes.map(n => ``).join(''); targetSel.innerHTML = html; - // 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`; - // 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)); @@ -117,80 +114,55 @@ export async function renderVMAdmin() { } else if (msg.type === 'status' && msg.status) { const ok = String(msg.status.exitstatus||'').toUpperCase() === 'OK'; const s = String(msg.status.status||'').toLowerCase(); - if (badgeCell) badgeCell.innerHTML = ok ? badge('running','ok') : - (s === 'stopped' ? badge('stopped','dark') : badge('working','info')); + setBadge(badgeCell, ok ? badge('running','ok') : + (s === 'stopped' ? badge('stopped','dark') : badge('working','info'))); } else if (msg.type === 'done') { const ok = !!msg.ok; - if (badgeCell) badgeCell.innerHTML = ok ? badge('running','ok') : badge('error','err'); + setBadge(badgeCell, ok ? badge('running','ok') : badge('error','err')); setRowBusy(false); 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 = () => { - // 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) { - badgeCell.innerHTML = ok ? badge('running','ok') : - (/stopp|shutdown|offline/.test(st) ? badge('stopped','dark') : badge(st||'—','info')); - } + setBadge(badgeCell, ok ? badge('running','ok') : + (/stopp|shutdown|offline/.test(st) ? badge('stopped','dark') : badge(st||'—','info'))); } - 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); } - else if (msg.type === 'task' && msg.upid && msg.status) { const stopped = String(msg.status||'').toLowerCase() === 'stopped'; if (stopped && typeof msg.exitstatus !== 'undefined') { const ok = String(msg.exitstatus||'').toUpperCase() === 'OK'; - if (badgeCell) badgeCell.innerHTML = ok ? badge('running','ok') : badge('error','err'); + setBadge(badgeCell, ok ? badge('running','ok') : badge('error','err')); } else { - if (badgeCell) badgeCell.innerHTML = badge('working','info'); + setBadge(badgeCell, badge('working','info')); } } - 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 {} } - else if (msg.type === 'done' && typeof msg.ok === 'boolean') { - if (badgeCell) badgeCell.innerHTML = msg.ok ? badge('running','ok') : badge('error','err'); + setBadge(badgeCell, msg.ok ? badge('running','ok') : badge('error','err')); } } catch {} @@ -199,16 +171,9 @@ export async function renderVMAdmin() { 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 = () => {}; }; const doAction = async (action, withTarget=false) => { @@ -216,22 +181,21 @@ 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); // pokaż subpanel logów dla akcji z zadaniami + if (action !== 'unlock') { + setBadge(badgeCell, badge('working','info')); + toggleSub(true); + } 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); // spinner wyłącz, status/log dojadą z observe + setRowBusy(false); return; } - // mamy UPID – od razu podłącz tail openTaskWS(resp.upid, resp.source_node); } catch (e) { @@ -241,34 +205,61 @@ 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', () => { - 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 { - ensureWatchOn(); - } - }); - } - - // startowo: LIVE bez klikania ensureWatchOn(); }); - // sprzątanie przy zamknięciu karty + reconcileTimer = 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])); + + 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 badgeCell = tr.children[4]; + const targetSel = tr.querySelector('.target-node'); + + const newNode = String(rowData.node || '').trim(); + if (nodeCell && newNode && nodeCell.textContent.trim() !== newNode) { + nodeCell.textContent = newNode; + if (targetSel) { + const options = Array.from(targetSel.options); + options.forEach(o => { o.disabled = (o.value === newNode); o.selected = (o.value === newNode); }); + const idx = options.findIndex(o => !o.disabled); + if (idx >= 0) targetSel.selectedIndex = idx; + } + } + + const running = /running/i.test(rowData.status || ''); + const currentBadge = badgeCell?.innerText?.toLowerCase() || ''; + const isWorking = currentBadge.includes('working'); + if (badgeCell && !isWorking) { + setBadge(badgeCell, running ? badge('running','ok') : badge(rowData.status || '—','dark')); + } + + const existing = liveSockets.get(sid); + if (!(existing && existing.obs && existing.obs.readyState <= 1)) { + const wsObs = new WebSocket(api.wsObserveURL(sid)); + liveSockets.set(sid, { obs: wsObs, task: existing?.task || null }); + wsObs.onmessage = () => {}; + wsObs.onerror = () => {}; + } + }); + + } catch {} + }, 3000); + window.addEventListener('beforeunload', stopAllAdminWatches, { once: true }); } diff --git a/static/js/api.js b/static/js/api.js index 1cae57f..86bac63 100644 --- a/static/js/api.js +++ b/static/js/api.js @@ -23,7 +23,6 @@ export const api = { const proto = (location.protocol === 'https:') ? 'wss' : 'ws'; return `${proto}://${location.host}/ws/task?upid=${encodeURIComponent(upid)}&node=${encodeURIComponent(node)}`; }, - // NEW: wsObserveURL: (sid) => { const proto = (location.protocol === 'https:') ? 'wss' : 'ws'; return `${proto}://${location.host}/ws/observe?sid=${encodeURIComponent(sid)}`; diff --git a/templates/index.html b/templates/index.html index b27eb29..9bb025c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -227,8 +227,7 @@ -
Akcje: Unlock (qm), Start/Stop/Shutdown, Offline migrate. Postęp widoczny na - żywo per wiersz.
+
Akcje: Unlock (qm), Start/Stop/Shutdown, Offline migrate.