Files
sultan/CLAUDE.md
brandClaude Opus 5.5 c2d6dd694f sultan token tenant update: Mandanten nachträglich ändern
Weitere Compose-Stacks eines Kunden sind der Regelfall; bisher ließen sich
Domains und Routenlimit nur per delete + neu anlegen (und damit ohne Knoten)
ändern. Die Änderung liegt bewusst nur auf dem Server: Ein Agent darf sich
keine Domains selbst freigeben.

- Domains und Features werden hinzugefügt/entfernt statt ersetzt; unbekannter
  Eintrag beim Entfernen ist ein Fehler, die letzte Domain bleibt
- Verlängerung in die Zukunft setzt expired auf active (der Ablauf-Job setzt
  das nie zurück); Quarantäne und Zahlungsverzug bleiben
- Wechsel auf kommerziell mit vergangenem valid_until verlangt --valid-until

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 18:32:28 +01:00

86 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Instruktionen
Lies die `.ai/*.md` Files. Diese liefern den Context für die App, die hier entstehen soll.
Maßgeblich ist aktuell [.ai/sultan-system-architecture.md](.ai/sultan-system-architecture.md) (Netzwerk-, Sicherheits-, Daten- und Anwendungsmodell inkl. SQL-Schema, Event-Format und Referenz-Compose-Files). Bei Widersprüchen zwischen diesem File und der Spec gilt die Spec. **Architekturentscheidungen werden in der Spec nachgepflegt** (betroffene Abschnitte + Entscheidungslog in Abschnitt 8) – das gehört zu jeder Änderung, die Architektur festlegt.
## Projektstatus
Go-Modul `sultan` (Go 1.26). `main.go` ist nur eine Hülle um `pkg/cli`; umgesetzt sind `sultan server|agent|token|accountant|dump-landing|version|help`. Packages:
- [pkg/traefik](pkg/traefik) – übersetzt flache `traefik.*`-Labels (`FromLabels`, HTTP und TCP für TLS-Passthrough) in Traefiks dynamische Konfiguration, führt Knoten-Configs zusammen (`Merge`) und rendert JSON/YAML (`Marshal`) für `/v1/traefik-config`. Traefik *pollt* diesen Endpunkt über seinen HTTP-Provider; Sultan schreibt nie in Traefik und emuliert keinen Docker-Socket.
- [pkg/stream](pkg/stream) – Server→Agent-Kanal als **SSE**: `Hub` (Server, ein Stream pro Knoten, Keepalive `:\n\n`, Neuverbindung ersetzt alte mit `ErrReplaced`, `OnConnect`/`OnDisconnect` nur bei echtem Online/Offline), `Client` (Agent, Idle-Timeout 90s, `Run` mit exponentiellem Backoff, `Prepare` zum Signieren), `Decoder`. Authentifizierung macht der Aufrufer vor `Hub.Serve`. Agent→Server (`service_state`) läuft *nicht* über diesen Stream, sondern über eigene Requests.
- [pkg/store](pkg/store) – SQLite-Persistenz (Schema Spec 4.3) über `modernc.org/sqlite` (reines Go, kein cgo → statisches Binary). Migrationen sind eine append-only Liste in `store.go`, Version in `PRAGMA user_version`; neuere DB-Version → `ErrSchemaTooNew`. Zeitstempel werden aus Go gesetzt (injizierbare Uhr `Store.now`) und als UTC-Text fester Breite gespeichert, damit SQL-Vergleiche funktionieren – keine `CURRENT_TIMESTAMP`-Defaults verwenden. Enrollment-Tokens nur als SHA-256; `Enroll` verbraucht Token und legt Knoten atomar an. Fehler: `ErrNotFound`/`ErrExists`/`ErrInvalid`/`ErrTokenUsed`/`ErrTokenExpired`. `OpenDB(ctx, path, migrations)`, `FormatTime`, `ScanTime` stellen Migrationen und Zeitformat auch anderen Datenbanken bereit (`pkg/ledger`).
- [pkg/auth](pkg/auth) – Ed25519-Request-Signatur, **in jedem Tier Pflicht** (kein Bypass, auch nicht für `community`/Homelab). `Signer.Sign` (passt als `stream.Client.Prepare`) signiert Methode, Pfad+Query, Zeitstempel, Nonce und Body-Hash (`X-Sultan-*`-Header). `Verifier.Verify` prüft in der Reihenfolge TLS → Signatur → Zeitfenster (±5min) → Nonce → Store, liefert `Identity{Node, Tenant}`; die Knoten-ID für `Hub.Serve` kommt *nur* daraus. `VerifyUnregistered` für Enrollment (Besitznachweis des Schlüssels). Gesperrt (`ErrForbidden`) wird nur bei Quarantäne; Zahlungsverzug/Ablauf/Drain regelt die Policy über das Routing. `StatusCode(err)` für HTTP-Antworten. Schlüsseldateien: PKCS#8-PEM, 0600, werden nie überschrieben.
- **Kein Klartext:** `Signer` signiert nur `https`-URLs, `Verifier` lehnt Requests ohne `r.TLS` ab (`ErrInsecureTransport`, 426). Tests für Agent↔Server laufen daher über TLS-Testserver.
- [pkg/crypto](pkg/crypto) – Transport: `LoadOrCreateServerCert(dataDir)` (selbstsigniert ECDSA P-256, keine CA, 20 Jahre, `server.key` 0600; fehlt eine der Dateien → Fehler statt Neuerzeugung), `ServerTLSConfig` (nur TLS 1.3), `PinnedTLSConfig(fingerprint)` – Pinning im Handshake am Fingerprint, **unabhängig von der Adresse**; ohne Fingerprint `ErrUnknownServer` (kein blindes TOFU). Enrollment-Token `sltn_enroll_<secret>.<fingerprint>[.<base64url(server-url)>]` (`NewEnrollmentToken(fp, url)`/`ParseEnrollmentToken`, `ValidateServerURL`); in den Store kommt nur `Token.Secret`. Paketname kollidiert mit Stdlib-`crypto` – bei Bedarf beim Import aliasen.
- [pkg/license](pkg/license) – Lizenz nach Spec 4.1: `Issuer.Issue` (normalisiert + signiert mit Master-Key, Präfix `SULTAN-LICENSE-V1`), `Parse` (strikt, unbekannte Felder abgelehnt) / `Verify`, `AllowsDomain` (`*.x.de` = alle Subdomains, nicht Apex; `*.<tld>` verboten), `AllowsRoutes` (-1 = unbegrenzt), `HasFeature`, `Expired`/`DaysLeft`.
- [pkg/policy](pkg/policy) – `HostsFromRule` (echter Parser für Traefik-Regeln; jeder `||`-Zweig muss Host-beschränkt sein, `HostRegexp` verboten), `StageFor` (blocked/landing/paywall/firemark/watermark, Mahnwesen nur kommerziell) und `Engine.Apply`: prüft Router (Domain, Referenzen, Middleware-Gates, Routenlimit), Namensraum `<tenant>__<node>__<name>`, leitet bei Stufe ≠ normal oder Healthcheck-Ausfall zu `sultan__interceptor` um (Header `X-Sultan-Stage/Tenant/Tier`); bei watermark/firemark zusätzlich Durchlass-Router `sultan__<tenant>__<node>__<router>` per Cookie `sultan_ack=<stufe>`. `DunningSchedule` (8/25/32 Tage) berechnet die Mahnstufe. Wartung: `Input.Maintenance` (Knoten) und `Input.ServiceMaintenance` (Label `sultan.maintenance`), Vorrang Ablauf/Paywall > Wartung > Mahnhinweis, Header `X-Sultan-Until`/`X-Sultan-Public-Reason` (URL-kodiert). Andockstelle `MiddlewareTargetCheck` (+ `EnforceMiddlewareTargets`) für die geplante Middleware-Zielbegrenzung; nil = dulden. TCP (`checkTCPRouter`/`tcpOutput`, `HostsFromSNIRule`): nur Feature `tcp_stream`, `tls.passthrough`, Entrypoint `TCPEntryPoint`, explizite `HostSNI`-Hosts; zählt gegen `max_routes` (HTTP zuerst); kein Interceptor → Route weg ab Paywall und bei Wartung.
- [pkg/registry](pkg/registry) – Aggregator: `Update(nodeID, NodeState)` (vollständiger Schnappschuss: Workloads mit Host+Labels, `DownServices`), `Remove` (der Server ruft es beim Trennen nur ohne aktive Wartung auf; `pruneDisconnected` räumt nach Wartungsende), `Refresh` (zeitabhängige Stufen, Directory-Änderungen; fail-static bei Fehlern, `ErrUnknownNode` entfernt), `Config`, `Handler` für `/v1/traefik-config`. `OnViolation` meldet jeden Verstoß nur beim ersten Auftreten. Mandant/Lizenz kommen über das `Directory`-Interface (Server-Implementierung: `pkg/server/directory.go`). Verstöße entfernen nur den Router und landen in `Result.Violations`. Ergebnisse mehrerer Knoten mit `traefik.Merge` kombinieren. `State(nodeID)` liefert eine tiefe Kopie des gemeldeten Zustands inkl. `Meta`/`Host` (die das Routing nie beeinflussen); der Server reicht das als `Server.NodeState` durch.
- [pkg/server](pkg/server) – `sultan server`: `New` (Datenverzeichnis 0700, DB, Zertifikat, `master.key`), `Start`/`Run`/`Shutdown` (Hub zuerst schließen, sonst hängen SSE-Requests; Payee vor dem Schließen der DB), Routen beider Ports (`handlers.go`), Knotenstatus über Hub-Callbacks, Refresh-Schleife, Audit-Log „Policy-Verstoß“ per `slog`. Payee (`payee.go`, Spec 5.3/6c): `Config.AccountantToken`/`AccountantURL`, `payee.key` + `accountant.json`, Hintergrund-Schleife (`AccountantInterval` Default 10s, Senden nur bei geändertem Hash oder nach `AccountantHeartbeat` 1h), `buildReport`/`reportRoutes`; Konfigurationsfehler → `New` scheitert, Accountant nicht erreichbar → nur Log. Tests laufen gegen echte Ports (`127.0.0.1:0`) mit Test-Agent (Pinning + Signatur).
- [pkg/cli](pkg/cli) – `Run(ctx, args, stdout, stderr) int`; Flags/Defaults von `sultan server`; `sultan token` in `token.go` (Befehlstabelle `tokenCmds`, `usageError` → Exit 2, `openDataDir` verweigert nicht initialisierte Verzeichnisse). Version per `-ldflags "-X sultan/pkg/cli.Version=…"`.
- [pkg/docker](pkg/docker) – minimaler Docker-Engine-Client über Unix-Socket (`List` mit `all=1`, `Inspect` → Ports auch gestoppter Container, `StartedAt`, `Health`; `PortBindings` als Kurzform; `Info` → OS des Hosts und Docker-Version; `Events`), bewusst ohne SDK.
- [pkg/agent](pkg/agent) – `sultan agent`: `Enroll` (node.key, Pinning über Einladungs-Fingerprint, node.json mit `server_fingerprint`; Adresse: `--server`/`SULTAN_SERVER` > URL in der Einladung > node.json, neue Adresse wird nach erfolgreichem Verbinden übernommen), `Run` (Stream mit `OnConnect` → Zustand neu senden, Docker-Watcher, Sammelintervall, kein Senden bei Docker-API-Fehler), `Workloads` (Container-Port → veröffentlichter Host-Port, Default-Service, unerreichbare Ports weglassen; gestoppte Container bleiben mit ihren Services in `down` → Wartungsseite, entfernte verschwinden, laufende gewinnen bei Namensgleichheit), `backendHost` (explizit → Interface → Route zum Server). Dazu `/etc/sultan.d`-YAML (`sultand.go`: `ParseServices`, `LoadServiceDir`, `FileWorkloads` → Traefik-Labels; Polling über `dirFingerprint`) und Healthchecks (`health.go`: `checkHealth`, `healthTracker` mit Fehlerschwelle) → `DownServices`. Zusatzdaten fürs Inventar (Spec 5.3, ohne neue Labels): `Workload.Meta` aus Compose-Labels + `Inspect` (`meta` in `workloads.go`), `NodeState.Host` aus `hostinfo.go` (`detectFQDN` mit injizierbarem `resolver`, `netbirdIP`, `osPrettyName`, Docker `Info`); `--fqdn` überschreibt. Tests laufen gegen echten `pkg/server` mit gefälschtem Docker-Socket.
- [pkg/lander](pkg/lander) – Interceptor-Seiten je `X-Sultan-Stage` (eingebettete `templates/*.html`, zweisprachig), Cookie `sultan_ack` bei watermark/firemark, `localTarget` erlaubt nur lokale Pfade (html/template filtert `meta refresh` **nicht**!), eigene Vorlagen aus `<data-dir>/landing` mit eingebettetem CSS/Bild, `Dump`. Läuft im Server auf `--lander-addr`; die Interceptor-URL der Policy folgt der gebundenen Adresse. Test-Server immer mit `LanderAddr: "127.0.0.1:0"` starten (sonst Portkonflikt auf 8081).
**Leases & Befehle (Schritt 2):** `license.TermsFor`/`IssueLease`/`ParseLease` (Präfix `SULTAN-LEASE-V1`); der Server stellt Leases beim Verbinden und im Refresh aus (`pkg/server/leases.go`: `ensureLease`, `dispatchCommands`, `leaseValid`), getrennte Knoten behalten Routen als Wartungsseite bis Lease-Ende (`Disconnected` → Policy-Stufe maintenance), drain entfernt sofort. Hub signiert alle Events (`Hub.SigningKey`, `stream.SignEvent`/`VerifyEvent`); der Agent verwirft unsignierte (`pkg/agent/events.go`). Befehle: Warteschlange `node_commands` (CLI reiht ein), feste Liste `stream.Cmd*`; `rotate_key` über `node.key.new` + `POST /api/v1/rotate-key` (abbruchsicher). Signer im Agent nur über `currentSigner()` lesen (wird im Betrieb getauscht). Tests mit kurzen Leases über `Config.LeaseDuration`.
**Monitoring & Jobs (Schritt 3):** [pkg/checkmk](pkg/checkmk) (`Render(State, now)`, reine Funktion, ASCII) hinter `GET /metrics/checkmk`; [pkg/notify](pkg/notify) (`Slack`, `Ntfy`, `Multi`); `pkg/server/jobs.go`: `runJobs` bei jedem Refresh (kommerzielle Verträge → `expired`, Benachrichtigungen über `sendOnce` mit Schlüssel inkl. Bezugsdatum in Tabelle `notifications`, Aufräumen nach 10 Tagen). Policy-Verstöße werden asynchron gemeldet (OnViolation läuft unter der Registry-Sperre). Tests nutzen `Config.Notify` mit einem aufzeichnenden Ziel.
**Mehrere Adressen (Schritt 4):** `Config.PublicURLs` (gemerkt als `settings.public_urls`, `server.LoadPublicURLs`), Versand per `config`-Befehl in `nodeOnline`; Agent: `Identity.ServerURLs`, `serverCandidates`/`nextServerURL` (Wechsel in `OnError` des Stream-Clients), `a.serverURL` nur über `currentServerURL()` lesen.
- [deploy/](deploy) – Auslieferung: `Dockerfile` im Root (distroless, nonroot 65532, Multi-Arch, kein `ENTRYPOINT`, `WORKDIR /`), `build.sh` (Binaries amd64/arm64 + `SHA256SUMS` nach `dist/<version>/`), `push-image.sh` (liest `deploy/.env` mit `REGISTRY`/`REGISTRY_USER`/`REGISTRY_PASSWORD`, Vorlage `deploy/.env.example`; buildx → Zot-Registry `zot.chmln.de/sultan`; ohne Passwort Exit 1), `publish-static.sh` (rsync nach `sultan/releases/<version>/`), `compose/{server,agent}` (Agent mit `network_mode: host`). Version überall aus `git describe`; `-dirty` wird nicht veröffentlicht. `.dockerignore` hält `suldata/`, `.ai/`, `CLAUDE.md` aus dem Build-Kontext.
- [pkg/ledger](pkg/ledger) – `accountant.db` (Migrationen über `store.OpenDB`): `payees`, `invites` (nur Hash), `settings`; `AddInvite` (ID per `policy.ValidID`, registrierter Payee → `ErrExists`), `Enroll` (ID aus der Einladung, atomar), `ListPayees` (mit `LastReport`), `GetPayeeByPublicKey`, `DeletePayee`, `SaveReport(ctx, payee, body, []RouteRecord)` (letzter Bericht + Historie `route_spans` je Ort in einer Transaktion, v3)/`LatestReport`, `RouteHistory(HistoryFilter)`, `SetClock` (Tests); Kunden (`customers.go`: `EnsureCustomers`, `MapTenant`, `CustomerResolver`, Zahlweisen `Cycle*`, Pauschale in Cent), Katalog/Zuordnung (`products.go`: Klassen `Class*`, Versionen je Monat `JJJJ-MM`, `Catalog.At`, `Assignment`), Migrationen v4/v5. Fehler aus `pkg/store`.
- [pkg/accountant](pkg/accountant) – `sultan accountant` (Spec 5.3): `New` (Default `./accdata`, eigenes Zertifikat), `Start`/`Run`/`Shutdown`, TLS-Port `:8444`, `POST /api/v1/enroll` (`VerifyUnregistered`, Body `{token, hostname}`), `POST /api/v1/report` (`report.go`: Typen `Report`/`ReportTenant`/`ReportNode`/`ReportRoute`, `LabelBilling`; max. 8 MiB), `LoadPublicURLs`, Rate-Limit über `pkg/ratelimit` (auch vom Server genutzt). Checkmk (`checkmk.go`): Loopback-Port `MetricsAddr` (Default `127.0.0.1:8082`), `buildCheckmk` (reine Funktion: Zustände, bereinigte/escapte Felder + `.Unescaped`), `renderCheckmk` (eigene Vorlage `<data-dir>/checkmk/checkmk.tmpl`, Fallback + CRIT), `DumpCheckmk`. Abfragen (`where.go`): `RouteRecords`, `Where` (aktuell + vorher + verschwunden, mit Kunde), `Routes`, `ItemsForHost`. `StoreReport` (Bericht + Historie + automatische Kunden – vom Endpunkt und von Tests benutzen). Abrechnung (`billing.go`): `Billing`/`billFrom` (reine Regeln, Ortszeit), `WriteBillCSV`; Geld (`money.go`): `ParseMoney`/`FormatMoney` (Cent, Dezimalkomma). CLI in `pkg/cli/accountant.go` (`invite --payee`, `payee list|report|delete`, `dump-checkmk`, `where <query>` (Suchbegriff per `splitQuery` vor/nach Flags), `routes`, `customer …` (`customers.go`), `product …`, `assign`, `assignments`, `billing` (`products.go`); ohne Unterbefehl bzw. nur Flags = starten).
**CLI-Umgebung:** `pkg/cli/env.go` – `annotateEnv`/`applyEnv` machen jedes Flag als `SULTAN_<FLAG>` setzbar (nach `fs.Parse`, nur für nicht gesetzte Flags; Listen-Flags `stringList` kommagetrennt). Neue Flags bekommen das automatisch. Rate-Limit: `pkg/server/ratelimit.go` (`EnrollLimit`, `-1` = aus).
Betriebsdoku: `docs/community.md`, `docs/wartung.md`, `docs/hoster.md` – bei Verhaltensänderungen mitpflegen. Projekt gilt seit 2026-10-03 vorerst als fertig; offene Punkte (systemd-Agent und kommerzieller Betrieb ungetestet, Lander-Design, Vanity-Registry, GitHub geparkt) und Fachfragen: Spec Abschnitt 9. Nächster Baustein: **Accountant** (Spec 5.3, Roadmap Schritt 6; 6a–6c und 6e umgesetzt) – Schritt 6 komplett (6a–6e); offen u. a. HTTP-API (Dokuwiki), Mandanten-Statusberichte, Abo-Abgleich mit Lexware (geparkt) – Spec 5.3 und Abschnitt 9.
Tests werden zuerst als Vorgaben geschrieben, dann implementiert. Zeitabhängige Tests nutzen kurze Intervalle über die Konfigurationsfelder bzw. das injizierbare `Client.sleep`; mit `-race -count=30` auf Stabilität prüfen.
```bash
go build -o sultan . && ./sultan server # Daten in ./suldata (gitignored, enthält Schlüssel!)
go vet ./...
go test -race ./... # alle Tests
go test -run TestFromLabels_Errors ./pkg/traefik # einzelner Test
go test -cover ./...
```
Middleware-Optionen werden bewusst als unveränderte Label-Strings in verschachtelten Maps durchgereicht (Traefik parst sie beim Einlesen wie Labels); nur Router- und Service-Felder sind typisiert. Nicht unterstützte Labels (UDP, TCP außer Passthrough-Routern/Port, unbekannte Attribute) liefern `ErrUnsupportedLabel` statt still ignoriert zu werden. Der Agent rechnet Ports für `http` und `tcp` getrennt um (`protocols` in `workloads.go`).
## Big Picture
Sultan ersetzt manuell gepflegtes Ingress-Routing (z. B. Nginx Proxy Manager): Backends deklarieren ihr Routing selbst (Traefik-Labels an Docker-Containern oder Drop-ins in `/etc/sultan.d/*.conf` für Bare-Metal/LAMP), ein Agent meldet das an einen zentralen Server, und Traefik auf dem Ingress-Knoten pollt daraus seine Konfiguration.
**Single Binary in Go** mit drei Subcommands:
- `sultan server` – Registry, Stream-Hub (hält Agent-Verbindungen im RAM, Dead-Node-Timeout 90s), Policy-Engine, Traefik-HTTP-Provider
- `sultan agent` – Watcher (Docker Events API auf `traefik.*`-Labels bzw. inotify auf `/etc/sultan.d`), State-Serializer (injiziert LAN- oder NetBird-IP `100.64.x.y` als Backend-Ziel), Transport mit Reconnect + exponentiellem Backoff
- `sultan token` – CLI für Schlüssel, Enrollment-Tokens, Lizenzausstellung
**Ports / Endpunkte des Servers:**
- `:8443` (TLS) – Control Plane, ausschließlich *ausgehend* vom Agent (NAT/CGNAT/DS-Lite-tauglich). Persistenter SSE-Stream (entschieden, siehe `pkg/stream`), 45s-Keepalive, Events werden sofort gepusht. Enrollment: `POST /api/v1/enroll`.
- `127.0.0.1:8080` – `GET /v1/traefik-config` (Traefik pollt alle 2–5s; flache Labels aller aktiven Nodes → verschachteltes Traefik-JSON) und `GET /metrics/checkmk` (Checkmk Local-Check-Format).
**Sicherheitsmodell (Kerninvarianten):**
- Server-TLS-Zertifikat self-signed (ECDSA P-256, TLS 1.3), Agent pinnt den SHA256-Fingerprint (aus der Einladung, danach `server_fingerprint` in `node.json`) – adressunabhängig, kein blindes TOFU. Kein Agent-Verkehr im Klartext.
- Agent-Keys (Ed25519) werden nur lokal erzeugt und verlassen den Host nie; Enrollment tauscht Public Key + einmaligen Token gegen Mandanten-Zuordnung und signierten Capability-Lease. Danach ist jeder Heartbeat mit `node.key` signiert.
- Jeder Agent-Payload wird serverseitig gegen die Mandanten-Policy geprüft: Hosts in `Host(...)`-Regeln müssen in `allowed_domains` stehen (sonst ablehnen + Audit-Log), Routen über `max_routes` werden verworfen.
- Lizenzen (`license.json`) sind mit dem Ed25519-Server-Master-Key signiert.
**Stream-Events** (JSON: `event_id`, `timestamp`, `type`, `node_id`, `payload`): `service_state` (Agent→Server, vollständiger Label-Snapshot, nicht Delta), `node_command` (`re_enroll`/`drain`/`maintenance`), `lease_renew`, `error`.
**Persistenz:** SQLite unter `<data-dir>/sultan.db` (Default-Datenverzeichnis `./suldata`, auch beim Agent), Tabellen `tenants`, `nodes`, `enrollment_tokens` (Tokens nur als Hash gespeichert).
**Policy/Lizenzablauf:** Mandanten-Status steuert das generierte Routing – Zahlungsverzug → Router auf Bezahlseite (402), Backend down/`service_down` → Statusseite 503 mit `Retry-After: 60`. Checkmk-Schwellen: WARN bei 30, CRIT bei 14 Tagen Restlaufzeit, bei Ablauf Umleitung auf Landingpage.
## Unstimmigkeiten in der Spec
Bisherige Widersprüche sind aufgelöst und im Entscheidungslog der Spec (Abschnitt 8) dokumentiert. Neue Unklarheiten vor der Implementierung mit dem User klären statt raten.