diff --git a/web/static/panel.js b/web/static/panel.js index 39897c5..1932601 100644 --- a/web/static/panel.js +++ b/web/static/panel.js @@ -115,6 +115,127 @@ applyTheme(getTheme()); })(); +/* VM visual cards from raw virsh output */ +(() => { + function qs(selector, root = document) { return root.querySelector(selector); } + function qsa(selector, root = document) { return Array.from(root.querySelectorAll(selector)); } + function escapeHtml(value) { + return String(value ?? '').replace(/[&<>"]/g, (ch) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[ch])); + } + function rawPreBySummary(summaryText) { + const details = qsa('.raw-details').find((item) => (qs('summary', item)?.textContent || '').includes(summaryText)); + return qs('pre', details)?.textContent.trim() || ''; + } + function parseKeyValues(raw) { + const data = {}; + raw.split('\n').forEach((line) => { + const idx = line.indexOf(':'); + if (idx < 0) return; + const key = line.slice(0, idx).trim().toLowerCase(); + const value = line.slice(idx + 1).trim(); + if (key) data[key] = value; + }); + return data; + } + function sizeToGiB(value) { + const raw = String(value || '').trim(); + const n = Number(raw.replace(',', '.').replace(/[^0-9.]/g, '')); + if (!Number.isFinite(n) || n <= 0) return 0; + if (/kib|kb/i.test(raw)) return n / 1024 / 1024; + if (/mib|mb/i.test(raw)) return n / 1024; + if (/gib|gb/i.test(raw)) return n; + return n / 1024 / 1024; + } + function renderInfo() { + const box = qs('#vm-info-visual'); + if (!box) return; + const raw = rawPreBySummary('dominfo'); + const data = parseKeyValues(raw); + const cpu = data['cpu(s)'] || data['cpu'] || '—'; + const state = data.state || 'unknown'; + const osType = data['os type'] || data['os'] || '—'; + const maxMem = sizeToGiB(data['max memory']); + const usedMem = sizeToGiB(data['used memory']); + const memPct = maxMem > 0 ? Math.max(0, Math.min(100, Math.round((usedMem / maxMem) * 100))) : 0; + const cpuPct = Math.max(8, Math.min(100, Number(cpu) ? Number(cpu) * 12 : 18)); + const autostartText = (qsa('.top-meta .status').find((item) => item.textContent.toLowerCase().includes('autostart'))?.textContent || 'autostart: —').replace(/^autostart:\s*/i, ''); + box.innerHTML = ` +
+
Состояние${escapeHtml(state)}
+
CPU${escapeHtml(cpu)}
+
Тип ОС${escapeHtml(osType)}
+
+
+
${memPct}%
+
+
Память${usedMem ? usedMem.toFixed(1) : '—'} / ${maxMem ? maxMem.toFixed(1) : '—'} GB
+
vCPU capacity${escapeHtml(cpu)} vCPU
+
Автозапуск${escapeHtml(autostartText)}
+
+
`; + } + function parseDisks(raw) { + const rows = []; + raw.split('\n').map((line) => line.trim()).filter(Boolean).forEach((line) => { + if (/^(target|type|[-\s]+$)/i.test(line)) return; + const parts = line.split(/\s+/); + if (parts.length >= 4 && /^(file|block|dir|network|volume)$/i.test(parts[0])) { + rows.push({ type: parts[0], device: parts[1], target: parts[2], source: parts.slice(3).join(' ') }); + } else if (parts.length >= 2) { + rows.push({ type: 'file', device: 'disk', target: parts[0], source: parts.slice(1).join(' ') }); + } + }); + return rows; + } + function renderDisks() { + const box = qs('#vm-disk-visual'); + if (!box) return; + const rows = parseDisks(rawPreBySummary('domblklist')); + if (!rows.length) { + box.innerHTML = '
Диски не найдены в выводе libvirt.
'; + return; + } + box.innerHTML = `
${rows.map((row) => ` +
+
+
${escapeHtml(row.target)}${escapeHtml(row.device)} · ${escapeHtml(row.type)}
${escapeHtml(row.source)}
+ disk +
`).join('')}
`; + } + function parseInterfaces(raw) { + const rows = []; + raw.split('\n').map((line) => line.trim()).filter(Boolean).forEach((line) => { + if (/^(interface|[-\s]+$)/i.test(line)) return; + const parts = line.split(/\s+/); + if (parts.length >= 5) rows.push({ iface: parts[0], type: parts[1], source: parts[2], model: parts[3], mac: parts[4] }); + else if (parts.length >= 2) rows.push({ iface: parts[0], type: parts[1] || '—', source: parts[2] || '—', model: parts[3] || '—', mac: parts[4] || '—' }); + }); + return rows; + } + function renderNetwork() { + const box = qs('#vm-network-visual'); + if (!box) return; + const rows = parseInterfaces(rawPreBySummary('domiflist')); + if (!rows.length) { + box.innerHTML = '
Сетевые интерфейсы не найдены в выводе libvirt.
'; + return; + } + box.innerHTML = `
${rows.map((row) => ` +
+
+
${escapeHtml(row.iface)}${escapeHtml(row.type)} · ${escapeHtml(row.source)} · ${escapeHtml(row.model)}
${escapeHtml(row.mac)}
+ link +
`).join('')}
`; + } + function renderVmVisuals() { + if (!qs('#vm-info-visual') && !qs('#vm-disk-visual') && !qs('#vm-network-visual')) return; + renderInfo(); + renderDisks(); + renderNetwork(); + } + if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', renderVmVisuals); else renderVmVisuals(); +})(); + /* Virtuality live status + toast layer */ (() => { if (window.VirtualityLiveStatus) return;