From e3b3ff235bbdfdb176248ab116d7f476e449ec0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 18 Oct 2025 21:28:42 +0200 Subject: [PATCH] refator_comm1 --- app.py | 64 +++++++++++++++++++++++++++------------------- static/js/admin.js | 49 ++++++++++++++++++++++------------- 2 files changed, 68 insertions(+), 45 deletions(-) diff --git a/app.py b/app.py index 0b68841..fe77d50 100644 --- a/app.py +++ b/app.py @@ -586,21 +586,22 @@ def ws_task(ws): # ---------------- WebSocket: broadcast observe per sid ---------------- @sock.route("/ws/observe") def ws_observe(ws): - """ - Streamuje zmiany stanu VM/CT i aktywne taski dla danego SID. - Query: sid=vm:101 (albo ct:123) - """ q = ws.environ.get("QUERY_STRING", "") params = {} for part in q.split("&"): if not part: continue k, _, v = part.partition("="); params[k] = v - sid = (params.get("sid") or "").strip() + sid_raw = (params.get("sid") or "").strip() + sid = norm_sid(sid_raw) if not sid: ws.send(json.dumps({"type":"error","error":"sid required"})); return - meta = cluster_vmct_meta() - tup = sid_to_tuple(sid, meta) + # Resolve tuple + node + def resolve_tuple() -> Optional[Tuple[str,int,str]]: + meta = cluster_vmct_meta() + return sid_to_tuple(sid, meta) + + tup = resolve_tuple() if not tup: ws.send(json.dumps({"type":"error","error":"unknown sid"})); return typ, vmid, node = tup @@ -609,35 +610,44 @@ def ws_observe(ws): last_hash = None seen_upids = set() + prev_node = node + try: while True: + ntup = resolve_tuple() + if ntup: + _, _, cur_node = ntup + if cur_node and cur_node != node: + 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 + base = f"/nodes/{node}/{typ}/{vmid}" cur = get_json(["pvesh", "get", f"{base}/status/current"]) or {} - # wyślij tylko gdy zmiana 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":norm_sid(sid),"node":node,"typ":typ,"vmid":vmid}})) + ws.send(json.dumps({"type":"vm","current":cur,"meta":{"sid":sid,"node":node,"typ":typ,"vmid":vmid}})) - tasks = get_json(["pvesh","get",f"/nodes/{node}/tasks","-limit","50"]) or [] - for t in tasks: - upid = t.get("upid") or t.get("pstart") # upid musi być stringiem - tid = (t.get("id") or "") # np. "qemu/101" lub "lxc/123" - if not isinstance(upid, str): continue - if (str(vmid) in tid) or (f"{'qemu' if typ=='qemu' else 'lxc'}/{vmid}" in tid): - # status szczegółowy - st = get_json(["pvesh","get",f"/nodes/{node}/tasks/{upid}/status"]) or {} - ev = {"type":"task","upid":upid,"status":st.get("status"),"exitstatus":st.get("exitstatus"),"node":node} - ws.send(json.dumps(ev)) - # nowy "running" task → ogłoś raz - if upid not in seen_upids and (str(st.get("status","")).lower() != "stopped"): - seen_upids.add(upid) - ws.send(json.dumps({"type":"task-start","upid":upid,"node":node})) - # zakończenie - if str(st.get("status","")).lower() == "stopped" or st.get("exitstatus"): - ws.send(json.dumps({"type":"done","upid":upid,"ok":str(st.get('exitstatus','')).upper()=='OK'})) + 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 [] + for t in tasks: + upid = t.get("upid") if isinstance(t, dict) else None + tid = (t.get("id") or "") if isinstance(t, dict) else "" + if not upid or not isinstance(upid, str): continue + # dopasuj po vmid lub ciągu qemu/|lxc/ + if (str(vmid) in tid) or (f"{'qemu' if typ=='qemu' else 'lxc'}/{vmid}" in tid): + st = get_json(["pvesh","get",f"/nodes/{nX}/tasks/{upid}/status"]) or {} + ws.send(json.dumps({"type":"task","upid":upid,"status":st.get("status"),"exitstatus":st.get("exitstatus"),"node":nX})) + # nowy running + if upid not in seen_upids and str(st.get("status","")).lower() != "stopped": + seen_upids.add(upid) + ws.send(json.dumps({"type":"task-start","upid":upid,"node":nX})) + # zakończone + if str(st.get("status","")).lower() == "stopped" or st.get("exitstatus"): + ws.send(json.dumps({"type":"done","upid":upid,"ok":str(st.get('exitstatus','')).upper()=='OK',"node":nX})) - time.sleep(2.0) + time.sleep(1.8) except Exception: try: ws.close() except Exception: pass diff --git a/static/js/admin.js b/static/js/admin.js index 7ca0f57..3fd7dd7 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -34,7 +34,8 @@ 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 badgeCell = tr.children[4]; + const nodeCell = tr.children[3]; // Node + const badgeCell = tr.children[4]; // Status // subpanel (log) let sub = tr.nextElementSibling; @@ -50,7 +51,11 @@ export async function renderVMAdmin() { let wsObs = null; // observe websocket let wsTask = null; // tail websocket (auto z observe) - const closeWS = () => { try { wsObs && wsObs.close(); } catch {} try { wsTask && wsTask.close(); } catch {} wsObs = wsTask = null; }; + const closeWS = () => { + try { wsObs && wsObs.close(); } catch {} + try { wsTask && wsTask.close(); } catch {} + wsObs = wsTask = null; + }; const setRowBusy = (busy) => { const nameCell = tr.children[2]; @@ -100,30 +105,41 @@ export async function renderVMAdmin() { wsObs.onmessage = (ev) => { try { const msg = JSON.parse(ev.data); + if (msg.type === 'vm' && msg.current) { - // aktualizuj badge na podstawie bieżącego statusu const st = String(msg.current.status || msg.current.qmpstatus || '').toLowerCase(); const ok = /running|online|started/.test(st); - badgeCell.innerHTML = 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) { - // automatycznie podłącz tail do nowo wykrytego taska + if (badgeCell) { + badgeCell.innerHTML = 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) { openTaskWS(msg.upid, msg.node); - } else if (msg.type === 'task' && msg.upid && msg.status) { - // szybkie mrugnięcie statusem + } + + 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'; - badgeCell.innerHTML = ok ? badge('running','ok') : badge('error','err'); + if (badgeCell) badgeCell.innerHTML = ok ? badge('running','ok') : badge('error','err'); } else { - badgeCell.innerHTML = badge('working','info'); + if (badgeCell) badgeCell.innerHTML = badge('working','info'); } - } else if (msg.type === 'done' && msg.upid) { - // koniec zewnętrznego zadania (bez naszego taila) - if (msg.ok) badgeCell.innerHTML = badge('running','ok'); - else badgeCell.innerHTML = badge('error','err'); + } + + else if (msg.type === 'moved' && msg.new_node) { + if (nodeCell) nodeCell.textContent = msg.new_node; try { document.getElementById('btnRefresh').click(); } catch {} } + + else if (msg.type === 'done' && msg.upid) { + if (typeof msg.ok === 'boolean') { + if (badgeCell) badgeCell.innerHTML = msg.ok ? badge('running','ok') : badge('error','err'); + } + } + } catch {} }; wsObs.onclose = () => { wsObs = null; }; @@ -157,10 +173,8 @@ export async function renderVMAdmin() { tr.querySelector('.act-shutdown')?.addEventListener('click', () => { toggleSub(true); doAction('shutdown'); }); tr.querySelector('.act-migrate')?.addEventListener('click', () => { toggleSub(true); doAction('migrate', true); }); - // Status – pokaz/ukryj subpanel (bez WS) tr.querySelector('.act-status')?.addEventListener('click', () => toggleSub(sub.classList.contains('d-none'))); - // NEW: Watch 🔔 – włącz/wyłącz broadcast observe const watchBtn = tr.querySelector('.act-watch'); if (watchBtn) { watchBtn.addEventListener('click', () => { @@ -175,6 +189,5 @@ export async function renderVMAdmin() { } }); } - }); }