Производительность панели была улучшена

Добавлен virt_backend: одно постоянное libvirt-соединение вместо
десятков вызовов virsh на каждую страницу, с автоматическим fallback
на virsh, если libvirt-python недоступен. Обзорные запросы (список VM,
пулы, сервисы) закешированы с коротким TTL и сбросом после действий.
Появился фоновый сборщик метрик хоста и спарклайны CPU/RAM на дашборде.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKUoZwHMCHnytfswFVRUHw
Этот коммит содержится в:
Claude
2026-08-15 19:23:14 +00:00
родитель d322c1352c
Коммит 374bcbb29f
6 изменённых файлов: 376 добавлений и 16 удалений
+64
Просмотреть файл
@@ -54,6 +54,29 @@
</article>
</section>
<section class="grid two">
<article class="card">
<div class="card-head">
<h2>CPU хоста</h2>
<span class="pill" id="cpu-now"></span>
</div>
<svg id="cpu-spark" class="sparkline" viewBox="0 0 300 60" preserveAspectRatio="none" aria-label="График CPU за час">
<polyline points="" fill="none"></polyline>
</svg>
<p class="muted small-note">Загрузка процессора за последний час, обновляется каждые 15 секунд.</p>
</article>
<article class="card">
<div class="card-head">
<h2>Память хоста</h2>
<span class="pill" id="mem-now"></span>
</div>
<svg id="mem-spark" class="sparkline" viewBox="0 0 300 60" preserveAspectRatio="none" aria-label="График памяти за час">
<polyline points="" fill="none"></polyline>
</svg>
<p class="muted small-note">Использование RAM за последний час. Хвост графика заполняется по мере работы панели.</p>
</article>
</section>
<section class="card">
<div class="card-head">
<h2>Виртуальные машины</h2>
@@ -185,6 +208,47 @@
</section>
</main>
</div>
<script>
(function () {
function drawSpark(svgId, values, max) {
const svg = document.getElementById(svgId);
if (!svg) return;
const line = svg.querySelector('polyline');
if (!values.length) { line.setAttribute('points', ''); return; }
const width = 300, height = 60, pad = 3;
const step = values.length > 1 ? (width - pad * 2) / (values.length - 1) : 0;
const points = values.map((value, index) => {
const x = pad + index * step;
const y = height - pad - Math.max(0, Math.min(1, value / max)) * (height - pad * 2);
return x.toFixed(1) + ',' + y.toFixed(1);
});
line.setAttribute('points', points.join(' '));
}
async function refreshMetrics() {
try {
const response = await fetch('/live/metrics', { cache: 'no-store' });
if (!response.ok) return;
const payload = await response.json();
if (!payload.ok || !payload.series) return;
const cpu = payload.series.map(sample => sample.cpu);
const mem = payload.series.map(sample => sample.mem_percent);
drawSpark('cpu-spark', cpu, 100);
drawSpark('mem-spark', mem, 100);
const current = payload.current;
if (current) {
const cpuNow = document.getElementById('cpu-now');
const memNow = document.getElementById('mem-now');
if (cpuNow) cpuNow.textContent = current.cpu.toFixed(1) + '% · load ' + current.load1.toFixed(2);
if (memNow) memNow.textContent = current.mem_used_mb + ' / ' + current.mem_total_mb + ' MB · ' + current.mem_percent.toFixed(0) + '%';
}
} catch (error) { /* сеть моргнула — попробуем в следующий цикл */ }
}
refreshMetrics();
setInterval(refreshMetrics, 15000);
})();
</script>
<script src="/static/panel.js" defer></script>
</body>
</html>