Commit Graph

16 Commits

Author SHA1 Message Date
Debian
5f92851a96 feat(radius): RADIUS-Server via FreeRADIUS (PAP/CHAP) — v1.2.93
Files-basierter RADIUS-Server (Clients + Users), managed analog DHCP/WireGuard.
- Migration 0042: radius_settings (singleton, node-lokal), radius_clients (secret_enc), radius_users (password_enc) — Secrets via secrets.Box verschlüsselt.
- internal/freeradius: Multi-File-Renderer (clients.conf + authorize) via Box.Open, Secret-Escaping (" \), Service default-off/an enabled gekoppelt. internal/services/radius + internal/handlers/radius.go: Settings + Client/User-CRUD, write-only Secret-Semantik, Validierung (IP/CIDR, name-charset), GET liefert secret_configured statt Secret.
- Firewall: udp 1812/1813 Auto-Rule bei enabled. Cluster: clients/users repliziert (hashSpec), radius_settings node-lokal.
- main.go + render.go + WithAllReloaders. Packaging: freeradius Dependency, setgid-Dir /etc/edgeguard/freeradius (Gruppe freeradius), Symlinks clients.conf+authorize, disable-on-install, sudoers.
- UI: RADIUS-Seite (Einstellungen + Clients + Benutzer) unter Sicherheit, Route/Nav/i18n de/en.
- Tests (guarded): Renderer-Inhalt + Secret-Escaping/Roundtrip + Masking. Scope v1: PAP/CHAP files-based (kein EAP/802.1X).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:46:42 +02:00
Debian
bac7c7e349 feat(dhcp): DHCPv4-Server via Kea (kea-dhcp4-server) — v1.2.92
Verwalteter DHCPv4-Server analog Unbound/Squid/Chrony.
- Migration 0041: dhcp_settings (singleton, node-lokal), dhcp_subnets, dhcp_reservations.
- internal/kea: Renderer baut Kea-JSON via Go-Struct→Marshal (garantiert valide), managed /etc/edgeguard/kea/kea-dhcp4.conf (Symlink von /etc/kea), Service-Lifecycle an enabled gekoppelt (default AUS, kein rogue DHCP). Interface per NAME (cluster-sicher, kein node-lokaler FK).
- internal/services/dhcp + internal/handlers/dhcp.go: Settings + Subnet/Reservation-CRUD, Validierung (CIDR/IP/MAC/interface exists).
- configgen: Stop/Enable/DisableService. Firewall: AutoFWRule.Iface → udp/67 pro LAN-Interface gescopt (kein WAN). Cluster: subnets/reservations repliziert (hashSpec), dhcp_settings node-lokal (localOnlyTables).
- main.go + render.go + WithAllReloaders Wiring. Packaging: kea-dhcp4-server Dependency, /etc/edgeguard/kea Dir, Symlink, disable-on-install, sudoers (restart/stop/enable/disable).
- UI: DHCP-Seite (Settings + Subnets + Reservierungen pro Subnet), Route/Nav/i18n de/en, HA-Warnung 'nur auf einer Node aktivieren'.
- Tests (guarded EG_FWTEST_DSN): Kea-Renderer gegen DB (valides JSON + Felder), FW-Auto-Rule-Iface inkl. nft -c. Scope v1: DHCPv4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 18:56:30 +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
99982ef9d1 feat(ui): Quick-Toggles, Config-Preview, Dashboard-Alerts, Domain-Detail-Health
Quick-Toggle-Switches (kein Modal nötig) für: Backends, Backend-Server,
DNS-Zonen, DNS-Records, Domains (active), Firewall-Rules, NAT-Rules,
Forward-Proxy ACLs, Routing-Rules.

Dashboard: Alert-Banner für komplett ausgefallene Backends (HAProxy-Stats)
und Domains im Maintenance-Mode.

Domain-Detail: HAProxy-Live-Health-Badge (15s Polling), TLS-Cert
ausstellen/erneuern direkt aus dem Detail, Routing-Rules-Panel inline.

Config-Preview (Settings): alle 4 Generatoren (haproxy, nftables, squid,
unbound) rendern via RenderToString ohne Disk-Write — GET /system/config-preview.

ActionButtons: Viewer-Rolle blendet Delete aus (RBAC-Ergänzung).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 08:55:22 +02:00
Debian
a490576420 feat(users): Multi-User-Management — DB-backed Login-Accounts + UI
- 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>
2026-05-23 10:22:27 +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
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
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
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
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
Debian
827c364335 feat(logs): Phase 4 — zentrales Logsystem /api/v1/logs + /system/logs
Aggregierter Reader für alle EdgeGuard-Service-Journale + audit_log.

internal/services/syslogs/
  - 9 Quellen: edgeguard-api, edgeguard-scheduler, haproxy, squid,
    unbound, chrony, wg-quick@*, ulogd2, audit
  - journalctl --output=json + parser für __REALTIME_TIMESTAMP,
    PRIORITY (0-7 → debug/info/warn/error), MESSAGE, _HOSTNAME
  - audit-Reader nutzt bestehende audit.Repo.ListRecent
  - Concurrent fan-out über alle gewählten Quellen, dann merge-sort
    by Timestamp DESC + cap auf Limit (max 1000)
  - Client-Filter: Level, Grep (case-insensitive über message +
    actor + action + subject)

internal/handlers/logs.go:
  GET /api/v1/logs            — Filter via Query-Params
  GET /api/v1/logs/sources    — statische Quellen-Liste fürs UI

postinst: edgeguard → systemd-journal + adm Gruppen, damit
journalctl ohne sudo lesen kann. Verifiziert auf der Box: id zeigt
`groups=adm,systemd-journal,haproxy,edgeguard`.

UI: management-ui/src/pages/Logs — Multi-Source-Select, Level-Color-
Tags, Time-Range-Picker, Volltext-Suche, Auto-Refresh 5s (Toggle),
CSV-Export. Sidebar-Eintrag "Logs" unter System (FileSearchOutlined).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:29:38 +02:00
Debian
66187e5b77 feat(firewall-log): Phase 3 — UI /firewall-live mit WS-Stream + Filter + CSV
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>
2026-05-12 21:14:43 +02:00
Debian
62505d547c feat(license): Lizenz-System mit Ed25519-Verify gegen license.netcell-it.com
Portiert mail-gateway/internal/license (Verify, Cache, Trial, Signature)
+ DB-Mirror (internal/services/license) + REST-Handler (status/verify/key/clear)
+ UI-Page /license (Activate, Status, Limits, Features, Re-verify)
+ <LicenseBanner /> neben UpdateBanner (trial-expiring, expired, verify-failed)
+ Scheduler: täglich Re-verify (24h-Tick)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 13:41:16 +02:00
Debian
ca03e69637 feat: Network/IP-Verwaltung + Mailguard-Design-Übernahme
Backend:
* Migration 0009_networks: network_interfaces (ethernet|vlan|bond|
  bridge|wireguard, role wan|lan|dmz|mgmt|cluster, parent + vlan_id
  für VLANs) + ip_addresses (interface_id FK, address+prefix, is_vip
  + vip_priority für Cluster-Failover-VIPs).
* Repos services/networkifs + services/ipaddresses + Models +
  Handler /api/v1/network-interfaces (CRUD + /:id/ip-addresses)
  und /api/v1/ip-addresses (CRUD).
* /api/v1/system/interfaces refactored auf Go-natives net.Interfaces()
  statt `ip -j addr show` shell-out — die systemd-Sandbox blockt
  AF_NETLINK auch für Go's runtime, deswegen edgeguard-api.service
  RestrictAddressFamilies um AF_NETLINK ergänzt. Output-Shape
  bleibt identisch (ifindex, ifname, flags[], mtu, link_type,
  address, addr_info[]) — Frontend muss nicht angepasst werden.

Frontend:
* Networks-Page (/networks): "System-discovered Interfaces"
  read-only Tags-Card oben, deklarierte Interfaces unten als
  Tabelle mit Modal-CRUD; Type-Switch zeigt parent+vlan_id-Felder
  bei type=vlan; Role-Tags farbig (wan blau, lan grün, dmz orange,
  mgmt purple, cluster magenta).
* IPAddresses-Page (/ip-addresses): Tabelle pro Interface, VIP-
  Toggle blendet vip_priority-Eingabe ein. Goldenes VIP-Tag in der
  Liste.
* Sidebar erweitert um Networks + IP-Adressen + section-grouping.

Design 1:1 von mail-gateway/management-ui/ übernommen:
* enterprise.css verbatim (Inter-Font via Google CDN statt local
  woff2), Sidebar 240px dunkler Gradient #0B1426→#101D33→#0D1829,
  branding-accent #1677ff für Active-State, abgerundete Cards mit
  shadow-Token, Header weiß mit subtilem backdrop-filter.
* AntD-Theme-Tokens: colorPrimary #0EA5E9, fontSize 13, fontFamily
  'Inter', controlHeight 34, borderRadius 6.
* Layout-Komponenten neu strukturiert: AppLayout/Sidebar/Header
  matchen mailguard-Klassen-Naming (.app-layout, .main-content,
  .sidebar-section, .sidebar-menu-item.active, .header-left, …).
* Sidebar mit 4 Sektionen (Übersicht / Routing / Netzwerk / System)
  + Logo-Header + Versions-Footer.

Live-deployed auf 89.163.205.6: Networks-Endpoint listet eth0
(89.163.205.6/24, MAC bc:24:11:64:29:e8) + lo, frontend zeigt sie
als System-Tags in der Networks-Page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:08:44 +02:00
Debian
b507d2a7d5 feat(ui): Frontend MVP — React 19 + AntD 6 + Vite + StaticFS-Wiring
Scaffold und Core-Infrastruktur 1:1 nach enconf-Pattern (netcell-
webpanel/management-ui), reduziert auf EdgeGuard-Scope (kein reseller/
customer-Roles, keine codemirror/extensions). Stack: React 19 + AntD 6
+ TS strict + Vite + TanStack-Query + zustand + react-i18next.

Layout: AppLayout (Sider+Header+Content), Sidebar (Dashboard/Domains),
Header (User-Dropdown + Logout). i18n mit de/en common.json.

Pages: Login (POST /auth/login), Setup-Wizard (POST /setup/complete),
Dashboard (Health-Polling + Statistics), Domains (volles CRUD via
TanStack-Query gegen /domains-API). UpdateBanner-Komponente
(/system/package-versions, alle 5 min poll, /system/upgrade trigger)
ist von Tag 1 wie vom User gefordert eingebaut.

API-Wiring: cmd/edgeguard-api/main.go mountUI() — gin StaticFS für
/usr/share/edgeguard/ui/ (overridebar via EDGEGUARD_UI_DIR), echte
Files werden direkt geserved, alle nicht-API-Pfade fallen via
NoRoute auf index.html für React-Router-SPA. Wenn dist/ fehlt:
HTML-Placeholder mit Build-Hinweis.

Verifiziert: bun install + npx tsc -b strict (0 errors) + bun run
build (12 chunks). End-to-end gegen /tmp/eg-api: / serviert echte
React-index.html, /domains SPA-Fallback, /api/v1/* JSON, /assets/*
direkt, /api/v1/nonexistent korrekt 404.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:16:04 +02:00