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

@@ -3,7 +3,10 @@ package handlers
import (
"context"
"errors"
"fmt"
"log/slog"
"os/exec"
"strings"
"github.com/gin-gonic/gin"
@@ -37,6 +40,7 @@ func (h *NTPHandler) Register(rg *gin.RouterGroup) {
g := rg.Group("/ntp")
g.GET("/settings", h.GetSettings)
g.PUT("/settings", h.UpdateSettings)
g.GET("/status", h.Status)
p := g.Group("/pools")
p.GET("", h.ListPools)
@@ -46,6 +50,74 @@ func (h *NTPHandler) Register(rg *gin.RouterGroup) {
p.DELETE("/:id", h.DeletePool)
}
// Status liefert den aktuellen chrony-Tracking-Status via `chronyc tracking`.
// Output-Felder: synced (bool), reference (Quell-Server), stratum (int),
// offset_ms (float), freq_ppm (float), rms_offset_ms (float), error (string
// wenn chronyc nicht verfügbar oder System-Takt nicht sync'd).
func (h *NTPHandler) Status(c *gin.Context) {
out, err := exec.Command("chronyc", "tracking").Output()
if err != nil {
response.OK(c, gin.H{
"synced": false,
"error": "chronyc nicht verfügbar: " + err.Error(),
})
return
}
resp := parseChronyTracking(string(out))
response.OK(c, resp)
}
type chronyStatus struct {
Synced bool `json:"synced"`
Reference string `json:"reference"`
Stratum int `json:"stratum"`
OffsetMs float64 `json:"offset_ms"`
FreqPPM float64 `json:"freq_ppm"`
RMSOffsetMs float64 `json:"rms_offset_ms"`
Error string `json:"error,omitempty"`
}
func parseChronyTracking(out string) chronyStatus {
s := chronyStatus{}
for _, line := range strings.Split(out, "\n") {
line = strings.TrimSpace(line)
key, val, ok := strings.Cut(line, ":")
if !ok {
continue
}
key = strings.TrimSpace(key)
val = strings.TrimSpace(val)
switch key {
case "Reference ID":
// "Reference ID : A29FC801 (time.cloudflare.com)"
if i := strings.Index(val, "("); i >= 0 {
s.Reference = strings.Trim(val[i:], "()")
} else {
s.Reference = val
}
s.Synced = val != "00000000 ()"
case "Stratum":
fmt.Sscanf(val, "%d", &s.Stratum)
if s.Stratum > 0 && s.Stratum < 16 {
s.Synced = true
}
case "System time":
// "0.000012345 seconds fast of NTP time"
var v float64
fmt.Sscanf(val, "%f", &v)
s.OffsetMs = v * 1000
case "Frequency":
// "-12.345 ppm slow" or "+12.345 ppm fast"
fmt.Sscanf(val, "%f", &s.FreqPPM)
case "RMS offset":
var v float64
fmt.Sscanf(val, "%f", &v)
s.RMSOffsetMs = v * 1000
}
}
return s
}
func (h *NTPHandler) GetSettings(c *gin.Context) {
s, err := h.Repo.GetSettings(c.Request.Context())
if err != nil {