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:
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -8,6 +9,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
catalogsync "gitea.dooplex.hu/admin/felhom-controller/internal/sync"
|
||||
@@ -16,14 +18,16 @@ import (
|
||||
|
||||
// Router handles all /api/* requests.
|
||||
type Router struct {
|
||||
cfg *config.Config
|
||||
stackMgr *stacks.Manager
|
||||
syncer *catalogsync.Syncer
|
||||
logger *log.Logger
|
||||
cfg *config.Config
|
||||
stackMgr *stacks.Manager
|
||||
syncer *catalogsync.Syncer
|
||||
cpuCollector *system.CPUCollector
|
||||
backupMgr *backup.Manager
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func NewRouter(cfg *config.Config, stackMgr *stacks.Manager, syncer *catalogsync.Syncer, logger *log.Logger) *Router {
|
||||
return &Router{cfg: cfg, stackMgr: stackMgr, syncer: syncer, logger: logger}
|
||||
func NewRouter(cfg *config.Config, stackMgr *stacks.Manager, syncer *catalogsync.Syncer, cpuCollector *system.CPUCollector, backupMgr *backup.Manager, logger *log.Logger) *Router {
|
||||
return &Router{cfg: cfg, stackMgr: stackMgr, syncer: syncer, cpuCollector: cpuCollector, backupMgr: backupMgr, logger: logger}
|
||||
}
|
||||
|
||||
type apiResponse struct {
|
||||
@@ -99,6 +103,14 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
case path == "/system/info" && req.Method == http.MethodGet:
|
||||
r.systemInfo(w, req)
|
||||
|
||||
// GET /api/backup/status
|
||||
case path == "/backup/status" && req.Method == http.MethodGet:
|
||||
r.backupStatus(w, req)
|
||||
|
||||
// POST /api/backup/run
|
||||
case path == "/backup/run" && req.Method == http.MethodPost:
|
||||
r.triggerBackup(w, req)
|
||||
|
||||
default:
|
||||
writeJSON(w, http.StatusNotFound, apiResponse{OK: false, Error: "endpoint not found"})
|
||||
}
|
||||
@@ -309,7 +321,7 @@ func (r *Router) triggerSync(w http.ResponseWriter, _ *http.Request) {
|
||||
}
|
||||
|
||||
func (r *Router) systemInfo(w http.ResponseWriter, _ *http.Request) {
|
||||
info := system.GetInfo(r.cfg.Paths.HDDPath)
|
||||
info := system.GetInfo(r.cfg.Paths.HDDPath, r.cpuCollector)
|
||||
syncStatus := r.syncer.Status()
|
||||
data := map[string]interface{}{
|
||||
"system": info,
|
||||
@@ -318,6 +330,69 @@ func (r *Router) systemInfo(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: data})
|
||||
}
|
||||
|
||||
// --- Backup handlers ---
|
||||
|
||||
func (r *Router) backupStatus(w http.ResponseWriter, _ *http.Request) {
|
||||
if r.backupMgr == nil {
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: map[string]interface{}{
|
||||
"enabled": false,
|
||||
}})
|
||||
return
|
||||
}
|
||||
|
||||
dbDump, backupSt := r.backupMgr.GetStatus()
|
||||
data := map[string]interface{}{
|
||||
"enabled": true,
|
||||
"running": r.backupMgr.IsRunning(),
|
||||
}
|
||||
|
||||
if dbDump != nil {
|
||||
data["db_dump"] = map[string]interface{}{
|
||||
"last_run": dbDump.LastRun,
|
||||
"success": dbDump.Success,
|
||||
"duration": dbDump.Duration.String(),
|
||||
"count": len(dbDump.Results),
|
||||
}
|
||||
}
|
||||
|
||||
if backupSt != nil {
|
||||
backupData := map[string]interface{}{
|
||||
"last_run": backupSt.LastRun,
|
||||
"success": backupSt.Success,
|
||||
"duration": backupSt.Duration.String(),
|
||||
}
|
||||
if backupSt.Snapshot != nil {
|
||||
backupData["snapshot_id"] = backupSt.Snapshot.SnapshotID
|
||||
backupData["files_new"] = backupSt.Snapshot.FilesNew
|
||||
backupData["data_added"] = backupSt.Snapshot.DataAdded
|
||||
}
|
||||
if backupSt.RepoStats != nil {
|
||||
backupData["repo_size"] = backupSt.RepoStats.TotalSize
|
||||
backupData["snapshot_count"] = backupSt.RepoStats.SnapshotCount
|
||||
}
|
||||
data["backup"] = backupData
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: data})
|
||||
}
|
||||
|
||||
func (r *Router) triggerBackup(w http.ResponseWriter, _ *http.Request) {
|
||||
if r.backupMgr == nil {
|
||||
writeJSON(w, http.StatusBadRequest, apiResponse{OK: false, Error: "Backup not configured"})
|
||||
return
|
||||
}
|
||||
|
||||
if r.backupMgr.IsRunning() {
|
||||
writeJSON(w, http.StatusConflict, apiResponse{OK: false, Error: "Mentés már folyamatban"})
|
||||
return
|
||||
}
|
||||
|
||||
r.logger.Println("[API] Manual backup triggered")
|
||||
go r.backupMgr.RunFullBackup(context.Background())
|
||||
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: "Mentés elindítva"})
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
func hasSuffix(path, suffix string) bool { return strings.HasSuffix(path, suffix) }
|
||||
@@ -342,4 +417,4 @@ func writeJSON(w http.ResponseWriter, status int, v interface{}) {
|
||||
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||||
log.Printf("[ERROR] Failed to write JSON response: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user