Files
felhom-controller/controller/internal/web/templates/monitoring.html
T
admin ff68b0b433
gates / gates (push) Successful in 20s
v0.248.0: i18n slice 1 release A — apps and settings in English, Hungarian byte-identical (R-556)
Ten pages converted against fixtures captured from unconverted templates (6be55a0); marker
coverage test; English page mask narrowed; English retrieval-promise stems; common keys;
title keys and infraMeta from the bundle; old wording tests read the Hungarian expansion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-17 17:36:08 +02:00

868 lines
37 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>{{T "monitoring.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>{{T "monitoring.szerver_allapota_gazdagep"}}</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">
{{T "monitoring.a_gazdagep_metrikai_jelenleg_nem"}}
</div>
<div id="host-health-body" style="display:none">
<div class="sysinfo-grid">
<div class="sysinfo-row">
<span class="sysinfo-label">{{T "monitoring.cpu_hasznalat"}}</span>
<span class="sysinfo-value" id="host-cpu">–</span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">{{T "monitoring.terheles_load"}}</span>
<span class="sysinfo-value" id="host-load">–</span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">{{T "monitoring.memoria"}}</span>
<span class="sysinfo-value" id="host-mem">–</span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">{{T "monitoring.cpu_homerseklet"}}</span>
<span class="sysinfo-value" id="host-temp">–</span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">{{T "monitoring.uzemido"}}</span>
<span class="sysinfo-value" id="host-uptime">–</span>
</div>
</div>
<h4 style="margin:1rem 0 .5rem">{{T "monitoring.tarolok_kapacitasa"}}</h4>
<div class="storage-bars" id="host-storage-bars"></div>
</div>
</div>
<!-- Section 1: System Overview -->
<div class="monitor-card">
<h3>{{T "monitoring.rendszer_attekintes"}}</h3>
<div class="sysinfo-grid">
<div class="sysinfo-row">
<span class="sysinfo-label">{{T "monitoring.gepnev"}}</span>
<span class="sysinfo-value" id="sysinfo-hostname">–</span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">{{T "monitoring.operacios_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">{{T "monitoring.processzor"}}</span>
<span class="sysinfo-value" id="sysinfo-cpu">–</span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">{{T "monitoring.uzemido"}}</span>
<span class="sysinfo-value" id="sysinfo-uptime">–</span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">{{T "monitoring.inditas"}}</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 "monitoring.tarhely"}}</h3>
<div class="storage-bars">
{{with .SystemInfo}}
<div class="storage-item meter {{usageColor .DiskPercent}}">
<div class="storage-header">
<span class="storage-label">{{T "common.rendszer_gyoker"}}</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">{{T "common.levalasztva"}}</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>{{T "monitoring.hub_kapcsolat"}}</h3>
{{if .HubEnabled}}
{{if .HubConnected}}
<div class="monitoring-banner monitoring-banner-green">
{{T "monitoring.kapcsolodva_a_kozponti_rendszer_aktivan"}}
</div>
{{else}}
<div class="monitoring-banner monitoring-banner-red">
{{T "monitoring.nem_elerheto_a_kozponti_rendszer"}}
</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">{{T "common.ugyfel_azonosito"}}</span>
<span class="sysinfo-value"><code>{{.CustomerID}}</code></span>
</div>
{{if not .HubLastSuccess.IsZero}}
<div class="sysinfo-row">
<span class="sysinfo-label">{{T "monitoring.utolso_sikeres_jelentes"}}</span>
<span class="sysinfo-value">{{.HubLastSuccess | timeAgo}}</span>
</div>
{{end}}
{{if .HubLastError}}
<div class="sysinfo-row">
<span class="sysinfo-label">{{T "monitoring.utolso_hiba"}}</span>
<span class="sysinfo-value"><span class="text-error">{{.HubLastError}}</span></span>
</div>
{{end}}
</div>
{{else}}
<div class="monitoring-banner monitoring-banner-yellow">
{{T "monitoring.a_hub_kapcsolat_nincs_bekapcsolva"}}
</div>
{{end}}
</div>
<!-- Section 3: System Metrics Charts -->
<div class="monitor-card">
<div class="monitor-card-header">
<h3>{{T "monitoring.rendszer_metrikak"}}</h3>
<div class="time-range-bar" id="system-range-bar">
<button class="filter-btn active" data-range="1h">{{T "monitoring.1_ora"}}</button>
<button class="filter-btn" data-range="6h">{{T "monitoring.6_ora"}}</button>
<button class="filter-btn" data-range="24h">{{T "monitoring.24_ora"}}</button>
<button class="filter-btn" data-range="7d">{{T "monitoring.7_nap"}}</button>
<button class="filter-btn" data-range="30d">{{T "monitoring.30_nap"}}</button>
</div>
</div>
<div class="charts-grid" id="system-charts">
<div class="chart-box">
<div class="chart-title">{{T "monitoring.cpu_hasznalat_2"}}</div>
<div class="chart-wrap"><canvas id="chart-cpu"></canvas></div>
</div>
<div class="chart-box">
<div class="chart-title">{{T "monitoring.memoria_hasznalat_gb"}}</div>
<div class="chart-wrap"><canvas id="chart-memory"></canvas></div>
</div>
<div class="chart-box">
<div class="chart-title">{{T "monitoring.homerseklet_c"}}</div>
<div class="chart-wrap"><canvas id="chart-temp"></canvas></div>
</div>
<div class="chart-box">
<div class="chart-title">{{T "monitoring.terheles_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">
{{T "monitoring.meg_nincsenek_adatok_a_metrikak"}}
</div>
</div>
<!-- Section 3.5: Memory Distribution Bar -->
<div class="monitor-card" id="memory-distribution-card" style="display:none">
<h3>{{T "monitoring.memoria_eloszlas"}}</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>{{T "monitoring.alkalmazas_eroforrasok"}}</h3>
<div class="container-charts-row" id="container-charts">
<div class="chart-box chart-box-half">
<div class="chart-title">{{T "monitoring.cpu_hasznalat_2"}}</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">{{T "monitoring.memoria_hasznalat_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">
{{T "monitoring.meg_nincsenek_kontener_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">{{T "monitoring.1_ora"}}</button>
<button class="filter-btn" data-range="6h">{{T "monitoring.6_ora"}}</button>
<button class="filter-btn" data-range="24h">{{T "monitoring.24_ora"}}</button>
<button class="filter-btn" data-range="7d">{{T "monitoring.7_nap"}}</button>
</div>
<button class="btn btn-outline btn-sm" onclick="closeContainerDetail()">{{T "common.bezaras"}}</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">{{T "monitoring.memoria_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 = '{{T "monitoring.hasznalt"}} ' + 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="{{T "monitoring.rendszer_title"}} ' + fmtMB(osMB) + '"></div>';
legendHtml += '<div class="memory-legend-item"><span class="memory-legend-dot" style="background:var(--text-3);opacity:0.5"></span>{{T "monitoring.rendszer_2"}}' + 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>{{T "monitoring.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 + ' {{T "monitoring.eroforras_elozmenyek"}}';
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)">{{T "monitoring.nincs_tarolo_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">{{T "monitoring.nem_elerheto"}}</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('{{T "monitoring.kopas"}} ' + 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 =
'{{T "monitoring.frissitve"}} ' + 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 + ' {{T "monitoring.nap"}} ' + hours + ' {{T "monitoring.ora"}}';
if (hours > 0) return hours + ' {{T "monitoring.ora_2"}} ' + minutes + ' {{T "monitoring.perc"}}';
return minutes + ' {{T "monitoring.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}}