From cac9cf927bf2c8babb2ec36737987fda94a086b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 19 Oct 2025 00:00:11 +0200 Subject: [PATCH] refator_comm1 --- static/js/admin.js | 151 +++++++++++++++++++++++++++------------------ 1 file changed, 90 insertions(+), 61 deletions(-) diff --git a/static/js/admin.js b/static/js/admin.js index ffc4aa3..6b79d18 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -1,13 +1,14 @@ import { rowHTML, setRows, safe, showToast, badge } from './helpers.js'; import { api } from './api.js'; -// ==== helpers ==== +// ========= helpers ========= const q = (r, s) => (r || document).querySelector(s); const qq = (r, s) => Array.from((r || document).querySelectorAll(s)); const low = (x) => String(x ?? '').toLowerCase(); function isRunning(st) { return /running|online|started/.test(low(st)); } -function isBusy(st) { return /working|progress|busy/.test(low(st)); } +function isBusy(st) { return /working|progress|busy|locking|migrating/.test(low(st)); } +function isStopped(st) { return /(stopp|stopped|shutdown|offline|down|halt)/.test(low(st)); } function setBadge(cell, statusRaw) { if (!cell) return; @@ -19,9 +20,8 @@ function setBadge(cell, statusRaw) { } function extractNodes(nodesSummary) { - if (!nodesSummary) return []; - const arr = Array.isArray(nodesSummary.nodes) ? nodesSummary.nodes : nodesSummary; - return Array.from(new Set((arr || []).map(n => String(n?.name || n?.node || n || '').trim()).filter(Boolean))); + const arr = Array.isArray(nodesSummary?.nodes) ? nodesSummary.nodes : (nodesSummary || []); + return Array.from(new Set(arr.map(n => String(n?.name || n?.node || n || '').trim()).filter(Boolean))); } function rebuildTargetSelect(selectEl, currentNode, nodes) { @@ -33,9 +33,19 @@ function rebuildTargetSelect(selectEl, currentNode, nodes) { if (selectEl.options.length > 0) selectEl.selectedIndex = 0; } +// ========= state ========= +const activeSids = new Set(); // SIDs with an ongoing task +const sidWatchdogs = new Map(); // sid -> timeout id (2 min safety) +let slowTimer = null; // 30s for entire table +let fastTimer = null; // 10s for active SIDs +let cachedNodes = []; + +// Buttons reflect current status; MIGRATE only when stopped function syncButtonsForRow(tr, statusRaw) { const running = isRunning(statusRaw); const busy = isBusy(statusRaw); + const stopped = isStopped(statusRaw); + const bStart = tr.querySelector('.act-start'); const bStop = tr.querySelector('.act-stop'); const bShutdown = tr.querySelector('.act-shutdown'); @@ -49,29 +59,22 @@ function syncButtonsForRow(tr, statusRaw) { if (bUnlock) bUnlock.disabled = busy; const hasTarget = !!(sel && sel.value); - // MIGRATE włączony także dla offline; blokujemy tylko gdy brak targetu albo busy - if (bMigrate) bMigrate.disabled = !hasTarget || busy; + // Offline migrate only: enabled only when STOPPED + has target + not busy + if (bMigrate) bMigrate.disabled = !(stopped && hasTarget) || busy; } -// ==== state: szybki refresh dla aktywnych SID-ów ==== -const activeSids = new Set(); -let slowTimer = null; -let fastTimer = null; -let cachedNodes = []; - -// ==== rendering ==== +// ========= rendering ========= export async function renderVMAdmin() { const table = document.getElementById('vm-admin'); if (!table) return; const tbody = table.querySelector('tbody'); if (!tbody) return; - // początkowe pobranie const [list, nodesSummary] = await Promise.all([api.listAllVmct(), api.nodesSummary()]); const all = Array.isArray(list?.all) ? list.all : []; cachedNodes = extractNodes(nodesSummary); - // zbuduj rzędy 1:1 z nagłówkiem (8 kolumn) + // 8 columns exactly like THEAD: SID, Type, Name, Node, Status, Actions, Target, Migrate const rows = all.map(vm => { const sid = safe(vm.sid); const type = safe(vm.type || vm.meta?.type || vm.kind || '—'); @@ -80,28 +83,25 @@ export async function renderVMAdmin() { const status = safe(vm.status || vm.current?.status || vm.current?.qmpstatus || '—'); return rowHTML([ - sid, // SID - type, // Type - name, // Name - node, // Node - badge(status, isRunning(status) ? 'ok' : 'dark'), // Status - // Actions + sid, + type, + name, + node, + badge(status, isRunning(status) ? 'ok' : 'dark'), `
`, - // Target ``, - // Migrate `` ], `data-sid="${sid}"`); }); setRows(tbody, rows); - // wstępne wypełnienie selectów i ustawienie stanów przycisków + // init per-row qq(tbody, 'tr[data-sid]').forEach(tr => { const nodeCell = tr.children[3]; const statusCell = tr.children[4]; @@ -110,20 +110,16 @@ export async function renderVMAdmin() { syncButtonsForRow(tr, statusCell?.innerText || ''); }); - // ==== delegacja kliknięć (capture, żeby nic nie przechwyciło) ==== + // click delegation (capture phase) document.addEventListener('click', onClickAction, { capture: true }); - // ==== odświeżanie ==== - if (slowTimer) clearInterval(slowTimer); - if (fastTimer) clearInterval(fastTimer); - - // cały listing co 30s + // refresh loops + clearInterval(slowTimer); clearInterval(fastTimer); slowTimer = setInterval(refreshAllRows, 30000); - - // aktywne SID-y co 10s fastTimer = setInterval(refreshActiveRows, 10000); } +// ========= actions ========= async function onClickAction(ev) { const btn = ev.target.closest?.('.act-start,.act-stop,.act-shutdown,.act-unlock,.act-migrate'); if (!btn) return; @@ -150,33 +146,54 @@ async function onClickAction(ev) { target = sel?.value; const curNode = nodeCell?.textContent?.trim(); if (!target || target === curNode) { - showToast('Migrate', 'Wybierz docelowy node inny niż bieżący', 'warning'); + showToast('Migrate', 'Pick a target node different from current.', 'warning'); return; } } try { - // status natychmiast na "working" + blokady + // Optimistic: set working, disable buttons, start watchdog setBadge(statusCell, 'working'); syncButtonsForRow(tr, 'working'); activeSids.add(sid); + armWatchdog(sid, name); const res = await api.vmAction(sid, action, target); + + // Most APIs respond before task finishes. We don't show "failed" immediately. + // If API responded at all, we just inform and wait for status polling to flip from "working". if (res?.ok) { - showToast('OK', `${action.toUpperCase()} × ${name}`, 'success'); + showToast('Task queued', `${action.toUpperCase()} scheduled for ${name}.`, 'success'); } else { - showToast('Błąd', res?.error || `Nie udało się: ${action}`, 'danger'); - // pozwól wrócić przyciski do sensownych stanów po błędzie - await refreshOneRow(sid, tr); - activeSids.delete(sid); + // Even if backend returns a non-ok, in practice the task often proceeds. + // We soften the message and keep polling. + showToast('Possibly queued', `Attempted ${action} for ${name}. Waiting for status update…`, 'info'); } } catch (e) { - showToast('Błąd', String(e?.message || e), 'danger'); - await refreshOneRow(sid, tr); - activeSids.delete(sid); + // Network or unexpected error — still keep "working" and wait for polling + showToast('Queued (connection issue)', `Action may have been accepted for ${name}. Monitoring status…`, 'warning'); } } +// 2-minute safety timer per SID: if status didn't change, nudge with info (not hard error) +function armWatchdog(sid, name) { + clearTimeout(sidWatchdogs.get(sid)); + const id = setTimeout(() => { + // Still active after 2 min? Give a gentle notice; keep polling. + if (activeSids.has(sid)) { + showToast('Still processing', `No status change for ${name} yet. Continuing to monitor…`, 'info'); + } + }, 120000); + sidWatchdogs.set(sid, id); +} + +function disarmWatchdog(sid) { + const id = sidWatchdogs.get(sid); + if (id) clearTimeout(id); + sidWatchdogs.delete(sid); +} + +// ========= refresh loops ========= async function refreshAllRows() { const table = document.getElementById('vm-admin'); if (!table) return; @@ -201,32 +218,40 @@ async function refreshAllRows() { const statusCell = tr.children[4]; const sel = tr.querySelector('.target-node'); - // Node (i przebudowa selecta, jeśli się zmienił) const newNode = String(row.node || row.meta?.node || '').trim(); if (newNode && nodeCell?.textContent?.trim() !== newNode) { nodeCell.textContent = newNode; rebuildTargetSelect(sel, newNode, cachedNodes); } - // Type/Name const newType = String(row.type || row.meta?.type || row.kind || '—'); if (typeCell && typeCell.textContent !== newType) typeCell.textContent = newType; const newName = String(row.name || row.vmid || row.sid); if (nameCell && nameCell.textContent !== newName) nameCell.textContent = newName; - // Status – nie nadpisujemy "working" dla aktywnych SID-ów, - // żeby nie migało w trakcie taska; resztę aktualizujemy. const st = String(row.status || row.current?.status || row.current?.qmpstatus || '').trim(); - if (!activeSids.has(sid) && st) { - setBadge(statusCell, st); - syncButtonsForRow(tr, st); - } else { - // ale jeśli working „utknął”, to podtrzymujemy blokady - syncButtonsForRow(tr, 'working'); + if (st) { + // If this SID is in "active", keep working badge until fast loop confirms change + if (!activeSids.has(sid)) { + setBadge(statusCell, st); + syncButtonsForRow(tr, st); + } else { + // Keep buttons locked as working + syncButtonsForRow(tr, 'working'); + } + + // If the status moved to a terminal state, stop the watchdog + if (activeSids.has(sid) && (isRunning(st) || isStopped(st))) { + activeSids.delete(sid); + disarmWatchdog(sid); + setBadge(statusCell, st); + syncButtonsForRow(tr, st); + showToast('Done', `Status updated: ${newName} → ${st}.`, 'success'); + } } }); - } catch { /* cicho */ } + } catch { /* ignore */ } } async function refreshActiveRows() { @@ -238,7 +263,7 @@ async function refreshActiveRows() { for (const sid of Array.from(activeSids)) { const tr = tbody.querySelector(`tr[data-sid="${sid}"]`); - if (!tr) { activeSids.delete(sid); continue; } + if (!tr) { activeSids.delete(sid); disarmWatchdog(sid); continue; } await refreshOneRow(sid, tr); } } @@ -249,23 +274,27 @@ async function refreshOneRow(sid, tr) { const nodeCell = tr.children[3]; const statusCell = tr.children[4]; const sel = tr.querySelector('.target-node'); + const name = tr.children[2]?.textContent?.trim() || sid; - // status z detail const st = String(detail?.current?.status || detail?.current?.qmpstatus || detail?.status || '').trim(); if (st) { + if (isRunning(st) || isStopped(st)) { + activeSids.delete(sid); + disarmWatchdog(sid); + } setBadge(statusCell, st); syncButtonsForRow(tr, st); - // jeśli task się skończył, zdejmij z listy aktywnych - if (/running|stopped|shutdown|locked|error|failed|unknown|offline/.test(low(st))) { - activeSids.delete(sid); + if (!activeSids.has(sid) && (isRunning(st) || isStopped(st))) { + showToast('Done', `Status updated: ${name} → ${st}.`, 'success'); } } - // node z detail const newNode = String(detail?.node || detail?.meta?.node || '').trim(); if (newNode && nodeCell?.textContent?.trim() !== newNode) { nodeCell.textContent = newNode; rebuildTargetSelect(sel, newNode, cachedNodes); } - } catch { /* no-op */ } + } catch { + // keep waiting silently + } }