154 Commits

Author SHA1 Message Date
noroot
9137c07c95 fix(tls): Management-Zertifikat des Standby lief ab und erneuerte sich nie
Auf utm-2 war das Zertifikat der Management-UI seit dem 28.08. abgelaufen
(entdeckt am 11.09.) und haette sich aus eigener Kraft nie wieder
erneuert. Zwei Ursachen trafen zusammen:

1) Das FQDN eines per Join dazugekommenen Nodes landet in KEINER
   tls_certs-Zeile — es wird beim Setup einmalig ausgestellt und danach
   von niemandem mehr angefasst. certrenewer arbeitet nur die Tabelle ab
   und sieht es deshalb nie. (utm-1 steht dort drin, weil sein FQDN beim
   Erst-Setup regulaer als Domain angelegt wurde — deshalb fiel es dort
   nicht auf.)
2) Der Scheduler blockt auf einem Nicht-VIP-Master jede ACME-Erneuerung
   (v1.3.20). Fuer geteilte Domains ist das richtig: die zeigen per DNS
   auf die VIP, nur der Master kann die Challenge bestehen. Fuer das
   eigene Management-FQDN stimmt es nicht — utm-2.netcell-it.de zeigt auf
   89.163.205.8, die eigene IP des Nodes, und Port 80 antwortet dort.

Neu: runManagementCertRenew laeuft auf JEDEM Node unabhaengig von der
VIP, aber ausschliesslich fuer das eigene FQDN aus setup.json. Prueft die
Restlaufzeit der Datei unter /etc/edgeguard/tls und erneuert ab 30 Tagen
Rest (gleicher Schwellwert wie bei den Domain-Certs).

Bewusst NICHT ueber tls_certs: das ist eine replizierte Shared-Table, und
cluster-reconcile-replication TRUNCATEt solche Tabellen beim Refresh —
eine lokal auf dem Subscriber eingefuegte Zeile waere beim naechsten
Paket-Upgrade wieder weg. Das Management-Zertifikat ist node-lokale
Infrastruktur und wird wie die cluster-tls-Certs als reine Datei
behandelt. Existiert dagegen bereits eine tls_certs-Zeile fuer das eigene
FQDN (Fall utm-1), bleibt alles beim Alten und certrenewer behaelt die
Zustaendigkeit — sonst haetten zwei Mechanismen dieselbe Datei.

Tests decken die Schwellwert-Entscheidung ab, inklusive des
utm-2-Falls (bereits abgelaufen) und kaputter PEM-Dateien.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 12:02:26 +02:00
Debian
b70db4ccf0 fix(scheduler): ACME-Renewal nur auf VIP-Master — Standby-403 verursachte Cert-Drift — v1.3.20
Der Scheduler fuhr runRenewer (ACME) ungated auf beiden Nodes. ACME-HTTP-01-
Challenges laufen aber auf :80 der VIP → nur der VIP-Master kann sie bestehen.
Der BACKUP-Node scheiterte immer mit 403 (invalid authorization) und setzte
tls_certs.status lokal auf "error" → Divergenz zur replizierten Row
(Primary=active) → Config-Drift-Banner + Log-Noise. runRenewer jetzt hinter
nodeHoldsVIP() gegated (Start + 6h-Tick); runCertExpiryCheck bleibt ungated
(read-only). Die Cert-Row/PEM repliziert ohnehin vom Master auf den Standby.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 08:35:03 +02:00
Debian
088910ee19 fix(scheduler): WG-Client-Tunnel-Check nutzt korrekten Tabellennamen — v1.3.11
runWGClientTunnelCheck fragte `wg_interfaces` ab — die Tabelle heißt überall
sonst `wireguard_interfaces`. Der Query-Fehler wurde verschluckt (if err return),
sodass der Check bei JEDEM 5-min-Lauf still no-opte (WG-Client-Tunnel-Monitoring
faktisch tot) und PostgreSQL alle 5 min `relation "wg_interfaces" does not exist`
ins Log schrieb (auf beiden Nodes). Fix: korrekter Tabellenname.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 20:58:22 +02:00
Debian
8e4759ccc6 feat(cluster): Replikations-Reconcile + Backend-Down nur auf VIP-Master — v1.3.7
- fix(scheduler): backend.down-Check läuft nur noch wenn dieser Node den VIP
  hält (nodeHoldsVIP). Ein keepalived-BACKUP-Node hat KEINE VLAN-IP → erreicht
  die Backend-Subnetze nicht → sah bisher ALLE Backends L4-down und feuerte
  Dauer-Fehlalarme (Hauptquelle des alert_events-Spams). Master sieht die
  echten States.
- feat(ctl): `cluster-reconcile-replication` — bringt Publication/Grants/
  Subscription idempotent in den Soll-Zustand (Publisher: fehlende Shared-
  Tables ADD, node-lokale DROP, GRANT SELECT für Replikator; Subscriber:
  neue Tabellen leeren + REFRESH). Läuft im postinst nach migrate.
- fix(packaging): postinst re-added network_interfaces/ip_addresses bei JEDEM
  Upgrade in die Publication (alter fester Block) → ersetzt durch den Reconcile.
  DAS war die Wiederkehr-Ursache.
- fix(replication): waf_alerts → localOnlyTables (Event-Daten, node-lokal wie
  alert_events).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 17:07:21 +02:00
Debian
0ac91a7c59 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>
2026-07-27 15:59:55 +02:00
Debian
32ab2c7f47 chore(lint): Backlog auf 0 + golangci-lint als HARTER Gate — v1.3.3
Go-Quality-Baseline-Rollout ABGESCHLOSSEN.

Code-Quality-Backlog (55 → 0):
- errcheck: unbehandelte Close/Rollback/Remove explizit `_ =`; fmt.Sscanf
  `_, _ =` (Zero-Value degradiert sauber).
- unused: toter Code entfernt (nodeIDOrHostname, stripTrailingNewline,
  acme.Service.user, strFold + ungenutzter Import).
- noctx (net/http): http.NewRequestWithContext mit vorhandenem ctx.
- staticcheck: QF1001/S1009/ST1005/SA9003.
- contextcheck: detached-by-design-Stellen mit begründetem //nolint.

Zwei echte Bugs beim Aufräumen gefunden+gefixt:
- backup/remote SFTP-Upload: dst.Close()-Flush-Fehler wurde verschluckt →
  unvollständiges Remote-File galt als Erfolg. Jetzt geprüft+gemeldet.
- haproxy_test: leere if-Assertion (SA9003) testete faktisch nichts →
  echte t.Errorf-Prüfung (kein HSTS für HSTS-disabled Domain).

Bewusste Config-Entscheidungen (.golangci.yml):
- noctx-on-os/exec ausgeschlossen: System-Command-Reloads (systemctl/nft/
  wg/pg) dürfen NICHT an den Request-Context gebunden werden — ein Client-
  Disconnect darf keinen laufenden Reload mitten in der Ausführung killen.
  net/http-noctx bleibt voll aktiv. KEINE exec-Zeile im Code angefasst.
- rowserrcheck/sqlclosecheck raus (database/sql-Linter, bei pgx nur FPs).

Gate scharf gestellt: Makefile release-check ruft golangci-lint jetzt als
HARTEN Gate (install-if-missing, pinned v2.12.2). `make release-check`
grün: vet, golangci-lint, govulncheck, build, test -race.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 00:49:07 +02:00
Debian
02096c8ad8 chore(lint): golangci-lint --fix — misspell + staticcheck-Autofixes (Backlog 142→~98)
Erster Schritt des golangci-lint-Rollouts (non-blocking): 44 misspell + 4
staticcheck automatisch behoben (32 Dateien, nur Tippfehler/mechanisch).
build+test grün. Kein Runtime-Change → kein Deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 23:25:22 +02:00
Debian
1d06b28064 feat: HA-Cluster v1.2.x — Split-Brain, TOTP, Enterprise-FW, Drift-Fix, VIP-Recovery
- keepalived: pg_role='standby' hat Vorrang vor role für BACKUP-Bestimmung
- keepalived-master.sh: gecrasht Dienste beim MASTER-Übergang starten (nicht nur reload)
- confighash: ip_addresses per Interface-Name hashen statt per FK (Cross-Node-Drift-Fix)
- TOTP/2FA: RFC 6238 — Setup-Flow, QR-Code, Admin-Disable; two-step Login
- Firewall-UI: Enterprise-Design — auto-Beschreibung, icon-only Actions, zero-hit Indikator
- fe80-Filter: Link-local IPv6 aus NTP/DNS Listen-Dropdowns entfernen
- VIP-Dashboard, Dual-Path VRRP, GW-Tracking (Migrations 0033/0034)
- Forward Proxy + DNS erweiterte Einstellungen (Migrations 0031/0032)
- unbound-control: edgeguard in unbound-Gruppe via postinst

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 18:18:31 +02:00
Debian
eaa6a04234 feat(cluster): PG Logical Replication setup + Master-Key-Sync + Firewall-Ports
cluster-init-replication:
- listen_addresses = '*' damit Cluster-Peers PG auf :5432 erreichen können
- max_replication_slots = 20 / max_wal_senders = 10 (verhindert Slot-Erschöpfung
  bei initaler Tabellen-Synchronisation mit vielen gleichzeitigen Sync-Workern)
- pg-replication-secret: Ownership an edgeguard-User (API-Lesezugriff)
- detectPGConfig() statt hardcoded PG 16 (System läuft PG 17)

cluster-setup-standby:
- syncMasterKey(): holt /var/lib/edgeguard/.master_key via mTLS vom Primary —
  ohne identischen Master-Key können replizierte WireGuard-Keys nicht entschlüsselt werden
- render-config: sudo -u edgeguard statt als root (DB-Zugriff)

nftables Template:
- Port 5432 (PG) + 6379 (KeyDB) für Cluster-Peers (@peer_ipv4/@peer_ipv6) freigegeben

handlers/cluster.go:
- GET /agent/cluster/master-key: gibt .master_key via mTLS zurück (hex-kodiert)

v1.2.15

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 12:56:32 +02:00
Debian
bc6db1fc2b feat(cluster): Rolling Update — Secondary-first upgrade orchestration (v1.2.3)
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>
2026-05-29 23:40:49 +02:00
Debian
c1a4ccff8f fix(cluster): Placeholder zuerst löschen vor UpsertSelf (UNIQUE-fqdn-Bug)
ha_nodes hat UNIQUE(fqdn). UpsertSelf nutzt ON CONFLICT(id) — schlägt
bei fqdn-Konflikten fehl. Wenn Placeholder und echter Node die gleiche
FQDN haben, schlug der INSERT der echten Row mit "duplicate key on
ha_nodes_fqdn_unique" fehl.

Root cause für "joining" stuck forever: der Peer blieb ewig als
Placeholder weil AgentRegisterPeer + reconcileJoiningPeers die Echte-
ID-Row nie erfolgreich einfügen konnten.

Fix: DeletePlaceholdersByFQDN vor UpsertSelf in beiden Code-Paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 19:45:02 +02:00
Debian
ffb579879d feat(cluster): pull-basierte Peer-Registrierung als autoRegister-Fallback
Wenn autoRegister (Push utm-2→utm-1) fehlschlägt, bleibt der Peer
dauerhaft im 'joining'-Status. Neuer Self-Healing-Mechanismus:

- GET /agent/cluster/identity auf dem Agent-Listener: gibt die eigene
  ha_nodes-Row zurück (echte Node-ID, FQDN, Version)
- ClusterHandler.Status() startet für jeden 'joining'/'pending'-Peer
  einen Background-Reconcile via Aggregator: holt Identity, upsertet
  mit echter ID (public_ip aus Placeholder übernommen für nftables),
  löscht Placeholder
- autoRegister-Fehler werden jetzt als Warn-Log sichtbar statt silent
  verworfen

Damit reicht ein Cluster-Page-Aufruf nach Update beider Nodes um den
Status von 'joining' auf 'online' zu bringen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 19:06:19 +02:00
Debian
6eb6d6d6a4 fix(jointoken): initialize HMAC secret on service startup
Consume() called loadSecret() which failed with "no such file" when
cluster-join-secret didn't exist yet. New() now calls ensureSecret()
so the file is always created before any Consume() attempt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 18:44:00 +02:00
Debian
357113e7be fix(cluster): Alle Placeholder-Rows bei autoRegister bereinigen
AgentRegisterPeer löschte bisher nur den neuen prenode-{fqdn}-Placeholder.
Alte pre-{timestamp}-Rows (aus Versionen vor 1.1.158) blieben stehen und
zeigten dauerhaft status=joining.

Fix: DeletePlaceholdersByFQDN löscht ALLE ha_nodes-Rows mit gleicher FQDN
außer der echten Node-ID — unabhängig vom ID-Format.

Auch preRegisterByFQDN nutzt jetzt das stabile prenode-{fqdn}-Format.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 18:33:50 +02:00
Debian
8d83de6b0f feat(cluster): Fix "joining" status + cross-node auth federation
1. preRegisterJoiner now runs SYNCHRONOUSLY before IssueCert responds,
   so nftables @peer_ipv4 is updated before the joiner calls autoRegister.
   Previously it was a goroutine → race → autoRegister failed → "joining"
   forever.

2. Stable node ID for pre-registered placeholder (prenode-{fqdn}) instead
   of time-based ID — re-joins are now idempotent.

3. AgentRegisterPeer sets status="online" immediately (peer proved it is
   online by connecting via mTLS) and deletes the prenode-{fqdn} placeholder.

4. autoRegister retries 3× with 2s delay in case of transient nftables lag.

5. Auth federation: cluster nodes forward failed logins to the primary via
   mTLS /agent/auth/check so users can log in on any node with primary
   credentials (no PG replication needed).
   - SystemHandler.AgentAuthCheck: new endpoint on :8443
   - AuthHandler.checkWithPrimary: mTLS call to primary when local auth fails
   - AuthHandler.WithClusterTLS: inject cluster TLS store
   - startAgentListener now uses the wired systemHdl with Users repo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 18:04:34 +02:00
Debian
6bb1c5c6d3 fix(aggregator): Envelope aus Peer-Responses entpacken vor Weiterleitung
callPeer speicherte den rohen HTTP-Response-Body ({"data": {...}, "error":
null}) in PeerResult.Data. Der lokale Pfad serialisiert dagegen die Struct
direkt ohne Envelope. Im UI führte das auf Peer-Nodes zu
`r.data.load_avg_1 = undefined` → TypeError: Cannot read properties of
undefined (reading 'toFixed') → Cluster-Seite nicht ladbar.

Fix: Envelope-Feld `data` extrahieren; fallback auf rohen Body wenn kein
gültiger Envelope.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:52:25 +02:00
Debian
de28523708 feat(cluster): startup peer-sync + stable primary-ID in ha_nodes
On API restart, cluster nodes now re-register their primary in the local
ha_nodes and reload nftables so @peer_ipv4 is correct after a package
update or reboot without requiring a re-join.

Also fixes duplicate ha_nodes rows: preRegisterPrimary previously used
time.Now().UnixNano() as node ID, creating a fresh row each call.
Now uses a deterministic ID derived from the FQDN so repeated upserts
are idempotent.

PrimaryFQDN is now persisted in setup.json during CompleteAsNode so the
startup sync knows which primary to contact.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:45:26 +02:00
Debian
8ac066d99a fix(cluster): Primary bidirektional in ha_nodes eintragen nach Join
Nach erfolgreichem cluster-join trägt der Joining-Node (utm-2) den Primary
per DNS-Lookup in seine lokale ha_nodes ein und lädt nftables neu.
Damit ist @peer_ipv4 auf utm-2 sofort mit utm-1's IP befüllt und Port 8443
ist bidirektional offen — der Aggregator auf utm-1 kann utm-2 auf :8443
abklappern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:36:06 +02:00
Debian
5ace250879 feat(cluster): FQDN-First Token-Flow — Node vor dem Join vorregistrieren
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>
2026-05-29 17:29:58 +02:00
Debian
fd69081336 fix(cluster): Joiner-IP sofort in @peer_ipv4 — Henne-Ei-Problem gelöst
IssueCert registriert den beitretenden Node jetzt direkt in ha_nodes
(public_ip = ClientIP, status='joining') und triggert den Firewall-Reload,
bevor die Response zurückgeht. Damit ist Port 8443 schon offen wenn der
Node im nächsten Schritt auto-register via mTLS versucht.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:25:38 +02:00
Debian
6a5aff69d9 fix(setup): CompleteAsNode idempotent — konvertiert Standalone zu Cluster-Node
Wenn setup bereits abgeschlossen war (z.B. zuvor als Standalone eingerichtet),
wird der Node jetzt ohne Fehler auf is_cluster_node=true umgestellt statt
mit "setup already completed" zu fehlschlagen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:21:17 +02:00
Debian
2e84268f61 fix(setup): TLS-Prüfungs-Checkbox aus Cluster-Join-GUI entfernt
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>
2026-05-29 17:18:33 +02:00
Debian
60e2aec5d9 fix(setup): Force-Flag für cluster-join — Bootstrap-Cert wird automatisch ersetzt
Der API-Boot erstellt für Single-Node-Betrieb automatisch ein selbst-signiertes
peer-Cert. Beim Join über die GUI wurde das als Fehler gemeldet.

clusterjoin.Request.Force=true im Setup-Handler überspringt die HasPeer-Prüfung;
CLI (edgeguard-ctl cluster-join) bleibt vorsichtig (Force=false, manuelles rm nötig).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:14:34 +02:00
Debian
afe2c951b9 feat(setup): Cluster-Join komplett in GUI — kein CLI mehr nötig
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>
2026-05-29 17:08:49 +02:00
Debian
7e5a7a83c0 feat(setup): Cluster-Node-Modus im Setup-Wizard + Install-Script-Port-Fix
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>
2026-05-29 16:07:19 +02:00
Debian
a0b3bccbc6 fix(cluster): ha_nodes INET-Spalten → TEXT — pgx-Scan-Fehler OID 869
pgx kann INET-Typen (OID 869) nicht direkt in *string scannen.
Migration 0028 konvertiert public_ip, internal_ip, mgmt_ip auf TEXT
(USING ip::TEXT erhält bestehende Werte). Basis-Migrationen 0002 + 0020
auf TEXT umgestellt damit frische Installs keine INET anlegen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 15:41:20 +02:00
Debian
33f6fef009 feat(cluster): Seite komplett neu — Blank-Page-Fix + Node-Join-Wizard
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>
2026-05-29 15:17:12 +02:00
Debian
31d3485a2f feat(dns): Validierung für DNS-Settings — upstream IPs, ACL-CIDRs, TTL-Kreuzcheck
Backend: validateSettings() prüft vor dem Reload-Trigger ob upstream_forwards
gültige IPs (inkl. @port), access_acl gültige IPs/CIDRs und listen_addresses
gültige IPs sind; cache_max_ttl ≥ cache_min_ttl.
Frontend: Pattern-Validatoren auf upstream_forwards + access_acl; TTL-
Kreuzvalidierung mit dependencies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 12:52:28 +02:00
Debian
0cbc781d4f feat(domains): cert error Tooltip mit last_error im Domain-Detail
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>
2026-05-29 09:25:33 +02:00
Debian
bf09fd2cef fix(scheduler): Cert-Renewal-Alerts pro Domain statt aggregierter Zählung (v1.1.143)
- certrenewer.Result.FailedDomains []string — jeder fehlgeschlagene Domain-Name
  wird erfasst (Issue/Parse/Write-Fehler)
- runRenewer: pro Domain eigener Alert + eigener Dedupe-Key statt einem
  shared "cert.renew_failed"-Key; Alert-Message nennt jetzt den Domain-Namen
  und gibt Hinweis auf ACME/DNS-Debugging

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 07:38:57 +02:00
Debian
7dd76d4b58 fix(backends): Backend-Löschen verweigern wenn Routing-Regeln darauf zeigen (v1.1.142)
- Service: Pre-Check auf routing_rules.backend_id → ErrInUse vor DELETE
- Handler: ErrInUse → HTTP 409 Conflict statt 500 (response.Conflict helper)
- UI: Delete-Button deaktiviert + Tooltip "Von N Regeln verwendet" wenn Referenzen existieren

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 07:15:16 +02:00
Debian
40414d472b fix(routing+backends): Pfad-Präfix-Validierung + Warnung bei leerem Backend-Pool (v1.1.141)
- RoutingRules: path_prefix Pattern-Validator (muss mit / beginnen) + destroyOnHidden
- Backends: Alert-Banner wenn aktives Backend 0 Server hat (→ HAProxy 503)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 21:25:55 +02:00
Debian
9dee904169 fix(backends): Adress-Validierung für Backend-Server + Quick-Backend (v1.1.140)
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>
2026-05-28 21:13:24 +02:00
Debian
d89569017d feat(firewall): Regel duplizieren + CIDR-Validierung (v1.1.139)
- 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>
2026-05-28 20:36:25 +02:00
Debian
dc2c1b7471 fix(settings): Bestätigungsdialog vor Service-Restart + status-dot CSS (v1.1.138)
- Popconfirm vor jedem Restart-Button — kein versehentlicher Neustart mehr
- status-dot inline styles → .status-dot.online/.offline CSS-Klassen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 17:14:44 +02:00
Debian
2a9310e947 fix(firewall): NAT-Regeln nach Priorität sortieren + Port-Range-Validierung (v1.1.137)
- dataSource: data→sortedNAT — Up/Down-Pfeile zeigten auf falschen Nachbarn
- Form.Item dependencies: dportEnd/targetPortEnd validiert ≥ Start-Port
- Modal destroyOnHidden: Formular-State nach Schließen zurückgesetzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 16:51:07 +02:00
Debian
76b5a4586c feat(firewall): Enterprise-UI-Redesign — KPI-Strip, Zone-Badges, Filter-Bar (v1.1.136)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 16:39:57 +02:00
Debian
fa86b14635 fix(layout): Header-Titel für SSL, DNS, NTP, WireGuard, ForwardProxy
PAGE_TITLES in AppLayout fehlten 5 Einträge — diese Seiten zeigten
statt dem Seitennamen nur den generischen App-Titel im Header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 16:20:02 +02:00
Debian
abde59f9b4 fix(firewall): SystemRules-Panel zeigt UDP/443 (HTTP/3 QUIC) Anti-Lockout
Die Systemregeln-Dokumentation listete nur TCP/443, aber das nftables-
Template enthält auch udp dport 443 für QUIC. Ohne den UDP-Eintrag wirkte
das Panel inkorrekt und erschwerte Firewall-Troubleshooting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 15:47:12 +02:00
Debian
0a31a253b3 fix(routing-rules): Tabelle zeigt Regeln in Priority-Reihenfolge
DataTable nutzte `rules` (API-Reihenfolge), sortedRules nur für
die Up/Down-Index-Berechnung — Move-Pfeile und Tabellenreihenfolge
waren dadurch inkonsistent. Fix: DataTable bekommt sortedRules.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 15:32:47 +02:00
Debian
e2b8560c65 fix(audit): log system.upgrade trigger
Upgrade war die einzige system-Mutation ohne Audit-Eintrag. Alle
anderen (maintenance, service-restart, haproxy-reload, IPv6, render-
configs, auto-update) loggen bereits. Jetzt wird system.upgrade mit
actor + unit-Name geloggt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 15:23:30 +02:00
Debian
22146c422d fix(auth): audit-log login success + failure attempts
Login handler hatte h.Audit injiziert (mit Kommentar "login-success/fail
ins audit_log fließen") aber nie aufgerufen. Jetzt werden geloggt:
  - auth.login.failed (reason: invalid_credentials | account_disabled)
  - auth.login.success (mit role + remote IP)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 15:14:13 +02:00
Debian
2a76a6599b fix(license): Rejection-Reason anzeigen + Placeholder NETC- korrigieren
Wenn der License-Server valid:false zurückgibt, wird payload.reason
jetzt als roter Alert angezeigt — bisher war der Grund nicht sichtbar
und der Operator sah nur "Abgelaufen" ohne Erklärung.

Formular-Placeholder NMG-XXXX → NETC-XXXX (mail-gateway copy-paste).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 15:06:48 +02:00
Debian
6a9019058a fix(license): Produktname + mail-gateway-Feature-Keys entfernen
Trial-Result hatte Product="NetCell MailGuard" und Feature-Keys
(reporting, eu_portal, digest, whitelabel, rest_api_write) die aus
dem mail-gateway copy-pastet wurden und für EdgeGuard bedeutungslos
sind. Bereinigt: Product="NetCell EdgeGuard", Features={} (leer bis
EdgeGuard-eigene Gates definiert werden), Limits-Struktur bleibt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 15:03:16 +02:00
Debian
a6ef320c57 fix(audit): fehlende Audit-Logs für DNS-Zone/Record-Delete + NTP-Pool-Delete
DeleteZone, DeleteRecord und NTPPool.DeletePool hatten keine Audit-
Einträge. Alle anderen Mutations derselben Handler loggen korrekt —
nur Delete war vergessen worden. Jetzt konsistent:
  dns.zone.delete, dns.record.delete, ntp.pool.delete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 14:57:35 +02:00
Debian
1c39a0e533 fix(alerts+diag): quickToggle SMTP-Datenverlust + Diagnostics Viewer-Gate
Alerts: quickToggle schickte den Channel durch buildPayload(), das SMTP-
Settings aus einem flachen ChannelFormValues rekonstruiert. Bei Email-
Channels wurde damit jedes aktiv/inaktiv-Toggle zum SMTP-Config-Wipe,
weil die Flat-Fields (smtp_host, smtp_port, from, ...) undefined waren.
Fix: raw Channel-Objekt mit geändertem active-Flag direkt PUT-ten.

testFire-Button fehlte disabled={isViewer} + Tooltip.

Diagnostics: kein isViewer-Check, Viewer sahen funktionierende Run-
Buttons und bekamen 403 ohne Erklärung. Fix: isViewer aus Auth-Store,
disabled-Prop in ToolCard, alle 5 Cards + Enter-Handler gegattet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 14:44:58 +02:00
Debian
d87068535d fix(backends): disabled={isViewer} fehlt am Server-Active-Switch in Detail.tsx
Der ServerPanel in Backends/Detail.tsx hat den isViewer-Prop korrekt
erhalten, ihn aber nicht auf den Active-Switch angewendet. Viewer
konnten dadurch den Server-Status im Backend-Detail direkt umschalten
(backends/index.tsx hatte den Switch korrekt gesperrt, Detail.tsx nicht).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 14:15:02 +02:00
Debian
7c50e28f55 fix(auth): ChangePassword unterstützt jetzt DB-User — nicht nur Setup-Admin
Vorher hat ChangePassword das Passwort ausschließlich gegen den
setup-store-Admin geprüft und dort gespeichert. User die via
User-Management angelegt wurden, bekamen immer 401.

Fix: DB-Lookup via Users.FindByEmail, Verify + SetPassword im DB-Store.
Setup-Store-Admin bleibt synchron wenn die E-Mail übereinstimmt.
Fallback auf setup-store bleibt erhalten für Legacy-Installs (pre-DB).

Bonus: %m-Formatverb-Fehler im Scheduler-Alert-Text behoben.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 13:52:48 +02:00
Debian
74d3f25d21 fix(users): Delete-Button für eigenes Konto deaktivieren
ActionButtons für die eigene User-Zeile bekommt deleteDisabled=true +
deleteDisabledReason=users.cannotDeleteSelf. Backend schützt gegen
Selbst-Löschung bereits (400 cannot delete own account); UI zeigte
aber den aktiven Delete-Button und ließ Operator ins Leere klicken.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 13:42:36 +02:00
Debian
44f184d505 fix(users): viewer-Rolle — Add/Edit/Delete/Set-PW/Active-Toggle deaktivieren
Users-Seite fehlte `isViewer` komplett: Viewer sahen alle Mutations-
Controls aktiv und bekamen erst nach dem Klick einen Backend-403.
Jetzt: „Benutzer hinzufügen"-Button + KeyOutlined-Set-PW-Button +
Active-Switch tragen `disabled={isViewer}`, ActionButtons-Pair liest
isViewer schon intern aus dem Auth-Store.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 10:27:12 +02:00