Commit Graph

23 Commits

Author SHA1 Message Date
Debian
0ac91a7c59 feat: per-Backend timeout server + Alerts-Deeplink + Retention + Cert-Prune — v1.3.4
- feat(backends): per-Backend `server_timeout_seconds` (nullable, Default 60s).
  Rendert `timeout server <N>s` im HAProxy-Backend-Block — für langsam
  antwortende Upstreams (KI-/Inferenz-Server mit gepufferter Antwort).
  Migration 0044 (+CHECK 1..86400), Model/Repo/Template/UI + Render-Test.
- fix(ui): Dashboard-Alert-Karte verlinkt auf /alerts?tab=events; Alerts-Seite
  respektiert ?tab= (Deeplink landete bisher auf leerem Channels-Tab).
- feat(scheduler): alert_events-Retention (90d) im täglichen Cleanup-Tick —
  Schutz vor unbounded growth der node-lokalen Health-Event-History.
- fix(cluster): Cert-Sync prunt jetzt lokale .pem die der Primary nicht mehr
  hat (Waisen gelöschter Domains); schützt _default.pem + eigenen Node-Cert.
- fix(security): x/text v0.37→v0.39 (GO-2026-5970, Infinite-Loop; via ACME+goose
  aktiv aufgerufen — govulncheck-Release-Gate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 15:59:55 +02:00
Debian
79cd68e460 feat(domains): 301-Weiterleitung Domain→Domain (redirect_to) — v1.2.108
Neue Domain kann per 301 auf eine andere Domain/URL umgeleitet werden, statt
auf ein Backend zu routen (Use-Case: kvs.netcell-it.de → https://zkm.netcell-it.de,
inkl. HTTPS). Variante (a): immer auf Ziel-Root (`redirect location`), pfad-
unabhängig.

- Migration 0043: domains.redirect_to text NOT NULL DEFAULT ''
- Model + domains-Service (SELECT/INSERT/UPDATE/scan)
- HAProxy-Generator: buildRedirectTo() sanitisiert (nur http(s), kein
  Whitespace/Quotes → sonst kein Redirect statt kaputter Config); Template
  emittiert `http-request redirect location <url> code 301 if hdr(host)`.
  Terminiert vor use_backend → Redirect-Domain routet auf kein Backend.
  http→https läuft über den vorhandenen :80-Redirect (zwei Hops, inkl. TLS).
- UI: Feld „Weiterleitung (301) nach" im Domain-Formular (de/en)
- Tests: Render-Zeile + buildRedirectTo-Sanitisierung

Hinweis: Die Redirect-Domain braucht weiterhin ein eigenes TLS-Zert (ACME).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:23:37 +02:00
Debian
c83bb7b137 fix(haproxy): option tcplog + no option forwardfor entfernt — v1.2.76
HAProxy 3.x kennt weder 'option tcplog' in Backends noch 'no option forwardfor'
als Negation — beides ALERT-Fehler die HAProxy am Start hindern.
SPOE-Backend benötigt weder forwardfor noch tcplog.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 10:27:05 +02:00
Debian
8041e3924d fix(waf): no option forwardfor + timeout server 1s für SPOE-Backend — v1.2.74
SPOE-Backend ist TCP-Mode — forwardfor aus defaults-Block gilt nicht,
HAProxy wirft Warning. no option forwardfor explizit setzen unterdrückt das.
timeout server auf 1s angehoben (konsistent mit processing-Timeout).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 07:04:26 +02:00
Debian
05ac3344fa fix(waf): HAProxy-Reload nach WAF-Config + cfg-file → config — v1.2.72
- handlers/waf.go: Reloader-Func; nach Upsert wird HAProxy async
  neu gerendert (SPOE-Filter erscheint/verschwindet je nach enabled-Stand)
- main.go: haproxyReloader an NewWafHandler übergeben
- haproxy.cfg.tpl: cfg-file → config (HAProxy 3.0 kennt cfg-file nicht)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 22:10:55 +02:00
Debian
bf16ce6666 feat(waf): Phase 3 — HAProxy SPOE-Integration — v1.2.68
- haproxy.cfg.tpl: filter spoe + deny_status 403 + spoe-edgeguard-waf
  Backend (nur gerendert wenn WAFEnabled=true)
- haproxy.go: WAFEnabled in View; WafRepo.ListEnabled() prüft ob WAF
  aktiv; SPOE-Config-File (coraza-spoe.cfg) wird bei WAFEnabled
  atomar geschrieben; SPOEConfigPath konfigurierbar
- waf/spoe.go: uri statt path+query (HAProxy url-Sample = volle URI)

SPOE-Config definiert:
  - Agent: edgeguard-waf-agent, var-prefix=waf, timeout processing 50ms
  - Message: src, method, uri=url, ver=req.ver, headers=req.hdrs,
             host=req.hdr(host)
  - Backend: spoe-edgeguard-waf → 127.0.0.1:9000

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 15:53:07 +02:00
Debian
67c5180168 fix(haproxy-stats): add stats socket to template + fix safeAt index-0 bug
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>
2026-05-27 07:36:31 +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
4e01b4569c feat(ipv6): IPv6-Support für HAProxy + Settings-Toggle
- setup.State.IPv6Enabled + Store.SetIPv6Enabled()
- GET/POST /system/ipv6 im SystemHandler; HAProxy-Reload on save
- HAProxy-Template: bind [::]:80, [::]:443, quic6@:443, [::]:3443
  werden nur emittiert wenn IPv6Enabled=true
- haproxy.View.IPv6Enabled aus SetupStore befüllt
- Settings-UI: neues IPv6-Card (zwischen Auto-Update und Passwort)
- i18n de+en ergänzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 16:48:26 +02:00
Debian
8293783fe6 feat(backends+domains): HTTP-Protokoll-Flags — force_http1 + disable_h3
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>
2026-05-22 07:18:48 +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
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
1bb13e8107 fix(haproxy): check-alpn http/1.1 für HTTPS-Backends mit Healthcheck
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>
2026-05-12 13:45:47 +02:00
Debian
26f321de9d feat(backends): WebSocket-Toggle pro Backend
Migration 0017 fügt backends.websocket BOOL. Wenn aktiv emittiert der
HAProxy-Renderer `timeout tunnel 1h` IM Backend-Block; defaults-Section
hat den Global-Timeout dafür verloren. Backends ohne WS-Workload bleiben
bei strikten HTTP-Timeouts (Connection-Hygiene). Migrations-Heuristik
schaltet vm-pool/proxmox/console/vnc-Namen auto auf true damit Proxmox-
Konsole nach Deploy weiterhin durchläuft.

UI: Switch im Backend-Modal + WS-Tag in der Übersichtstabelle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:51:09 +02:00
Debian
da35097041 fix(haproxy): timeout tunnel 1h für WebSocket/noVNC
Proxmox-Console-Spinner-Bug — nach HTTP-Upgrade greift timeout tunnel
statt client/server. Default war fehlend → fiel auf timeout server 60s
zurück und kappte WS-Verbindungen. 1h hält Console-Sessions, SSH-WS-
Tunnel und ähnliche langlebige Streams stabil.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:31:28 +02:00
Debian
8aac24b566 feat(backends): Pool-Modell — Backend = Pool, N Server pro Backend
Migration 0016: backend_servers (id, backend_id, name, address, port,
weight, backup, active) + backends.lb_algorithm. Daten-Migration kopiert
bestehende backends.address/port als ersten Server, dann DROP COLUMN.

HAProxy-Renderer: rendert pro Backend einen Block mit `balance <algo>`
+ N `server`-Zeilen (weight, backup-Flag, optional check inter 5s).
LB-Algorithmen: roundrobin / leastconn / source.

REST: /backends/:id/servers (GET/POST), /backend-servers/:id (PUT/DELETE).
Re-rendert HAProxy nach jeder Server-Mutation.

UI: address/port aus Backend-Form raus, lb_algorithm-Select rein. Server
verwaltet ein expandable Sub-Panel pro Backend-Row (Tabelle + Add/Edit/
Delete-Modal). Domain-Attachment-Multi-Select bleibt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 20:55:47 +02:00
Debian
52da8d7c9e feat(haproxy): timeout client/server 30s → 60s
KVS / management-center brauchen länger als 30s für TOTP-Validierung
(externe Session-Storage). Erhöht beide auf 60s — sicher noch unter
keepalive-grenzen, deckt aber legacy-Auth-flows ab.

Plus: VERSION bump 1.0.20 (asg{1,2}-cleanup direkt am DB-State,
keine code-Änderung dafür).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:54:19 +02:00
Debian
6290cde45f fix(haproxy): backend.scheme auswerten (https → ssl verify none alpn h2,h1)
Bug: backends.scheme war im Datenmodell + UI vorhanden, aber der
HAProxy-Renderer hat das Feld komplett ignoriert. Jeder Backend
wurde als plain HTTP angesprochen — wenn das Upstream (nginx etc.)
HTTPS erwartet, kam '400 The plain HTTP request was sent to HTTPS
port' zurück, was im Browser als 404/Fehler erschien.

Fix im Template: server-Zeile bekommt 'ssl verify none alpn h2,http/1.1'
wenn .Scheme == "https". 'verify none' weil interne Backends meist
self-signed; ALPN deckt H1 + H2 via Aushandlung ab (also funktioniert
sowohl proxy_protocol=https als auch =h2 aus dem alten EdgeGuard).

Version 1.0.19.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:50:38 +02:00
Debian
0d51b26170 feat(haproxy): Admin-UI auf eigenem Port :3443 (mailgateway-Pattern)
* HAProxy neues Frontend mgmt_https :3443 → api_backend (Mgmt-UI).
  Selbe TLS-Cert-Strecke wie :443 (gleicher /etc/edgeguard/tls/-Pool).
* :443 verliert default_backend → unbekannte Hosts kriegen 503,
  nicht mehr versehentlich die Admin-UI. Plus default-Route auf
  primary_backend pro Domain (catch-all-Routing dort, wo gewollt).
* Anti-Lockout in nft-Template um tcp dport 3443 erweitert
  (zusätzlich zu 22 + 443).
* SystemRulesCard zeigt 3443 als 3. Anti-Lockout-Eintrag.

Erreichbarkeit:
* Public Backends: https://<domain>:443 (mit eigenem Cert oder LE)
* Admin-UI: https://<host>:3443 (jeder Hostname, default_backend)
* SSH: :22 (rate-limited 10/min)

Version 1.0.13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:37:53 +02:00
Debian
51ea1fc802 feat: Zonen als first-class Entity + Domain↔Backend-Verknüpfung sichtbar
* Migration 0012: firewall_zones (id, name UNIQUE, description, builtin),
  Seed wan/lan/dmz/mgmt/cluster als builtin. CHECK-Constraints auf
  network_interfaces.role + firewall_rules.{src,dst}_zone +
  firewall_nat_rules.{in,out}_zone gedroppt — Validation lebt jetzt
  app-side (Handler prüft Existenz in firewall_zones).
* Backend: firewall.ZonesRepo (CRUD + Exists + References-Lookup),
  /api/v1/firewall/zones, builtin geschützt (Name nicht änderbar,
  Delete blockiert), Rename eines Custom-Zone aktuell ohne Cascade
  (Handler-Sorge bei Rules/NAT/Networks).
* Handler-Validation in CreateRule/UpdateRule/CreateNAT/UpdateNAT +
  NetworksHandler: Zone-Existence-Check pro Mutation, 400 bei Tippfehler.
* Frontend: Firewall-Tab "Zonen" (CRUD mit builtin-Schutz). Networks-
  Form lädt Rollen aus /firewall/zones (statt hardcoded Liste); Rules-
  und NAT-Forms ziehen die Zone-Auswahl ebenfalls aus der API.
* Domain-Form bekommt Primary-Backend-Picker (Field war im Modell,
  fehlte im UI). Backends-Tabelle zeigt umgekehrt welche Domains
  darauf zeigen — bidirektionale Sicht ohne Schemaänderung.
* HAProxy-Renderer: safeID-FuncMap escaped Server-Namen mit Whitespace
  ("Control Master 1" → "Control_Master_1"). Vorher ist haproxy beim
  Reload an Spaces im Backend-Namen kaputt gegangen.
* Version 1.0.3 → 1.0.6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:05:27 +02:00
Debian
f0589e5628 feat(deploy): (d) end-to-end .deb install on 89.163.205.6 grün
Box (Debian 13 Trixie, amd64): apt install ./edgeguard-{api,ui,meta}.deb
zieht postgresql-17, haproxy 3.0, certbot, openssl, etc. nach.
postinst flow läuft sauber: migrate check → initdb → migrate up
(8 migrations) → render-config → install HAProxy drop-in → restart
haproxy → enable api+scheduler. Self-register in ha_nodes nach
Setup-Wizard funktioniert.

End-to-end smoke gegen 89.163.205.6:
* :80  → 301 Moved Permanently → https://
* :443 → TLS termination (self-signed _default.pem aus postinst)
        → JSON envelope vom api_backend (HTTP/2 + HSTS)
* /    → React index.html aus /usr/share/edgeguard/ui/

Änderungen:
* control: keydb-server von Depends nach Recommends — single-node v1
  installiert ohne KeyDB. Phase-3.1 multi-node bringt es zurück nach
  Depends sobald ein eigenes APT-Repo das Paket bereitstellt.
* postinst: render-config (--no-reload) + HAProxy-Drop-in installen +
  systemctl restart haproxy als zusätzliche Schritte.
* postrm: drop-in auf remove + purge entfernen, daemon-reload, ggf.
  haproxy auf distro-default zurückreloaden.
* deploy/systemd/haproxy-edgeguard.conf: Drop-in lenkt HAProxy-Unit
  auf /etc/edgeguard/haproxy/haproxy.cfg statt /etc/haproxy/haproxy.cfg.
  After=edgeguard-api.service vermeidet 503-Race in den ersten 5s.
* scripts/apt-repo/build-package.sh: shippt Drop-in unter
  /etc/edgeguard/systemd/haproxy-edgeguard.conf in der edgeguard-api.deb.
* haproxy.cfg.tpl: http-request redirect vor use_backend → keine
  HAProxy-Warning beim Parsen mehr.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:02:14 +02:00
Debian
914538eed1 feat(configgen): Phase 2 Config-Generator + nginx → HAProxy-only Pivot
Architektur-Pivot: nginx fällt komplett weg. HAProxy 2.8+ übernimmt
TLS-Termination, L7-Routing per Host-Header und LB. ACME-Webroot
und Management-UI werden von edgeguard-api ausgeliefert (Phase 3
implementiert die zugehörigen Handler); HAProxy proxied
/.well-known/acme-challenge/* und Management-FQDN-Traffic an
127.0.0.1:9443. Eine Distro-Abhängigkeit weniger, ein Renderer
weniger, sauberere Trennung.

Renderer (alle mit Embed-Templates + Tests):
* internal/configgen/  — atomic write + systemctl reload helpers
* internal/haproxy/    — :80 + :443, ACME-ACL, Host-Header-Routing,
                         Stats-Frontend, api_backend Fallback
* internal/firewall/   — default-deny input, stateful baseline,
                         SSH-Rate-Limit, :80/:443 accept,
                         Cluster-Peer-Set für mTLS :8443,
                         Custom-Rules aus PG
* internal/{squid,wireguard,unbound}/ — Stubs (ErrNotImplemented)

Orchestrator + CLI:
* internal/services/configorch/  — fester Reihenfolge-Run, Stubs
                                   sind soft-skip statt fatal
* cmd/edgeguard-ctl render-config [--no-reload] [--only=svc1,svc2]

Packaging:
* postinst: /etc/edgeguard/nginx raus, /var/lib/edgeguard/acme rein,
  self-signed _default.pem via openssl req (damit HAProxy startet
  bevor certbot etwas issuet hat)
* control: Depends nginx raus, openssl rein
* edgeguard-ui: dependency auf nginx weg, "Served by edgeguard-api
  gin StaticFS"

Live-Smoke: render-config gegen lokale PG schreibt /etc/edgeguard/
haproxy/haproxy.cfg + nftables.d/ruleset.nft korrekt; CRUD-Test aus
Phase 2 läuft weiter unverändert.

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