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', `
Node | Status | IP | NodeID | Level |
${rows.join('')}
`);
}
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 = `
Node | JobID | Enabled | Target | LastSync | NextSync | Duration | FailCount | State |
${rows.join('')}
`;
}
// 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');
}
};
});
}