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:
2026-02-16 10:14:46 +01:00
parent 87e79548b0
commit 3e8baebfa5
19 changed files with 1691 additions and 5 deletions
+110
View File
@@ -0,0 +1,110 @@
//go:build linux
package metrics
import (
"bufio"
"fmt"
"os"
"runtime"
"strings"
"time"
)
// GetStaticInfo reads host-level static system information.
// Reads from /proc and /etc (which reflect the host in Docker containers).
func GetStaticInfo() StaticSystemInfo {
info := StaticSystemInfo{}
// Hostname
info.Hostname, _ = os.Hostname()
// OS — try host mount first, fall back to container's
info.OS = readOSRelease("/host/etc/os-release")
if info.OS == "" {
info.OS = readOSRelease("/etc/os-release")
}
// Kernel version
if data, err := os.ReadFile("/proc/sys/kernel/osrelease"); err == nil {
info.Kernel = strings.TrimSpace(string(data))
}
// Architecture
if data, err := os.ReadFile("/proc/sys/kernel/arch"); err == nil {
info.Architecture = strings.TrimSpace(string(data))
}
if info.Architecture == "" {
// Fallback: use uname -m equivalent from /proc/cpuinfo or runtime
info.Architecture = runtime.GOARCH
// Try to get the actual host arch
if data, err := os.ReadFile("/proc/version"); err == nil {
v := string(data)
if strings.Contains(v, "x86_64") {
info.Architecture = "x86_64"
} else if strings.Contains(v, "aarch64") {
info.Architecture = "aarch64"
}
}
}
// CPU model and cores
info.CPUModel, info.CPUCores = readCPUInfo()
if info.CPUCores == 0 {
info.CPUCores = runtime.NumCPU()
}
// Uptime
if data, err := os.ReadFile("/proc/uptime"); err == nil {
var uptimeSec float64
if _, err := fmt.Sscanf(string(data), "%f", &uptimeSec); err == nil {
info.UptimeSeconds = int64(uptimeSec)
info.BootTime = time.Now().Add(-time.Duration(info.UptimeSeconds) * time.Second)
}
}
return info
}
// readOSRelease reads PRETTY_NAME from an os-release file.
func readOSRelease(path string) string {
f, err := os.Open(path)
if err != nil {
return ""
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "PRETTY_NAME=") {
val := strings.TrimPrefix(line, "PRETTY_NAME=")
val = strings.Trim(val, `"`)
return val
}
}
return ""
}
// readCPUInfo reads the CPU model name and number of cores from /proc/cpuinfo.
func readCPUInfo() (model string, cores int) {
f, err := os.Open("/proc/cpuinfo")
if err != nil {
return "", 0
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "model name") {
if idx := strings.Index(line, ":"); idx >= 0 {
model = strings.TrimSpace(line[idx+1:])
}
}
if strings.HasPrefix(line, "processor") {
cores++
}
}
return model, cores
}