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:
Debian
2026-05-19 16:18:41 +02:00
parent 3178e25e78
commit 35b7308ce2
82 changed files with 8408 additions and 392 deletions

View File

@@ -0,0 +1,72 @@
package cluster
// Phase-3.2: periodischer Heartbeat + Stale-Sweeper.
//
// Hintergrund: EnsureSelfRegistered schreibt last_seen einmal beim
// API-Boot. Ohne periodisches Re-Schreiben friert last_seen auf den
// Boot-Zeitpunkt ein — Peers (im Multi-Node-Setup) hätten keinen Weg
// zu erkennen ob dieser Node noch lebt. Die Heartbeat-Goroutine in der
// API bumpt das alle 30s; der Scheduler räumt mit SweepStaleNodes Peers
// die länger als <threshold> nicht gemeldet haben auf status='offline'.
//
// Single-Node-Effekt: Cluster-UI zeigt korrekt "last seen 12s" statt
// "last seen 3h" weil last_seen frisch ist. UI-Drift-Banner-Logik im
// ClusterHandler.Status nutzt die selben Felder.
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
// Heartbeat bumpt last_seen + status='online' für die eigene Node-Row
// und aktualisiert version + config_hash. Idempotent. UPDATE-only — die
// Row muss existieren (wird via EnsureSelfRegistered beim Boot angelegt).
//
// Hash-Berechnung läuft synchron — typisch <50ms auf einer realistischen
// DB-Größe; falls die compute-SQL fehlt (Migration im Flux) wird der
// vorhandene config_hash via COALESCE behalten.
func Heartbeat(ctx context.Context, pool *pgxpool.Pool, localID, version string) error {
if pool == nil || localID == "" {
return nil
}
hash, _ := ComputeConfigHash(ctx, pool)
_, err := pool.Exec(ctx, `
UPDATE ha_nodes SET
last_seen = NOW(),
status = 'online',
version = COALESCE(NULLIF($1, ''), version),
config_hash = COALESCE(NULLIF($2, ''), config_hash),
updated_at = NOW()
WHERE id = $3`, version, hash, localID)
return err
}
// SweepStaleNodes flippt status='online' → 'offline' für Peers deren
// last_seen älter als threshold ist. Liefert die Anzahl gefliptpter
// Rows zurück (für Logging). Idempotent — markiert keine Rows die
// schon offline sind.
//
// Threshold-Konvention: 4× Heartbeat-Intervall = 2 min bei 30s-Tick.
// Lässt Platz für eine verpasste API-Tick (Restart, GC-Pause, kurzer
// Network-Glitch) ohne false-positive Offline.
func SweepStaleNodes(ctx context.Context, pool *pgxpool.Pool, threshold time.Duration) (int64, error) {
if pool == nil || threshold <= 0 {
return 0, nil
}
// Wir bauen das Interval als String — pgx kann time.Duration nicht
// direkt als INTERVAL serialisieren.
interval := fmt.Sprintf("%d seconds", int(threshold.Seconds()))
tag, err := pool.Exec(ctx, `
UPDATE ha_nodes SET
status = 'offline',
updated_at = NOW()
WHERE last_seen < NOW() - $1::interval
AND status = 'online'`, interval)
if err != nil {
return 0, err
}
return tag.RowsAffected(), nil
}