262 lines
10 KiB
HTML
262 lines
10 KiB
HTML
{% extends "base.html" %}
|
|
{% block content %}
|
|
|
|
<div class="metrics-row">
|
|
<div class="metric-card cpu">
|
|
<div class="metric-top">
|
|
<div class="metric-label">CPU USAGE</div>
|
|
<div class="metric-icon"><i class="fas fa-microchip"></i></div>
|
|
</div>
|
|
<div class="metric-value" id="m-cpu">{{ system.cpu_pct or '…' }}<span>%</span></div>
|
|
<div class="gauge-bar"><div class="gauge-fill" id="g-cpu" style="width:{{ system.cpu_pct or 0 }}%"></div></div>
|
|
</div>
|
|
<div class="metric-card mem">
|
|
<div class="metric-top">
|
|
<div class="metric-label">MEMORY</div>
|
|
<div class="metric-icon"><i class="fas fa-memory"></i></div>
|
|
</div>
|
|
<div class="metric-value small" id="m-mem">{{ system.memory or '…' }}</div>
|
|
<div class="gauge-bar"><div class="gauge-fill" id="g-mem" style="width:{{ system.mem_pct or 0 }}%"></div></div>
|
|
</div>
|
|
<div class="metric-card disk">
|
|
<div class="metric-top">
|
|
<div class="metric-label">DISK /</div>
|
|
<div class="metric-icon"><i class="fas fa-hard-drive"></i></div>
|
|
</div>
|
|
<div class="metric-value small" id="m-disk">{{ system.disk or '…' }}</div>
|
|
<div class="gauge-bar"><div class="gauge-fill" id="g-disk" style="width:{{ system.disk_pct or 0 }}%"></div></div>
|
|
</div>
|
|
<div class="metric-card load">
|
|
<div class="metric-top">
|
|
<div class="metric-label">LOAD AVG</div>
|
|
<div class="metric-icon"><i class="fas fa-gauge-high"></i></div>
|
|
</div>
|
|
<div class="metric-value small" id="m-load">{{ system.load or '…' }}</div>
|
|
<div class="gauge-bar"><div class="gauge-fill" id="g-load" style="width:10%"></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="card-title"><i class="fas fa-chart-line"></i> Overview</div>
|
|
<span class="card-meta" id="overview-meta">Docker {{ system.docker_v or '…' }} · {{ main_server }}</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<div class="stat-card"><div class="stat-number" id="stat-total">{{ container_count }}</div><div class="stat-label">All Containers</div></div>
|
|
<div class="stat-card"><div class="stat-number" id="stat-running" style="color:var(--green);">{{ running_count }}</div><div class="stat-label">Running</div></div>
|
|
<div class="stat-card"><div class="stat-number" id="stat-stopped" style="color:var(--red);">{{ container_count - running_count }}</div><div class="stat-label">Stopped</div></div>
|
|
<div class="stat-card"><div class="stat-number" id="stat-sites" style="color:var(--purple);">{{ site_count }}</div><div class="stat-label">App Sites</div></div>
|
|
<div class="stat-card"><div class="stat-number" id="stat-users">{{ users|length }}</div><div class="stat-label">Linux Users</div></div>
|
|
<div class="stat-card"><div class="stat-number" id="stat-local-bk">{{ backups|length }}</div><div class="stat-label">Local Backups</div></div>
|
|
<div class="stat-card"><div class="stat-number" id="stat-vm-bk">{{ vm_backups|length }}</div><div class="stat-label">VM Backups</div></div>
|
|
<div class="stat-card" style="cursor:pointer;" onclick="window.location='/cloud'">
|
|
<div class="stat-number" id="stat-cloud-bk" style="color:var(--accent2);">—</div>
|
|
<div class="stat-label">☁ Cloud Backups</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dash-charts">
|
|
<div class="dash-chart-card">
|
|
<h3><i class="fas fa-cubes"></i> Containers</h3>
|
|
<div class="dash-chart-wrap"><canvas id="chart-containers"></canvas></div>
|
|
<div class="dash-chart-legend" id="legend-containers"></div>
|
|
<div class="dash-quick-links">
|
|
<a href="/containers" class="btn btn-ghost btn-sm"><i class="fas fa-layer-group"></i> All Containers</a>
|
|
</div>
|
|
</div>
|
|
<div class="dash-chart-card">
|
|
<h3><i class="fas fa-globe"></i> Application Sites</h3>
|
|
<div class="dash-chart-wrap"><canvas id="chart-sites"></canvas></div>
|
|
<div class="dash-chart-legend" id="legend-sites"></div>
|
|
<div class="dash-quick-links">
|
|
<a href="/sites" class="btn btn-ghost btn-sm"><i class="fas fa-arrow-up-right-from-square"></i> Open Sites</a>
|
|
</div>
|
|
</div>
|
|
<div class="dash-chart-card">
|
|
<h3><i class="fas fa-database"></i> Backups & Users</h3>
|
|
<div class="dash-chart-wrap"><canvas id="chart-ops"></canvas></div>
|
|
<div class="dash-chart-legend" id="legend-ops"></div>
|
|
<div class="dash-quick-links">
|
|
<a href="/backups" class="btn btn-ghost btn-sm"><i class="fas fa-database"></i> Backups</a>
|
|
<a href="/users" class="btn btn-ghost btn-sm"><i class="fas fa-users"></i> Users</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
|
<script>
|
|
(function () {
|
|
const charts = {};
|
|
const isDark = () => (document.documentElement.getAttribute('data-theme') || 'dark') === 'dark';
|
|
const textColor = () => isDark() ? '#8892a4' : '#4a5578';
|
|
const gridColor = () => isDark() ? '#1e2330' : '#dde1ed';
|
|
|
|
function chartDefaults() {
|
|
Chart.defaults.color = textColor();
|
|
Chart.defaults.borderColor = gridColor();
|
|
}
|
|
|
|
function makeDonut(canvasId, labels, values, colors) {
|
|
const canvas = document.getElementById(canvasId);
|
|
if (!canvas || typeof Chart === 'undefined') return null;
|
|
if (charts[canvasId]) {
|
|
charts[canvasId].destroy();
|
|
}
|
|
charts[canvasId] = new Chart(canvas, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels,
|
|
datasets: [{
|
|
data: values,
|
|
backgroundColor: colors,
|
|
borderWidth: 2,
|
|
borderColor: isDark() ? '#0e1017' : '#ffffff',
|
|
hoverOffset: 8,
|
|
hoverBorderWidth: 0,
|
|
}],
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
cutout: '62%',
|
|
plugins: {
|
|
legend: { display: false },
|
|
},
|
|
},
|
|
});
|
|
return charts[canvasId];
|
|
}
|
|
|
|
function renderLegend(elId, items) {
|
|
const el = document.getElementById(elId);
|
|
if (!el) return;
|
|
el.innerHTML = items.map(i =>
|
|
`<span><span class="dot" style="background:${i.color}"></span>${i.label}: ${i.value}</span>`
|
|
).join('');
|
|
}
|
|
|
|
function applySystem(s) {
|
|
if (!s) return;
|
|
const cpu = parseFloat(s.cpu_pct) || 0;
|
|
const set = (id, v) => { const n = document.getElementById(id); if (n) n.textContent = v; };
|
|
const mCpu = document.getElementById('m-cpu');
|
|
if (mCpu) mCpu.innerHTML = `${cpu.toFixed(1)}<span>%</span>`;
|
|
set('m-mem', s.memory || '—');
|
|
set('m-disk', s.disk || '—');
|
|
set('m-load', s.load || '—');
|
|
const gCpu = document.getElementById('g-cpu');
|
|
const gMem = document.getElementById('g-mem');
|
|
const gDisk = document.getElementById('g-disk');
|
|
if (gCpu) gCpu.style.width = `${Math.min(cpu, 100)}%`;
|
|
if (gMem) gMem.style.width = `${Math.min(parseFloat(s.mem_pct) || 0, 100)}%`;
|
|
if (gDisk) gDisk.style.width = `${Math.min(parseFloat(s.disk_pct) || 0, 100)}%`;
|
|
if (s.docker_v) {
|
|
const meta = document.getElementById('overview-meta');
|
|
if (meta) meta.textContent = `Docker ${s.docker_v} · {{ main_server }}`;
|
|
}
|
|
}
|
|
|
|
function applyStats(d) {
|
|
const set = (id, val) => {
|
|
const el = document.getElementById(id);
|
|
if (el && val !== undefined && val !== null) el.textContent = val;
|
|
};
|
|
set('stat-total', d.container_count);
|
|
set('stat-running', d.running_count);
|
|
set('stat-stopped', d.stopped_count);
|
|
set('stat-sites', d.sites_count);
|
|
set('stat-users', d.user_count);
|
|
set('stat-local-bk', d.local_backups);
|
|
set('stat-vm-bk', d.vm_backups);
|
|
|
|
chartDefaults();
|
|
const running = d.running_count || 0;
|
|
const stopped = d.stopped_count || 0;
|
|
makeDonut('chart-containers', ['Running', 'Stopped'], [running, stopped], ['#22c55e', '#ef4444']);
|
|
renderLegend('legend-containers', [
|
|
{ label: 'Running', value: running, color: '#22c55e' },
|
|
{ label: 'Stopped', value: stopped, color: '#ef4444' },
|
|
]);
|
|
|
|
const local = d.local_backups || 0;
|
|
const vm = d.vm_backups || 0;
|
|
const users = d.user_count || 0;
|
|
makeDonut('chart-ops', ['Local', 'VM', 'Users'], [local, vm, users], ['#3b82f6', '#a78bfa', '#22d3ee']);
|
|
renderLegend('legend-ops', [
|
|
{ label: 'Local backups', value: local, color: '#3b82f6' },
|
|
{ label: 'VM backups', value: vm, color: '#a78bfa' },
|
|
{ label: 'Linux users', value: users, color: '#22d3ee' },
|
|
]);
|
|
}
|
|
|
|
function applySitesChart(sites) {
|
|
if (!sites || !sites.length) {
|
|
makeDonut('chart-sites', ['No data'], [1], ['#4a5568']);
|
|
renderLegend('legend-sites', [{ label: 'Sites', value: 0, color: '#4a5568' }]);
|
|
return;
|
|
}
|
|
let healthy = 0, unhealthy = 0, offline = 0;
|
|
sites.forEach(s => {
|
|
if (!s.health) return;
|
|
if (s.health.status === 'up') healthy++;
|
|
else if (s.health.status === 'offline') offline++;
|
|
else unhealthy++;
|
|
});
|
|
const unchecked = sites.length - healthy - unhealthy - offline;
|
|
const labels = ['Healthy', 'Unhealthy', 'Offline'];
|
|
const values = [healthy, unhealthy, offline];
|
|
const colors = ['#22c55e', '#ef4444', '#6b7280'];
|
|
if (unchecked > 0) {
|
|
labels.push('Pending');
|
|
values.push(unchecked);
|
|
colors.push('#3b82f6');
|
|
}
|
|
makeDonut('chart-sites', labels, values, colors);
|
|
renderLegend('legend-sites', labels.map((l, i) => ({
|
|
label: l, value: values[i], color: colors[i],
|
|
})));
|
|
const el = document.getElementById('stat-sites-healthy');
|
|
if (el) el.textContent = healthy;
|
|
}
|
|
|
|
async function loadCloudCount() {
|
|
try {
|
|
const res = await fetch('/api/cloud/r2/stats');
|
|
const data = await res.json();
|
|
const el = document.getElementById('stat-cloud-bk');
|
|
if (el) el.textContent = data.count ?? '—';
|
|
} catch (_) {}
|
|
}
|
|
|
|
async function loadDashboardAsync() {
|
|
chartDefaults();
|
|
try {
|
|
const [dashRes, sitesRes] = await Promise.all([
|
|
fetch('/api/dashboard'),
|
|
fetch('/api/sites?health=1'),
|
|
]);
|
|
if (!dashRes.ok) throw new Error(`HTTP ${dashRes.status}`);
|
|
const data = await dashRes.json();
|
|
applySystem(data.system);
|
|
applyStats(data);
|
|
if (sitesRes.ok) {
|
|
const sitesData = await sitesRes.json();
|
|
applySitesChart(sitesData.sites || []);
|
|
}
|
|
} catch (err) {
|
|
console.error('[dashboard] load failed:', err);
|
|
}
|
|
loadCloudCount();
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', loadDashboardAsync);
|
|
} else {
|
|
loadDashboardAsync();
|
|
}
|
|
setInterval(loadDashboardAsync, 60000);
|
|
})();
|
|
</script>
|
|
{% endblock %}
|