Commit Graph

143 Commits

Author SHA1 Message Date
Debian
326f79cf00 feat(dashboard): WG server tunnels show peer summary instead of per-peer rows
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>
2026-05-26 20:01:49 +02:00
Debian
4406f9ac5b feat(scheduler): WireGuard-Client-Tunnel-Down-Alert + dedupe-Korrekturen — v1.1.110
- 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>
2026-05-26 19:16:03 +02:00
Debian
feae18772c feat(scheduler): NTP-Sync-Alert — warnt wenn chrony keine Zeitquelle hat — v1.1.109
- 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>
2026-05-26 17:40:16 +02:00
Debian
b9dfab6664 feat(scheduler): Conntrack-Alert — silent-drop Warnung bevor Tabelle voll läuft — v1.1.108
- 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>
2026-05-26 06:21:16 +02:00
Debian
ac6c580318 feat(scheduler+ntp): Memory-Alerts + NTP-Status freq/rms — v1.1.107
- 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>
2026-05-26 05:54:57 +02:00
Debian
99b4225e64 feat(alerts): backend.down trigger — HAProxy socket check every 2 min, 12h dedupe
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:30:49 +02:00
Debian
711c4c7eb1 feat(fwd-proxy): Squid cache stats card — GET /forward-proxy/stats + squidclient mgr:counters
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 16:47:31 +02:00
Debian
f29d74bad7 feat(fwd-proxy): move-up/down buttons for ACL priority ordering
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:36:22 +02:00
Debian
f6615fd27d feat(dns): Resolver stats tab + NAT rule move buttons
- GET /dns/stats via unbound-control stats_noreset: total queries,
  cache hits/miss, hit-rate progress bar, recursive replies, prefetch,
  rate-limited, unwanted, RRset/msg cache memory
- DNS page: new "Resolver stats" tab with 30s auto-refresh
- NAT rules: ↑↓ move buttons (same pattern as firewall rules 1.1.102)

v1.1.103

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:20:07 +02:00
Debian
844f9ddc83 feat(firewall): Move-up/down buttons for rule priority reordering
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>
2026-05-25 12:07:10 +02:00
Debian
4629679ba9 feat(ntp): Live peer status tab (chronyc sources)
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>
2026-05-25 11:57:04 +02:00
Debian
57b9cd89b2 feat(settings): Copy + Download buttons for config preview
v1.1.100

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:48:45 +02:00
Debian
757b58f607 feat(ux): Hostname in Sidebar + CPU-Anzahl neben Load Average
- 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>
2026-05-25 09:49:15 +02:00
Debian
c99b0cef4c feat(system): health-Endpoint liefert jetzt hostname + OS + kernel
- /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>
2026-05-25 06:43:56 +02:00
Debian
cfb0e9ed01 feat(config-preview): chrony + wireguard in Config-Preview verfügbar
- 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>
2026-05-25 06:28:48 +02:00
Debian
73619c17f8 fix(system): render-configs rendert jetzt ALLE Dienste, nicht nur HAProxy
- 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>
2026-05-24 22:29:00 +02:00
Debian
8d7a43bc8c feat(wireguard): selektiver .conf-Import — Auswahl einzelner Interfaces per Modal
- 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>
2026-05-24 22:09:37 +02:00
Debian
ac068bc9dd feat(dns+ntp): DNS-Cache-Flush + NTP-Force-Sync — operative Aktionen (1.1.94)
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>
2026-05-24 16:52:20 +02:00
Debian
0856c2fc10 feat(rbac): RBAC-Audit abschliessen — Viewer-Sperre für alle verbleibenden Seiten (1.1.93)
Domains, Backends (inkl. ServerPanel), Backups/History und Alerts:
isViewer-Check + disabled Switches, Tooltip-wrapped Add-Buttons,
bedingte Delete/Restore-Render (Viewer → disabled Tooltip statt Popconfirm).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 16:39:24 +02:00
Debian
d3a3b93c1b feat(rbac): Viewer-Rolle auf alle verbleibenden CRUD-Seiten durchsetzen — v1.1.92
DNS, NTP, ForwardProxy, RoutingRules, Networks (Interfaces + Routes),
IPAddresses, WireGuard (Servers + Clients + Peers), Backups/RemoteTargets, SSL:
Add-Buttons, quickToggle-Switches und Settings-Save-Buttons für Viewer deaktiviert.
ActionButtons-Komponente schützt Edit/Delete bereits zentral; hier wurden nur
die verbleibenden Mutationspunkte (Add, Switch, Submit) nachgezogen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 13:56:24 +02:00
Debian
d4267e2003 feat(rbac): Viewer-Rolle auf alle Firewall-CRUD-Seiten durchsetzen — v1.1.91
AddressObjects, AddressGroups, Services, ServiceGroups, Rules, NATRules, Zones:
Add/Edit-Buttons disabled + Tooltip, Delete bedingt gerendert (viewer vs. Popconfirm).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 13:20:54 +02:00
Debian
8b0e8a8ab4 fix(rbac): Viewer-Rolle in Settings + Cluster — operative Buttons sperren
Settings: HAProxy-Reload, Render-Configs, Backup-Now, Service-Restart,
E-Mail-Save, Maintenance-Toggle, Backup-/Audit-Retention, Auto-Update-
und IPv6-Toggle disabled für Viewer (Passwort-Änderung bleibt aktiv).
Cluster: Join-Token-Generation, Peer-Removal, mTLS-Cert-Renew disabled.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 13:14:51 +02:00
Debian
2d027b3044 fix(rbac): Viewer-Rolle in Detail-Pages + ActionButtons — Edit ebenfalls sperren
- 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>
2026-05-24 13:10:29 +02:00
Debian
9708e4441b fix(api): WireGuard Private-Key-Verlust bei Peer-Update + Domain-Header-Ownership
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>
2026-05-24 12:59:13 +02:00
Debian
1aaca2c056 fix(i18n): WireGuard QR/.conf Buttons + Backend 'Upgrade gestartet' → Englisch
- Servers.tsx: 'QR' und '.conf' Button-Labels in t() überführt (wg.peer.qr/conf)
- system.go: hardkodierter deutscher API-Response-String 'Upgrade gestartet'
  → 'upgrade started' (API-Responses sollen immer Englisch sein)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 12:55:36 +02:00
Debian
49e3e39ea6 fix(i18n): RemoteTargets S3/SFTP-Labels + NTP-Interface-Labels vollständig lokalisieren
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>
2026-05-24 12:52:13 +02:00
Debian
bb14d2ac6c fix(i18n): hardkodierte Strings in Cluster/Users/Backups/Alerts bereinigen
- Cluster: statusTag() bekommt t-Parameter; Status-Labels (online/offline/
  joining/leaving/unknown) → cluster.status.* Keys
- Users: 'You'-Tag → t('users.you') / de: 'Ich'
- Backups: 'OK'-Status-Tag → t('backups.okTag')
- Alerts: Kanal-Kind-Labels + alle SMTP-Formular-Labels in t() überführt
- en/de: cluster.status.*, users.you, backups.okTag, alerts.smtp.*,
  alerts.kindWebhook/kindEmail ergänzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 12:47:28 +02:00
Debian
6e52dc0620 fix(ui): fehlende onError-Handler + i18n in Backends/Domains/Dashboard
- Backends: create + del Mutations bekommen onError; 'Backup'-Tag → t()
- Domains: create + del Mutations bekommen onError
- Dashboard: LicenseChip-Texte ('License OK', 'Trial') in t() überführt;
  useTranslation() direkt in der Komponente verwendet
- en/de: dashboard.licenseOk/licenseTrial/licenseTrialDays ergänzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 12:42:37 +02:00
Debian
ca1ee64829 fix(firewall): fehlende onError-Handler + i18n in Rules + NATRules
- 6 Mutations (create/update/del je Rules + NATRules) bekommen onError-Handler
- Deutsche Placeholder-Texte → t() überführt
- Action-Labels (accept/drop/reject) und Kind-Labels (any/object/group/cidr)
  in t() überführt statt hardkodiert anzeigen
- CIDR-Label-Hardkodierung in Rules-Formular behoben
- en/de: fw.rule.actions.*, fw.rule.kinds.*, fw.rule.namePlaceholder,
  fw.nat.namePlaceholder ergänzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 12:40:08 +02:00
Debian
92eece936d fix(haproxy+unbound): http_to_https per Domain + leere Forward-Zones überspringen
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>
2026-05-24 12:36:53 +02:00
Debian
519522b8ee fix(i18n): hardkodierte Strings in DNS/RoutingRules/Settings/Backends bereinigen
- DNS Settings: Loopback/allIPv4/allIPv6 Interface-Labels in t() überführt
- RoutingRules: UP/DOWN-Tags + fehlende onError-Handler in create/update
- Settings: t-Variable-Shadowing in top_tables-Map behoben (t → tbl)
- Backends: 'Live'-Spaltenheader → t('backends.server.live')
- common.up/down + dns.settings.allIPv4/allIPv6/loopback in en+de ergänzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 12:30:43 +02:00
Debian
e04bddd574 feat(wireguard): QR-Modal nach Peer-Erstellung automatisch öffnen
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>
2026-05-24 12:25:31 +02:00
Debian
e37e472960 fix(i18n): License-Status-Tags + letzte hardkodierte Strings entfernt
- statusTag() akzeptiert jetzt t als Parameter — Aktiv/Trial/
  Abgelaufen/Ungültig gehen durch license.tag* Keys
- en+de common.json: license.tagActive/tagTrial/tagExpired/tagInvalid

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 12:20:03 +02:00
Debian
8c1963cac5 chore: fallback-version in scheduler + ctl auf 1.1.78 syncen
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>
2026-05-24 12:17:07 +02:00
Debian
35b7308ce2 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>
2026-05-19 16:18:41 +02:00
Debian
3178e25e78 feat(haproxy): X-Forwarded-Proto + X-Real-IP an alle Backends weiterleiten
User-Frage: „Werden via haproxy die echten IPs durchgereicht?". Antwort:
X-Forwarded-For ja (option forwardfor), aber Apps wie WordPress/Mailcow
brauchen zusätzlich X-Forwarded-Proto=https um Redirect-Loops zu
vermeiden, und X-Real-IP ist die bequeme single-value-Variante die viele
Tools out-of-the-box lesen (ohne die XFF-Chain parsen zu müssen).

Beide Frontends (public_https + mgmt_https) emittieren jetzt:
  http-request set-header X-Forwarded-Proto https
  http-request set-header X-Real-IP %[src]

Was Backends sehen:
  X-Forwarded-For:  <client-ip>             (defaults: option forwardfor)
  X-Forwarded-Proto: https                  (NEW)
  X-Real-IP:        <client-ip>             (NEW, single value)

PROXY-Protocol-Toggle pro Backend kommt nicht in diesem Release — der
Operator hat „nur Header-Variante" gewählt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 19:28:41 +02:00
Debian
a2d08eaa47 fix(wireguard): off-by-one im wg-show-Parser — UI zeigte verbundene
Peers als „offline"

Bug-Report: Unify-Home-Tunnel ist aktiv (handshake live, 2.8 GB rx),
UI zeigte „Letzter Handshake: nie / Traffic: — / Offline".

Root cause: handlers/wireguard.go Status() parsed `wg show all dump`
mit `PeerPublicKey: fields[2]` — fields[2] ist aber der pre-shared
key (PSK), NICHT der peer_pubkey. Format mit "all"-prefix ist:

  iface lines (5 cols):  iface priv_key pubkey listen_port fwmark
  peer  lines (9 cols):  iface peer_pubkey psk endpoint allowed_ips
                         last_hs rx tx keepalive

Damit matched die UI gegen den PSK → DB-Peer-Row (gespeichert per
pubkey) traf nie zu, Status blieb „nie/—" auch bei aktivem Tunnel.

Fix: fields[1] statt fields[2]. Endpoint/AllowedIPs/Handshake/RX/TX
waren bereits korrekt verschoben.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 19:09:38 +02:00
Debian
c79bfe84ec feat(auth): Self-Service-Admin-Password-Reset via CLI-Token
Operator hat Admin-Passwort vergessen aber SSH-Zugang zur Box →
schneller Reset ohne SMTP/Email-Setup.

Flow:
  1. `sudo edgeguard-ctl reset-password` auf der Box → 32-hex-Token
     + ISO-Expiry werden nach /var/lib/edgeguard/.reset-token (mode
     0600 edgeguard:edgeguard) geschrieben, Token kommt auf stdout.
     TTL: 30 min.
  2. Login-Seite hat „Passwort vergessen?"-Link → /reset-password.
  3. Reset-Page: Token + neues Passwort (min. 12). POST /auth/reset-
     password validiert Token (constant-time compare), prüft Expiry,
     löscht das File (single-use), hash't das Passwort + speichert
     in setup.json.

internal/services/setup/:
  - SetAdminPassword() — bcrypt-hash + save, fehler wenn setup nicht
    completed
  - GenerateResetToken() / ConsumeResetToken() — File-basiert,
    Format: "<token>|<RFC3339-expiry>"

internal/handlers/auth.go: POST /api/v1/auth/reset-password.
cmd/edgeguard-ctl/main.go: `reset-password` command.

UI: /reset-password Page mit Info-Alert für CLI-Snippet
(„sudo edgeguard-ctl reset-password" im dunklen Code-Block); Login-
Seite bekommt den „Passwort vergessen?"-Link.

Verifiziert auf 1.0.76: CLI druckt Token + schreibt File mit 0600
edgeguard:edgeguard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 19:04:25 +02:00
Debian
27ac7b53fc feat(backup): Off-Site-Upload nach S3 + SFTP
Schutz gegen Box-Total-Loss — lokale Backups in /var/backups/edgeguard
helfen nicht, wenn die Disk stirbt oder die Box brennt. Nach jedem
erfolgreichen lokalen Backup wird die tar.gz an alle aktiven
Off-Site-Ziele hochgeladen.

Migration 0022: backup_remotes (kind=s3|sftp, target_url, settings
JSONB, active, last_upload_at, last_error) + backups.remote_uploads
JSONB (per-Target-Result).

internal/services/backup/remote/:
  - UploadAll() — pro aktivem Target ein Upload, Failures non-fatal
  - S3 via minio-go/v7 — funktioniert mit AWS, MinIO, Backblaze B2,
    Cloudflare R2, Hetzner Object Storage (alle S3-API-kompatibel)
  - SFTP via golang.org/x/crypto/ssh + pkg/sftp. Password + Private-
    Key (OpenSSH, base64-encoded) als Auth. Optional host_key_
    fingerprint-Pinning (SHA256:...); leer = TOFU (unsicher vs MitM,
    OK für initial setup).
  - Test() lädt eine 1KB-Probe + löscht sie wieder — Operator-UI hat
    einen „Verbindung testen"-Button.

backup.Service.RemoteUploader-Interface: nach erfolgreichem
recordSuccess() läuft UploadAll, Results landen in backups.remote_
uploads JSONB. last_upload_at/last_error in backup_remotes pro Target
gepflegt. API + Scheduler injizieren beide den Adapter.

internal/handlers/backup_remotes.go: CRUD + POST /:id/test. Sensitive
Felder (secret_key, password, private_key) werden in GET-Responses
durch ***SET*** maskiert; UpdateChannel merged das zurück damit der
Operator bei Edit ohne Re-Eingabe speichern kann.

UI: Backups-Page jetzt mit Tabs "Sicherungen" + "Off-Site-Ziele".
Tab 2 hat CRUD-Tabelle mit kind-konditionalem Form (S3-Felder oder
SFTP-Felder), Test-Button pro Row, last_upload-Status mit FAIL-Tag
bei Errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:49:02 +02:00
Debian
81a8217493 feat(alerts): Health-Alarme via Webhook + Email-SMTP
Sidebar → System → Alarme.

Migration 0021: alert_channels (kind=webhook|email, target, settings,
active) + alert_events (kind, severity=info/warning/error/critical,
subject, message, sent_to JSONB).

internal/services/alerts/:
  - Fire(kind, severity, subject, message) — broadcastet an alle
    aktiven Channels + persistiert Event mit per-Channel-Result
    (ok/error) in sent_to.
  - Webhook-Sender: POST JSON {kind, severity, subject, message,
    content, text, fired_at}. Slack/Discord/Teams akzeptieren das
    out-of-the-box ohne Adapter (content + text-Felder gleichzeitig).
  - Email-Sender: net/smtp + STARTTLS optional. Settings (smtp_host,
    smtp_port, username/password, from, use_tls) liegen in
    channel.settings JSONB.

internal/handlers/alerts.go: CRUD + POST /alerts/test + GET
/alerts/events (history).

Scheduler-Trigger:
  - cert.expiring  — TLS-Cert <14 Tage Restzeit (12h-dedupe pro cert)
                     severity warning, <3 Tage → error
  - cert.renew_failed       — Renewer-Cycle hat fails
  - cert.renewer.run_failed — Renewer-Cycle abgebrochen
  - backup.failed  — Scheduled Backup error
  - license.invalid — License-Server liefert valid=false

In-process Dedupe (12h TTL, map[key]time.Time) verhindert dass
identische Alerts in Schleifen feuern.

UI (pages/Alerts): Tabs Channels (CRUD-Tabelle, Add-Modal mit
conditional-Email-Fields) + History (200 letzte Events mit
severity-Tag + per-Channel-Delivery-Status). Header-Button
„Test-Alert" feuert einen Test-Event in alle aktiven Channels.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:57:05 +02:00
Debian
4a34629023 feat(audit): Live-Stream im Dashboard via WebSocket
Recent-Activity-Karte zeigt neue audit_log-Events jetzt sofort statt
in 15s-Polls.

internal/services/audit/audit.go:
  - Repo bekommt Subscribe()-Methode mit fan-out-channel (Buffer 32,
    non-blocking-send — langsame Clients droppen Events statt die
    Pipeline zu blockieren).
  - Log() macht jetzt INSERT … RETURNING id, created_at und broadcastet
    den fertigen Entry an alle Subscribers. Broadcast nur nach
    erfolgreichem INSERT — failed inserts erscheinen nicht.

internal/handlers/audit.go:
  - Neuer GET /api/v1/audit/live (WebSocket): sendet beim Connect die
    letzten 50 Einträge (oldest→newest), danach Live-Stream aus
    Subscribe-Channel. 30s-Ping gegen HAProxy-Tunnel-Timeout.
  - Recent (Poll-Endpoint) bleibt für Fallbacks erhalten.

UI Dashboard:
  - useAuditLive(keep=15)-Hook ersetzt das 15s-useQuery-Poll.
  - WebSocket auf wss://<host>/api/v1/audit/live; Auto-Reconnect alle
    2s nach Drop.
  - dedupe per id (Snapshot + erste live-Events können sich kurz
    überschneiden während des Subscribe-Race).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:39:04 +02:00
Debian
5bdea1bced feat(diagnostics): UI-Tools — ping/traceroute/dig/curl/tcp
Operator-Diagnose ohne SSH. Sidebar → System → Diagnose.

internal/services/diagnostics/:
  - Ping        — ping -c4 -W2 (12s timeout)
  - Traceroute  — traceroute -n -w2 -q1 -m20 (60s timeout)
  - Dig         — dig +timeout=3 +tries=2 <TYPE> <NAME> (Types A/AAAA/
                  CNAME/MX/TXT/NS/SOA/PTR/SRV/CAA whitelisted)
  - Curl        — curl -IsSv --max-time 10 (nur http(s)://, kein
                  file:// / smb:// / data://)
  - TCPProbe    — nc -zv -w5 (8s timeout)

Sicherheit: validTarget() prüft jeden Input gegen Allow-List
[a-zA-Z0-9.:/_-]; verhindert Shell-Metachar-Injection. exec.Command
mit nackten Argument-Slices (kein /bin/sh, kein Glob-Expansion).

internal/handlers/diagnostics.go: POST /api/v1/diagnostics/<tool>
hinter requireAuth.

UI (pages/Diagnostics): 5 Tool-Cards, jede mit eigenem Input + Run-
Button + monospace-Output-Pane (dunkel, scrollbar, max-height 320px).
Pro Tool ein Status-Tag (OK / exit N) + Dauer-ms. info-Alert oben
erklärt dass Tools auf der Box laufen, nicht im Browser; security-
Alert unten erklärt die Restrictions.

control: iputils-ping, traceroute, dnsutils, curl, netcat-openbsd
als Depends. Auf Test-Box bereits da (waren Distro-defaults).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:30:07 +02:00
Debian
e07b484a48 feat(cluster): Config-Hash-Compute für Drift-Detection
Setzt die Foundation aus 1.0.70 fort — bisher war ha_nodes.config_hash
noch NULL und das UI konnte keinen Drift erkennen.

internal/cluster/confighash.go:
  - ComputeConfigHash() berechnet SHA-256 (truncated auf 16 hex chars)
    über alle replizierbaren Tabellen. Pattern 1:1 aus mail-gateway/
    internal/handlers/cluster_status.go (driftHashSpec).
  - Pro Tabelle: md5((to_jsonb(t) - id - updated_at - created_at -
    excludes)::text) per row, dann string_agg ORDER BY rh.
  - Singleton-Tabellen (dns_settings, ntp_settings, mail_config-Stil)
    hashen direkt ohne agg.
  - 23 Tabellen: domains, backends, backend_servers, routing_rules,
    network_interfaces, ip_addresses, tls_certs (mit ExtraExclude
    last_renewed_at + last_error damit cert-renewal keinen drift
    erzeugt), firewall_zones+address_objects+address_groups+services+
    service_groups+rules+nat_rules, wireguard_interfaces+peers,
    forward_proxy_acls, dns_zones+records+settings, ntp_pools+settings,
    static_routes.
  - RefreshLocalHash() schreibt den Hash in die eigene ha_nodes-Row.

Scheduler:
  - 5-min-Tick ruft RefreshLocalHash. Pro-Mutation-Refresh wäre zu
    teuer (jede UI-Action triggert sonst 23 jsonb-Queries).
  - Initial-Refresh beim Scheduler-Boot damit /cluster/status nicht
    5 min auf den ersten Wert wartet.

handlers/cluster.go:
  - Status() ruft RefreshLocalHash mit 2s-Timeout on-demand. Damit
    sieht das UI auch zwischen den Scheduler-Ticks immer frische
    Werte; bei Timeout fallback auf den DB-Wert (eventuell stale).

Verifiziert auf 1.0.71: ha_nodes-Row hat config_hash=728834dce5ca4e48,
scheduler-log "config-hash refresh enabled tick=5m0s".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 08:33:42 +02:00
Debian
ea7c356455 feat(cluster): Phase 3 Foundation — node.conf + ha_nodes-Drift + UI
Code-Vorbereitung für Multi-Node, ohne dass eine zweite Box nötig ist.
Single-Node-Mode bleibt der Default; alles existiert und wird sichtbar,
sobald ein 2. Knoten joined (Phase 3.2 später).

Migration 0020:
  ha_nodes += version (edgeguard-api-Version)
              config_hash (drift-Detection-Hash)
              mgmt_ip (Management-IP, niemals VIP)
              status (online|offline|joining|leaving|unknown)

internal/cluster/local_config.go:
  /etc/edgeguard/node.conf — INI-style, node-lokale Identität:
  NODE_ID, HOSTNAME, MGMT_IP, ROLE, PEER_HOSTS. NIEMALS zwischen
  Cluster-Peers replizieren. LoadLocalConfig / SaveLocalConfig /
  EnsureLocalConfig (auto-Generierung beim ersten Boot).
  MgmtIP-Default = firstNonLoopbackIPv4(); Operator kann
  überschreiben (mehrere Interfaces).

internal/cluster/store.go:
  - HANode-Model um die 4 neuen Felder erweitert
  - UpsertSelf nimmt jetzt mgmt_ip/version/config_hash/status, COALESCE
    erhält werte wenn der Caller sie nicht setzt
  - EnsureSelfRegistered-Signatur: + role + version-Argument

internal/handlers/cluster.go:
  GET /api/v1/cluster/status — strukturierter Endpoint:
    {local_id, local_node, peers[], mode, health, drift_found, updated_at}
  GET /api/v1/cluster/nodes bleibt für Tools.

UI (pages/Cluster):
  - Header zeigt Mode-Tag (Single-Node / Cluster) + Health-Tag (OK /
    degraded / split-brain)
  - Self-Card: Descriptions mit FQDN, Node-ID, Status, Role, Version,
    MGMT-IP, API-URL, Config-Hash
  - Peers-Tabelle nur wenn vorhanden, mit "drift"-Marker pro Row
  - Drift-Alert-Banner wenn ein Peer einen anderen config_hash hat
  - Single-Node-Mode Hinweis-Alert ("cluster-join kommt in 3.2")

postinst: leeres /etc/edgeguard/node.conf wird angelegt (chown
edgeguard); API auto-befüllt beim ersten boot.

main.go ruft EnsureLocalConfig + EnsureSelfRegistered mit version.

Verifiziert auf der Box (1.0.70):
  - /etc/edgeguard/node.conf hat NODE_ID, HOSTNAME, MGMT_IP=89.163.205.6,
    ROLE=primary
  - ha_nodes-Row: status=online, version=1.0.70, mgmt_ip=89.163.205.6

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 08:27:00 +02:00
Debian
df77b814ff feat(firewall): default-drop logging in input + forward chain
User-Feedback: das Live-Log zeigte nur die Smoke-Test-Snapshots von
gestern weil keine einzige Firewall-Rule den log-Flag hatte. „Das ist
kein Live-Log."

Fix: das nft-Template emittiert jetzt am Ende der input und forward
chain einen `limit rate 10/second log prefix "edgeguard:drop-*" group 0`
direkt vor dem default `policy drop`. Damit fließen ALLE Pakete die
keine Custom-Rule erlaubt hat ins Log — ohne dass der Operator pro
Rule den Log-Switch setzen muss.

limit rate 10/second burst 5: schützt vor Log-Floods durch Port-
Scanner, ohne die normale Visibility zu verlieren. Bei einer typischen
Edge-Box mit 99% Drop auf WAN-Inbound liegt das Volumen so bei ~300
Events/min = 5MB/h gzipped — logrotate keeps 14 days.

Reader: drop-input/drop-forward-Prefix wird NICHT als RuleID gemappt
(es gibt keine zugehörige Rule), Action explizit auf "drop". UI rendert
die mit eigenem Tag "default-input" / "default-fwd" (volcano-Farbe) in
der Rule-Spalte.

Verifiziert auf der Box: 26 echte Drop-Pakete in 5s nach Re-render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 07:15:23 +02:00
Debian
24c40bc776 refactor(fwlog): Live-Log als Child-Route /firewall/live statt Firewall-Tab
User-Feedback: Tab fühlt sich falsch an, will eine eigene Page mit
URL-Pfad unter /firewall.

UI:
- pages/Firewall/LiveLog.tsx → pages/FirewallLive/index.tsx
- FirewallPage entfernt den live-Tab aus tabs[]
- App.tsx routet /firewall/live → FirewallLivePage
- Sidebar: neuer Eintrag „Firewall-Log" eingerückt direkt unter
  „Firewall" in der Security-Section (child: true Flag → CSS-Klasse
  sidebar-menu-item--child mit padding-left 28px + dünnem vertikalem
  Trenn-Stab links). Sibling-Active-Logik exklusiv: /firewall matched
  NICHT mehr wenn /firewall/live aktiv ist.
- AppLayout PAGE_TITLES bekommt /firewall/live VOR /firewall damit
  der Title-Lookup den spezifischeren Pfad zuerst trifft.

Keine Backend-Änderungen.

Bekanntes Verhalten zu erklären: Im Live-Log sehen User aktuell nur
Smoke-Test-Events (oob.prefix=edgeguard:smoke / edgeguard:42, src/dst
127.0.0.1) — das sind die manuell-injizierten nft-Rules vom End-to-
End-Test der Pipeline. Reale Pakete fließen erst durch, wenn der
Operator auf einer firewall_rule den Log-Switch aktiviert (Firewall
→ Regeln → bearbeiten → Logging an). Aktuell hat keine einzige Rule
log=true.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 07:04:19 +02:00
Debian
b031725dfe feat(routes): Static-Routes-Management + Live-View (Networks-Tab)
Migration 0019: static_routes (id, destination, gateway, dev, metric,
table_name, active, comment).

internal/services/staticroutes/:
  - CRUD-Repo
  - Generator schreibt /etc/edgeguard/routes.conf (pipe-format) und
    triggert `sudo systemctl restart edgeguard-routes.service`
  - LiveAll() ruft `ip -j route show table all` und parsed JSON

internal/handlers/routes.go:
  GET /api/v1/routes           — managed (DB)
  POST/PUT/DELETE              — CRUD (re-render + apply on mutate)
  GET /api/v1/routes/live      — kernel-state via ip(8)

postinst:
  - /usr/sbin/edgeguard-apply-routes (root-owned shell-script). Liest
    routes.conf, flusht `proto 250` (= edgeguard), setzt neue Routen
    mit proto 250. Andere Quellen (kernel/dhcp/manuell) bleiben
    unangetastet.
  - /etc/systemd/system/edgeguard-routes.service (Type=oneshot,
    After=network-online.target). Beim Boot automatisch via
    multi-user.target.
  - /etc/iproute2/rt_protos.d/edgeguard.conf — Symbol "edgeguard" =
    250 damit `ip route show proto edgeguard` funktioniert.
    (Debian 13 hat kein /etc/iproute2 default → .d-Pattern statt
    rt_protos-Anhängen.)
  - sudoers: edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl
    restart edgeguard-routes.service

UI: Networks-Page jetzt mit Tabs (Interfaces + Routen). Routes-Tab
hat zwei Cards:
  - Live-Routen (read-only, 30s refresh, `proto edgeguard` farblich
    hervorgehoben)
  - Verwaltete Routen (CRUD-Tabelle, Add/Edit-Modal mit destination/
    gateway/dev/metric/table/active/comment)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:50:26 +02:00
Debian
dbc14a24a4 feat(backup): Restore-Pfad — POST /backups/:id/restore + UI
backup.Service.Restore(id) schreibt /var/lib/edgeguard/restore.sh
und dispatcht via `sudo systemd-run --unit=edgeguard-restore.service`.
Skript-Ablauf:
  1. tar -xzf der Backup-Datei → /var/lib/edgeguard/restore-tmp
  2. state-files (setup.json/license/jwt/node.conf/acme-account) per
     cp -a zurück, chown edgeguard
  3. systemctl stop edgeguard-api + scheduler (DB-Connections freigeben)
  4. sudo -u postgres psql -f dump.sql (--clean droppt + recreated)
  5. edgeguard-ctl render-config (haproxy/nft/squid/unbound/chrony)
  6. systemctl start edgeguard-api + scheduler
  7. rm -rf restore-tmp + restore.sh

UI: pro Backup-Row neuer Restore-Button mit Popconfirm. Beim Trigger
zeigt sich das vertraute Fullscreen-Overlay (Klassen .update-modal*
re-used) mit 4 Steps (Extract / DB-Restore / Render / Restart) + Live-
Timer. Health-Poll alle 3s detektiert API-Restart + reload. Safety-
Timeout 3 min für große DB-Dumps.

postinst: sudoers für `systemd-run --unit=edgeguard-restore.service
--description=... --collect bash /var/lib/edgeguard/restore.sh` +
zugehöriges `systemctl reset-failed`. Pfad fix damit kein Wildcard
nötig wird.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:22:55 +02:00
Debian
571f51ba9a feat(backup): pg_dump + state-tarball + daily auto + UI
Production-Box braucht Backups — bisher keine. Jetzt komplette
Pipeline:

Backend (internal/services/backup/):
  - Output: /var/backups/edgeguard/eg-YYYYMMDD-HHMMSS.tar.gz
  - Inhalt: dump.sql (pg_dump --clean --if-exists --no-owner --no-acl),
    files/setup.json, files/license_key, files/license.cache,
    files/.jwt_fingerprint, files/node.conf, files/acme-account/* +
    manifest.json (Version, kind, hostname, sizes)
  - sha256 während-write via TeeWriter, Size + sha in backups-DB-Row
  - Failure-Path: row mit status=failed + error, kein orphan-tarball
  - Prune(keepN=14) löscht erfolgreiche Backups älter als die letzten N

Migration 0018: backups(id, file, size, sha256, db/files bytes, kind,
status, error, host, started/finished).

Scheduler (cmd/edgeguard-scheduler):
  - 24h-Tick → backup.Run(KindScheduled) + Prune. Beim Boot wird ein
    initialer Backup NICHT sofort gezogen (kein nervöses Spam),
    sondern erst beim nächsten 24h-Slot.

REST (internal/handlers/backup.go):
  GET    /api/v1/backups              — list (newest first)
  POST   /api/v1/backups              — trigger manual (sync, audit'ed)
  GET    /api/v1/backups/:id          — single
  GET    /api/v1/backups/:id/download — sendfile tar.gz
  DELETE /api/v1/backups/:id          — entferne file + row

UI (management-ui/src/pages/Backups):
  - Liste mit Time, File+sha (first 16), Kind-Tag, Status, Size (mit
    DB + Files Aufschlüsselung), Dauer
  - „Backup jetzt erstellen" Button, Refresh, Download, Delete
  - Auto-Refresh 30s
  - Sidebar-Eintrag „Backups" unter System

postinst:
  - /var/backups/edgeguard 0750 edgeguard:edgeguard (enthält sensitive
    pg_dump + license_key → NICHT world-readable)
  - sudoers-Whitelist `sudo -u postgres /usr/bin/pg_dump --clean
    --if-exists --no-owner --no-acl edgeguard` — exakte Form

Verifiziert auf der Box: backups-Tabelle existiert, scheduler logged
„backup enabled tick=24h dir=/var/backups/edgeguard keep_n=14",
pg_dump-via-sudoers liefert 2808 lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:08:18 +02:00
Debian
9642a6adfe refactor(fwlog): Live-Log als Firewall-Tab, default-aus, Start-Button
UI-Restruktur nach User-Feedback:
- Sidebar-Eintrag „Firewall-Live" entfernt — gehört thematisch unter
  Firewall, kein Top-Level-Item. Standalone-Page /firewall-live raus.
- Neuer Firewall-Tab „Live-Log" zwischen NAT und Zonen.
- Default = AUS: zeigt Empty-State mit Start-Button. WebSocket
  verbindet erst nach Klick. Stop-Button schließt explizit.
- Filter-Inputs (src/dst/rule_id) jetzt 300ms debounced — vorher
  triggerte jeder Tastendruck einen WS-Reconnect.

Server-Pipeline „wirklich live" gepinnt:
- ulogd.conf NFLOG-Plugin bekommt qthreshold=1 + qtimeout=1. Default
  des Kernels batched Pakete bis 1024 oder 1s; mit 1/1 fließt jedes
  Paket sofort. Critical für die Wahrnehmung „live" statt „bursty".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:50:52 +02:00