Files
deploy-felhom-compose/controller/internal/web/templates/monitoring.html
T
admin ad4c005e01 v0.27.3: Use real system memory everywhere, add monitoring memory bar
Deploy page, pre-start check, and deploy validation now use actual
/proc/meminfo usage instead of declared mem_request sums. New
GetMemoryMB() helper for lightweight real-time memory reads. Monitoring
page gains a stacked memory distribution bar showing per-container
usage, OS overhead, and free memory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 10:06:03 +01:00

705 lines
28 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 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">
<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="system-bar">
<div class="system-bar-fill {{usageColor .DiskPercent | printf "system-bar-%s"}}" style="width:{{printf "%.1f" .DiskPercent}}%"></div>
</div>
</div>
{{range $.StorageBars}}
{{if .Disconnected}}
<div class="storage-item storage-disconnected">
<div class="storage-header">
<span class="storage-label">{{.Label}}</span>
<span class="storage-value badge-error" style="font-size:.75rem">Leválasztva</span>
</div>
<div class="system-bar"><div class="system-bar-disconnected"></div></div>
</div>
{{else}}
<div class="storage-item">
<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="system-bar">
<div class="system-bar-fill {{usageColor .Percent | printf "system-bar-%s"}}" style="width:{{printf "%.1f" .Percent}}%"></div>
</div>
</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: '#0088cc', bg: 'rgba(0,136,204,0.1)'},
memory: {border: '#238636', bg: 'rgba(35,134,54,0.1)'},
temp: {border: '#d29922', bg: 'rgba(210,153,34,0.1)'},
load: {border: '#db6d28', bg: 'rgba(219,109,40,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: '#1c2128',
titleColor: '#e6edf3',
bodyColor: '#8b949e',
borderColor: '#30363d',
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(48,54,61,0.5)'},
ticks: {
color: '#8b949e',
maxTicksLimit: 8,
callback: function(v) {
return formatTimeLabel(v);
}
}
},
y: {
grid: {color: 'rgba(48,54,61,0.5)'},
ticks: {color: '#8b949e'},
beginAtZero: beginAtZero !== false,
title: {display: !!yLabel, text: yLabel || '', color: '#6e7681', 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: '#1c2128',
titleColor: '#e6edf3',
bodyColor: '#8b949e',
borderColor: '#30363d',
borderWidth: 1
}
},
scales: {
x: {
grid: {color: 'rgba(48,54,61,0.5)'},
ticks: {color: '#8b949e'},
beginAtZero: true,
title: {display: true, text: xLabel, color: '#6e7681', font: {size: 11}}
},
y: {
grid: {display: false},
ticks: {color: '#8b949e', 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: '#0088cc', borderRadius: 4}]},
options: barOpts('%')
});
chartContainerMem = new Chart(document.getElementById('chart-container-mem'), {
type: 'bar',
data: {labels: [], datasets: [{data: [], backgroundColor: '#238636', borderRadius: 4}]},
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 = ['#238636','#0088cc','#d29922','#da3633','#8b5cf6','#ec6547','#2ea043','#1f6feb','#e3b341','#f47067'];
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) usedMB = json.data.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) + '%)';
// 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-muted);opacity:0.5" title="Rendszer: ' + fmtMB(osMB) + '"></div>';
legendHtml += '<div class="memory-legend-item"><span class="memory-legend-dot" style="background:var(--text-muted);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-secondary);border:1px solid var(--border-color)"></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();
});
// =============================================
// 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();
// Auto-refresh every 60 seconds
setInterval(function() {
loadSystemMetrics();
loadContainerSummary();
if (detailContainer) loadContainerDetail();
}, 60000);
})();
</script>
{{template "layout_end" .}}
{{end}}