feat: umfangreiches UI+API-Polish (v1.1.36–1.1.42)
Backend: - Audit-Log: Search-Endpoint mit ILIKE-Filter (actor/action/subject/date) - NTP: /ntp/status via chronyc tracking (Stratum, Offset, Quelle) - System: /service-restart mit Allowlist (haproxy/squid/unbound/chrony/scheduler) - Domain-Response-Headers + Rate-Limit (Migration 0024) - Join-Tokens (Migration 0025), Cluster-mTLS, Aggregator-Fan-Out - apt-Service für Update-Banner (apt-get update + Versionsprüfung) - Backup-Retry mit exponential backoff (retry_apt 3×) - publish.sh fail-fast + cleanup-old.sh (max 10 Versionen) Frontend: - Audit-Log-Page (/audit) mit Filter + Pagination - ErrorBoundary an React-Root + Vite build-target festgenagelt (iOS 15+) - Storage-Schema-Stamp: auto-wipe bei Versions-Mismatch (blank-page-Fix) - EmptyState-Komponente überall ausgerollt - SSL: Aggregate-Karte (total/expiring/expired/errors) - Backups: Aggregate-Karte (letzter Backup/Größe/Fehlschläge 24h) + Backup-Now - NTP: Sync-Status-Karte (chronyc tracking live) - Domains: Backend-UP/DOWN-Chip aus HAProxy-Stats - Backends: HAProxy-Status-Spalte (UP/DEGRADED/DOWN) - Settings: Service-Neustart-Karte (haproxy/squid/unbound/chrony/scheduler) - Settings: Upgrade-Status-Card, Wartungsmodus, Auto-Update, Retention - Dashboard: Recent-Alerts, Cluster-Health, License-Chip, Onboarding-Hint - System-Regeln im Firewall als eigener Tab Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,15 +15,18 @@ import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/cluster"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/cluster/clustertls"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/database"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/license"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/acme"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/alerts"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/audit"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/backup"
|
||||
backupremote "git.netcell-it.de/projekte/edgeguard-native/internal/services/backup/remote"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/certrenewer"
|
||||
@@ -53,10 +56,47 @@ const (
|
||||
// Retention: 14 erfolgreiche Backups (default in backup.Service).
|
||||
backupTickInterval = 24 * time.Hour
|
||||
|
||||
// configHashTickInterval — alle 5 min config_hash neu berechnen
|
||||
// und in ha_nodes der eigenen Row schreiben. Cluster-UI nutzt
|
||||
// das fürs Drift-Banner — pro-Mutation-Refresh wäre teuer.
|
||||
configHashTickInterval = 5 * time.Minute
|
||||
// staleSweepTickInterval — Phase 3.2: alle 30s prüfen ob Peers
|
||||
// last_seen länger als staleThreshold nicht gemeldet haben →
|
||||
// status='offline'. Symmetrisch zum 30s-API-Heartbeat.
|
||||
staleSweepTickInterval = 30 * time.Second
|
||||
|
||||
// staleThreshold — Peer gilt als offline wenn last_seen älter als
|
||||
// das ist. 4× Heartbeat-Intervall lässt einen verpassten Tick
|
||||
// (Restart, GC-Pause, kurzer Network-Glitch) zu ohne false positive.
|
||||
staleThreshold = 2 * time.Minute
|
||||
|
||||
// clusterCertCheckInterval — täglicher Check ob CA + peer-cert
|
||||
// in den nächsten clusterCertWarnDays ablaufen. Bei Hit feuert
|
||||
// ein Alert (dedupe 12h damit der Operator nicht alle 24h dieselbe
|
||||
// Warnung sieht).
|
||||
clusterCertCheckInterval = 24 * time.Hour
|
||||
|
||||
// clusterCertWarnDays — Schwelle für die Cert-Expiry-Warnung.
|
||||
// Operator hat damit min. 30 Tage Vorlauf für `edgeguard-ctl
|
||||
// cluster-renew-self` oder einen manuellen Re-Join.
|
||||
clusterCertWarnDays = 30
|
||||
|
||||
// clusterCertAutoRenewDays — Schwelle ab der wir automatisch
|
||||
// neu signieren (nur Founder mit lokaler CA). Wir liegen 2× vor
|
||||
// der Warn-Schwelle damit ein verpasster Tick + ein verpasster
|
||||
// Restart-Window noch passen.
|
||||
clusterCertAutoRenewDays = 60
|
||||
|
||||
// diskCheckInterval — stündliche Disk-Usage-Prüfung. Fire-Schwellen
|
||||
// in runDiskCheck (warning 80%, error 90%). Stündlich ist schnell
|
||||
// genug damit der Operator vor /var = 100% noch Zeit zum Aufräumen
|
||||
// hat, ohne Log-Spam zu produzieren (Dedupe 12h pro Severity).
|
||||
diskCheckInterval = 1 * time.Hour
|
||||
|
||||
diskWarnPct = 80.0
|
||||
diskCriticalPct = 90.0
|
||||
|
||||
// auditCleanupInterval — täglicher Cleanup. Audit-Rows älter als
|
||||
// auditRetentionDays werden gelöscht. Idempotent — wenn nichts da
|
||||
// ist passiert nichts.
|
||||
auditCleanupInterval = 24 * time.Hour
|
||||
auditRetentionDays = 90
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -98,6 +138,7 @@ func main() {
|
||||
"dir", backupSvc.BackupDir, "keep_n", backup.DefaultKeepN)
|
||||
|
||||
alertSvc := alerts.New(pool)
|
||||
auditRepo := audit.New(pool)
|
||||
alertDedupe := newDedupe(12 * time.Hour)
|
||||
|
||||
if renewer != nil {
|
||||
@@ -105,13 +146,15 @@ func main() {
|
||||
}
|
||||
runLicenseVerify(ctx, licClient, licKeyStore, licRepo, nodeID, alertSvc, alertDedupe)
|
||||
|
||||
// Lokale Node-ID für config-hash-refresh. EnsureNodeID liefert
|
||||
// dieselbe ID die die API hat (gleiches /var/lib/edgeguard/node-id).
|
||||
// Lokale Node-ID für Heartbeat. EnsureNodeID liefert dieselbe ID
|
||||
// die die API hat (gleiches /var/lib/edgeguard/node-id).
|
||||
localID, _ := cluster.EnsureNodeID("")
|
||||
slog.Info("scheduler: config-hash refresh enabled", "tick", configHashTickInterval, "node_id", localID)
|
||||
// Initial-Refresh damit /cluster/status nach API+Scheduler-Boot
|
||||
// nicht 5min auf den ersten Wert wartet.
|
||||
runConfigHash(ctx, pool, localID)
|
||||
slog.Info("scheduler: stale-sweeper enabled",
|
||||
"tick", staleSweepTickInterval, "threshold", staleThreshold, "node_id", localID)
|
||||
// Initial-Sweep + initial-Heartbeat damit /cluster/status nach
|
||||
// Scheduler-Boot direkt einen frischen Zustand sieht.
|
||||
runHeartbeat(ctx, pool, localID, version)
|
||||
runStaleSweep(ctx, pool)
|
||||
|
||||
renewTick := time.NewTicker(renewTickInterval)
|
||||
defer renewTick.Stop()
|
||||
@@ -119,8 +162,23 @@ func main() {
|
||||
defer licTick.Stop()
|
||||
backupTick := time.NewTicker(backupTickInterval)
|
||||
defer backupTick.Stop()
|
||||
hashTick := time.NewTicker(configHashTickInterval)
|
||||
defer hashTick.Stop()
|
||||
sweepTick := time.NewTicker(staleSweepTickInterval)
|
||||
defer sweepTick.Stop()
|
||||
clusterCertTick := time.NewTicker(clusterCertCheckInterval)
|
||||
defer clusterCertTick.Stop()
|
||||
// Initial-Cert-Check direkt beim Start, sonst sieht der Operator
|
||||
// einen Warning erst nach 24h ab Boot.
|
||||
runClusterCertExpiryCheck(ctx, alertSvc, alertDedupe)
|
||||
|
||||
diskTick := time.NewTicker(diskCheckInterval)
|
||||
defer diskTick.Stop()
|
||||
// Initial-Disk-Check: wenn die Box schon bei 95% steht beim
|
||||
// Scheduler-Boot, wollen wir keine Stunde auf den ersten Alert
|
||||
// warten.
|
||||
runDiskCheck(ctx, alertSvc, alertDedupe)
|
||||
|
||||
auditTick := time.NewTicker(auditCleanupInterval)
|
||||
defer auditTick.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -132,13 +190,227 @@ func main() {
|
||||
case <-licTick.C:
|
||||
runLicenseVerify(ctx, licClient, licKeyStore, licRepo, nodeID, alertSvc, alertDedupe)
|
||||
case <-backupTick.C:
|
||||
runBackup(ctx, backupSvc, version, alertSvc)
|
||||
case <-hashTick.C:
|
||||
runConfigHash(ctx, pool, localID)
|
||||
runBackup(ctx, backupSvc, version, alertSvc, setupStore)
|
||||
case <-sweepTick.C:
|
||||
// Symmetrisches Heartbeat aus dem Scheduler — falls die API
|
||||
// pausiert/hängt, hält der Scheduler die eigene Row warm.
|
||||
// Idempotent zur API-Heartbeat-Goroutine.
|
||||
runHeartbeat(ctx, pool, localID, version)
|
||||
runStaleSweep(ctx, pool)
|
||||
case <-clusterCertTick.C:
|
||||
runClusterCertExpiryCheck(ctx, alertSvc, alertDedupe)
|
||||
case <-diskTick.C:
|
||||
runDiskCheck(ctx, alertSvc, alertDedupe)
|
||||
case <-auditTick.C:
|
||||
runAuditCleanup(ctx, auditRepo, setupStore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runAuditCleanup löscht audit_log-Rows älter als die konfigurierte
|
||||
// Retention. Operator kann den Wert in den Settings übersteuern; ohne
|
||||
// Setup-Custom fällt's auf auditRetentionDays-Default zurück.
|
||||
// Schutz vor unbounded growth — auf einer aktiven Box wird das Log
|
||||
// nach 1-2 Jahren mehrere GB groß und macht die /logs-Page langsam.
|
||||
// Best-effort: Fehler werden nur geloggt, der Tick läuft beim nächsten
|
||||
// Zyklus wieder.
|
||||
func runAuditCleanup(ctx context.Context, r *audit.Repo, setupStore *setup.Store) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
keepDays := auditRetentionDays
|
||||
if setupStore != nil {
|
||||
if st, err := setupStore.Load(); err == nil && st != nil && st.AuditRetentionDays > 0 {
|
||||
keepDays = st.AuditRetentionDays
|
||||
}
|
||||
}
|
||||
cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
n, err := r.Cleanup(cctx, keepDays)
|
||||
if err != nil {
|
||||
slog.Warn("scheduler: audit cleanup failed",
|
||||
"keep_days", keepDays, "error", err)
|
||||
return
|
||||
}
|
||||
if n > 0 {
|
||||
slog.Info("scheduler: audit cleanup",
|
||||
"deleted", n, "keep_days", keepDays)
|
||||
}
|
||||
}
|
||||
|
||||
// runDiskCheck prüft die Belegung von / via statfs. Fire-Schwellen:
|
||||
// - >= 90% → Critical (error). Box ist akut gefährdet — beim
|
||||
// nächsten Backup-Run oder größeren apt-Update droht "no space
|
||||
// left" und damit failed services.
|
||||
// - >= 80% → Warning. Operator hat noch Luft aber sollte aufräumen.
|
||||
// - < 80% → kein Alert.
|
||||
//
|
||||
// Dedupe-Keys pro Severity, damit ein lang-belegtes Filesystem nicht
|
||||
// jede Stunde feuert (12h pro Stufe). Bei Übergang warning→critical
|
||||
// gibt's einen frischen Alert weil die Keys verschieden sind.
|
||||
//
|
||||
// Fix-Hint im Body: was der Operator als Erstes prüfen soll
|
||||
// (/var/backups/edgeguard, /var/log/edgeguard, /var/cache/apt).
|
||||
func runDiskCheck(ctx context.Context, a *alerts.Service, d *dedupe) {
|
||||
if a == nil || d == nil {
|
||||
return
|
||||
}
|
||||
var fs syscall.Statfs_t
|
||||
if err := syscall.Statfs("/", &fs); err != nil {
|
||||
slog.Warn("scheduler: disk-check statfs failed", "error", err)
|
||||
return
|
||||
}
|
||||
total := float64(fs.Blocks) * float64(fs.Bsize)
|
||||
free := float64(fs.Bavail) * float64(fs.Bsize)
|
||||
if total <= 0 {
|
||||
return
|
||||
}
|
||||
usedPct := (total - free) * 100 / total
|
||||
freeGB := free / (1024 * 1024 * 1024)
|
||||
totalGB := total / (1024 * 1024 * 1024)
|
||||
|
||||
var key, title string
|
||||
var sev alerts.Severity
|
||||
switch {
|
||||
case usedPct >= diskCriticalPct:
|
||||
key = "disk.full.critical"
|
||||
sev = alerts.SeverityError
|
||||
title = fmt.Sprintf("Disk kritisch voll: %.0f%%", usedPct)
|
||||
case usedPct >= diskWarnPct:
|
||||
key = "disk.full.warning"
|
||||
sev = alerts.SeverityWarning
|
||||
title = fmt.Sprintf("Disk-Belegung hoch: %.0f%%", usedPct)
|
||||
default:
|
||||
return
|
||||
}
|
||||
if !d.shouldFire(key) {
|
||||
return
|
||||
}
|
||||
desc := fmt.Sprintf(
|
||||
"Wurzel-Filesystem (/) ist zu %.1f%% belegt — noch %.2f GB von %.2f GB frei.\n\n"+
|
||||
"Häufige Verursacher checken:\n"+
|
||||
" sudo du -hs /var/backups/edgeguard /var/log/edgeguard /var/cache/apt /var/lib/postgresql\n\n"+
|
||||
"Backup-Retention ist 14 (default). Manuell aufräumen:\n"+
|
||||
" ls -lhS /var/backups/edgeguard | head\n"+
|
||||
" sudo apt-get clean # /var/cache/apt leeren",
|
||||
usedPct, freeGB, totalGB)
|
||||
if _, err := a.Fire(ctx, "disk.full", sev, title, desc); err != nil {
|
||||
slog.Warn("scheduler: disk-check alert fire failed", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// runClusterCertExpiryCheck warnt wenn CA oder peer.crt in <
|
||||
// clusterCertWarnDays Tagen ablaufen (oder schon abgelaufen sind).
|
||||
// Dedupe pro Cert-Typ + 12h.
|
||||
//
|
||||
// Zusätzlich (Phase 1.1.1): wenn das peer.crt < clusterCertAutoRenewDays
|
||||
// remaining hat UND eine lokale CA existiert, wird automatisch neu
|
||||
// signiert. Restart-Hinweis als Info-Alert — wir starten edgeguard-api
|
||||
// nicht selbst neu, das passiert beim nächsten geplanten Update/Reboot.
|
||||
func runClusterCertExpiryCheck(ctx context.Context, a *alerts.Service, d *dedupe) {
|
||||
if a == nil || d == nil {
|
||||
return
|
||||
}
|
||||
store := clustertls.New("")
|
||||
|
||||
// Auto-Renew zuerst — danach lesen wir die (eventuell frischen)
|
||||
// Cert-Infos für den Alert-Check ab.
|
||||
tryAutoRenew(ctx, store, a, d)
|
||||
|
||||
check := func(kind, key string, info *clustertls.CertInfo, err error) {
|
||||
if err != nil {
|
||||
return // Cert nicht vorhanden / unleserlich — keine Warnung.
|
||||
}
|
||||
if info.DaysRemaining > clusterCertWarnDays {
|
||||
return
|
||||
}
|
||||
if !d.shouldFire(key) {
|
||||
return
|
||||
}
|
||||
sev := alerts.SeverityWarning
|
||||
title := "Cluster-" + kind + " läuft bald ab"
|
||||
desc := fmt.Sprintf("%s (CN=%s) läuft in %d Tagen ab (NotAfter=%s).",
|
||||
kind, info.CommonName, info.DaysRemaining, info.NotAfter.Format(time.RFC3339))
|
||||
if info.DaysRemaining < 0 {
|
||||
sev = alerts.SeverityError
|
||||
title = "Cluster-" + kind + " ist abgelaufen"
|
||||
desc = fmt.Sprintf("%s (CN=%s) ist seit %d Tagen abgelaufen (NotAfter=%s).",
|
||||
kind, info.CommonName, -info.DaysRemaining, info.NotAfter.Format(time.RFC3339))
|
||||
}
|
||||
desc += "\n\nFix: sudo edgeguard-ctl cluster-renew-self (founder/single-node)\n sudo systemctl restart edgeguard-api"
|
||||
if _, err := a.Fire(ctx, "cluster.cert.expiring", sev, title, desc); err != nil {
|
||||
slog.Warn("scheduler: cluster cert alert fire failed", "kind", kind, "error", err)
|
||||
}
|
||||
}
|
||||
if store.HasCA() {
|
||||
info, err := store.CACertInfo()
|
||||
check("CA", "cluster.cert.expiring:ca", info, err)
|
||||
}
|
||||
if store.HasPeer() {
|
||||
info, err := store.PeerCertInfo()
|
||||
check("peer-Cert", "cluster.cert.expiring:peer", info, err)
|
||||
}
|
||||
}
|
||||
|
||||
// tryAutoRenew: wenn das peer.crt < clusterCertAutoRenewDays Tage
|
||||
// remaining hat UND wir eine lokale CA haben (= Founder-Node), wird
|
||||
// automatisch ein frisches peer.{crt,key} signiert. Edgeguard-api
|
||||
// muss anschließend manuell restartet werden damit der Listener das
|
||||
// neue Material lädt — wir alarmieren das, restarten aber nicht
|
||||
// selbst (würde laufende Requests + die Heartbeat-Goroutine killen).
|
||||
//
|
||||
// Joiner-Nodes (keine eigene CA) ignorieren wir hier; sie laufen über
|
||||
// einen anderen Renewal-Pfad (Phase 3.6, Renewal-Token via mTLS).
|
||||
func tryAutoRenew(ctx context.Context, store *clustertls.Store, a *alerts.Service, d *dedupe) {
|
||||
if !store.HasPeer() || !store.HasCA() {
|
||||
return
|
||||
}
|
||||
info, err := store.PeerCertInfo()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if info.DaysRemaining > clusterCertAutoRenewDays {
|
||||
return
|
||||
}
|
||||
// CN aus dem alten Cert übernehmen — sonst würde ein Hostname-
|
||||
// Wechsel mitten in der Renewal unbemerkt durchgehen.
|
||||
cn := info.CommonName
|
||||
if cn == "" {
|
||||
cn = "edgeguard-node"
|
||||
}
|
||||
if err := store.RenewSelfSigned(cn, []string{cn}, nil, nil); err != nil {
|
||||
slog.Warn("scheduler: cluster cert auto-renew failed", "error", err)
|
||||
// Failure-Alert dedupe 12h — Operator soll daran erinnert werden.
|
||||
if d.shouldFire("cluster.cert.auto_renew.failed") {
|
||||
_, _ = a.Fire(ctx, "cluster.cert.auto_renew.failed",
|
||||
alerts.SeverityError,
|
||||
"Cluster-Peer-Cert Auto-Renew fehlgeschlagen",
|
||||
"clustertls.RenewSelfSigned: "+err.Error()+
|
||||
"\n\nFix: sudo edgeguard-ctl cluster-renew-self")
|
||||
}
|
||||
return
|
||||
}
|
||||
// Success — neuer Cert auf Disk, alter Cert noch im API-Speicher.
|
||||
// Info-Alert mit Restart-Hinweis. Dedupe 24h damit nicht
|
||||
// gefloodet wird wenn der Operator nicht restartet.
|
||||
if d.shouldFire("cluster.cert.auto_renew.ok") {
|
||||
fresh, _ := store.PeerCertInfo()
|
||||
until := info.NotAfter.Format(time.RFC3339)
|
||||
if fresh != nil {
|
||||
until = fresh.NotAfter.Format(time.RFC3339)
|
||||
}
|
||||
_, _ = a.Fire(ctx, "cluster.cert.auto_renewed",
|
||||
alerts.SeverityInfo,
|
||||
"Cluster-Peer-Cert automatisch erneuert",
|
||||
fmt.Sprintf("Neues Peer-Cert auf Disk (CN=%s, gültig bis %s). "+
|
||||
"Damit edgeguard-api das neue Cert in den mTLS-Listener lädt:\n\n"+
|
||||
" sudo systemctl restart edgeguard-api\n\n"+
|
||||
"Bis dahin nutzt der laufende Prozess das alte Cert.", cn, until))
|
||||
}
|
||||
slog.Info("scheduler: cluster peer cert auto-renewed", "cn", cn,
|
||||
"old_days_remaining", info.DaysRemaining)
|
||||
}
|
||||
|
||||
// dedupe verhindert dass derselbe Alert-Key (z.B. "cert.expiring:utm-1.netcell-it.de")
|
||||
// öfter als alle 12h gefeuert wird. In-memory — Scheduler-Restart
|
||||
// resettet, was OK ist (Operator soll bei restart wieder einen kennen-
|
||||
@@ -200,18 +472,38 @@ func runCertExpiryCheck(ctx context.Context, repo *tlscerts.Repo,
|
||||
}
|
||||
}
|
||||
|
||||
// runConfigHash berechnet den Hash und schreibt ihn in ha_nodes.
|
||||
// Pool kann nil sein (scheduler-pool-fail beim boot) — dann no-op.
|
||||
func runConfigHash(ctx context.Context, pool *pgxpoolPool, localID string) {
|
||||
// runHeartbeat schreibt last_seen + status=online + version + config_hash
|
||||
// auf die eigene ha_nodes-Row. Pool kann nil sein (scheduler-pool-fail
|
||||
// beim Boot) — dann no-op. Errors landen im WARN, kein Abort der Schleife.
|
||||
func runHeartbeat(ctx context.Context, pool *pgxpoolPool, localID, version string) {
|
||||
if pool == nil || localID == "" {
|
||||
return
|
||||
}
|
||||
hash, err := cluster.RefreshLocalHash(ctx, pool, localID)
|
||||
if err != nil {
|
||||
slog.Warn("scheduler: config-hash refresh failed", "error", err)
|
||||
hbCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
if err := cluster.Heartbeat(hbCtx, pool, localID, version); err != nil {
|
||||
slog.Warn("scheduler: heartbeat failed", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// runStaleSweep markiert Peers mit last_seen < NOW()-staleThreshold als
|
||||
// offline. Logged nur wenn Rows betroffen sind (sonst floodet das Log
|
||||
// mit "0 rows" alle 30s).
|
||||
func runStaleSweep(ctx context.Context, pool *pgxpoolPool) {
|
||||
if pool == nil {
|
||||
return
|
||||
}
|
||||
slog.Debug("scheduler: config-hash refreshed", "hash", hash)
|
||||
swCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
flipped, err := cluster.SweepStaleNodes(swCtx, pool, staleThreshold)
|
||||
if err != nil {
|
||||
slog.Warn("scheduler: stale-sweep failed", "error", err)
|
||||
return
|
||||
}
|
||||
if flipped > 0 {
|
||||
slog.Info("scheduler: marked stale peers offline",
|
||||
"count", flipped, "threshold", staleThreshold)
|
||||
}
|
||||
}
|
||||
|
||||
// pgxpoolPool ist ein lokaler Alias damit die Signatur stabil bleibt
|
||||
@@ -220,7 +512,7 @@ type pgxpoolPool = pgxpool.Pool
|
||||
|
||||
// runBackup führt einen scheduled Backup aus + prunet alte. Failures
|
||||
// loggen wir + alarmieren — verlorene Backups sind kritisch.
|
||||
func runBackup(ctx context.Context, svc *backup.Service, version string, a *alerts.Service) {
|
||||
func runBackup(ctx context.Context, svc *backup.Service, version string, a *alerts.Service, setupStore *setup.Store) {
|
||||
res, err := svc.Run(ctx, backup.KindScheduled, version)
|
||||
if err != nil {
|
||||
slog.Warn("scheduler: backup failed", "error", err, "file", res.File)
|
||||
@@ -235,7 +527,13 @@ func runBackup(ctx context.Context, svc *backup.Service, version string, a *aler
|
||||
"file", res.File, "size", res.SizeBytes,
|
||||
"db_bytes", res.DBDumpBytes, "files_bytes", res.FilesBytes,
|
||||
"sha256", res.SHA256)
|
||||
if err := svc.Prune(ctx, backup.DefaultKeepN); err != nil {
|
||||
keepN := backup.DefaultKeepN
|
||||
if setupStore != nil {
|
||||
if st, err := setupStore.Load(); err == nil && st != nil && st.BackupRetentionKeep > 0 {
|
||||
keepN = st.BackupRetentionKeep
|
||||
}
|
||||
}
|
||||
if err := svc.Prune(ctx, keepN); err != nil {
|
||||
slog.Warn("scheduler: backup prune failed", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user