diff --git a/static/js/admin.js b/static/js/admin.js index 5ac9001..7090887 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -2,7 +2,9 @@ import { badge, rowHTML, setRows, safe, showToast } from './helpers.js'; import { api } from './api.js'; const liveSockets = new Map(); -let reconcileTimer = null; +let slowTimer = null; +let fastTimer = null; +const activeSids = new Set(); function closeForSid(sid) { const entry = liveSockets.get(sid); @@ -13,7 +15,9 @@ function closeForSid(sid) { } export function stopAllAdminWatches() { - if (reconcileTimer) { clearInterval(reconcileTimer); reconcileTimer = null; } + if (slowTimer) { clearInterval(slowTimer); slowTimer = null; } + if (fastTimer) { clearInterval(fastTimer); fastTimer = null; } + activeSids.clear(); for (const sid of Array.from(liveSockets.keys())) closeForSid(sid); } @@ -22,6 +26,21 @@ function setBadge(cell, val) { cell.innerHTML = val; } +function setMigrateDisabled(tr, disabled) { + const btn = tr.querySelector('.act-migrate'); + if (btn) btn.disabled = !!disabled; +} + +function rebuildTargetSelect(selectEl, currentNode, nodes) { + if (!selectEl) return; + const html = nodes.map(n => + `` + ).join(''); + selectEl.innerHTML = html; + const idx = Array.from(selectEl.options).findIndex(o => !o.disabled); + if (idx >= 0) selectEl.selectedIndex = idx; +} + export async function renderVMAdmin() { stopAllAdminWatches(); @@ -33,7 +52,7 @@ export async function renderVMAdmin() { const rows = arr.map(x => { const sid = safe(x.sid), type = safe(x.type), name = safe(x.name), node = safe(x.node); - const st = /running/i.test(x.status || '') ? badge(x.status,'ok') : badge(x.status || '—','dark'); + const st = /running/i.test(x.status || '') ? badge('running','ok') : badge(x.status || '—','dark'); const actions = `
@@ -82,16 +101,6 @@ export async function renderVMAdmin() { if (!busy && spin) spin.remove(); }; - const rebuildTargetSelect = (currentNode) => { - if (!targetSel) return; - const current = currentNode || (nodeCell?.textContent || '').trim(); - const html = availableNodes.map(n => - ``).join(''); - targetSel.innerHTML = html; - const idx = Array.from(targetSel.options).findIndex(o => !o.disabled); - if (idx >= 0) targetSel.selectedIndex = idx; - }; - const getTarget = () => targetSel?.value || ''; const openTaskWS = (upid, node) => { @@ -104,6 +113,7 @@ export async function renderVMAdmin() { const wsTask = new WebSocket(api.wsTaskURL(upid, node)); entry.task = wsTask; liveSockets.set(sid, entry); + activeSids.add(sid); wsTask.onmessage = (ev) => { try { @@ -120,7 +130,8 @@ export async function renderVMAdmin() { const ok = !!msg.ok; setBadge(badgeCell, ok ? badge('running','ok') : badge('error','err')); setRowBusy(false); - setTimeout(() => toggleSub(false), 1500); + setTimeout(() => toggleSub(false), 1000); + setTimeout(() => activeSids.delete(sid), 5000); } } catch {} }; @@ -141,30 +152,36 @@ export async function renderVMAdmin() { if (msg.type === 'vm' && msg.current) { const st = String(msg.current.status || msg.current.qmpstatus || '').toLowerCase(); - const ok = /running|online|started/.test(st); - setBadge(badgeCell, ok ? badge('running','ok') : + const isRunning = /running|online|started/.test(st); + setBadge(badgeCell, isRunning ? badge('running','ok') : (/stopp|shutdown|offline/.test(st) ? badge('stopped','dark') : badge(st||'—','info'))); + setMigrateDisabled(tr, isRunning); + if (!isRunning && st) activeSids.add(sid); } else if (msg.type === 'task-start' && msg.upid && msg.node) { openTaskWS(msg.upid, msg.node); + activeSids.add(sid); } 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'; setBadge(badgeCell, ok ? badge('running','ok') : badge('error','err')); + setTimeout(() => activeSids.delete(sid), 5000); } else { setBadge(badgeCell, badge('working','info')); + activeSids.add(sid); } } else if (msg.type === 'moved' && msg.new_node) { if (nodeCell) nodeCell.textContent = msg.new_node; - rebuildTargetSelect(msg.new_node); + rebuildTargetSelect(targetSel, msg.new_node, Array.from(new Set([...(window.__nodesCache||[]), msg.new_node]))); + activeSids.add(sid); } else if (msg.type === 'done' && typeof msg.ok === 'boolean') { setBadge(badgeCell, msg.ok ? badge('running','ok') : badge('error','err')); + setTimeout(() => activeSids.delete(sid), 5000); } - } catch {} }; @@ -185,6 +202,7 @@ export async function renderVMAdmin() { if (action !== 'unlock') { setBadge(badgeCell, badge('working','info')); toggleSub(true); + activeSids.add(sid); } const resp = await api.vmAction(sid, action, target); @@ -193,6 +211,7 @@ export async function renderVMAdmin() { if (!resp.upid) { logPre.textContent = `Waiting for task… (${action})\n`; setRowBusy(false); + activeSids.add(sid); return; } @@ -202,6 +221,7 @@ export async function renderVMAdmin() { showToast('Error', 'ERROR: ' + (e.message || e), 'danger'); setRowBusy(false); toggleSub(false); + activeSids.delete(sid); } }; @@ -210,17 +230,23 @@ export async function renderVMAdmin() { 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)); - tr.querySelector('.act-status')?.addEventListener('click', () => toggleSub(sub.classList.contains('d-none'))); ensureWatchOn(); + + const initialRunning = /running/i.test(tr.querySelector('td:nth-child(5)')?.innerText || ''); + setMigrateDisabled(tr, initialRunning); }); - reconcileTimer = setInterval(async () => { + window.__nodesCache = availableNodes.slice(); + + 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'); @@ -234,32 +260,49 @@ export async function renderVMAdmin() { 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; - } + rebuildTargetSelect(targetSel, newNode, nodesNow); } - 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 = () => {}; - } + const isRunning = /running/i.test(rowData.status || ''); + setBadge(badgeCell, isRunning ? badge('running','ok') : badge(rowData.status || '—','dark')); + setMigrateDisabled(tr, isRunning); }); - } catch {} - }, 3000); + }, 30000); + + 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 badgeCell = tr.children[4]; + const targetSel = tr.querySelector('.target-node'); + + const stRaw = String((detail.current && (detail.current.status || detail.current.qmpstatus)) || '').toLowerCase(); + const isRunning = /running|online|started/.test(stRaw); + setBadge(badgeCell, isRunning ? badge('running','ok') : + (/stopp|shutdown|offline/.test(stRaw) ? badge('stopped','dark') : badge(stRaw||'—','info'))); + setMigrateDisabled(tr, isRunning); + + 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 || []); + } + + if (stRaw && /running|stopped|shutdown/.test(stRaw)) { + setTimeout(() => activeSids.delete(sid), 5000); + } + } + } catch {} + }, 10000); window.addEventListener('beforeunload', stopAllAdminWatches, { once: true }); }