feat: per-Backend timeout server + Alerts-Deeplink + Retention + Cert-Prune — v1.3.4

- feat(backends): per-Backend `server_timeout_seconds` (nullable, Default 60s).
  Rendert `timeout server <N>s` im HAProxy-Backend-Block — für langsam
  antwortende Upstreams (KI-/Inferenz-Server mit gepufferter Antwort).
  Migration 0044 (+CHECK 1..86400), Model/Repo/Template/UI + Render-Test.
- fix(ui): Dashboard-Alert-Karte verlinkt auf /alerts?tab=events; Alerts-Seite
  respektiert ?tab= (Deeplink landete bisher auf leerem Channels-Tab).
- feat(scheduler): alert_events-Retention (90d) im täglichen Cleanup-Tick —
  Schutz vor unbounded growth der node-lokalen Health-Event-History.
- fix(cluster): Cert-Sync prunt jetzt lokale .pem die der Primary nicht mehr
  hat (Waisen gelöschter Domains); schützt _default.pem + eigenen Node-Cert.
- fix(security): x/text v0.37→v0.39 (GO-2026-5970, Infinite-Loop; via ACME+goose
  aktiv aufgerufen — govulncheck-Release-Gate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-07-27 15:57:35 +02:00
parent 32ab2c7f47
commit 0ac91a7c59
17 changed files with 227 additions and 41 deletions

View File

@@ -554,6 +554,44 @@ func TestRender_WebSocketEmitsTunnelTimeout(t *testing.T) {
}
}
func TestRender_ServerTimeoutOverride(t *testing.T) {
tmo := 300
v := View{
Backends: []BackendView{
{
Backend: models.Backend{ID: 11, Name: "ai", Scheme: "http",
LBAlgorithm: "roundrobin", ServerTimeoutSeconds: &tmo, Active: true},
Servers: []models.BackendServer{
{BackendID: 11, Name: "ai-1", Address: "10.0.5.30", Port: 8000, Weight: 100, Active: true},
},
},
{
Backend: models.Backend{ID: 12, Name: "web", Scheme: "http",
LBAlgorithm: "roundrobin", Active: true},
Servers: []models.BackendServer{
{BackendID: 12, Name: "web-1", Address: "10.0.5.31", Port: 8080, Weight: 100, Active: true},
},
},
},
}
out := renderView(t, v)
idxAI := strings.Index(out, "backend eg_backend_11")
idxWeb := strings.Index(out, "backend eg_backend_12")
if idxAI < 0 || idxWeb < 0 {
t.Fatalf("backend sections missing in output:\n%s", out)
}
aiBlock := out[idxAI:idxWeb]
webBlock := out[idxWeb:]
// ai (nil-Override gesetzt) soll `timeout server 300s` bekommen …
if !strings.Contains(aiBlock, "timeout server 300s") {
t.Errorf("ai-Block sollte `timeout server 300s` enthalten:\n%s", aiBlock)
}
// … web (kein Override) soll KEINE eigene timeout-server-Zeile bekommen.
if strings.Contains(webBlock, "timeout server") {
t.Errorf("web-Block soll KEIN eigenes `timeout server` enthalten:\n%s", webBlock)
}
}
func TestRender_MultiServerPool(t *testing.T) {
v := View{
Backends: []BackendView{