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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
The Backend interface declared address/port fields that don't exist on the
Backend model (those belong to BackendServer). The table column and the
backend select dropdown both rendered "name (undefined:undefined)". Fixed
by replacing phantom fields with scheme, matching the same fix applied to
Domains pages in v1.1.121.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BackendLite interfaces in Domains list and Detail declared address/port
fields that don't exist on the Backend model (those live on BackendServer).
Every backend tag rendered as "name (undefined:undefined)". Fixed by
replacing the phantom fields with scheme, so tags show e.g. "my-app (https)".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend Detail page: HAProxy stat rows use safeID(name) as server token
(spaces/dots → '_') but the UI matched on the raw DB name, so servers
with non-alphanumeric names never showed live status. Added matching
safeID helper in TypeScript (mirrors haproxy.go implementation).
Routing Rules: commit priority up/down swap buttons (developed in a
previous session, were left uncommitted).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Expired certs now show a red error alert ("renew immediately") instead
of being silently folded into the orange "expiring soon" warning.
HAProxy frontend technical names (public_http, public_https, mgmt_https)
now display as human-readable labels (HTTP In, HTTPS In, Management).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
api_backend (management API) and rl_* (rate-limit stick-tables) were
appearing in the dashboard's backend health card alongside customer
backends. Skip these internal HAProxy proxy entries in the stats parser.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both NTP and Backends pages cast r.data.data directly to Array<…> but
the handler wraps the list as gin.H{"services": out}. The wrong cast
returned an object to .find()/.filter(), crashing with "s.find is not
a function" and breaking the NTP page entirely. Firewall/ForwardProxy/
DNS/Settings pages already used the correct {services:…} shape. v1.1.117.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a backend server is DOWN, the HAProxy card now shows the check_status
code (L4CON, L4TOUT, L7STS, L6CON, …) as a volcano-colored tag next to
the DOWN badge. Hovering shows a plain-English explanation of the failure
reason so operators can diagnose without leaving the dashboard. v1.1.116.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fieldAt() in the scheduler used i<=0 instead of i<0, causing pxname
(column 0 in HAProxy CSV) to always return "". The HasPrefix("",
"eg_backend_") check then filtered every row → byBackend was always
empty → backend.down alert never fired since the check was introduced.
v1.1.115.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The HAProxy stats card now shows the configured backend name (e.g.
"My Backend/web1") instead of the internal HAProxy name
("eg_backend_1/web1"). Also surfaces the socket error message when
HAProxy is unreachable (⚠ HAProxy socket: <error>) instead of the
generic "no backends" empty state — operators immediately see whether
the box is unconfigured or the socket is down. v1.1.114.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two root causes for Dashboard backends not showing:
1. haproxy.cfg.tpl had no stats socket directive → /run/haproxy/admin.sock
never created → handler got "connection refused" → empty array returned.
Fixed: added stats socket /run/haproxy/admin.sock mode 0660 level admin.
2. safeAt() used i<=0 instead of i<0 → pxname (column 0) always returned ""
→ all backend names were empty in the API response.
After upgrade: HAProxy reload (triggered by any domain/backend save, or
edgeguard-ctl render-config) will create the socket. v1.1.113.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Operators can now narrow the history table by severity (INFO/WARNING/
ERROR/CRITICAL) and by alert kind (dynamically derived from loaded
events, searchable). Filtering is client-side — no extra API calls.
v1.1.112.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Server tunnels with many peers previously flooded the WG card. Now
server mode shows "X/Y peers online · ▼rx ▲tx" (3-min handshake
threshold) while client tunnels retain full per-peer detail.
Adds i18n keys dashboard.wgCard.peersOnline (EN/DE). v1.1.111.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- runWGClientTunnelCheck() prüft alle aktiven Client-Tunnels (mode='client')
alle 5 Min; feuert Error-Alert wenn kein Handshake seit >5 Min oder
noch nie (12h dedupe pro Tunnel-Name)
- Dedupe-Angaben in alerts.scopeDesc korrigiert: war "1h" für mem/conntrack/ntp,
tatsächlich 12h (shared alertDedupe) — beide Sprachen bereinigt
- wg.tunnel.down-Trigger in beiden i18n-Dateien dokumentiert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- runNTPSyncCheck() läuft alle 10 Minuten: ruft chronyc tracking auf,
prüft ob Stratum 0 / ≥16 oder Reference ID 00000000 — feuert
Warning mit Fix-Hints (1h dedupe)
- Initial-Check absichtlich NICHT beim Boot, da chrony nach dem Start
einige Sekunden zur ersten Synchronisation braucht
- ntp.unsync-Trigger in beiden i18n-Dateien dokumentiert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- runConntrackCheck() liest /proc/sys/net/netfilter/nf_conntrack_count +
nf_conntrack_max, feuert Warning bei ≥80% und Critical bei ≥90%
(2-Min-Takt, 1h dedupe pro Severity-Key)
- Bei 100% Auslastung dropped nftables alle neuen Verbindungen lautlos;
der Alert kommt damit rechtzeitig bevor das passiert
- conntrack.high-Trigger in beiden i18n-Dateien dokumentiert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Scheduler: runMemoryCheck() liest /proc/meminfo, feuert Warning bei ≥85% und Critical bei ≥95% RAM-Auslastung (5-Min-Intervall, 1h dedupe pro Severity-Key)
- NTP-Status-Karte: freq_ppm (±ppm mit Warnung bei >100) und rms_offset_ms werden jetzt angezeigt — Felder waren im Backend schon vorhanden, aber nie gerendert
- Alerts scopeDesc: mem.high-Trigger in beiden i18n-Dateien dokumentiert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds ↑↓ icon buttons in the firewall rules table. Clicking swaps
the rule's priority value with its sorted neighbor via two sequential
PUTs. First rule's ↑ and last rule's ↓ are disabled. Viewer role
cannot reorder.
v1.1.102
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds GET /ntp/sources endpoint (runs chronyc sources, parses tabular
output) and a new "Peer status" tab in the NTP page showing all
configured peers with mode, state badge, stratum, poll interval,
reach register, last-rx and offset/error sample.
v1.1.101
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Sidebar: Hostname unter der Versionsnummer (aus /system/health);
10px, gedimmt — beim Verwalten mehrerer Boxen sofort erkennbar
- /system/resources: neues Feld num_cpus (via runtime.NumCPU)
- Dashboard Resources-Card: Load-Average-Label zeigt "(N CPU)" —
1.80 auf 1 CPU ist saturiert, auf 4 CPUs ist es harmlos
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- /system/health gibt zusätzlich hostname, os (PRETTY_NAME aus
/etc/os-release) und kernel (/proc/version, ohne Build-Details) zurück
- Settings-Seite: SystemHealth-Interface erweitert, System-Info-Card
zeigt Hostname, Betriebssystem und Kernel-Version an (conditional,
nur wenn vorhanden)
- Nützlich beim Verwalten mehrerer Boxen — ohne SSH sofort sehen
welches System gerade geöffnet ist
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- chrony.RenderToString(): rendert /etc/chrony/conf.d/edgeguard.conf
ohne Datei-Write oder Service-Reload
- wireguard.RenderToString(): kombiniert alle aktiven Interface-Configs;
PrivateKey + PresharedKey werden als <redacted> ausgegeben (sicher für
UI-Anzeige)
- main.go: beide in WithConfigPreviewers eingetragen
- Settings UI: chrony + wireguard im Generator-Dropdown
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SystemHandler.ExtraReloaders: neues Feld für nftables, wireguard,
squid, unbound, chrony Reloader
- WithAllReloaders(): Wire-Methode; main.go bindet alle Reloader nach
Initialisierung ein
- RenderConfigs: läuft alle Reloader durch, sammelt Fehler, antwortet
mit {ok, rendered[], errors{}} — partiell OK wird als warning gemeldet
- UI: Settings-Button-Label "Re-render all configs"; partial-error
Toast zeigt welche Dienste fehlschlugen
- Timeout: 30s statt 10s (alle Dienste brauchen mehr Zeit)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GET /wireguard/importable listet alle .conf-Dateien in /etc/wireguard/
mit already_in-Flag (bereits in DB)
- POST /wireguard/import nimmt optionale Names-Liste; ohne Namen → alles
- edgeguard-ctl wg-import [iface…] importiert nur die genannten Interfaces
- UI: Checkbox-Modal mit "Alle neuen auswählen" + Einzelauswahl;
bereits importierte Interfaces disabled + grüner Tag
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend: POST /dns/flush-cache (unbound-control flush_zone .)
POST /ntp/force-sync (chronyc makestep)
Beide werden im Audit-Log festgehalten.
UI: Schaltflächen in DNS-Settings und NTP-Settings neben Save,
mit Tooltip-Beschreibung + i18n (de+en).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ActionButtons: Edit-Button wird für Viewer wie Delete gesperrt (Tooltip zeigt Reason)
- Domains/Detail: isViewer-Flag an alle Sub-Panels weitergegeben; Save-, TLS-Cert-,
Routing-Rules- und Headers-Buttons für Viewer disabled
- Backends/Detail: Save-Button + ServerPanel Add-Button für Viewer disabled
- i18n: domains.backendUp/backendDown Keys (waren noch hardkodiert)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
wireguard.go: UpdatePeer zerstörte den gespeicherten encPriv wenn der Client
denselben public_key im Update-Body mitschickte (encPriv = nil auf Zeile 532).
Fix: encPriv wird nur geleert wenn der public_key sich tatsächlich ÄNDERT.
Andernfalls (gleicher Public Key = Benutzer ändert z.B. nur Allowed-IPs)
bleibt das verschlüsselte Private Key erhalten und QR/conf-Download
bleiben verfügbar.
domains.go + domainheaders.go: UpdateHeader() ignorierte die domainID aus der
URL. Fix: Update-SQL prüft jetzt WHERE id=$4 AND domain_id=$5 — falsch
adressierte Requests kriegen 404 statt blind zu updaten.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RemoteTargets: alle 20+ hardkodierten Form-Labels und deutschen Hilfetexte
(S3: Endpoint/Region/Bucket/AccessKey/SecretKey/PathPrefix/HTTPS;
SFTP: Host/Port/Username/Password/PrivateKey/RemoteDir/HostKeyFp)
sind jetzt in t()-Aufrufen mit remotes.s3.*/remotes.sftp.*-Keys.
NTP: Listen-Address-Dropdown-Labels (alle IPv4/IPv6-Interfaces, Loopback)
verwenden jetzt dns.settings.allIPv4/allIPv6/loopback (identisch zu DNS-Settings).
NTP-Pool-Adresse-Placeholder → t('ntp.pool.addressPlaceholder').
en/de: alle neuen Keys ergänzt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
haproxy: http_to_https=false Domains bekommen jetzt eigene use_backend-Regeln
im public_http-Frontend statt dem globalen HTTPS-Redirect. Das Feld war bisher
in DB + UI vorhanden aber vom Config-Generator komplett ignoriert.
unbound: Forward-Zones ohne forward-addr (ForwardTo=nil oder leer) werden jetzt
übersprungen. Unbound lehnt solche Blöcke beim Start ab.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wenn beim Anlegen eines neuen Peers ein Schlüsselpaar server-seitig
generiert wurde (has_private_key=true), öffnet sich das QR-Modal
direkt nach dem Speichern — kein manuelles Klicken nötig.
Außerdem: version-Fallback-Strings in allen drei Binaries auf 1.1.80 synct.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Makefile überschreibt via -ldflags; der Fallback war seit v1.0.78
eingefroren. Kein funktionaler Effekt, aber verwirrendes Graying beim
go run ohne Build.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- haproxy_stats.go: FRONTEND-Rows aus show-stat CSV auslesen und als
frontends[] zurückgeben (Name, Sessions, MaxSess, Bytes, ReqTot/Rate)
- Dashboard: Listener-Sektion über Backend-Liste; query jetzt unified
haproxyStats mit backends/frontends alias für Abwärtskompatibilität
- common.relTime.*-Keys (en+de) eingeführt; relativeTime/relativeFromIso
in Dashboard + relTime in WireGuard Servers/Clients auf t-Parameter
umgestellt statt hardcodierter Strings
- CertExpiry in Cluster-Seite nutzt jetzt certDaysRemaining/certExpiredDaysAgo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
relativeTime() in Dashboard, relTime() in WireGuard Clients+Servers und
CertExpiry() im Cluster-Page haben bisher hardcodiertes Deutsch
(„vor 5s", „abgelaufen vor X Tagen") gerendert, unabhängig von der
gewählten UI-Sprache.
Neu: gemeinsame i18n-Keys common.relTime.{Xs,Xm,Xh,Xd,never} in EN+DE,
sowie cluster.{certDaysRemaining,certExpiredDaysAgo}. Alle Helper-
Funktionen nehmen jetzt t() als Parameter und sind damit sprachunabhängig.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NTP-Pools, User-Accounts, Backend-Server im Detail-Panel und
Routing-Rules im Domain-Detail-Panel — alle aktiv-Felder sind
jetzt inline schaltbar, kein Modale nötig.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Inline Switch statt read-only StatusDot — konsistent mit allen anderen Seiten.
Server-Interfaces, Client-Interfaces und Peers im Peer-Drawer.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- internal/services/users: Repo mit CRUD, bcrypt (cost 12), Upsert für
setup-store-Admin-Migration, RecordLogin
- internal/handlers/users: GET/POST/PUT /users, POST /users/:id/password,
DELETE /users/:id; Schutz gegen Selbst-Löschung
- auth.go Login: DB-Nutzer first, Fallback auf setup-store-Admin;
bei erfolgreichem Fallback wird der Admin per Upsert in die DB
migriert (kein manueller Eingriff nötig)
- management-ui: /users-Seite mit Tabelle, Anlegen-, Bearbeiten-,
Passwort-setzen- und Löschen-Modals; "You"-Badge für eigenen Account
- Sidebar + Route + i18n (de/en) ergänzt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend: force_http1 (bool, default false) — zwingt HAProxy auf der
Backend-Verbindung zu HTTP/1.1 statt h2,http/1.1 zu verhandeln.
Nötig für Legacy-Apps die kein h2 sprechen.
Domain: disable_h3 (bool, default false) — unterdrückt Alt-Svc-
Response-Header für diese Domain. Browser erhalten keinen h3/QUIC-
Hinweis und bleiben auf h2/http1.1.
Migration 0027, Model+Service+HAProxy-Template+UI+i18n.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- WireGuard-Peer-Änderungen landeten nicht im laufenden Interface:
/etc/wireguard/ ist root:root 700, os.Readlink schlug fehl →
ensureWGQuickSymlink fiel immer in den Error-Pfad. Fix: Symlink
via sudo /bin/ln -sf (sudoers-Entry in postinst ergänzt).
- Site-to-Site-Masquerade: Roadwarrior-Clients (z. B. 192.168.99.3)
konnten LANs hinter anderen Peers nicht erreichen, weil das remote
Gateway die VPN-Client-IP nicht als Tunnel-Route kannte. Fix: auto
masquerade in nftables postrouting_nat pro WireGuard-Server-Interface
(oifname "wg7" ip saddr 192.168.99.0/24 masquerade).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Neues Feld 'client_routes' auf wireguard_interfaces: der Operator
trägt dort kommagetrennte Netzwerke ein (z. B. 10.0.10.0/24 für ein
LAN hinter der Box). Der Peer-Config-Download fügt diese automatisch
als zusätzliche AllowedIPs in den [Peer]-Block der Client-Config ein.
Bisher wurde nur das Server-Tunnel-Subnetz (ifc.address_cidr) als
AllowedIPs exportiert — Peers konnten so keine anderen Netze über
den Tunnel erreichen ohne die Config manuell anzupassen.
Migration: 0026_wg_client_routes.sql
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Zwei unabhängige Fixes für das WireGuard peer-to-peer Problem:
1. Startup-nftables-Render: edgeguard-api rendert beim Start die
nftables-Konfiguration neu. Damit werden Template-Änderungen aus
einem Update (z.B. 1.1.51 WG-forward-Rule) sofort aktiv, ohne
dass der Operator manuell eine Firewall-Mutation triggern müsste.
2. UI-Hint: Peer-AllowedIPs-Feld erklärt explizit warum /32 nötig
ist und was /24 kaputtmacht (Server routet ganzen Subnet-Block zu
einem Peer → andere Peers nicht mehr erreichbar).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>