import { $, safe, ensureArr, badge, rowHTML, setRows, pct, humanBytes, fmtSeconds, kvGrid, showToast } from './helpers.js'; import { api } from './api.js'; import { renderVmDetailCard } from './vmDetail.js'; import { renderNodeDetailCard } from './nodeDetail.js'; // DOM refs export const refs = { nodeInput: $('#node'), healthDot: $('#healthDot'), healthTitle: $('#healthTitle'), healthSub: $('#healthSub'), qSummary: $('#q-summary'), qCardsWrap: $('#q-cards'), unitsBox: $('#units'), replBox: $('#repl'), tblHaRes: $('#ha-res'), tblHaStatus: $('#ha-status'), // (tblHaStatus nieużywany, sekcja wycięta w HTML) tblNodes: $('#nodes'), tblNonHA: $('#nonha'), pvecmPre: $('#pvecm'), cfgtoolPre: $('#cfgtool'), footer: $('#footer') }; // Health export function setHealth(ok, vq, unitsActive) { refs.healthDot.classList.toggle('ok', !!ok); refs.healthDot.classList.toggle('bad', !ok); refs.healthTitle.textContent = ok ? 'HA: OK' : 'HA: PROBLEM'; refs.healthSub.textContent = `Quorate=${String(vq.quorate)} | units=${unitsActive ? 'active' : 'inactive'} | members=${safe(vq.members)} | quorum=${safe(vq.quorum)}/${safe(vq.expected)}`; } // Cluster cards export function renderClusterCards(arr) { const a = ensureArr(arr); refs.qCardsWrap.innerHTML = ''; if (!a.length) { refs.qCardsWrap.innerHTML = badge('No data','dark'); return; } const cluster = a.find(x => x.type === 'cluster') || {}; const qB = cluster.quorate ? badge('Quorate: yes','ok') : badge('Quorate: no','err'); refs.qCardsWrap.insertAdjacentHTML('beforeend', `
${safe(cluster.name)}
id: ${safe(cluster.id)}
${qB}
nodes: ${safe(cluster.nodes)}
version: ${safe(cluster.version)}
`); const nodes = a.filter(x => x.type === 'node'); const rows = nodes.map(n => { const online = n.online ? badge('online','ok') : badge('offline','err'); const local = n.local ? ' ' + badge('local','info') : ''; return rowHTML([safe(n.name), online + local, safe(n.ip), safe(n.nodeid), safe(n.level)]); }); refs.qCardsWrap.insertAdjacentHTML('beforeend', `
${rows.join('')}
NodeStatusIPNodeIDLevel
`); } export function renderUnits(units) { refs.unitsBox.innerHTML = ''; if (!units || !Object.keys(units).length) { refs.unitsBox.innerHTML = badge('No data','dark'); return; } const map = { active:'ok', inactive:'err', failed:'err', activating:'warn' }; Object.entries(units).forEach(([k, v]) => refs.unitsBox.insertAdjacentHTML('beforeend', `${badge(k, map[v] || 'dark')}`)); } // Replication — ALL NODES export function renderReplicationTable(repAll) { const arr = ensureArr(repAll.jobs); if (!arr.length) { refs.replBox && (refs.replBox.innerHTML = 'No replication jobs'); return; } const rows = arr.map(x => { const en = /^yes$/i.test(x.enabled) ? badge('Yes','ok') : badge('No','err'); const st = /^ok$/i.test(x.state) ? badge(x.state,'ok') : badge(x.state,'err'); const fc = x.fail > 0 ? badge(String(x.fail),'err') : badge(String(x.fail),'ok'); return rowHTML([safe(x.node), safe(x.job), en, safe(x.target), safe(x.last), safe(x.next), safe(x.dur), fc, st]); }); refs.replBox.innerHTML = `
${rows.join('')}
NodeJobIDEnabledTargetLastSyncNextSyncDurationFailCountState
`; } // HA resources (expand) export function renderHAResources(list) { const tbody = refs.tblHaRes.querySelector('tbody'); const arr = ensureArr(list); const rows = []; arr.forEach(x => { const st = x.state || '—'; const stB = /start/i.test(st) ? badge(st,'ok') : (/stop/i.test(st) ? badge(st,'err') : badge(st,'dark')); const sid = safe(x.sid); rows.push(rowHTML([``, sid, stB, safe(x.node), safe(x.group), safe(x.flags || x.comment)], `class="expandable vm-row" data-sid="${sid}"`)); rows.push(`
`); }); setRows(tbody, rows.length ? rows : [rowHTML(['—','—','—','—','—','—'])]); Array.from(refs.tblHaRes.querySelectorAll('tr.vm-row')).forEach((tr, i) => { tr.onclick = async () => { const detailRow = refs.tblHaRes.querySelectorAll('tr.vm-detail')[i]; const content = detailRow.querySelector('.vm-json'); const spin = detailRow.querySelector('.spinner-border'); const open = detailRow.classList.contains('d-none'); // collapse all refs.tblHaRes.querySelectorAll('tr.vm-detail').forEach(r => r.classList.add('d-none')); refs.tblHaRes.querySelectorAll('tr.vm-row').forEach(r => r.classList.remove('expanded')); if (open) { detailRow.classList.remove('d-none'); tr.classList.add('expanded'); spin.classList.remove('d-none'); const sid = tr.getAttribute('data-sid'); try { const d = await api.vmDetail(sid); content.innerHTML = renderVmDetailCard(d); } catch (e) { content.textContent = 'ERROR: ' + e; } spin.classList.add('d-none'); } }; }); } // Non-HA VM/CT (expand) export async function renderNonHA() { const r = await api.listNonHA(); const arr = ensureArr(r.nonha); const tbody = refs.tblNonHA.querySelector('tbody'); if (!arr.length) { setRows(tbody, [rowHTML(['','No non-HA VMs/CTs','','','',''])]); return; } const rows = []; arr.forEach(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'); rows.push(rowHTML([``, sid, type, name, node, st], `class="expandable vm-row" data-sid="${sid}"`)); rows.push(`
`); }); setRows(tbody, rows); Array.from(tbody.querySelectorAll('tr.vm-row')).forEach((tr, i) => { tr.onclick = async () => { const detailRow = tbody.querySelectorAll('tr.vm-detail')[i]; const content = detailRow.querySelector('.vm-json'); const spin = detailRow.querySelector('.spinner-border'); const open = detailRow.classList.contains('d-none'); tbody.querySelectorAll('tr.vm-detail').forEach(r => r.classList.add('d-none')); tbody.querySelectorAll('tr.vm-row').forEach(r => r.classList.remove('expanded')); if (open) { detailRow.classList.remove('d-none'); tr.classList.add('expanded'); spin.classList.remove('d-none'); const sid = tr.getAttribute('data-sid'); try { const d = await api.vmDetail(sid); content.innerHTML = renderVmDetailCard(d); } catch (e) { content.textContent = 'ERROR: ' + e; } spin.classList.add('d-none'); } }; }); } // Nodes table (expand) export function renderNodesTable(nodes) { const tbody = refs.tblNodes.querySelector('tbody'); const nrows = ensureArr(nodes).map(n => { const isOn = /online|running/i.test(n.status || '') || /online/i.test(n.hastate || '') || (n.uptime > 0) || (n.cpu != null && n.maxcpu != null) || (n.mem != null && n.maxmem != null); const statusTxt = isOn ? 'online' : (n.status || 'offline'); const sB = isOn ? badge(statusTxt,'ok') : badge(statusTxt,'err'); const mem = (n.mem != null && n.maxmem) ? `${humanBytes(n.mem)} / ${humanBytes(n.maxmem)} (${pct(n.mem / n.maxmem)})` : '—'; const rfs = (n.rootfs != null && n.maxrootfs) ? `${humanBytes(n.rootfs)} / ${humanBytes(n.maxrootfs)} (${pct(n.rootfs / n.maxrootfs)})` : '—'; const load = (n.loadavg != null) ? String(n.loadavg) : '—'; const cpu = (n.cpu != null) ? pct(n.cpu) : '—'; const main = ` ${safe(n.node)} ${sB}${cpu}${load}${mem}${rfs}${fmtSeconds(n.uptime)} `; const detail = `
`; return main + detail; }); setRows(tbody, nrows); Array.from(tbody.querySelectorAll('tr.node-row')).forEach((tr, i) => { tr.onclick = async () => { const detailRow = tbody.querySelectorAll('tr.node-detail')[i]; const content = detailRow.querySelector('.node-json'); const spin = detailRow.querySelector('.spinner-border'); const open = detailRow.classList.contains('d-none'); tbody.querySelectorAll('tr.node-detail').forEach(r => r.classList.add('d-none')); tbody.querySelectorAll('tr.node-row').forEach(r => r.classList.remove('expanded')); if (open) { detailRow.classList.remove('d-none'); tr.classList.add('expanded'); spin.classList.remove('d-none'); const name = tr.getAttribute('data-node'); try { const d = await api.nodeDetail(name); content.innerHTML = renderNodeDetailCard(d); } catch (e) { content.textContent = 'ERROR: ' + e; } spin.classList.add('d-none'); } }; }); }