diff --git a/static/js/admin.js b/static/js/admin.js index 7c9f1c4..cbc3871 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -1,11 +1,18 @@ import { rowHTML, setRows, safe, showToast, badge } from './helpers.js'; import { api } from './api.js'; +// --- State --- const liveSockets = new Map(); let slowTimer = null; let fastTimer = null; const activeSids = new Set(); +// --- Small helpers (no optional chaining) --- +function qSel(root, sel){ return root ? root.querySelector(sel) : null; } +function text(el){ return (el && el.textContent) ? el.textContent : ''; } +function val(el){ return el ? el.value : undefined; } +function low(x){ return String(x||'').toLowerCase(); } + function injectOnceCSS() { if (document.getElementById('vmadmin-live-css')) return; const style = document.createElement('style'); @@ -22,75 +29,128 @@ function flashDot(cell) { const dot = document.createElement('span'); dot.className = 'pulse-dot'; cell.appendChild(dot); - setTimeout(() => dot.remove(), 1500); + setTimeout(() => { if (dot && dot.parentNode) dot.parentNode.removeChild(dot); }, 1500); } function setBadgeCell(cell, textOrState) { - if (!cell) return; + if (!cell) return false; let html = ''; - const s = String(textOrState || '').toLowerCase(); + const s = low(textOrState); if (/running|online|started/.test(s)) html = badge('running','ok'); else if (/stopp|shutdown|offline/.test(s)) html = badge('stopped','dark'); else if (/working|progress|busy/.test(s)) html = badge('working','info'); else html = badge(textOrState || '—','dark'); - if (cell.innerHTML !== html) { - cell.innerHTML = html; - return true; // changed - } + if (cell.innerHTML !== html) { cell.innerHTML = html; return true; } return false; } function rebuildTargetSelect(selectEl, currentNode, nodes) { if (!selectEl) return []; + const current = String(currentNode || '').trim(); const others = (nodes || []) - .map(n => String(n).trim()) + .map(n => String(n && (n.name || n.node || n)).trim()) .filter(Boolean) - .filter(n => n !== String(currentNode || '').trim()); + .filter(n => n !== current); selectEl.innerHTML = others.map(n => ``).join(''); - if (selectEl.options.length > 0) { - selectEl.selectedIndex = 0; - } - return others; // return list to decide enable/disable of MIGRATE + if (selectEl.options.length > 0) selectEl.selectedIndex = 0; + return others; } function updateMigrateButton(tr, isRunning) { - const btn = tr?.querySelector('.act-migrate'); - const targetSel = tr?.querySelector('.target-node'); + const btn = qSel(tr, '.act-migrate'); + const targetSel = qSel(tr, '.target-node'); + const hasTarget = !!(targetSel && targetSel.options && targetSel.options.length > 0); + const enable = !!(isRunning && hasTarget); if (!btn) return; - const hasTarget = targetSel && targetSel.options && targetSel.options.length > 0; - const enable = isRunning && hasTarget; - if (enable) { - btn.removeAttribute('disabled'); - btn.classList.remove('disabled'); - } else { - btn.setAttribute('disabled', ''); - btn.classList.add('disabled'); - } + if (enable) { btn.removeAttribute('disabled'); btn.classList.remove('disabled'); } + else { btn.setAttribute('disabled',''); btn.classList.add('disabled'); } } function updateActionButtons(tr, isRunning) { - const bStart = tr?.querySelector('.act-start'); - const bStop = tr?.querySelector('.act-stop'); - const bShutdown = tr?.querySelector('.act-shutdown'); - if (bStart) { - if (isRunning) { bStart.setAttribute('disabled',''); bStart.classList.add('disabled'); } - else { bStart.removeAttribute('disabled'); bStart.classList.remove('disabled'); } - } - if (bStop) { - if (isRunning) { bStop.removeAttribute('disabled'); bStop.classList.remove('disabled'); } - else { bStop.setAttribute('disabled',''); bStop.classList.add('disabled'); } - } - if (bShutdown) { - if (isRunning) { bShutdown.removeAttribute('disabled'); bShutdown.classList.remove('disabled'); } - else { bShutdown.setAttribute('disabled',''); bShutdown.classList.add('disabled'); } - } + const bStart = qSel(tr, '.act-start'); + const bStop = qSel(tr, '.act-stop'); + const bShutdown = qSel(tr, '.act-shutdown'); + if (bStart) { if (isRunning) { bStart.setAttribute('disabled',''); bStart.classList.add('disabled'); } else { bStart.removeAttribute('disabled'); bStart.classList.remove('disabled'); } } + if (bStop) { if (isRunning) { bStop.removeAttribute('disabled'); bStop.classList.remove('disabled'); } else { bStop.setAttribute('disabled',''); bStop.classList.add('disabled'); } } + if (bShutdown) { if (isRunning) { bShutdown.removeAttribute('disabled'); bShutdown.classList.remove('disabled'); } else { bShutdown.setAttribute('disabled',''); bShutdown.classList.add('disabled'); } } } - ? Array.from(new Set(ns.nodes.map(n => String(n.name || n.node || n).trim()).filter(Boolean))) - : (Array.isArray(ns) ? Array.from(new Set(ns.map(n => String(n.name || n.node || n).trim()).filter(Boolean))) : []); +export function stopAllAdminWatches() { + liveSockets.forEach(function(ws){ try { ws.close(); } catch(e){} }); + liveSockets.clear(); + if (slowTimer) clearInterval(slowTimer); + if (fastTimer) clearInterval(fastTimer); + slowTimer = null; fastTimer = null; activeSids.clear(); +} + +function ensureWatchOn() { + const tbody = document.querySelector('#vm-admin tbody'); + if (!tbody) return; + const rows = Array.from(tbody.querySelectorAll('tr[data-sid]')); + rows.forEach(function(tr){ + const sid = tr.getAttribute('data-sid'); + if (!sid || liveSockets.has(sid)) return; + try { + const wsProto = (location.protocol === 'https:') ? 'wss' : 'ws'; + const ws = new WebSocket(wsProto + '://' + location.host + '/ws/observe?sid=' + encodeURIComponent(sid)); + liveSockets.set(sid, ws); + ws.onopen = function(){}; + ws.onclose = function(){ liveSockets.delete(sid); }; + ws.onerror = function(){}; + ws.onmessage = function(ev){ + try { + 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 = qSel(tr, '.target-node'); + + if (msg.type === 'status') { + const stRaw = low(msg.status); + const changed = setBadgeCell(statusCell, stRaw); + const isRunning = /running|online|started/.test(stRaw); + updateMigrateButton(tr, isRunning); + updateActionButtons(tr, isRunning); + if (changed) flashDot(nameCell); + if (stRaw && /running|stopped|shutdown/.test(stRaw)) { + setTimeout(function(){ activeSids.delete(sid); }, 3000); + } + } + + if (msg.type === 'node' && msg.node) { + const newNode = String(msg.node).trim(); + if (nodeCell && newNode && text(nodeCell).trim() !== newNode) { + nodeCell.textContent = newNode; + rebuildTargetSelect(targetSel, newNode, window.__nodesCache || []); + flashDot(nameCell); + } + } + } catch(e){} + }; + } catch(e){} + }); +} + +function dedupe(arr){ return Array.from(new Set(arr)); } +function extractNodeNames(ns){ + if (!ns) return []; + if (Array.isArray(ns.nodes)) return dedupe(ns.nodes.map(n => String(n.name || n.node || n).trim()).filter(Boolean)); + if (Array.isArray(ns)) return dedupe(ns.map(n => String(n.name || n.node || n).trim()).filter(Boolean)); + return []; +} + +export async function startAdminWatches() { + injectOnceCSS(); + const tbody = document.querySelector('#vm-admin tbody'); + if (!tbody) return; + + const ns = await api.nodesSummary(); + const availableNodes = extractNodeNames(ns); setRows(tbody, []); - // initial table fill try { const latest = await api.listAllVmct(); const all = Array.isArray(latest.all) ? latest.all : []; @@ -105,28 +165,29 @@ function updateActionButtons(tr, isRunning) { })); const htmlRows = 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'), - `