Files
felhom-controller/controller/internal/web/templates/monitoring.html
T
admin f100cef842 D0 Part 3: deploy + backups + monitoring in v2
- deploy.html: stacked memory bar -> meter track with two segments
  (committed solid blue, new app 35%-opacity blue), neutral threshold
  ticks; integration status badges -> tags; meta pills -> metarows;
  3-step deploy progress icons -> sprite (check/x/triangle-alert/
  spinning rotate-cw); emoji -> icons or plain text incl. the JS
  confirm string.
- backups.html: storage bars -> meters, disconnected drives render an
  empty track with a neutral 'Leválasztva' (no red), drive-state badges
  -> warn tags, checkmark/warning glyphs -> sprite icons, JS flash/info
  strings de-emojified.
- monitoring.html: bars -> meters in both template and JS-generated
  host-storage markup (usageColorClass now emits nominal/warn/crit);
  Chart.js palette per spec (cpu #2EA8F5, memory #8E7CE8, temp #E0A93E,
  load #5EC4B6 — categorical data series keep distinct hues); tooltip/
  grid/tick colors moved to v2 literals; mem-dist palette rebalanced.
- Templates repo-wide: legacy var(--*) names in inline styles and JS
  string literals renamed to v2 tokens (old names no longer resolve).
- style.css: memory-bar family rewritten for the two-segment meter,
  .ico-spin animation added (reduced-motion respected).
2026-07-02 14:32:00 +02:00

868 lines
36 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "monitoring"}}
{{template "layout_start" .}}
<div class="page-header">
<h2>Rendszermonitor</h2>
</div>
<!-- Section 0: Host (Proxmox box) health — slice 9, served by the host agent over the local API.
The de-privileged controller can't read the host itself; this card shows the real box. -->
<div class="monitor-card" id="host-health-card">
<div class="monitor-card-header">
<h3>Szerver állapota (gazdagép)</h3>
<span class="sysinfo-value" id="host-health-updated" style="font-size:.75rem;color:var(--text-3)"></span>
</div>
<div id="host-health-unavailable" class="monitoring-banner monitoring-banner-yellow" style="display:none">
A gazdagép metrikái jelenleg nem elérhetők.
</div>
<div id="host-health-body" style="display:none">
<div class="sysinfo-grid">
<div class="sysinfo-row">
<span class="sysinfo-label">CPU használat</span>
<span class="sysinfo-value" id="host-cpu"></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Terhelés (load)</span>
<span class="sysinfo-value" id="host-load"></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Memória</span>
<span class="sysinfo-value" id="host-mem"></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">CPU hőmérséklet</span>
<span class="sysinfo-value" id="host-temp"></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Üzemidő</span>
<span class="sysinfo-value" id="host-uptime"></span>
</div>
</div>
<h4 style="margin:1rem 0 .5rem">Tárolók kapacitása</h4>
<div class="storage-bars" id="host-storage-bars"></div>
</div>
</div>
<!-- Section 1: System Overview -->
<div class="monitor-card">
<h3>Rendszer áttekintés</h3>
<div class="sysinfo-grid">
<div class="sysinfo-row">
<span class="sysinfo-label">Gépnév</span>
<span class="sysinfo-value" id="sysinfo-hostname"></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Operációs rendszer</span>
<span class="sysinfo-value" id="sysinfo-os"></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Kernel</span>
<span class="sysinfo-value" id="sysinfo-kernel"></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Processzor</span>
<span class="sysinfo-value" id="sysinfo-cpu"></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Üzemidő</span>
<span class="sysinfo-value" id="sysinfo-uptime"></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Indítás</span>
<span class="sysinfo-value" id="sysinfo-boot"></span>
</div>
</div>
</div>
<!-- Section 1.5: Storage (moved here for visibility) -->
<div class="monitor-card">
<h3>Tárhely</h3>
<div class="storage-bars">
{{with .SystemInfo}}
<div class="storage-item meter {{usageColor .DiskPercent}}">
<div class="storage-header">
<span class="storage-label">Rendszer (/)</span>
<span class="storage-value">{{fmtGB .DiskUsedGB}} / {{fmtGB .DiskTotalGB}} ({{printf "%.0f" .DiskPercent}}%)</span>
</div>
<div class="meter-track">
<div class="meter-fill" style="width:{{printf "%.1f" .DiskPercent}}%"></div>
</div>
</div>
{{range $.StorageBars}}
{{if .Disconnected}}
<div class="storage-item meter meter-empty storage-disconnected">
<div class="storage-header">
<span class="storage-label">{{.Label}}</span>
<span class="storage-value"><span class="state-text-neutral">Leválasztva</span></span>
</div>
<div class="meter-track"></div>
</div>
{{else}}
<div class="storage-item meter {{usageColor .Percent}}">
<div class="storage-header">
<span class="storage-label">{{.Label}}</span>
<span class="storage-value">{{fmtGB .UsedGB}} / {{fmtGB .TotalGB}} ({{printf "%.0f" .Percent}}%)</span>
</div>
<div class="meter-track">
<div class="meter-fill" style="width:{{printf "%.1f" .Percent}}%"></div>
</div>
{{if .Purpose}}<div class="storage-purpose" style="font-size:.72rem;opacity:.65;margin-top:.2rem">{{.Purpose}}</div>{{end}}
</div>
{{end}}
{{end}}
{{end}}
</div>
{{if .DiskWarnings}}
<div class="inline-warnings">
{{range .DiskWarnings}}
<div class="inline-warning inline-warning-{{.Level}}">
<span class="inline-warning-dot"></span>
<span class="inline-warning-text">{{.Message}}</span>
{{if .Link}}<a href="{{.Link}}" class="inline-warning-link">{{.LinkText}} →</a>{{end}}
</div>
{{end}}
</div>
{{end}}
</div>
<!-- Section 2: Hub Connection Status -->
<div class="monitor-card">
<h3>Hub kapcsolat</h3>
{{if .HubEnabled}}
{{if .HubConnected}}
<div class="monitoring-banner monitoring-banner-green">
Kapcsolódva — a központi rendszer aktívan figyeli a szervert.
</div>
{{else}}
<div class="monitoring-banner monitoring-banner-red">
Nem elérhető — a központi rendszer nem kapott friss jelentést.
</div>
{{end}}
<div class="sysinfo-grid" style="margin-top: 0.75rem">
<div class="sysinfo-row">
<span class="sysinfo-label">Hub URL</span>
<span class="sysinfo-value"><code>{{.HubURL}}</code></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Ügyfél azonosító</span>
<span class="sysinfo-value"><code>{{.CustomerID}}</code></span>
</div>
{{if not .HubLastSuccess.IsZero}}
<div class="sysinfo-row">
<span class="sysinfo-label">Utolsó sikeres jelentés</span>
<span class="sysinfo-value">{{.HubLastSuccess | timeAgo}}</span>
</div>
{{end}}
{{if .HubLastError}}
<div class="sysinfo-row">
<span class="sysinfo-label">Utolsó hiba</span>
<span class="sysinfo-value"><span class="text-error">{{.HubLastError}}</span></span>
</div>
{{end}}
</div>
{{else}}
<div class="monitoring-banner monitoring-banner-yellow">
A Hub kapcsolat nincs bekapcsolva — a központi monitoring nem aktív.
</div>
{{end}}
</div>
<!-- Section 3: System Metrics Charts -->
<div class="monitor-card">
<div class="monitor-card-header">
<h3>Rendszer metrikák</h3>
<div class="time-range-bar" id="system-range-bar">
<button class="filter-btn active" data-range="1h">1 óra</button>
<button class="filter-btn" data-range="6h">6 óra</button>
<button class="filter-btn" data-range="24h">24 óra</button>
<button class="filter-btn" data-range="7d">7 nap</button>
<button class="filter-btn" data-range="30d">30 nap</button>
</div>
</div>
<div class="charts-grid" id="system-charts">
<div class="chart-box">
<div class="chart-title">CPU használat (%)</div>
<div class="chart-wrap"><canvas id="chart-cpu"></canvas></div>
</div>
<div class="chart-box">
<div class="chart-title">Memória használat (GB)</div>
<div class="chart-wrap"><canvas id="chart-memory"></canvas></div>
</div>
<div class="chart-box">
<div class="chart-title">Hőmérséklet (°C)</div>
<div class="chart-wrap"><canvas id="chart-temp"></canvas></div>
</div>
<div class="chart-box">
<div class="chart-title">Terhelés (Load Average)</div>
<div class="chart-wrap"><canvas id="chart-load"></canvas></div>
</div>
</div>
<div class="chart-empty" id="system-charts-empty" style="display:none">
Még nincsenek adatok. A metrikák gyűjtése elindult, az első adatok néhány perc múlva megjelennek.
</div>
</div>
<!-- Section 3.5: Memory Distribution Bar -->
<div class="monitor-card" id="memory-distribution-card" style="display:none">
<h3>Memória eloszlás</h3>
<div id="mem-dist-header" class="memory-dist-header"></div>
<div class="memory-dist-bar" id="mem-dist-bar"></div>
<div class="memory-bar-legend" id="mem-dist-legend"></div>
</div>
<!-- Section 4: Container Resources -->
<div class="monitor-card">
<h3>Alkalmazás erőforrások</h3>
<div class="container-charts-row" id="container-charts">
<div class="chart-box chart-box-half">
<div class="chart-title">CPU használat (%)</div>
<div class="chart-wrap chart-wrap-bar"><canvas id="chart-container-cpu"></canvas></div>
</div>
<div class="chart-box chart-box-half">
<div class="chart-title">Memória használat (MB)</div>
<div class="chart-wrap chart-wrap-bar"><canvas id="chart-container-mem"></canvas></div>
</div>
</div>
<div class="chart-empty" id="container-charts-empty" style="display:none">
Még nincsenek konténer adatok.
</div>
</div>
<!-- Section 5: Per-container detail (expandable) -->
<div class="monitor-card" id="container-detail-panel" style="display:none">
<div class="monitor-card-header">
<h3 id="container-detail-title"></h3>
<div class="time-range-bar" id="container-range-bar">
<button class="filter-btn active" data-range="1h">1 óra</button>
<button class="filter-btn" data-range="6h">6 óra</button>
<button class="filter-btn" data-range="24h">24 óra</button>
<button class="filter-btn" data-range="7d">7 nap</button>
</div>
<button class="btn btn-outline btn-sm" onclick="closeContainerDetail()">Bezárás</button>
</div>
<div class="charts-grid charts-grid-2">
<div class="chart-box">
<div class="chart-title">CPU %</div>
<div class="chart-wrap"><canvas id="chart-detail-cpu"></canvas></div>
</div>
<div class="chart-box">
<div class="chart-title">Memória (MB)</div>
<div class="chart-wrap"><canvas id="chart-detail-mem"></canvas></div>
</div>
</div>
</div>
<script src="/static/chart.min.js"></script>
<script>
(function() {
'use strict';
// --- Chart.js dark theme defaults ---
var colors = {
cpu: {border: '#2EA8F5', bg: 'rgba(46,168,245,0.1)'},
memory: {border: '#8E7CE8', bg: 'rgba(142,124,232,0.1)'},
temp: {border: '#E0A93E', bg: 'rgba(224,169,62,0.1)'},
load: {border: '#5EC4B6', bg: 'rgba(94,196,182,0.1)'}
};
// --- Range helper: returns milliseconds for a range string ---
function parseRangeMs(range) {
switch (range) {
case '1h': return 3600000;
case '6h': return 21600000;
case '24h': return 86400000;
case '7d': return 604800000;
case '30d': return 2592000000;
default: return 3600000;
}
}
// --- X-axis tick label format: short time for <=24h, date for longer ---
function tickFormatForRange(range) {
if (range === '7d' || range === '30d') return 'date';
return 'time';
}
// --- Chart options for line charts with LINEAR x-axis ---
function chartOpts(yLabel, beginAtZero) {
var now = Date.now();
var defaultRangeMs = parseRangeMs('1h');
return {
responsive: true,
maintainAspectRatio: false,
animation: {duration: 300},
plugins: {
legend: {display: false},
tooltip: {
backgroundColor: '#0F1B2E',
titleColor: '#EDF2F9',
bodyColor: '#94A6BF',
borderColor: '#22344F',
borderWidth: 1,
callbacks: {
title: function(items) {
if (!items.length) return '';
var raw = items[0].raw;
if (raw && typeof raw === 'object' && raw.x) {
return formatTimestamp(raw.x);
}
if (items[0].parsed && items[0].parsed.x) {
return formatTimestamp(items[0].parsed.x);
}
return '';
}
}
}
},
scales: {
x: {
type: 'linear',
min: now - defaultRangeMs,
max: now,
grid: {color: 'rgba(34,52,79,0.5)'},
ticks: {
color: '#94A6BF',
maxTicksLimit: 8,
callback: function(v) {
return formatTimeLabel(v);
}
}
},
y: {
grid: {color: 'rgba(34,52,79,0.5)'},
ticks: {color: '#94A6BF'},
beginAtZero: beginAtZero !== false,
title: {display: !!yLabel, text: yLabel || '', color: '#5E7392', font: {size: 11}}
}
}
};
}
var lineDataset = function(color) {
return {
borderColor: color.border,
backgroundColor: color.bg,
borderWidth: 2,
pointRadius: 0,
pointHitRadius: 10,
tension: 0.3,
fill: true,
spanGaps: true
};
};
// --- Timezone formatting ---
var budaTZ = 'Europe/Budapest';
// Current range for choosing date vs time format in tick labels
var currentTickFormat = 'time';
function formatTimestamp(ts) {
if (ts === null || ts === undefined || ts === '') return '';
if (typeof ts === 'string') ts = Number(ts);
if (isNaN(ts)) return '';
// ts should already be in ms (linear axis stores ms values)
var d = new Date(ts);
return d.toLocaleString('hu-HU', {timeZone: budaTZ, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'});
}
function formatTimeLabel(ts) {
if (ts === null || ts === undefined || ts === '') return '';
if (typeof ts === 'string') ts = Number(ts);
if (isNaN(ts)) return '';
var d = new Date(ts);
if (currentTickFormat === 'date') {
return d.toLocaleDateString('hu-HU', {timeZone: budaTZ, month: '2-digit', day: '2-digit'});
}
return d.toLocaleTimeString('hu-HU', {timeZone: budaTZ, hour: '2-digit', minute: '2-digit'});
}
// --- Shared: build {x, y} point array from timestamps + values ---
function buildXYData(timestamps, values) {
var points = [];
for (var i = 0; i < timestamps.length; i++) {
points.push({x: timestamps[i], y: values[i]});
}
return points;
}
// --- Shared: set x-axis min/max on a chart based on range ---
function setChartXBounds(chart, range) {
var now = Date.now();
var rangeMs = parseRangeMs(range);
chart.options.scales.x.min = now - rangeMs;
chart.options.scales.x.max = now;
}
// --- Shared: update a line chart with {x, y} data ---
function updateLineChart(chart, timestamps, values) {
chart.data.datasets[0].data = buildXYData(timestamps, values);
chart.update('none');
}
// =============================================
// SYSTEM CHARTS
// =============================================
var systemRange = '1h';
var chartCPU, chartMem, chartTemp, chartLoad;
function initSystemCharts() {
var mkChart = function(id, color, yLabel, beginAtZero) {
return new Chart(document.getElementById(id), {
type: 'line',
data: {datasets: [{data: [], ...lineDataset(color)}]},
options: chartOpts(yLabel, beginAtZero)
});
};
chartCPU = mkChart('chart-cpu', colors.cpu, '%', true);
chartMem = mkChart('chart-memory', colors.memory, 'GB', true);
chartTemp = mkChart('chart-temp', colors.temp, '°C', false);
chartLoad = mkChart('chart-load', colors.load, '', true);
}
async function loadSystemMetrics() {
try {
var resp = await fetch('/api/metrics/system?range=' + systemRange + '&resolution=200');
var json = await resp.json();
if (!json.ok || !json.data) return;
var d = json.data;
if (!d.labels || d.labels.length === 0) {
document.getElementById('system-charts').style.display = 'none';
document.getElementById('system-charts-empty').style.display = 'block';
return;
}
document.getElementById('system-charts').style.display = '';
document.getElementById('system-charts-empty').style.display = 'none';
// Convert Unix seconds to milliseconds
var timestamps = d.labels.map(function(ts) { return ts * 1000; });
// Update tick label format based on range
currentTickFormat = tickFormatForRange(systemRange);
// Set x-axis bounds to the full requested range
var allCharts = [chartCPU, chartMem, chartTemp, chartLoad];
allCharts.forEach(function(c) { setChartXBounds(c, systemRange); });
// Update each chart with {x, y} data
updateLineChart(chartCPU, timestamps, d.cpu);
updateLineChart(chartMem, timestamps, d.memory);
updateLineChart(chartTemp, timestamps, d.temp);
updateLineChart(chartLoad, timestamps, d.load1);
} catch(e) {
console.error('Failed to load system metrics:', e);
}
}
// Range bar clicks
document.getElementById('system-range-bar').addEventListener('click', function(e) {
var btn = e.target.closest('.filter-btn');
if (!btn) return;
this.querySelectorAll('.filter-btn').forEach(function(b) { b.classList.remove('active'); });
btn.classList.add('active');
systemRange = btn.dataset.range;
loadSystemMetrics();
});
// =============================================
// CONTAINER BAR CHARTS
// =============================================
var chartContainerCPU, chartContainerMem;
var containerNames = [];
function initContainerCharts() {
var barOpts = function(xLabel) {
return {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
animation: {duration: 300},
plugins: {
legend: {display: false},
tooltip: {
backgroundColor: '#0F1B2E',
titleColor: '#EDF2F9',
bodyColor: '#94A6BF',
borderColor: '#22344F',
borderWidth: 1
}
},
scales: {
x: {
grid: {color: 'rgba(34,52,79,0.5)'},
ticks: {color: '#94A6BF'},
beginAtZero: true,
title: {display: true, text: xLabel, color: '#5E7392', font: {size: 11}}
},
y: {
grid: {display: false},
ticks: {color: '#94A6BF', font: {size: 12}}
}
},
onClick: function(evt, elements) {
if (elements.length > 0) {
var idx = elements[0].index;
if (containerNames[idx]) {
showContainerDetail(containerNames[idx]);
}
}
}
};
};
chartContainerCPU = new Chart(document.getElementById('chart-container-cpu'), {
type: 'bar',
data: {labels: [], datasets: [{data: [], backgroundColor: '#2EA8F5', borderRadius: 2}]},
options: barOpts('%')
});
chartContainerMem = new Chart(document.getElementById('chart-container-mem'), {
type: 'bar',
data: {labels: [], datasets: [{data: [], backgroundColor: '#8E7CE8', borderRadius: 2}]},
options: barOpts('MB')
});
}
async function loadContainerSummary() {
try {
var resp = await fetch('/api/metrics/containers/summary');
var json = await resp.json();
if (!json.ok || !json.data) return;
var data = json.data;
if (!data.length) {
document.getElementById('container-charts').style.display = 'none';
document.getElementById('container-charts-empty').style.display = 'block';
return;
}
document.getElementById('container-charts').style.display = '';
document.getElementById('container-charts-empty').style.display = 'none';
containerNames = data.map(function(c) { return c.name; });
var cpuData = data.map(function(c) { return Math.round(c.cpu_percent * 100) / 100; });
var memData = data.map(function(c) { return Math.round(c.mem_usage_mb); });
// Adjust bar chart height based on container count
var h = Math.max(200, data.length * 35 + 60);
document.querySelectorAll('.chart-wrap-bar').forEach(function(el) { el.style.height = h + 'px'; });
chartContainerCPU.data.labels = containerNames;
chartContainerCPU.data.datasets[0].data = cpuData;
chartContainerCPU.update('none');
chartContainerMem.data.labels = containerNames;
chartContainerMem.data.datasets[0].data = memData;
chartContainerMem.update('none');
buildMemoryDistributionBar(data);
} catch(e) {
console.error('Failed to load container summary:', e);
}
}
var memDistPalette = ['#2EA8F5','#8E7CE8','#5EC4B6','#E0A93E','#D67BB5','#7FA6E8','#59B88C','#E8935E','#6FCADF','#B3A1F0'];
async function buildMemoryDistributionBar(containers) {
var totalMB = {{.SystemInfo.TotalMemMB}};
if (!totalMB) return;
// Get real-time used memory from API
var usedMB = 0;
try {
var resp = await fetch('/api/system/info');
var json = await resp.json();
if (json.ok && json.data && json.data.system) usedMB = json.data.system.used_mem_mb || 0;
} catch(e) {}
if (!usedMB) return;
var card = document.getElementById('memory-distribution-card');
var bar = document.getElementById('mem-dist-bar');
var legend = document.getElementById('mem-dist-legend');
var header = document.getElementById('mem-dist-header');
// Sum container memory
var appTotal = 0;
containers.forEach(function(c) { appTotal += c.mem_usage_mb || 0; });
var osMB = Math.max(0, usedMB - appTotal);
var freeMB = Math.max(0, totalMB - usedMB);
function fmtMB(mb) { return mb >= 1024 ? (mb/1024).toFixed(1) + ' GB' : Math.round(mb) + ' MB'; }
header.textContent = 'Használt: ' + fmtMB(usedMB) + ' / ' + fmtMB(totalMB) + ' (' + Math.round(usedMB/totalMB*100) + '%)';
// Sort containers by memory usage descending
containers.sort(function(a, b) { return (b.mem_usage_mb || 0) - (a.mem_usage_mb || 0); });
// Build bar segments
var html = '';
var legendHtml = '';
containers.forEach(function(c, i) {
var mb = c.mem_usage_mb || 0;
if (mb < 1) return;
var pct = (mb / totalMB * 100).toFixed(2);
var color = memDistPalette[i % memDistPalette.length];
html += '<div class="memory-bar-segment" style="width:' + pct + '%;background:' + color + '" title="' + c.name + ': ' + fmtMB(mb) + '"></div>';
legendHtml += '<div class="memory-legend-item"><span class="memory-legend-dot" style="background:' + color + '"></span>' + c.name + ' (' + fmtMB(mb) + ')</div>';
});
// OS / system overhead
if (osMB > 10) {
var osPct = (osMB / totalMB * 100).toFixed(2);
html += '<div class="memory-bar-segment" style="width:' + osPct + '%;background:var(--text-3);opacity:0.5" title="Rendszer: ' + fmtMB(osMB) + '"></div>';
legendHtml += '<div class="memory-legend-item"><span class="memory-legend-dot" style="background:var(--text-3);opacity:0.5"></span>Rendszer (' + fmtMB(osMB) + ')</div>';
}
// Free space legend only
legendHtml += '<div class="memory-legend-item"><span class="memory-legend-dot" style="background:var(--bg-2);border:1px solid var(--line)"></span>Szabad (' + fmtMB(freeMB) + ')</div>';
bar.innerHTML = html;
legend.innerHTML = legendHtml;
card.style.display = '';
}
// =============================================
// CONTAINER DETAIL (per-container history)
// =============================================
var detailChartCPU, detailChartMem;
var detailContainer = '';
var detailRange = '1h';
function initDetailCharts() {
var mkChart = function(id, color, yLabel) {
return new Chart(document.getElementById(id), {
type: 'line',
data: {datasets: [{data: [], ...lineDataset(color)}]},
options: chartOpts(yLabel, true)
});
};
detailChartCPU = mkChart('chart-detail-cpu', colors.cpu, '%');
detailChartMem = mkChart('chart-detail-mem', colors.memory, 'MB');
}
window.showContainerDetail = async function(name) {
detailContainer = name;
document.getElementById('container-detail-title').textContent = name + ' — Erőforrás előzmények';
document.getElementById('container-detail-panel').style.display = '';
document.getElementById('container-detail-panel').scrollIntoView({behavior: 'smooth'});
await loadContainerDetail();
};
window.closeContainerDetail = function() {
document.getElementById('container-detail-panel').style.display = 'none';
detailContainer = '';
};
async function loadContainerDetail() {
if (!detailContainer) return;
try {
var resp = await fetch('/api/metrics/containers/' + encodeURIComponent(detailContainer) + '?range=' + detailRange + '&resolution=150');
var json = await resp.json();
if (!json.ok || !json.data) return;
var d = json.data;
// Convert Unix seconds to milliseconds
var timestamps = (d.labels || []).map(function(ts) { return ts * 1000; });
// Set x-axis bounds
setChartXBounds(detailChartCPU, detailRange);
setChartXBounds(detailChartMem, detailRange);
// Update with {x, y} data
updateLineChart(detailChartCPU, timestamps, d.cpu || []);
updateLineChart(detailChartMem, timestamps, d.memory || []);
} catch(e) {
console.error('Failed to load container detail:', e);
}
}
document.getElementById('container-range-bar').addEventListener('click', function(e) {
var btn = e.target.closest('.filter-btn');
if (!btn) return;
this.querySelectorAll('.filter-btn').forEach(function(b) { b.classList.remove('active'); });
btn.classList.add('active');
detailRange = btn.dataset.range;
loadContainerDetail();
});
// =============================================
// HOST (PROXMOX BOX) HEALTH — slice 9, proxied from the host agent
// =============================================
function usageColorClass(pct) {
if (pct >= 85) return 'crit';
if (pct >= 70) return 'warn';
return 'nominal';
}
function fmtBytesGB(bytes) {
if (!bytes || bytes <= 0) return '0 GB';
var gb = bytes / 1073741824;
if (gb >= 1024) return (gb / 1024).toFixed(2) + ' TB';
if (gb >= 100) return Math.round(gb) + ' GB';
return gb.toFixed(1) + ' GB';
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, function(c) {
return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];
});
}
function showHostUnavailable() {
document.getElementById('host-health-unavailable').style.display = '';
document.getElementById('host-health-body').style.display = 'none';
document.getElementById('host-health-updated').textContent = '';
}
function renderHostHealth(d) {
var h = d.host || {};
document.getElementById('host-health-unavailable').style.display = 'none';
document.getElementById('host-health-body').style.display = '';
// CPU % + load average
document.getElementById('host-cpu').textContent =
(h.cpu_percent != null ? h.cpu_percent.toFixed(1) : '') + '%';
var load = Array.isArray(h.loadavg) ? h.loadavg.join(' ') : '';
document.getElementById('host-load').textContent = load || '';
// Memory used/total
if (h.memory_total_bytes > 0) {
document.getElementById('host-mem').textContent =
fmtBytesGB(h.memory_used_bytes) + ' / ' + fmtBytesGB(h.memory_total_bytes) +
' (' + Math.round(h.memory_percent || 0) + '%)';
} else {
document.getElementById('host-mem').textContent = '';
}
// CPU temp — null renders as "n/a" cleanly (graceful-null from the agent)
document.getElementById('host-temp').textContent =
(h.cpu_temp_c == null) ? 'n/a' : (h.cpu_temp_c + ' °C');
// Uptime (reuse the existing formatter)
document.getElementById('host-uptime').textContent =
(h.uptime_seconds > 0) ? formatUptime(h.uptime_seconds) : '';
// Per-storage capacity bars
var bars = document.getElementById('host-storage-bars');
var targets = Array.isArray(d.storage_targets) ? d.storage_targets : [];
if (!targets.length) {
bars.innerHTML = '<div class="sysinfo-value" style="color:var(--text-3)">Nincs tároló adat.</div>';
} else {
var html = '';
targets.forEach(function(t) {
// Friendly label (server-supplied) with the raw PVE storage id shown muted for clarity.
var friendly = escapeHtml(t.label || t.name || '');
var rawId = escapeHtml(t.name || '');
var idHtml = rawId ? ' <span style="color:var(--text-3);font-size:.72rem">(' + rawId + ')</span>' : '';
var label = friendly + idHtml;
var purposeHtml = t.purpose ? '<div class="storage-purpose" style="font-size:.72rem;opacity:.65;margin-top:.2rem">' + escapeHtml(t.purpose) + '</div>' : '';
if (t.state && t.state !== 'attached') {
html += '<div class="storage-item meter meter-empty storage-disconnected">' +
'<div class="storage-header"><span class="storage-label">' + label + '</span>' +
'<span class="storage-value">— <span class="state-text-neutral">Nem elérhető</span></span></div>' +
'<div class="meter-track"></div>' + purposeHtml + '</div>';
return;
}
var pct = (t.used_fraction != null ? t.used_fraction * 100 : 0);
// Extra detail: thin-pool fill (corrupts every guest if full) + disk temp/wear.
var extra = [];
if (t.thin_pool && t.thin_pool.data_used_fraction != null) {
extra.push('thin-pool: ' + Math.round(t.thin_pool.data_used_fraction * 100) + '%');
}
if (t.smart) {
if (t.smart.temperature_c != null) extra.push(t.smart.temperature_c + ' °C');
if (t.smart.percentage_used != null) extra.push('kopás: ' + t.smart.percentage_used + '%');
}
var extraHtml = extra.length ? ' <span style="color:var(--text-3);font-size:.75rem">· ' + escapeHtml(extra.join(' · ')) + '</span>' : '';
html += '<div class="storage-item meter ' + usageColorClass(pct) + '">' +
'<div class="storage-header">' +
'<span class="storage-label">' + label + extraHtml + '</span>' +
'<span class="storage-value">' + fmtBytesGB(t.used_bytes) + ' / ' + fmtBytesGB(t.total_bytes) +
' (' + Math.round(pct) + '%)</span></div>' +
'<div class="meter-track"><div class="meter-fill' +
'" style="width:' + Math.min(100, pct).toFixed(1) + '%"></div></div>' + purposeHtml + '</div>';
});
bars.innerHTML = html;
}
var now = new Date();
document.getElementById('host-health-updated').textContent =
'Frissítve: ' + now.toLocaleTimeString('hu-HU', {timeZone: budaTZ, hour: '2-digit', minute: '2-digit', second: '2-digit'});
}
async function loadHostMetrics() {
try {
var resp = await fetch('/api/host-metrics');
var json = await resp.json();
if (!json.ok || !json.data) { showHostUnavailable(); return; }
renderHostHealth(json.data);
} catch(e) {
console.error('Failed to load host metrics:', e);
showHostUnavailable();
}
}
// =============================================
// STATIC SYSTEM INFO
// =============================================
async function loadSysInfo() {
try {
var resp = await fetch('/api/metrics/sysinfo');
var json = await resp.json();
if (!json.ok || !json.data) return;
var d = json.data;
document.getElementById('sysinfo-hostname').textContent = d.hostname || '';
document.getElementById('sysinfo-os').textContent = d.os || '';
document.getElementById('sysinfo-kernel').textContent = d.kernel || '';
var cpuText = d.cpu_model || '';
if (d.cpu_cores > 0) cpuText += ' (' + d.cpu_cores + ' mag)';
document.getElementById('sysinfo-cpu').textContent = cpuText;
if (d.uptime_seconds > 0) {
document.getElementById('sysinfo-uptime').textContent = formatUptime(d.uptime_seconds);
}
if (d.boot_time) {
var bt = new Date(d.boot_time);
document.getElementById('sysinfo-boot').textContent = bt.toLocaleString('hu-HU', {timeZone: budaTZ, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'});
}
} catch(e) {
console.error('Failed to load sysinfo:', e);
}
}
function formatUptime(seconds) {
var days = Math.floor(seconds / 86400);
var hours = Math.floor((seconds % 86400) / 3600);
var minutes = Math.floor((seconds % 3600) / 60);
if (days > 0) return days + ' nap, ' + hours + ' óra';
if (hours > 0) return hours + ' óra, ' + minutes + ' perc';
return minutes + ' perc';
}
// =============================================
// INIT
// =============================================
initSystemCharts();
initContainerCharts();
initDetailCharts();
loadSysInfo();
loadSystemMetrics();
loadContainerSummary();
loadHostMetrics();
// Auto-refresh every 60 seconds
setInterval(function() {
loadSystemMetrics();
loadContainerSummary();
if (detailContainer) loadContainerDetail();
}, 60000);
// Host (Proxmox box) health is a live snapshot — poll it more often while the page is open.
setInterval(loadHostMetrics, 8000);
})();
</script>
{{template "layout_end" .}}
{{end}}