- 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>
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>
Statt aller 15+ Felder in einem Modal gibt es jetzt eine eigene Seite
unter /domains/:id. Der Create-Flow bleibt im Modal (Name, Backend, Active,
HTTP→HTTPS), ein Hinweistext führt nach dem Speichern auf die Detailseite
weiter.
Die Detailseite enthält links das vollständige Settings-Formular (HSTS,
Rate-Limit, Maintenance, Max-Body, Notes, WWW-Redirect) und rechts das
HeadersPanel zum Verwalten custom HTTP-Response-Header.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ruleset.nft.tpl: operator-rules bekommen `counter <action> comment "egid:<id>"`
→ nft zählt Packets + Bytes per Regel ab dem letzten Ruleset-Apply
- handlers/firewall_counters.go: GET /firewall/counters parst
`sudo nft list table inet edgeguard` per Regex, liefert [{rule_id,packets,bytes}]
- handlers/firewall_counters_test.go: unit-tests für parseNFTCounters
- Firewall/Rules.tsx: neue "Hits"-Spalte (Packet-Count, Tooltip mit Bytes),
10s-Polling via TanStack Query ['fw','counters']
- i18n de+en: fw.rule.hits
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GET /haproxy/stats liefert jetzt req_tot (kumulierte Requests seit
HAProxy-Start) und req_rate (Requests/s im letzten Messfenster).
Backend-Detail zeigt in der Live-Spalte 'N sess · X/s' wenn Traffic
fließt; Dashboard-HAProxy-Card zeigt req/s ebenfalls an.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Domain-Attachments werden beim Speichern korrekt sync't
(syncDomainAttachments war in Detail.tsx vergessen worden)
- Server-Tabelle in der Detail-Seite zeigt jetzt eine 'Live'-Spalte
mit HAProxy-Status (UP/DOWN), aktive Sessions und Bytes per Hover-
Tooltip; Daten kommen aus dem gemeinsamen ['haproxy','stats']-Query
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
/backends/:id zeigt Pool-Einstellungen (links) und Server-Liste
(rechts) auf einer eigenen Seite. Der Edit-Button in der Liste
navigiert dorthin; Anlegen bleibt ein schlankes Modal ohne Server-
Panel. Außerdem: Backend-Detail lädt den Pool per GET /backends/:id
und speichert alle Felder direkt, keine Sync-Logik nötig.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GET /system/interfaces liefert jetzt rx_bytes/tx_bytes/rx_packets/
tx_packets/rx_drop/tx_drop aus /proc/net/dev. Die System-Interfaces-
Card in Netzwerk → Interfaces zeigt die Werte als Mini-Tabelle mit
10s-Refetch; Hover auf den Bytes-Werten zeigt Paketzähler + Drops.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Backends- und Routing-Rules-Seite zeigen jetzt beim Hover auf den
UP/DOWN-Chip die aggregierten HAProxy-Stats des jeweiligen Backends:
aktive Sessions + kumulierte Bytes rein/raus seit letztem HAProxy-Start.
Daten kommen aus dem bereits vorhandenen /haproxy/stats-Endpunkt.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Neue Page management-ui/src/pages/FirewallLive — Live-Tail der NFLOG-
Events aus /api/v1/firewall/log/live (WebSocket). Features:
- Status-Indicator (Live/getrennt), Auto-Reconnect alle 2s nach Drop
- Filter-Bar (action/proto/src/dst/rule_id) — bei Änderung wird der
WS neu verbunden, Server schickt frischen Snapshot
- Pause-Toggle: während Pause werden Events gebuffert (max 1000),
beim Resume in die Tabelle gemerged
- CSV-Export der aktuellen Tabelle (timestamp/rule/action/proto/src/
dst/iface/size)
- Color-coded Action-Tags (ACCEPT=grün, DROP=rot, REJECT=orange)
- Ring-Buffer 1000 im UI damit die DOM-Last bei Hochlast bleibt
- Sidebar-Eintrag "Firewall-Log" unter Sicherheit (Eye-Icon)
- DE/EN i18n
haproxy: backend api_backend bekommt `timeout tunnel 1h` damit der
WebSocket-Stream nicht nach `timeout server 60s` ohne Events stirbt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Foundation für Live-Log + Firewall-History (Logsystem Phase 1):
- nft-Renderer: `log prefix "edgeguard:<rule-id>" group 0` für Rules
mit log=true. Ohne `group` schrieb nft in kernel-log (dmesg), nie
in netlink → ulogd2 sah nichts.
- ulogd2 + ulogd2-json als Depends, postinst legt /etc/ulogd.conf
(NFLOG group 0 → /var/log/edgeguard/firewall.jsonl) + logrotate-
Profil (14d, daily, copytruncate) + enable/restart ulogd2.service.
- /var/log/edgeguard/ ist root:edgeguard 0640 — ulogd2 schreibt
(root), edgeguard-api liest (UI-Endpoints kommen in Phase 2).
End-to-End smoke-test bestätigt: ICMP echo → JSON-Line mit allen
Feldern (src_ip, dest_ip, oob.prefix, oob.in, icmp.*) in ~30ms.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- <aside> → <nav> als Root-Element
- Section-Label-Div NEBEN dem <ul> (vorher verschachtelt)
- <NavLink>-Callback → <Link> + location.pathname-Vergleich (Pattern
identisch zu netcell-webpanel/management-ui/src/components/Layout/Sidebar.tsx)
- nmg-prefix CSS-Variable `--nmg-sidebar-text` entfernt, Textfarbe
direkt #94A3B8 (enconf-Standard)
Färbung war schon enconf-konform (Gradient #0B1426→#101D33→#0D1829,
Akzent-Stab #1677ff mit color-mix tint), nur Struktur+Markup zogen
nach.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
L7TOUT-Bug: server-Stmt setzt `alpn h2,http/1.1` → Server handelt h2
aus → `option httpchk` sendet HTTP/1.x → Server antwortet nicht →
HAProxy markiert Backend DOWN → 503 für alle Requests. Fix: explizit
`check-alpn http/1.1` an die Server-Direktive wenn Scheme=https UND
Healthcheck aktiv. HTTP-only-Backends bleiben unverändert.
Bonus 1: Inter-Font lokal in public/fonts/ (DSGVO, Performance, Offline-
Dev) — Pattern 1:1 aus netcell-webpanel. Kein Google-CDN-Roundtrip mehr.
Test: TestRender_HTTPSHealthcheckPinsAlpnHTTP1 stellt sicher dass der
Pin gesetzt wird und HTTP-Backends KEIN check-alpn bekommen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>