v0.5.0: Backup bugfixes + monitoring page with metrics store
- Fix "Helyi mentés" showing "–" after controller restart by synthesizing
LastBackup from snapshot history and LastDBDump from dump files on disk
- New monitoring page (/monitoring) with system info, metrics charts, and
container resource overview
- SQLite metrics store (modernc.org/sqlite, pure Go, no CGO) with 60s
collection interval and 30-day auto-prune
- REST API endpoints: /api/metrics/system, /api/metrics/containers/summary,
/api/metrics/containers/{name}, /api/metrics/sysinfo
- Chart.js 4.4.7 embedded locally for offline environments
- System info provider reads hostname, OS, kernel, CPU, uptime from /proc
- Docker compose updated with /etc/os-release host mount
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
<li><a href="/" class="{{if eq .Page "dashboard"}}active{{end}}">Vezérlőpult</a></li>
|
||||
<li><a href="/stacks" class="{{if eq .Page "stacks"}}active{{end}}">Alkalmazások</a></li>
|
||||
<li><a href="/backups" class="{{if eq .Page "backups"}}active{{end}}">Biztonsági mentés</a></li>
|
||||
<li><a href="/monitoring" class="{{if eq .Page "monitoring"}}active{{end}}">Rendszermonitor</a></li>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
<span class="version">v{{.Version}}</span>
|
||||
|
||||
@@ -0,0 +1,478 @@
|
||||
{{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 2: 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" data-range="1h">1 óra</button>
|
||||
<button class="filter-btn" data-range="6h">6 óra</button>
|
||||
<button class="filter-btn active" 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: 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 4: 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" data-range="1h">1 óra</button>
|
||||
<button class="filter-btn" data-range="6h">6 óra</button>
|
||||
<button class="filter-btn active" 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>
|
||||
|
||||
<!-- Section 5: Storage -->
|
||||
<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">SSD (/)</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>
|
||||
{{if .HDDConfigured}}
|
||||
<div class="storage-item">
|
||||
<div class="storage-header">
|
||||
<span class="storage-label">Külső HDD</span>
|
||||
<span class="storage-value">{{fmtGB .HDDUsedGB}} / {{fmtGB .HDDTotalGB}} ({{printf "%.0f" .HDDPercent}}%)</span>
|
||||
</div>
|
||||
<div class="system-bar">
|
||||
<div class="system-bar-fill {{usageColor .HDDPercent | printf "system-bar-%s"}}" style="width:{{printf "%.1f" .HDDPercent}}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/chart.min.js"></script>
|
||||
<script>
|
||||
(function() {
|
||||
// --- Chart.js dark theme defaults ---
|
||||
const 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)'},
|
||||
};
|
||||
|
||||
const chartOpts = (yLabel, beginAtZero) => ({
|
||||
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 '';
|
||||
return formatTimestamp(items[0].parsed.x || items[0].label);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {color: 'rgba(48,54,61,0.5)'},
|
||||
ticks: {color: '#8b949e', maxTicksLimit: 8, callback: function(v) { return formatTimeLabel(this.getLabelForValue(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}}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const lineDataset = (color) => ({
|
||||
borderColor: color.border,
|
||||
backgroundColor: color.bg,
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
pointHitRadius: 10,
|
||||
tension: 0.3,
|
||||
fill: true,
|
||||
spanGaps: true
|
||||
});
|
||||
|
||||
// --- Timezone formatting ---
|
||||
const budaTZ = 'Europe/Budapest';
|
||||
function formatTimestamp(ts) {
|
||||
if (!ts) return '';
|
||||
const d = new Date(typeof ts === 'number' && ts < 1e12 ? ts * 1000 : 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) return '';
|
||||
const d = new Date(typeof ts === 'number' && ts < 1e12 ? ts * 1000 : ts);
|
||||
return d.toLocaleTimeString('hu-HU', {timeZone: budaTZ, hour: '2-digit', minute: '2-digit'});
|
||||
}
|
||||
|
||||
// --- System charts ---
|
||||
let systemRange = '24h';
|
||||
let chartCPU, chartMem, chartTemp, chartLoad;
|
||||
|
||||
function initSystemCharts() {
|
||||
const mkChart = (id, color, yLabel, beginAtZero) => {
|
||||
return new Chart(document.getElementById(id), {
|
||||
type: 'line',
|
||||
data: {labels: [], 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 {
|
||||
const resp = await fetch('/api/metrics/system?range=' + systemRange + '&resolution=200');
|
||||
const json = await resp.json();
|
||||
if (!json.ok || !json.data) return;
|
||||
const 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';
|
||||
|
||||
const labels = d.labels.map(ts => ts * 1000); // ms for Date
|
||||
|
||||
function updateChart(chart, labels, data) {
|
||||
chart.data.labels = labels;
|
||||
chart.data.datasets[0].data = data;
|
||||
chart.update('none');
|
||||
}
|
||||
|
||||
updateChart(chartCPU, labels, d.cpu);
|
||||
updateChart(chartMem, labels, d.memory);
|
||||
updateChart(chartTemp, labels, d.temp);
|
||||
updateChart(chartLoad, labels, d.load1);
|
||||
} catch(e) {
|
||||
console.error('Failed to load system metrics:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Range bar clicks
|
||||
document.getElementById('system-range-bar').addEventListener('click', function(e) {
|
||||
const btn = e.target.closest('.filter-btn');
|
||||
if (!btn) return;
|
||||
this.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
systemRange = btn.dataset.range;
|
||||
loadSystemMetrics();
|
||||
});
|
||||
|
||||
// --- Container bar charts ---
|
||||
let chartContainerCPU, chartContainerMem;
|
||||
let containerNames = [];
|
||||
|
||||
function initContainerCharts() {
|
||||
const barOpts = (xLabel) => ({
|
||||
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) {
|
||||
const 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 {
|
||||
const resp = await fetch('/api/metrics/containers/summary');
|
||||
const json = await resp.json();
|
||||
if (!json.ok || !json.data) return;
|
||||
|
||||
const 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(c => c.name);
|
||||
const cpuData = data.map(c => Math.round(c.cpu_percent * 100) / 100);
|
||||
const memData = data.map(c => Math.round(c.mem_usage_mb));
|
||||
|
||||
// Adjust bar chart height based on container count
|
||||
const h = Math.max(200, data.length * 35 + 60);
|
||||
document.querySelectorAll('.chart-wrap-bar').forEach(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 ---
|
||||
let detailChartCPU, detailChartMem;
|
||||
let detailContainer = '';
|
||||
let detailRange = '24h';
|
||||
|
||||
function initDetailCharts() {
|
||||
const mkChart = (id, color, yLabel) => {
|
||||
return new Chart(document.getElementById(id), {
|
||||
type: 'line',
|
||||
data: {labels: [], 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 {
|
||||
const resp = await fetch('/api/metrics/containers/' + encodeURIComponent(detailContainer) + '?range=' + detailRange + '&resolution=150');
|
||||
const json = await resp.json();
|
||||
if (!json.ok || !json.data) return;
|
||||
const d = json.data;
|
||||
const labels = (d.labels || []).map(ts => ts * 1000);
|
||||
|
||||
detailChartCPU.data.labels = labels;
|
||||
detailChartCPU.data.datasets[0].data = d.cpu || [];
|
||||
detailChartCPU.update('none');
|
||||
|
||||
detailChartMem.data.labels = labels;
|
||||
detailChartMem.data.datasets[0].data = d.memory || [];
|
||||
detailChartMem.update('none');
|
||||
} catch(e) {
|
||||
console.error('Failed to load container detail:', e);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('container-range-bar').addEventListener('click', function(e) {
|
||||
const btn = e.target.closest('.filter-btn');
|
||||
if (!btn) return;
|
||||
this.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
detailRange = btn.dataset.range;
|
||||
loadContainerDetail();
|
||||
});
|
||||
|
||||
// --- Static system info ---
|
||||
async function loadSysInfo() {
|
||||
try {
|
||||
const resp = await fetch('/api/metrics/sysinfo');
|
||||
const json = await resp.json();
|
||||
if (!json.ok || !json.data) return;
|
||||
const d = json.data;
|
||||
|
||||
document.getElementById('sysinfo-hostname').textContent = d.hostname || '–';
|
||||
document.getElementById('sysinfo-os').textContent = d.os || '–';
|
||||
document.getElementById('sysinfo-kernel').textContent = d.kernel || '–';
|
||||
|
||||
let 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) {
|
||||
const 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) {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const 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}}
|
||||
@@ -1477,6 +1477,125 @@ a.stat-card:hover {
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
/* --- Monitoring page --- */
|
||||
.monitor-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.25rem;
|
||||
border: 1px solid var(--border-color);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.monitor-card h3 {
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
.monitor-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.monitor-card-header h3 {
|
||||
margin-bottom: 0;
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
.time-range-bar {
|
||||
display: flex;
|
||||
gap: .35rem;
|
||||
}
|
||||
.sysinfo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: .5rem;
|
||||
}
|
||||
.sysinfo-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: .35rem .5rem;
|
||||
border-bottom: 1px solid rgba(48, 54, 61, 0.3);
|
||||
font-size: .9rem;
|
||||
}
|
||||
.sysinfo-row:last-child { border-bottom: none; }
|
||||
.sysinfo-label {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
.sysinfo-value {
|
||||
color: var(--text-primary);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: .85rem;
|
||||
}
|
||||
.charts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
.charts-grid-2 {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.chart-box {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
padding: .75rem;
|
||||
border: 1px solid rgba(48, 54, 61, 0.5);
|
||||
}
|
||||
.chart-box-half {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.chart-title {
|
||||
font-size: .8rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: .5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .3px;
|
||||
}
|
||||
.chart-wrap {
|
||||
position: relative;
|
||||
height: 180px;
|
||||
}
|
||||
.chart-wrap-bar {
|
||||
position: relative;
|
||||
height: 250px;
|
||||
}
|
||||
.container-charts-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
.chart-empty {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--text-muted);
|
||||
font-size: .9rem;
|
||||
}
|
||||
.storage-bars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
.storage-item {
|
||||
padding: .25rem 0;
|
||||
}
|
||||
.storage-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: .4rem;
|
||||
}
|
||||
.storage-label {
|
||||
font-size: .85rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.storage-value {
|
||||
font-size: .8rem;
|
||||
color: var(--text-muted);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media(max-width: 768px) {
|
||||
.sidebar { width: 100%; height: auto; position: relative; border-right: none; border-bottom: 1px solid var(--border-color); }
|
||||
@@ -1491,4 +1610,7 @@ a.stat-card:hover {
|
||||
.stats-grid { grid-template-columns: repeat(3, 1fr); }
|
||||
.deploy-info { flex-direction: column; }
|
||||
.system-info-items { flex-direction: column; gap: 1rem; }
|
||||
.charts-grid { grid-template-columns: 1fr; }
|
||||
.container-charts-row { flex-direction: column; }
|
||||
.sysinfo-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user