v0.4.0: monitoring & backup — scheduler, CPU/temp metrics, healthchecks, restic backups

Phase 2 (Monitoring & Health):
- Central job scheduler replacing ad-hoc goroutines (internal/scheduler)
- CPU usage collector via /proc/stat background sampling (internal/system/cpu_linux.go)
- Temperature reading from /sys/class/thermal + /host/sys (Docker mount)
- Load average from /proc/loadavg
- Healthchecks.io-compatible HTTP pinger (internal/monitor/pinger.go)
- System health checks: disk, memory, CPU, temp, Docker, protected containers (internal/monitor/healthcheck.go)

Phase 3 (Backups):
- Database auto-discovery via docker ps + docker inspect (internal/backup/dbdump.go)
- Database dumping via docker exec (pg_dump / mariadb-dump) with atomic writes
- Restic backup integration with auto-password generation (internal/backup/restic.go)
- Backup orchestrator: DB dumps + restic snapshots + weekly prune (internal/backup/backup.go)
- Manual backup trigger via dashboard button and POST /api/backup/run

Dashboard UI:
- CPU usage bar with load average display
- Temperature with colored indicator dot
- Backup status card with last run time, DB count, repo stats
- "Mentés most" button for manual backup trigger

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 11:17:10 +01:00
parent 8a988c5998
commit d32d9fb44b
21 changed files with 2060 additions and 82 deletions
@@ -33,6 +33,29 @@
<div class="system-bar-fill system-bar-{{usageColor .SystemInfo.MemPercent}}" style="width:{{printf "%.0f" .SystemInfo.MemPercent}}%"></div>
</div>
</div>
<div class="system-info-item">
<div class="system-info-header">
<span class="system-info-label">CPU</span>
<span class="system-info-value">{{printf "%.0f" .SystemInfo.CPUPercent}}%</span>
</div>
<div class="system-bar">
<div class="system-bar-fill system-bar-{{usageColor .SystemInfo.CPUPercent}}" style="width:{{printf "%.0f" .SystemInfo.CPUPercent}}%"></div>
</div>
<div class="system-load-avg">Load: {{fmtLoad .SystemInfo.LoadAvg1}} / {{fmtLoad .SystemInfo.LoadAvg5}} / {{fmtLoad .SystemInfo.LoadAvg15}}</div>
</div>
{{if .SystemInfo.TemperatureCelsius}}
<div class="system-info-item system-info-item-compact">
<div class="system-info-header">
<span class="system-info-label">Hőmérséklet</span>
<span class="system-info-value">
<span class="temp-dot temp-dot-{{tempColor .SystemInfo.TemperatureCelsius}}"></span>
{{fmtTemp .SystemInfo.TemperatureCelsius}}
</span>
</div>
</div>
{{end}}
</div>
<div class="system-info-items" style="margin-top: 1rem;">
<div class="system-info-item">
<div class="system-info-header">
<span class="system-info-label">SSD tárhely</span>
@@ -57,6 +80,46 @@
</div>
{{end}}
{{if .BackupEnabled}}
<div class="backup-status-card">
<h3>Biztonsági mentés</h3>
{{if .BackupStatus}}
<div class="backup-info-row">
<span class="backup-label">Utolsó mentés:</span>
<span class="backup-value">
{{if .BackupStatus.Success}}
<span class="backup-status-ok">{{.BackupStatus.LastRun.Format "2006-01-02 15:04"}}</span>
{{else}}
<span class="backup-status-fail">Sikertelen</span>
{{end}}
</span>
</div>
{{else}}
<div class="backup-info-row">
<span class="backup-label">Utolsó mentés:</span>
<span class="backup-value backup-status-none">Még nem futott</span>
</div>
{{end}}
{{if .DBDumpStatus}}
<div class="backup-info-row">
<span class="backup-label">Adatbázisok:</span>
<span class="backup-value">{{len .DBDumpStatus.Results}} mentve</span>
</div>
{{end}}
{{if .BackupStatus}}{{if .BackupStatus.RepoStats}}
<div class="backup-info-row">
<span class="backup-label">Tároló méret:</span>
<span class="backup-value">{{.BackupStatus.RepoStats.TotalSize}} ({{.BackupStatus.RepoStats.SnapshotCount}} pillanatkép)</span>
</div>
{{end}}{{end}}
<div class="backup-actions" style="margin-top: .75rem;">
<button class="btn btn-sm btn-primary" onclick="triggerBackup()" id="backup-btn">
{{if .BackupRunning}}Mentés folyamatban...{{else}}Mentés most{{end}}
</button>
</div>
</div>
{{end}}
<h3>Alkalmazások állapota</h3>
<div class="stack-list">
@@ -97,5 +160,28 @@
{{end}}
</div>
<script>
function triggerBackup() {
const btn = document.getElementById('backup-btn');
btn.disabled = true;
btn.textContent = 'Mentés indítása...';
fetch('/api/backup/run', { method: 'POST' })
.then(r => r.json())
.then(data => {
if (data.ok) {
btn.textContent = 'Mentés folyamatban...';
btn.classList.add('loading');
} else {
btn.textContent = data.error || 'Hiba';
btn.disabled = false;
}
})
.catch(() => {
btn.textContent = 'Hiba';
btn.disabled = false;
});
}
</script>
{{template "layout_end" .}}
{{end}}
@@ -1122,6 +1122,62 @@ select.form-control option { background: var(--bg-secondary); color: var(--text-
justify-content: flex-end;
}
/* Load average text */
.system-load-avg {
font-size: .7rem;
color: var(--text-muted);
font-family: 'JetBrains Mono', monospace;
margin-top: .25rem;
}
/* Temperature dot */
.temp-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: .25rem;
vertical-align: middle;
}
.temp-dot-green { background: var(--green); box-shadow: 0 0 4px rgba(35, 134, 54, 0.5); }
.temp-dot-yellow { background: var(--yellow); box-shadow: 0 0 4px rgba(210, 153, 34, 0.5); }
.temp-dot-red { background: var(--red); box-shadow: 0 0 4px rgba(218, 54, 51, 0.5); }
.system-info-item-compact {
flex: 0 1 auto;
min-width: 120px;
}
/* Backup status card */
.backup-status-card {
background: var(--bg-card);
border-radius: var(--radius);
padding: 1rem 1.25rem;
border: 1px solid var(--border-color);
margin-bottom: 2rem;
}
.backup-status-card h3 {
margin-bottom: .75rem;
}
.backup-info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: .25rem 0;
font-size: .85rem;
}
.backup-label {
color: var(--text-secondary);
}
.backup-value {
color: var(--text-primary);
font-family: 'JetBrains Mono', monospace;
font-size: .8rem;
}
.backup-status-ok { color: var(--green); }
.backup-status-fail { color: var(--red); }
.backup-status-none { color: var(--text-muted); }
/* Responsive */
@media(max-width: 768px) {
.sidebar { width: 100%; height: auto; position: relative; border-right: none; border-bottom: 1px solid var(--border-color); }