- 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>
Neue Domain kann per 301 auf eine andere Domain/URL umgeleitet werden, statt
auf ein Backend zu routen (Use-Case: kvs.netcell-it.de → https://zkm.netcell-it.de,
inkl. HTTPS). Variante (a): immer auf Ziel-Root (`redirect location`), pfad-
unabhängig.
- Migration 0043: domains.redirect_to text NOT NULL DEFAULT ''
- Model + domains-Service (SELECT/INSERT/UPDATE/scan)
- HAProxy-Generator: buildRedirectTo() sanitisiert (nur http(s), kein
Whitespace/Quotes → sonst kein Redirect statt kaputter Config); Template
emittiert `http-request redirect location <url> code 301 if hdr(host)`.
Terminiert vor use_backend → Redirect-Domain routet auf kein Backend.
http→https läuft über den vorhandenen :80-Redirect (zwei Hops, inkl. TLS).
- UI: Feld „Weiterleitung (301) nach" im Domain-Formular (de/en)
- Tests: Render-Zeile + buildRedirectTo-Sanitisierung
Hinweis: Die Redirect-Domain braucht weiterhin ein eigenes TLS-Zert (ACME).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bug: Dispatch an utm-2 schlug fehl ('dieser Node ist der Cluster-Primary'), weil ha_nodes je Node lokal ist und sich JEDE Node selbst als role=primary markiert. Fix: Primary-Erkennung über pg_publication (edgeguard_shared, für jeden DB-User lesbar) statt role/pg_role. Primary gibt dem Subscriber seine eigene Adresse als primary_host mit (PostPeerWithBody); Agent-Handler vertraut dem mTLS-Dispatch mit Safety-Guard 'läuft nie auf dem Publication-Primary'. Funktioniert auch bei Direktzugriff auf den Subscriber. UI-Gating vereinfacht (Drift + Peer + Admin).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pg_role bleibt nach cluster-setup-standby auf 'standalone' (nur 'promote' setzt 'primary'), daher erschien der Button auf dem Primary (role=primary, pg_role=standalone) nicht. Gating + Dispatch + Status nutzen jetzt isPrimaryNode = role=='primary' || pg_role=='primary' (wie keepalived); Resync-Ziel = Nicht-Primary-Peer. Backend (cluster_repair.go) + UI (Cluster/index.tsx).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cluster/Replication:
- Drift-Banner: Button 'Resync erzwingen' baut die PG-Logical-Replication-
Subscription neu auf (via edgeguard-ctl cluster-setup-standby).
- Primary-Dispatch: Button auf dem Primary delegiert per mTLS an den
Standby (POST /agent/cluster/repair-replication); auf dem Standby lokal.
- Status-Proxy Primary->Standby via Aggregator.FanOut; Erfolg = Job-success
ODER drift_found wird false (--collect-Unit verschwindet nach Erfolg).
- Job als transiente systemd-Unit edgeguard-repair-replication.service
(sudoers exact-match + festes Script wie upgrade.sh).
- Banner-Text korrigiert (keine 'Outbox').
Frontend-Stabilität:
- Stale-Chunk-Auto-Reload: Lazy-Import-Fehler nach Deploy ('Failed to fetch
dynamically imported module') lösen einen einmaligen Reload aus (Loop-
Schutz via sessionStorage) statt einer Fehlerseite. Globaler
vite:preloadError-Listener + ErrorBoundary-Integration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- crsRules.ts neu: 331 echte CRS v4.7.0 Regeln aus installierten Dateien
(v3-Nummernschema war falsch, v4 hat andere IDs — 949152 war skip-Regel)
- spoe.go: Regeln ohne Message nicht als Alert speichern
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Migration 0039: exclusion_notes JSONB in waf_configs (rule_id → note)
- Model/Service/Handler: exclusion_notes in Upsert + GET durchgereicht
- Alerts-Tab: "Als Ausnahme"-Button öffnet Modal mit Notiz-Textarea;
Notiz wird in exclusion_notes gespeichert
- Config-Drawer: Ausnahmen als Liste (Rule-ID + Notiz + Entfernen-Button)
statt rohem Textfeld; Ausnahmen nur noch via Alert-Tab hinzufügbar
- i18n EN + DE
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Jede Alert-Zeile hat jetzt einen "Als Ausnahme"-Button. Klick:
1. Lädt aktuelle WAF-Config der betroffenen Domain
2. Fügt die Rule-ID zu rule_exclusions hinzu (dedupliziert)
3. Speichert via PUT /waf/configs/:domain_id → triggert HAProxy-Reload
4. Erfolgsmeldung + WAF-Config-Query invalidiert
Button disabled wenn domain_id fehlt (Domain nicht aufgelöst) oder Viewer-Rolle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- pages/WAF/index.tsx: neue WAF-Seite mit Status-Strip,
Domänen-Tabelle (Toggle/Mode/PL) + Konfigurations-Drawer pro Domain
(enabled, mode, paranoia_level 1-4, rule_exclusions, trusted_proxies,
custom_rules). Quick-Toggle ohne Drawer; Hinweis: erst Detection, dann Blocking.
- App.tsx: /waf Route + lazy import
- Sidebar.tsx: WAF im Security-Bereich
- i18n EN + DE
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ErrorBoundary lag in main.tsx außerhalb des Routers und hatte keinen
Zugriff auf useLocation. Einmal gecatchter Fehler blieb erhalten bis
zum nächsten Reload — daher "EdgeGuard konnte nicht laden" bei
Navigation zum Dashboard.
Fix: LocationKeyBoundary-Wrapper innerhalb des BrowserRouter mit
key={pathname} — React remountet die ErrorBoundary bei jedem
Routenwechsel und löscht damit den Error-State automatisch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Regeln werden standardmäßig nach Zone-Pair (src→dst) gruppiert
- Jede Gruppe hat einen farbigen Header mit Zone-Badges + Regelanzahl
- Toggle-Button in der Filter-Bar: Gruppen-Ansicht ↔ flache Liste
- Move-up/down bleibt global korrekt (Priority über alle Gruppen)
- CSS: .fw-zone-section* mit nahtlosem Header → Tabelle Übergang
- i18n EN + DE
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Migration 0035: note TEXT + labels TEXT[] für firewall_rules + nat_rules
- PATCH /firewall/rules/:id + /nat-rules/:id für note/labels Updates
- InlineNote: gold Tag mit MessageOutlined, Klick zum Bearbeiten (Enter/Blur speichert)
- InlineLabels: geekblue Tags mit X-Button zum Entfernen, "+" zum Hinzufügen
- Name-Spalte: Name + Labels + Note in Zeile 1, comment/auto-desc in Zeile 2
- Gleiche UX für NAT-Regeln
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- FinishRollingUpdateIfPending() auf API-Startup: transitiert
updating-primary → done damit der UI-Flow nach Restart abschließt
- RollingUpdateStatus: setzt done nach Auslieferung auf idle zurück
(verhindert Stale-done bei Page-Reload)
- wasRollingActiveRef: reagiert auf done nur wenn rolling in DIESER
Session aktiv war — kein sofortiger Reload bei Stale-State
- UI-Fallback für updating-primary: poll auf /system/health version-flip
- Cluster-Erkennung via /cluster/status; Rolling-Update-Button nur im Cluster
- Update-Banner-Button nicht mehr gequetscht (flex-shrink:0 + nowrap)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /cluster/rolling-update startet den gestaffelten Upgrade-Prozess:
1. Secondary via mTLS /agent/cluster/trigger-update anstoßen
2. /agent/cluster/version pollen bis Secondary Version-Flip zeigt (max 10 min)
3. Primary self-upgrade via systemd-run (identisch zu /system/upgrade)
State wird in /var/lib/edgeguard/rolling-update-state.json persistiert:
Phasen: updating-secondary → waiting-secondary → updating-primary.
"done" wird nicht geschrieben — Prozess stirbt beim Upgrade. UI erkennt
Abschluss via /system/health version-flip (analog Single-Node-Upgrade).
UI: UpdateBanner erkennt Cluster-Modus (/cluster/status mode="cluster")
und tauscht den "Install now"-Button gegen "Rolling Update (Cluster)" aus.
Multi-Step-Modal zeigt die drei Phasen; ab updating-primary wechselt der
Client auf /system/health polling.
Aggregator.PostPeer: neuer einzel-POST-Helper für mTLS-trigger-update.
WithVersion(): ClusterHandler bekommt Binary-Version für /agent/cluster/version.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PG Logical Replication: edgeguard_shared PUBLICATION auf Primary,
edgeguard_sub SUBSCRIPTION auf Secondary. Nur geteilte Config-Tabellen
werden repliziert; node-eigene Daten (network_interfaces, ip_addresses,
static_routes, cluster_settings, dns_settings, ntp_settings) bleiben
lokal — OPNsense-Muster.
- cluster-init-replication: Erstellt PUBLICATION, Rolle + pg_hba-Einträge
(logical + replication), WAL-Level auf logical.
- cluster-setup-standby: Erstellt SUBSCRIPTION (copy_data=true), pollt
pg_subscription_rel bis alle Tabellen sync = 'r', rendert dann Configs.
- promote: manueller Failover via pg_promote() + touch recovery.signal.
- VIP/Keepalived: cluster_settings-Tabelle (vip_address, vip_interface,
vrrp_router_id), /cluster/vip-settings API, Keepalived-Config-Generator
mit VRRP + check_script + notify-Skripten in /usr/lib/edgeguard/scripts/.
- config_hash sync: Secondary pusht alle 5 Min seinen Hash via mTLS an
Primary (PushSelfToPrimary). Heartbeat schreibt nur LOCAL, daher ohne
aktiven Push wäre Primary-Sicht des Secondary-Hash stale gewesen.
- runSecondaryConfigRender: Goroutine auf Secondary rendert HAProxy+nftables
neu wenn config_hash sich ändert (Logical-Replication-Nachzügler).
- confighash: node-spezifische Tabellen aus hashSpec entfernt.
- postinst: Keepalived-Skripte installieren, sudoers für keepalived.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cluster-Seite: Schritt 2 fragt jetzt den FQDN des neuen Knotens bevor
der Token generiert wird. Der Knoten wird sofort in ha_nodes (status=pending)
eingetragen. Nach Token-Generierung wird direkt die Setup-Wizard-URL des
neuen Knotens angezeigt (https://<fqdn>:3443/setup).
Backend: POST /cluster/join-tokens nimmt jetzt node_fqdn entgegen,
pre-registriert via preRegisterByFQDN(). Die IP wird beim issue-cert
nachgetragen (preRegisterJoiner).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sicherheit des Joins kommt vom HMAC-Token, nicht von TLS-Cert-Trust.
Insecure=true wird jetzt immer im Handler gesetzt — GUI-Nutzer müssen
das nicht kennen oder konfigurieren.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /setup/join-cluster macht alles was bisher edgeguard-ctl cluster-join
tat: CSR generieren, Certs vom Primary holen, schreiben, auto-registrieren,
Setup als Cluster-Node markieren.
Setup-Wizard Node-Modus fragt jetzt direkt Primary-FQDN + Join-Token ab.
Nach Submit: Erfolgsmeldung + einziger verbleibender Schritt (systemctl restart).
Neue interne Bibliothek: internal/services/clusterjoin — wird von Handler
und CLI (edgeguard-ctl cluster-join) gleichermaßen genutzt, keine Duplizierung.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Setup-Wizard zeigt jetzt beim ersten Aufruf eine Modusauswahl:
- "Neuinstallation" → bisheriger Flow (Admin-Account + FQDN + ACME)
- "Cluster-Knoten beitreten" → nur FQDN + ACME, kein Admin-Account;
nach dem Submit werden die cluster-join-Befehle direkt angezeigt
Backend: NodeRequest + Store.CompleteAsNode() + POST /setup/complete-node
State: is_cluster_node Flag; login via PG-Replikation vom Primary
Install-Script: Setup-URL zeigt jetzt korrekt :3443/setup statt /setup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Seite war komplett leer wenn die Cluster-API einen Fehler zurückgab
(if(!data) return null). Jetzt: sichtbarer Error-Banner mit Retry,
keine leere Seite mehr.
Neuer 4-Schritt-Wizard "Zweiten Node hinzufügen" immer sichtbar:
1. Installer-Oneliner (copyable)
2. Join-Token generieren (Button → POST /cluster/join-tokens)
3. cluster-join-Befehl inkl. Token inline auf der Seite (kein Modal)
4. systemctl restart edgeguard-api
Token + CA-Fingerprint + Befehl erscheinen direkt in Schritt 3
nach Token-Generierung — kein separates Modal mehr nötig.
common.retry i18n-Key ergänzt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Abgelaufene und fehlerhafte Zertifikate zeigen jetzt beim Hover den
Fehlertext (last_error) aus der DB. TLSCertLite um last_error erweitert.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pattern-Validator lehnt offensichtlich ungültige Adressen (Sonderzeichen,
Leerzeichen) schon im Frontend ab, bevor sie HAProxy-Config-Generierung
zerstören können. Gilt für Backend-Server-Modal und Domain-Quick-Backend.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Duplicate-Button (CopyOutlined): klont Regel mit priority+1, disabled=true, Name "(copy)"
- CIDR-Felder: Pattern-Validator für IPv4/IPv6-CIDR-Notation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>