diff --git a/app.py b/app.py index 7714d84..31b77cb 100644 --- a/app.py +++ b/app.py @@ -537,6 +537,7 @@ def ws_observe(ws): if not node: ws.send(json.dumps({"type":"error","error":"could not resolve node"})); return + last_hash = None seen_upids = set() prev_node = node @@ -549,6 +550,18 @@ def ws_observe(ws): ws.send(json.dumps({"type":"moved","old_node":node,"new_node":cur_node,"meta":{"sid":sid,"vmid":vmid,"typ":typ}})) prev_node, node = node, cur_node + # bieżący status VM/CT -> event "vm" + try: + base = f"/nodes/{node}/{typ}/{vmid}" + cur = get_json(["pvesh", "get", f"{base}/status/current"]) or {} + cur_hash = json.dumps(cur, sort_keys=True) + if cur_hash != last_hash: + last_hash = cur_hash + ws.send(json.dumps({"type":"vm","current":cur,"meta":{"sid":sid,"node":node,"typ":typ,"vmid":vmid}})) + except Exception: + pass + + # zadania na aktualnym i poprzednim nodzie nodes_to_scan = [node] + ([prev_node] if prev_node and prev_node != node else []) for nX in nodes_to_scan: tasks = get_json(["pvesh","get",f"/nodes/{nX}/tasks","-limit","50"]) or [] diff --git a/static/js/admin.js b/static/js/admin.js index df1197d..4c88b9c 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -1,4 +1,4 @@ -import { rowHTML, setRows, safe, showToast } from './helpers.js'; +import { rowHTML, setRows, safe, showToast, badge } from './helpers.js'; import { api } from './api.js'; const liveSockets = new Map(); @@ -45,6 +45,21 @@ function flashDot(cell) { setTimeout(() => { try { dot.remove(); } catch {} }, 4200); } +function setBadgeCell(cell, textOrState) { + if (!cell) return; + let html = ''; + const s = String(textOrState || '').toLowerCase(); + 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; // zmiana + } + return false; +} + function rebuildTargetSelect(selectEl, currentNode, nodes) { if (!selectEl) return; const html = nodes.map(n => @@ -73,6 +88,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 nameCell = `${name}`; + const statusCell = badge('—','dark'); const actions = `
@@ -84,8 +100,8 @@ export async function renderVMAdmin() { ${availableNodes.map(n => ``).join('')} `; const migrateBtn = ``; - // Kolumny: SID | TYPE | NAME | NODE | ACTIONS | TARGET | MIGRATE - return rowHTML([sid, type.toUpperCase(), nameCell, node, actions, sel, migrateBtn], `data-sid="${sid}"`); + // SID | TYPE | NAME | NODE | STATUS | ACTIONS | TARGET | MIGRATE + return rowHTML([sid, type.toUpperCase(), nameCell, node, statusCell, actions, sel, migrateBtn], `data-sid="${sid}"`); }); setRows(tbody, rows); @@ -93,6 +109,7 @@ export async function renderVMAdmin() { Array.from(tbody.querySelectorAll('tr[data-sid]')).forEach(tr => { const sid = tr.getAttribute('data-sid'); const nodeCell = tr.children[3]; + const statusCell= tr.children[4]; const nameCell = tr.children[2]; const targetSel = tr.querySelector('.target-node'); @@ -108,13 +125,21 @@ export async function renderVMAdmin() { try { const msg = JSON.parse(ev.data); - if (msg.type === 'task-start') { + if (msg.type === 'vm' && msg.current) { + const st = String(msg.current.status || msg.current.qmpstatus || '').toLowerCase(); + const changed = setBadgeCell(statusCell, st); + const isRunning = /running|online|started/.test(st); + setMigrateDisabled(tr, isRunning); + if (changed) flashDot(nameCell); + } + else if (msg.type === 'task-start') { + setBadgeCell(statusCell, 'working'); activeSids.add(sid); flashDot(nameCell); } else if (msg.type === 'task') { + setBadgeCell(statusCell, 'working'); activeSids.add(sid); - flashDot(nameCell); } else if (msg.type === 'moved' && msg.new_node) { if (nodeCell && nodeCell.textContent.trim() !== msg.new_node) { @@ -125,6 +150,7 @@ export async function renderVMAdmin() { activeSids.add(sid); } else if (msg.type === 'done') { + // status końcowy dociągnie kolejny pakiet "vm" lub szybki refresh setTimeout(() => activeSids.delete(sid), 4000); flashDot(nameCell); } @@ -140,7 +166,10 @@ export async function renderVMAdmin() { const doAction = async (action, withTarget=false) => { try { ensureWatchOn(); - if (action !== 'unlock') activeSids.add(sid); + if (action !== 'unlock') { + setBadgeCell(statusCell, 'working'); + activeSids.add(sid); + } const target = withTarget ? (targetSel?.value || '') : undefined; const resp = await api.vmAction(sid, action, target); if (!resp.ok) throw new Error(resp.error || 'unknown'); @@ -177,6 +206,7 @@ export async function renderVMAdmin() { if (!rowData) return; const nodeCell = tr.children[3]; + const statusCell= tr.children[4]; const nameCell = tr.children[2]; const targetSel = tr.querySelector('.target-node'); @@ -186,11 +216,17 @@ export async function renderVMAdmin() { rebuildTargetSelect(targetSel, newNode, nodesNow); flashDot(nameCell); } + + // status z wolnego reconcile — tylko gdy brak „working”, żeby nie zagłuszać WS + const currentTxt = (statusCell?.innerText || '').toLowerCase(); + if (!/working/.test(currentTxt)) { + // brak statusu w liście — zostaw jak jest, dociągnie WS/fastTimer + } }); } catch {} }, 30000); - // tylko aktywne – co 10 s + // tylko aktywne – co 10 s (dociąga precyzyjny status + node) fastTimer = setInterval(async () => { try { const sids = Array.from(activeSids); @@ -200,19 +236,30 @@ export async function renderVMAdmin() { if (!detail || !detail.meta) continue; const tr = tbody.querySelector(`tr[data-sid="${sid}"]`); if (!tr) continue; + const nodeCell = tr.children[3]; + const statusCell= tr.children[4]; const nameCell = tr.children[2]; const targetSel = tr.querySelector('.target-node'); + const stRaw = String((detail.current && (detail.current.status || detail.current.qmpstatus)) || '').toLowerCase(); + const changed = setBadgeCell(statusCell, stRaw); + const isRunning = /running|online|started/.test(stRaw); + setMigrateDisabled(tr, isRunning); + if (changed) flashDot(nameCell); + 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 || []); flashDot(nameCell); } - rebuildTargetSelect(targetSel, newNode, window.__nodesCache || []); } - setTimeout(() => activeSids.delete(sid), 4000); + + if (stRaw && /running|stopped|shutdown/.test(stRaw)) { + setTimeout(() => activeSids.delete(sid), 4000); + } } } catch {} }, 10000); diff --git a/templates/index.html b/templates/index.html index 5aaa883..8dd06b4 100644 --- a/templates/index.html +++ b/templates/index.html @@ -215,6 +215,7 @@ Type Name Node + Status Actions Target Migrate @@ -222,7 +223,7 @@ - Loading… + Loading…