Files
deploy-felhom-compose/controller/internal/web/templates/monitoring.html
T
admin bdbe170a54 feat: storage watchdog — USB disconnect detection, auto-stop, safe eject, auto-reconnect (v0.17.0)
New storage watchdog monitors registered storage paths every 5s. On disconnect
(3 consecutive probe failures), auto-stops affected apps, lazy-unmounts stale
VFS entries, fires alerts/notifications/hub report. On reconnect (UUID detected),
auto-remounts via fstab, cleans stale restic locks, offers app restart.

Safe disconnect UI for USB drives: confirmation dialog, stop apps, sync, unmount.
Disconnected state visible across all pages (dashboard, settings, backups, monitoring)
with hatched red bars and badges. Backup guards skip disconnected drives.

22 files changed (1 new: monitor/watchdog.go), ~1500 lines added.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 19:42:26 +01:00

627 lines
25 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: Remote Monitoring Status -->
<div class="monitor-card">
<h3>Távoli monitoring</h3>
{{if not .MonitoringEnabled}}
<div class="monitoring-banner monitoring-banner-red">
⚠️ A távoli monitoring ki van kapcsolva. Az üzemeltető nem kap értesítést hibák esetén.
</div>
{{else}}
{{if .AllPingsConfigured}}
<div class="monitoring-banner monitoring-banner-green">
✅ Minden távoli monitoring aktív — az üzemeltető értesítést kap hibák esetén.
</div>
{{else}}
<div class="monitoring-banner monitoring-banner-yellow">
⚠️ Egyes monitoring ellenőrzések nincsenek beállítva. Kérd az üzemeltetőt a konfiguráláshoz.
</div>
{{end}}
<div class="sysinfo-grid" style="margin-top: 0.75rem">
{{range .PingStatus}}
<div class="sysinfo-row">
<span class="sysinfo-label">{{.Icon}} {{.Label}}</span>
<span class="sysinfo-value">
{{if .Configured}}<span class="ping-status-ok">✅ Beállítva</span>{{else}}<span class="ping-status-warn">⚠️ Nincs beállítva</span>{{end}}
<span class="ping-schedule">{{.Schedule}}</span>
</span>
</div>
{{end}}
</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 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');
} catch(e) {
console.error('Failed to load container summary:', e);
}
}
// =============================================
// 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}}