docs(auth): add spec for mobile sign-in entry and app links

Spec for issue #49: sign-in prompt on the public home screen and
Universal Links / App Links for /auth/confirm on iOS and Android,
web fallback unchanged. Includes options, test strategy, task split
and tasks only Chris can do (Apple/Google accounts, domain files,
SITE_URL on prod).

Refs #49

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
cschlaefke 2026-11-03 19:46:25 +00:00
parent 8c003ddece
commit ef1fddfb72

View file

@ -0,0 +1,351 @@
# Spec #49 – Mobil: Einstieg in die Anmeldung und Rückweg aus E-Mail-Links (iOS/Android)
| | |
|---|---|
| Issue | #49 (`type/tech`, `prio/should`, Milestone R1), Refs #1 |
| Status | Entwurf, wartet auf Freigabe durch Chris |
| Autor | architect |
| Stand | 2026-11-03 |
| Abhängigkeit | KTO-01 (#1) umgesetzt; berührt QUE-09 (#29) Kriterium 13 und KTO-11 (#28); Betrieb nach ADR-0002 (Aufgabe D1 aus Spec #1, noch ohne eigenes Issue) |
| Quellen | Issue #49, `docs/specs/1-auth-registrierung-anmeldung.md` (E3, E4, § 5.3, § 5.4, § 6), `docs/auth-konfiguration.md` (AUTH-CFG-01, -02, § 3), `app/lib/core/routing/`, `app/lib/features/auth/presentation/providers/auth_redirect.dart`, `requirements/20-r1-akzeptanzkriterien.md` § QUE-09, § KTO-11, `requirements/r1-klaerungen.md` N9, ADR-0001, ADR-0002, Quelltext `supabase_flutter` 2.x (`SupabaseAuth`) und `app_links` 7.2.2 (iOS-Plugin) |
## 1. Ziel
Auf iOS und Android sollen Nutzer (a) aus der App heraus zur Anmeldung und Registrierung kommen und (b) mit den Links aus der Bestätigungs- und der Reset-Mail **in der App** landen, wenn sie installiert ist. Ohne App und auf anderen Geräten bleibt die Web-App der Zielort (Spec #1 E3, E4). Das Verhalten im Web ändert sich bei den Mail-Links nicht.
Befund (Issue #49, iOS-Simulator, main @ 455b3bf):
1. Die öffentliche Startseite (`HomeScreen`) hat keinen Weg zu `/login` oder `/register`. `privateRoutePaths` ist leer, deshalb leitet auch kein Guard dorthin. Auf Mobilgeräten sind die Auth-Screens dadurch nicht erreichbar.
2. Die Mail-Links zeigen auf `<SITE_URL>/auth/confirm?token_hash=…&type=email|recovery`. Die App beansprucht diese URLs nicht, deshalb öffnet der Link den Browser. Lokal zeigt `SITE_URL` auf `http://localhost:3000`, wo im Simulator keine Web-App läuft; dort lässt sich die Bestätigung gar nicht abschließen. In Produktion würde der Link in der mobilen Web-App funktionieren, der Nutzer wäre dann aber im Browser angemeldet und nicht in der App.
### 1.1 Optionen
**Einstieg in die Anmeldung**
| Option | Beschreibung | Vorteile | Nachteile |
|---|---|---|---|
| E-a | **Hinweis-Karte auf der Startseite** (abgemeldet): kurzer Text, „Anmelden“, „Konto erstellen“ | Entspricht QUE-09 Kriterium 13 („Hinweis mit dem Weg zum Anmelden und Registrieren“, N9); QUE-09 kann die Karte übernehmen; klein | Nur auf der Startseite sichtbar (bisher die einzige öffentliche Seite) |
| E-b | Aktion „Anmelden“ in der AppBar | Von jeder Seite mit AppBar erreichbar | Es gibt noch keine gemeinsame AppBar bzw. Shell; das wäre eine Shell-Entscheidung vor QUE-09/KTO-11; schwächeres Touch-Ziel und Text-Platz bei 200 % Schrift |
| E-c | Navigationsleiste mit Tab „Konto“ | Späterer Ort für KTO-11 | Vorgriff auf Navigation und Einstellungen ohne Inhalt; größerer Eingriff |
| E-d | Private Startseite (Guard leitet abgemeldet nach `/login`) | Kein UI nötig | Widerspricht QUE-09 Kriterium 13 und Sicherheit Kriterium 2 (Katalog öffentlich), Web-Startseite wäre nicht mehr öffentlich |
**Rückweg aus den Mail-Links**
| Option | Beschreibung | Vorteile | Nachteile |
|---|---|---|---|
| L-a | **Universal Links (iOS) und App Links (Android) auf der bestehenden https-Adresse `<SITE_URL>/auth/confirm`** | Ein Link für alle Geräte; ohne App öffnet derselbe Link die Web-App (Fallback ohne Zusatzaufwand); Mailvorlagen, GoTrue und Web bleiben unverändert; kein Fremdanbieter; Links sind an Domain, Team-ID und Signatur gebunden, eine fremde App kann sie nicht abfangen | Braucht Apple-Developer-Konto (Team-ID), Play-Signaturschlüssel, endgültige Domain und IDs sowie öffentlich erreichbare Zuordnungsdateien; lokal kaum Ende-zu-Ende testbar; manche Mail-Programme (z. B. Outlook Safe Links, In-App-Browser) lösen Universal Links nicht aus |
| L-b | Eigenes URL-Schema (`shuffle://auth/confirm?…`) direkt in der Mail | Lokal sofort testbar, keine Konten nötig | **Abgelehnt.** Ohne App öffnet der Link nichts, das widerspricht dem Fallback im Web (Kriterium 4) und dem geräteübergreifenden Ablauf (E3, Registrierung am Handy, Mail am PC); viele Mail-Programme machen Nicht-http-Links nicht klickbar; jede App kann dasselbe Schema registrieren und den `token_hash` abfangen, beim Reset-Link wäre das eine Kontoübernahme |
| L-c | Eigenes Schema als `redirect_to` über `{{ .ConfirmationURL }}` (GoTrue `/verify` leitet zur App weiter) | Supabase-Standardweg | **Abgelehnt.** Hebt E3 auf: Der Link würde beim ersten Aufruf eingelöst, Mail-Scanner verbrauchen ihn; geräteübergreifend nur mit PKCE-Verifier; Abfang-Risiko wie L-b; Allow-List müsste das Schema aufnehmen |
| L-d | Code statt Link (6-stelliges OTP `{{ .Token }}` in der Mail, Eingabe in der App) | Keine Domain-Dateien, lokal testbar | Widerspricht KTO-01 Kriterium 8 („Link“); neue UX und neue Mailvorlagen; 6 Ziffern sind ohne Drosselung je Konto ratbar (Drosselung nur je IP, AUTH-CFG-12) |
| L-e | Status quo: Link öffnet immer die Web-App, danach in der App anmelden | Kein Aufwand, funktioniert in Produktion | Erfüllt Kriterien 2 und 3 nicht; lokal im Simulator nicht abschließbar |
### 1.2 Empfehlung und Kernentscheidungen
| # | Entscheidung (Vorschlag) | Begründung |
|---|---|---|
| E1 | **Einstieg über eine Hinweis-Karte auf der Startseite (E-a)**, auf allen Plattformen inklusive Web, nur im Status `signedOut`. Die Karte ist ein eigenes Widget `SignInPrompt` in `features/auth/presentation/widgets/`, das QUE-09 später auf der neuen Startseite wiederverwendet. Keine AppBar-Aktion, keine Navigationsleiste. | Deckt Kriterium 1 und vorab den Anmelde-Teil von QUE-09 Kriterium 13 ab. Eine Shell oder Navigationsleiste ist eine eigene Entscheidung mit QUE-09/KTO-11. Gleiches Verhalten auf allen Plattformen ist einfacher als eine Weiche nur für Mobile. |
| E2 | **Universal Links und App Links (L-a), beschränkt auf genau den Pfad `/auth/confirm`.** `/reset-password` gehört **nicht** dazu: Es ist nie Ziel eines Mail-Links, die App erreicht es per Redirect nach `verifyOTP` (Spec #1 § 5.3). Alle anderen Web-Adressen öffnen weiter den Browser. | Kleinster nötiger Umfang. Spätere öffentliche Web-Adressen (Katalog, Spieldetail) sollen nicht ungeplant in der App landen. Eine Erweiterung ist eine eigene Entscheidung (F1). KTO-11 (E-Mail-Wechsel, `type=email_change`) nutzt denselben Pfad und profitiert ohne Änderung. |
| E3 | **Kein eigenes URL-Schema**, weder in Mails noch als Fallback (L-b, L-c abgelehnt). | Sicherheit (Abfangen des Reset-Tokens), Fallback ohne App, E3 aus Spec #1. |
| E4 | **Keine Backend-Änderung.** Keine Migration, keine Edge Function, Mailvorlagen und `GOTRUE_URI_ALLOW_LIST` (AUTH-CFG-02) bleiben unverändert. Einzige Bedingung: Der Host von `SITE_URL` (AUTH-CFG-01) ist genau die Domain, die die Zuordnungsdateien ausliefert und in den App-Projekten eingetragen ist. | Die Vorlagen bauen den Link aus `{{ .SiteURL }}` und `{{ .TokenHash }}`, nicht aus GoTrue `/verify?redirect_to=…`. Die Allow-List greift dabei nicht. |
| E5 | **Deep Links über das eingebaute Flutter-Deep-Linking an go_router**, kein zusätzliches Paket. `FlutterDeepLinkingEnabled` (iOS) und `flutter_deeplinking_enabled` (Android) werden ausdrücklich auf `true` gesetzt. `EmailLinkScreen` bleibt unverändert der Zielort, auch der Knopfdruck vor `verifyOTP` (Spec #1 E3) bleibt. | go_router verarbeitet Pfad und Query bereits (Route `/auth/confirm` existiert). `supabase_flutter` hört über `app_links` (transitiv vorhanden) mit, reagiert aber nur auf `access_token`, `code`, `error*` (Quelltext `SupabaseAuth._defaultIsAuthCallbackDeeplink`); unsere Links mit `token_hash` ignoriert es. Das iOS-Plugin von `app_links` gibt Links standardmäßig an andere Empfänger weiter (`defaultUrlHandling = .never`). Beides bestätigt der Spike S1 am Gerät bzw. Simulator. |
| E6 | **Ein Issue, zwei PRs:** PR A „Einstieg“ (sofort umsetzbar, Kriterium 1), PR B „App Links“ (Kriterien 2–4). PR B ist ohne Chris-Aufgaben mergebar (Dateien mit Platzhaltern, geprüft durch Tests), die Gerätebestätigung der Kriterien 2 und 3 hängt an den Chris-Aufgaben C1–C6 (§ 8.3). | PR A soll nicht auf Konten und Domain warten. |
| E7 | **Zuordnungsdateien liegen als Vorlage im Repo** (Team-ID und Zertifikat-Fingerabdrücke sind nicht geheim); ein Test prüft sie gegen Bundle-ID, Application-ID, Pfad und Host. Wo sie ausgeliefert werden (Web-Build oder Gateway), klärt S1. | Repo und Betrieb bleiben deckungsgleich (ADR-0002: Einstellungen im Repo festhalten). |
| E8 | **Host je Plattform an genau einer Stelle konfiguriert** (Android: Gradle-Property, iOS: xcconfig-Variable), Standard ist die Produktions-Domain (Platzhalter `shuffle.cshomelabs.work`, F8 aus Spec #1). | Domain ist noch Platzhalter; eine Änderung soll eine Zeile je Plattform sein. Staging-Builds sind offen (F5). |
## 2. Akzeptanzkriterien
Übernommen aus Issue #49 (Entwurf). Referenz in Tests und PRs: „#49 Kriterium n“.
1. Auf iOS und Android ist die Anmeldung aus der App heraus erreichbar.
2. Der Bestätigungslink aus der Mail öffnet die App und schließt die Registrierung ab.
3. Der Passwort-Reset-Link öffnet die App und führt in den Reset-Ablauf.
4. Verhalten ohne installierte App (Fallback im Web) ist beschrieben.
**Vorschläge zur Präzisierung (nicht entschieden, Freigabe durch Chris, siehe F10):**
- Zu 1: Bin ich abgemeldet, zeigt die Startseite einen Hinweis mit „Anmelden“ und „Konto erstellen“, die zu `/login` bzw. `/register` führen. Das gilt auch im Web. Bin ich angemeldet, erscheint der Hinweis nicht.
- Zu 2: Gegeben die App ist installiert und ich tippe auf dem Gerät auf den Bestätigungslink, dann öffnet sich die Bestätigungsseite in der App; nach dem Knopfdruck „E-Mail-Adresse bestätigen“ bin ich **in der App** angemeldet. Das gilt bei kaltem Start und bei laufender App.
- Zu 3: wie 2 für den Reset-Link; nach dem Knopfdruck führt die App zu „Neues Passwort“, danach gelten KTO-01 Kriterien 8 und 9 unverändert.
- Zu 4 (testbar formuliert): Gegeben die App ist nicht installiert, ich öffne den Link an einem anderen Gerät, oder das Mail-Programm übergibt den Link nicht an die App, dann öffnet sich die Web-App und schließt Bestätigung bzw. Reset ab wie bisher (Spec #1 § 5.4); nach einer Bestätigung im mobilen Browser sehe ich den bestehenden Hinweis „Du kannst dich jetzt auch in der App anmelden.“
- Zusätzlich 5: Im Web ändert sich das Verhalten der Mail-Links nicht (die bestehenden Playwright-Tests `web/*` bleiben unverändert grün).
**Relevante Querschnittskriterien:** QUE-05 Kriterien 2 und 4 (neue Texte in ARB, de/en); QUE-06 Kriterien 1–4 (Karte: Beschriftung, Kontrast AA, Touch-Ziele ≥ 48 dp, 200 % Schrift, Tastatur im Web); Sicherheit der Daten Kriterium 2 (Startseite bleibt öffentlich, keine privaten Daten); Plattformen Kriterien 1 und 2.
## 3. Datenmodell
Keine Änderung. Keine neuen Tabellen, Spalten, Policies oder pgTAP-Tests.
**RLS:** unverändert. Der Deep Link überträgt nur `token_hash` und `type`; eingelöst wird er wie bisher per `verifyOTP` gegen GoTrue.
## 4. Backend
### 4.1 Supabase / GoTrue
| Punkt | Wert | Änderung |
|---|---|---|
| Mailvorlagen (AUTH-CFG-17, -18) | Link `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email` bzw. `…&type=recovery` | **keine** |
| `GOTRUE_SITE_URL` (AUTH-CFG-01) | Prod: `https://<Produktions-Domain>` | Bedingung: Host = Associated Domain (APPLINK-01). Auf Prod setzt das Chris (C5). |
| `GOTRUE_URI_ALLOW_LIST` (AUTH-CFG-02) | `<SITE_URL>/auth/confirm` | **keine** (kein `redirect_to` im Einsatz) |
| lokal `config.toml` | `site_url = "http://localhost:3000"` | **keine**; lokal gibt es keine Universal Links (siehe 6.2) |
| RPCs / Edge Functions | – | keine |
### 4.2 Neue Konfigurations-IDs (Abschnitt in `docs/auth-konfiguration.md`)
Neuer Abschnitt „App Links / Universal Links“ mit eigenen IDs, damit Tests und Release-Checkliste darauf verweisen. Die bestehenden AUTH-CFG-IDs bleiben unverändert.
| ID | Einstellung | Wert | Ort |
|---|---|---|---|
| APPLINK-01 | Associated Domain | Host von `SITE_URL` (Prod), Platzhalter `shuffle.cshomelabs.work` | Android: Gradle-Property `shuffleAppLinkHost` (Standard in `android/gradle.properties`), über `manifestPlaceholders["appLinkHost"]`; iOS: `APP_LINK_HOST` in `ios/Flutter/AppLinks.xcconfig`, eingebunden in `Debug.xcconfig` und `Release.xcconfig` |
| APPLINK-02 | Beanspruchter Pfad | genau `/auth/confirm` (mit beliebiger Query) | Android-Intent-Filter, AASA `components`, assetlinks (Android verifiziert je Host, der Pfad steht im Intent-Filter) |
| APPLINK-03 | iOS-App-ID | `<TEAM_ID>.work.cshomelabs.shuffle` (Bundle-ID Platzhalter, R15 aus Spec #1) | `apple-app-site-association` |
| APPLINK-04 | Android-Paket und Signatur | `work.cshomelabs.shuffle`, SHA-256 des Upload-Schlüssels **und** des Play-App-Signing-Schlüssels | `assetlinks.json` |
| APPLINK-05 | Auslieferung | `https://<Domain>/.well-known/apple-app-site-association` und `/.well-known/assetlinks.json`: HTTPS mit gültigem Zertifikat, Status 200 **ohne Weiterleitung**, `Content-Type: application/json`, öffentlich erreichbar (Apple lädt über sein CDN), **vom SPA-Rewrite ausgenommen** | Gateway bzw. Web-Hosting (D1, C5) |
| APPLINK-06 | Flutter-Deep-Linking | `FlutterDeepLinkingEnabled = true` (Info.plist), `flutter_deeplinking_enabled = true` (Manifest) | App-Projekte |
### 4.3 Zuordnungsdateien (Vorlagen)
Ablageort laut S1: bevorzugt `app/web/.well-known/` (wenn `flutter build web` versteckte Verzeichnisse mitkopiert und das Hosting sie korrekt ausliefert), sonst `deploy/well-known/` mit Auslieferung am Gateway. Platzhalter werden im Repo durch die echten Werte ersetzt, sobald C1–C3 erledigt sind.
`apple-app-site-association` (ohne Dateiendung):
```json
{
"applinks": {
"details": [
{
"appIDs": ["<TEAM_ID>.work.cshomelabs.shuffle"],
"components": [
{ "/": "/auth/confirm", "comment": "E-Mail-Links (Bestätigung, Reset, später E-Mail-Wechsel)" }
]
}
]
},
"webcredentials": {
"apps": ["<TEAM_ID>.work.cshomelabs.shuffle"]
}
}
```
`assetlinks.json`:
```json
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "work.cshomelabs.shuffle",
"sha256_cert_fingerprints": ["<UPLOAD_KEY_SHA256>", "<PLAY_APP_SIGNING_SHA256>"]
}
}
]
```
`webcredentials` (iOS) und `delegate_permission/common.get_login_creds` (Android, optional) verbinden die Passwortspeicher von App und Web. Ob sie aufgenommen werden, entscheidet F2 (Empfehlung: `webcredentials` ja).
### 4.4 Fehlerfälle
Keine neuen Server-Fehler. Alle Fehler beim Einlösen laufen wie bisher über das Mapping aus Spec #1 § 4.4 (`linkInvalidOrExpired`, `network`, `rateLimited`, `unexpected`). Ein Link einer anderen Umgebung (z. B. Staging-Link in der Prod-App) ergibt beim Einlösen `linkInvalidOrExpired`; das ist harmlos.
## 5. Frontend
### 5.1 PR A: Einstieg (Kriterium 1)
**Neues Widget `SignInPrompt`** (`app/lib/features/auth/presentation/widgets/sign_in_prompt.dart`):
- Inhalt: kurze Überschrift und ein Satz (Vorschlag ui-designer, z. B. „Melde dich an, um deine Sammlung und Partien zu verwalten.“), Primärknopf „Anmelden“ → `context.go(AppRoutes.login)`, Sekundärknopf „Konto erstellen“ → `context.go(AppRoutes.register)`. Kein `from`-Parameter (Ziel nach dem Login ist ohnehin `/`).
- Komponenten aus `core/widgets` (`AppPrimaryButton`, für den Sekundärknopf gegebenenfalls eine neue Variante durch ui-designer). Touch-Ziele ≥ 48 dp, Semantics über die Knopf-Labels, Überschrift als `Semantics(header: true)`.
- Dumm: bekommt keine Logik, nur Navigation per Callback oder `context.go`.
**`HomeScreen`** wird `ConsumerWidget`:
- Liest `authStatusProvider`. Zustände:
- `signedOut` → Platzhaltertext und darunter `SignInPrompt`.
- `signedIn` → Platzhaltertext wie bisher, **kein** Prompt (keine Abmelden-Funktion, siehe F4).
- `passwordRecovery` → tritt auf `/` nicht auf (Redirect nach `/reset-password`).
- Laden (Stream ohne ersten Wert) → Platzhaltertext ohne Prompt, damit beim Start mit gespeicherter Sitzung der Prompt nicht kurz aufblinkt. Der Wert kommt praktisch sofort (`initialSession`).
- Fehler im Stream → wie `signedOut` behandeln (Prompt anzeigen), Fehler nur ins Log.
- Die Entscheidung „Prompt ja/nein“ trifft ein kleiner Provider `showSignInPromptProvider` (bool) in `features/auth/presentation/providers/`, nicht das Widget. Damit bleibt der HomeScreen ohne Logik und QUE-09 übernimmt Provider und Widget.
- Breite Bildschirme: Karte max. 480 dp breit, zentriert (wie `AppFormScaffold`).
**Texte (ARB, de/en):** `signInPromptTitle`, `signInPromptBody`, `signInPromptSignIn`, `signInPromptRegister`. Bestehende Schlüssel für „Anmelden“/„Konto erstellen“ dürfen wiederverwendet werden, wenn der Wortlaut passt.
### 5.2 PR B: App Links (Kriterien 2–4)
**Keine neue Route, kein neuer Screen.** Der Deep Link landet auf der bestehenden Route `/auth/confirm` (`EmailLinkScreen` mit `token_hash` und `type`).
Abläufe in der App:
| Situation | Verhalten |
|---|---|
| Kaltstart über den Link | Flutter übergibt `/auth/confirm?token_hash=…&type=…` als Startroute; go_router nutzt sie statt `initialLocation` (S1 bestätigt; sonst `overridePlatformDefaultLocation` prüfen). Redirect-Regeln wie bisher: `signedOut`/`signedIn` → bleibt; `passwordRecovery` → `/reset-password`. |
| App läuft (warm) | Flutter meldet die neue Route, go_router ersetzt den Stack (`go`). Nicht gespeicherte Eingaben auf dem vorherigen Screen gehen verloren (R10). |
| Bestätigung erfolgreich | Hinweis „E-Mail bestätigt“, Knopf „Weiter“ → `/`. Nutzer ist in der App angemeldet, die Startseite zeigt keinen Prompt mehr. Der Zusatz „auch in der App anmelden“ erscheint in der App nicht (`isMobileWebProvider` ist dort `false`). |
| Reset erfolgreich | Status `passwordRecovery` → `/reset-password` (Spec #1 § 5.4, 5.5 unverändert, Guard nur im Prozess). |
| Link ungültig/abgelaufen | Fehlerzustand mit „Neuen Link anfordern“ wie bisher. |
| Zurück-Taste (Android) auf `/auth/confirm` nach Kaltstart | Kein Eintrag darunter: Die App schließt sich. Akzeptiert; „Weiter“ führt zur Startseite. |
| Link öffnet, während ein anderes Konto angemeldet ist | `verifyOTP` ersetzt die Sitzung, wie heute im Web (F3). |
**Plattform-Dateien** (Umsetzung flutter-dev):
- `android/app/src/main/AndroidManifest.xml`: in `MainActivity` zusätzlicher Intent-Filter mit `android:autoVerify="true"`, `action VIEW`, `category DEFAULT` und `BROWSABLE`, `data android:scheme="https" android:host="${appLinkHost}" android:path="/auth/confirm"`. Dazu `meta-data flutter_deeplinking_enabled = true`. `launchMode="singleTop"` bleibt (warme Links kommen über `onNewIntent`).
- `android/app/build.gradle.kts`: `manifestPlaceholders["appLinkHost"]` aus der Gradle-Property `shuffleAppLinkHost` (APPLINK-01).
- `ios/Runner/Runner.entitlements` (neu): `com.apple.developer.associated-domains` = `applinks:$(APP_LINK_HOST)` und (F2) `webcredentials:$(APP_LINK_HOST)`; `CODE_SIGN_ENTITLEMENTS` im Xcode-Projekt für alle Build-Konfigurationen setzen. Für Tests auf Geräten im Entwicklermodus optional `applinks:$(APP_LINK_HOST)?mode=developer` (umgeht den Apple-CDN-Cache), **nur** in Debug.
- `ios/Runner/Info.plist`: `FlutterDeepLinkingEnabled = true`.
- `ios/Runner/SceneDelegate.swift`: unverändert, sofern S1 zeigt, dass `FlutterSceneDelegate` Universal Links (`scene(_:willConnectTo:options:)` und `scene(_:continue:)`) an die Engine weitergibt.
**Logging:** `token_hash` darf in keinem Log erscheinen (Spec #1: keine personenbezogenen bzw. geheimen Daten). Zu prüfen: go_router `debugLogDiagnostics` bleibt aus, `AppLogger` und der globale Fehler-Handler loggen keine vollständigen Routen-URIs, `EmailLinkController` loggt nur Typ und Ergebnis. `supabase_flutter` loggt den URI nur für Links, die es selbst verarbeitet (nicht unsere).
### 5.3 Domänenmodelle, Provider
- Neu: `showSignInPromptProvider` (bool aus `authStatusProvider`, Regeln aus 5.1).
- Unverändert: `AuthRepository`, `AuthStatus`, `EmailLinkController`, Redirect-Regeln (`authRedirectFor`), `privateRoutePaths` (bleibt leer).
### 5.4 Neue Pakete
**Keine.** `app_links` ist über `supabase_flutter` bereits transitiv vorhanden (7.2.2, Apache-2.0, 160/160 Punkte, pub.dev geprüft 2026-11-03) und wird **nicht** direkt genutzt. Rückfall, falls S1 zeigt, dass Flutters eingebautes Deep-Linking mit dem `app_links`-Plugin kollidiert: `FlutterDeepLinkingEnabled=false` setzen, `app_links` als direkte Abhängigkeit aufnehmen und dessen `uriLinkStream` in einem kleinen Adapter in `core/routing` an `router.go()` weitergeben. Diese Variante bräuchte eine kurze Ergänzung der Spec.
## 6. Plattform-Besonderheiten
### 6.1 Übersicht
| Thema | iOS | Android | Web |
|---|---|---|---|
| Einstieg (Krit. 1) | Hinweis-Karte auf `/` | Hinweis-Karte auf `/` | Hinweis-Karte auf `/` (auch hier, QUE-09 Krit. 13); URLs `/login`, `/register` weiter direkt aufrufbar |
| Link-Zuordnung | Universal Links: Entitlement `applinks:<Host>`, AASA wird bei Installation und regelmäßig über Apples CDN geladen | App Links: Intent-Filter mit `autoVerify`, `assetlinks.json` wird bei Installation geprüft (Android 12+: ohne erfolgreiche Prüfung öffnet der Browser) | keine Änderung; `/auth/confirm` wie bisher (SPA-Rewrite, D1) |
| Voraussetzungen (Konten) | Bezahltes Apple-Developer-Programm (Team-ID, Capability „Associated Domains“; mit einem kostenlosen Personal Team nach unserem Kenntnisstand nicht möglich, C1 bestätigt) | Play Console und Play App Signing (Fingerabdruck), eigener Upload-Schlüssel statt Debug-Signatur (heute signiert `release` mit dem Debug-Schlüssel) | – |
| Wann öffnet die App nicht? | Link in Safari-Adresszeile eingegeben; Link über Weiterleitungsdienste (Outlook Safe Links, Tracking-Redirects); manche In-App-Browser von Mail-Apps; Nutzer hat „In Safari öffnen“ gewählt (iOS merkt sich das je Domain) | App-Link-Prüfung fehlgeschlagen; Nutzer hat in den Einstellungen „Unterstützte Links öffnen“ deaktiviert; manche In-App-Browser | – |
| Fallback | Web-App öffnet `/auth/confirm` und schließt den Schritt ab; der Nutzer ist dann im Browser angemeldet, nicht in der App; Hinweis „auch in der App anmelden“ (bestehend) | gleich | ist der Normalfall |
| Debug-Builds | Kaltstart einer Debug-Build per Link funktioniert nicht ohne Flutter-Tooling (bekannte Flutter-Einschränkung); Gerätetests mit Profile/Release | Debug funktioniert, solange der Debug-Fingerabdruck in `assetlinks.json` steht (nur Staging, F5) | – |
| Kamera, Dateien, Push, Offline | nicht betroffen; Einlösen braucht Netz (Offline-Hinweis aus Spec #1) | gleich | gleich |
### 6.2 Lokale Entwicklung
- Lokal (`site_url = http://localhost:3000`) gibt es keine Universal/App Links. Die Mails in Mailpit zeigen weiter auf `localhost:3000`, dort funktioniert der Web-Ablauf.
- **Android lokal prüfbar:** Den `token_hash` aus der Mailpit-Mail nehmen und mit dem App-Link-Host aufrufen: `adb shell am start -W -a android.intent.action.VIEW -d "https://<APP_LINK_HOST>/auth/confirm?token_hash=<…>&type=email" work.cshomelabs.shuffle`. Mit expliziter Paketangabe startet die App auch ohne erfolgreiche Verifizierung; sie löst den Token gegen das lokale Supabase ein (Env `dev.json`). Verifizierungsstatus: `adb shell pm get-app-links work.cshomelabs.shuffle`.
- **iOS lokal:** Ohne Team-ID und erreichbare AASA nicht Ende-zu-Ende prüfbar. Die App-Logik deckt ein Widget-Test mit simulierter Plattform-Route ab (7.1). Ende-zu-Ende erst auf Staging/Prod mit echter Domain (C6).
## 7. Teststrategie
Werkzeuge: `flutter_test` + `mocktail` (Unit/Widget), bestehende Playwright-Tests (`e2e/`, Projekt `web`) als Regression, ein Konfigurations-Test für die Plattform-Dateien, manuelle Gerätematrix.
### 7.1 Abdeckungsmatrix
| AK | Unit | Widget | Integration/E2E | RLS | Task |
|---|---|---|---|---|---|
| #49 Krit. 1 | `showSignInPromptProvider`: `signedOut` → true, `signedIn` → false, Laden → false, Fehler → true | HomeScreen abgemeldet: Prompt mit zwei Knöpfen sichtbar; Tipp „Anmelden“ → LoginScreen, „Konto erstellen“ → RegisterScreen; angemeldet: kein Prompt; a11y-Guidelines (`labeledTapTargetGuideline`, `androidTapTargetGuideline`, `iOSTapTargetGuideline`, `textContrastGuideline`), `textScaler` 2.0 ohne Overflow, de/en | Playwright `web`: Startseite abgemeldet zeigt den Prompt, Klick führt zu `/login` und `/register` (Web gehört dazu, E1) | – | U1, A1, T1 |
| #49 Krit. 2 | – | Kaltstart: `tester.binding.platformDispatcher.defaultRouteNameTestValue = '/auth/confirm?token_hash=x&type=email'` → `EmailLinkScreen` mit Typ Bestätigung; warm: `tester.binding.handlePushRoute(...)` bzw. `SystemChannels.navigation` `pushRouteInformation` → `EmailLinkScreen`; Knopfdruck ruft `verifyEmailLink` (Mock) genau einmal; Erfolg → „Weiter“ → `/`, Prompt verschwunden | manuell M1–M4 (7.2) | – | A2, T2 |
| #49 Krit. 3 | – | wie Krit. 2 mit `type=recovery`: nach Erfolg und Status `passwordRecovery` → ResetPasswordScreen; Deep Link während `passwordRecovery` → bleibt auf `/reset-password` | manuell M5–M6 | – | A2, T2 |
| #49 Krit. 4 | – | – | Playwright `web` (bestehend, unverändert): kompletter Bestätigungs- und Reset-Ablauf im Browser; manuell M7–M9 (App nicht installiert, PC, Outlook) | – | T2 |
| Vorschlag 5 (Web unverändert) | bestehende Redirect-Tests (`authRedirectFor`) unverändert grün | – | alle Playwright-Projekte `api`, `web`, `throttle` unverändert grün | – | T2 |
| APPLINK-01 bis -06 | **Konfigurations-Test** `app/test/platform/app_links_config_test.dart` (liest Dateien relativ zu `app/`): Manifest enthält Intent-Filter mit `autoVerify="true"`, `scheme=https`, `host=${appLinkHost}`, `path=/auth/confirm` und `flutter_deeplinking_enabled=true`; Gradle-Property gesetzt; Entitlements enthalten `applinks:$(APP_LINK_HOST)`; `CODE_SIGN_ENTITLEMENTS` in allen Build-Konfigurationen; Info.plist `FlutterDeepLinkingEnabled=true`; AASA und assetlinks sind gültiges JSON, Bundle-/Package-ID stimmen mit Xcode-Projekt bzw. `applicationId` überein, Pfad ist genau `/auth/confirm`; xcconfig-Host = Gradle-Host | – | Release-Checkliste (7.3) | – | A2, T2 |
| Logging | Test: Router-Fehler- und Logpfade erhalten bei einem `/auth/confirm`-URI keinen `token_hash` (z. B. `AppLogger` mit Test-Handler, Route mit Token aufrufen, Logausgabe prüfen) | – | – | – | T2 |
### 7.2 Manuelle Gerätematrix (Profile- oder Release-Build, nach C1–C6)
Je Plattform (iOS-Gerät, Android-Gerät bzw. Emulator mit Play-Services) und jeweils kalt und warm:
| # | Fall | Erwartung |
|---|---|---|
| M1 | Bestätigungslink in Apple Mail (iOS) bzw. Gmail (Android) | App öffnet `/auth/confirm`, Knopf, angemeldet in der App |
| M2 | Bestätigungslink in Gmail-App (iOS) | App öffnet oder Fallback Web (dokumentieren, welches) |
| M3 | Bestätigungslink in Outlook (beide) | voraussichtlich Fallback Web wegen Safe Links (dokumentieren) |
| M4 | App läuft im Hintergrund auf einer anderen Seite | App kommt nach vorn, `/auth/confirm` |
| M5 | Reset-Link | App, Knopf, „Neues Passwort“, Erfolg, andere Sitzung wird abgemeldet (KTO-01 Krit. 9) |
| M6 | Reset-Link zweimal benutzt | zweites Mal „Link abgelaufen oder schon benutzt“ |
| M7 | App nicht installiert | Web-App schließt den Schritt ab |
| M8 | Link am PC geöffnet, Registrierung war am Handy | Web-App am PC bestätigt, Anmeldung in der App danach möglich |
| M9 | iOS: Nutzer wählt „In Safari öffnen“ | Web-App; beim nächsten Link bleibt iOS beim Browser (bekanntes iOS-Verhalten, dokumentieren) |
Ergebnis als Tabelle im PR B bzw. im Issue #49; Abweichungen (z. B. M2, M3) werden beschrieben, nicht als Fehler gewertet, solange der Fallback greift.
### 7.3 Ergänzung Release-Checkliste (`docs/auth-konfiguration.md` § 5)
- [ ] `curl -sI https://<Domain>/.well-known/apple-app-site-association` und `/.well-known/assetlinks.json`: Status 200, keine Weiterleitung, `Content-Type: application/json`, Inhalt identisch zum Repo (APPLINK-05).
- [ ] Apple-CDN-Sicht: `https://app-site-association.cdn-apple.com/a/v1/<Domain>` liefert die aktuelle Datei.
- [ ] Google-Prüfung: Digital Asset Links API (`https://digitalassetlinks.googleapis.com/v1/statements:list?source.web.site=https://<Domain>&relation=delegate_permission/common.handle_all_urls`) listet Paket und Fingerabdrücke.
- [ ] `GOTRUE_SITE_URL`-Host = APPLINK-01.
- [ ] Gerätematrix 7.2 auf der Release-Build durchgeführt.
## 8. Aufgaben
### 8.1 PR A – Einstieg (`feature/49-anmeldung-einstieg`, `Refs #49`)
| # | Agent | Aufgabe | abhängig von |
|---|---|---|---|
| U1 | ui-designer | Gestaltung `SignInPrompt` (Karte, Primär- und Sekundärknopf, Abstände, breite Bildschirme), Sekundärknopf-Variante in `core/widgets` falls nötig, Textvorschläge de/en, a11y (Überschrift als Header, Fokusreihenfolge Web) | – |
| A1 | flutter-dev | `SignInPrompt`, `showSignInPromptProvider`, `HomeScreen` als `ConsumerWidget` (5.1), ARB-Schlüssel de/en, `build_runner` | U1 |
| T1 | test-engineer | Unit-Test Provider, Widget-Tests HomeScreen/Prompt (7.1 Krit. 1), Playwright `web`: Prompt und Navigation | A1 |
### 8.2 PR B – App Links (`feature/49-app-links`, `Refs #49`; #49 wird erst nach der Gerätebestätigung geschlossen, siehe F7)
| # | Agent | Aufgabe | abhängig von |
|---|---|---|---|
| S1 | flutter-dev | **Spike (max. ½ Tag), Ergebnis als Änderungsprotokoll in dieser Spec.** Zu klären: (a) Flutter 3.47: Ist Deep-Linking ohne Flag aktiv, und übergibt die Engine Pfad **und** Query an go_router (Kaltstart, warm)? (b) iOS mit UIScene (`SceneDelegate.swift`, `FlutterSceneDelegate`): kommen `scene(_:willConnectTo:)` und `scene(_:continue:)` mit `NSUserActivityTypeBrowsingWeb` bei go_router an, während das `app_links`-Plugin registriert ist? (c) Android mit `singleTop`: `onNewIntent` erreicht go_router? (d) Verarbeitet `supabase_flutter` die Links tatsächlich nicht (kein `getSessionFromUrl`)? (e) Kopiert `flutter build web` `web/.well-known/` in `build/web/`, und liefert `serve -s` bzw. das geplante Hosting die AASA mit `application/json` aus? (f) Ersetzt go_router bei warmem Link den Stack (`go`) oder legt er ihn oben drauf? Prüfweg: Android-Emulator per `adb … am start` (6.2), iOS-Simulator mit `xcrun simctl openurl` **mit einem eigenen Test-Schema nur im lokalen Spike-Branch** (nicht committen), um den Weg bis go_router zu sehen; Universal-Link-Zuordnung selbst ist ohne C1 nicht prüfbar. | – |
| A2 | flutter-dev | Plattform-Dateien nach 5.2 (Manifest-Intent-Filter, Gradle-Property, Entitlements, xcconfig, `CODE_SIGN_ENTITLEMENTS`, Info.plist-Flag); Logging-Prüfung (5.2); falls S1 negativ: Rückfall-Adapter nach 5.4 (vorher Spec-Ergänzung) | S1 |
| D1a | devops | AASA- und assetlinks-Vorlagen nach 4.3 am Ort aus S1 (e); Doku-Abschnitt APPLINK-01 bis -06 in `docs/auth-konfiguration.md`; Release-Checkliste 7.3; Gateway-Vorgaben (Auslieferung ohne Redirect, `application/json`, `/.well-known/` vom SPA-Rewrite ausgenommen, öffentlich erreichbar, nicht hinter VPN/Basic-Auth) als Ergänzung zu D1 aus Spec #1; Android-Release-Signatur vorbereiten (Upload-Keystore außerhalb des Repos, `key.properties` in `.gitignore`, Signing-Config in `build.gradle.kts`), ohne Schlüssel selbst anzulegen | S1 |
| T2 | test-engineer | Konfigurations-Test (7.1 APPLINK), Widget-Tests Deep Link kalt/warm (Krit. 2, 3), Logging-Test, Regression Playwright; Vorlage der Gerätematrix 7.2 im PR | A2, D1a |
| W1 | docs-writer | Outline-Seite KTO-01 bzw. neue Seite „App Links“ (Ablauf, Fallback, Grenzen) nach Merge | A2, D1a |
### 8.3 Aufgaben, die nur Chris erledigen kann
| # | Aufgabe | Warum Chris | blockiert |
|---|---|---|---|
| C1 | **Apple Developer Program** beitreten (kostenpflichtig) und Team-ID mitteilen; bestätigen, dass „Associated Domains“ für das App-ID-Profil aktiviert ist | Vertrag, Zahlung, Identität | iOS-Teil von Krit. 2/3, M1–M9 iOS |
| C2 | **Google Play Console** einrichten, Play App Signing aktivieren, SHA-256 des App-Signing-Schlüssels mitteilen; Upload-Keystore erzeugen und sicher verwahren (nicht im Repo), SHA-256 mitteilen | Konto, Schlüsselhoheit | Android-Teil von Krit. 2/3 in Release-Builds |
| C3 | **Bundle-ID / Application-ID endgültig festlegen** (heute Platzhalter `work.cshomelabs.shuffle`, R15 aus Spec #1) | Nach Store-Veröffentlichung nicht mehr änderbar; AASA und assetlinks hängen daran | C1, C2, finale Dateien |
| C4 | **Produktions-Domain endgültig festlegen** (heute Platzhalter `shuffle.cshomelabs.work`, F8 aus Spec #1) und sicherstellen, dass sie öffentlich per HTTPS erreichbar ist (kein VPN, keine Zugangssperre vor `/.well-known/`) | Domain und DNS gehören Chris | APPLINK-01, -05 |
| C5 | **Auf Prod:** Zuordnungsdateien ausliefern (Werte aus C1–C3), `GOTRUE_SITE_URL` auf diese Domain setzen, Release-Checkliste 7.3 abhaken | Zugriff auf Server und Prod-Konfiguration nur Chris (kein Agent greift auf Server zu) | Krit. 2/3 Ende-zu-Ende |
| C6 | Gerätematrix 7.2 auf echten Geräten durchführen oder abnehmen (iPhone, Android) | Physische Geräte, Apple-ID | Abschluss #49 |
| C7 | Offene Fragen F1–F10 entscheiden, Spec freigeben | Freigabe | Umsetzung |
### 8.4 Reihenfolge
```
sofort PR A: U1 → A1 → T1 (unabhängig von allem anderen)
parallel S1 (Spike) → A2, D1a → T2 → PR B (mergebar mit Platzhaltern)
Chris C3, C4 → C1, C2 → C5 → C6 → #49 schließen (F7)
danach W1
```
PR A und PR B sind unabhängig; PR B ändert `HomeScreen` nicht. Kein Backend-PR nötig (backend-dev hat in #49 keine Aufgabe).
## 9. Risiken & offene Fragen
### 9.1 Risiken
| # | Risiko | Auswirkung | Gegenmaßnahme |
|---|---|---|---|
| R1 | Mail-Programme und Weiterleitungsdienste (Outlook Safe Links, Tracking-Redirects, In-App-Browser) lösen Universal/App Links nicht aus | Link öffnet die Web-App statt der App | Fallback Web ist vollwertig (Krit. 4); Gerätematrix dokumentiert das Verhalten; kein Tracking-Redirect in eigenen Mails (bereits so, Spec #1 § 4.1) |
| R2 | SPA-Rewrite liefert `index.html` statt der AASA aus, oder es gibt eine Weiterleitung (z. B. http→https, `www`) | Universal/App Links fallen still aus | APPLINK-05, Release-Checkliste 7.3 mit `curl`; `/.well-known/` ausdrücklich vom Rewrite ausnehmen (D1a) |
| R3 | Apple lädt die AASA über sein CDN und cacht sie (Stunden bis Tage); Android prüft `assetlinks.json` vor allem bei der Installation | Änderungen wirken verzögert; eine bei Installation fehlende Datei lässt die App-Link-Prüfung scheitern | Dateien **vor** der ersten Store-Installation ausliefern; iOS-Entwicklermodus (`?mode=developer`) für Tests; Android: `pm verify-app-links --re-verify` |
| R4 | Signatur passt nicht zu `assetlinks.json` (heute `release` mit Debug-Schlüssel, Play App Signing signiert neu) | App Links auf Android ohne Wirkung | Beide Fingerabdrücke eintragen (C2); Release-Signatur vorbereiten (D1a); Prüfung über die Digital Asset Links API |
| R5 | Bundle-/Application-ID oder Domain ändern sich nach Veröffentlichung | Dateien und Builds müssen neu, alte Installationen öffnen keine Links mehr | C3, C4 vor dem ersten Store-Build; Werte nur an einer Stelle (E8) |
| R6 | Ein späterer, breiterer Pfad-Umfang zieht Web-Adressen ungewollt in die App | Nutzer landen in der App, wo die Seite fehlt (404) | Genau `/auth/confirm` (E2); Erweiterung nur mit Entscheidung (F1) |
| R7 | `token_hash` gelangt in Logs (App-Logger, Router-Diagnose, Android-System-Log bei `am start`) | Einmal-Token lesbar, beim Reset Kontoübernahme bis zum Ablauf (1 h) | Logging-Test (7.1); `debugLogDiagnostics` aus; Token ist einmalig und läuft nach 1 h ab (AUTH-CFG-07); `adb`-Aufrufe nur lokal |
| R8 | Flutter-Deep-Linking und das transitive `app_links`-Plugin stören sich (iOS UIScene), oder eine künftige `supabase_flutter`-Version wertet `token_hash` selbst aus | Links kommen nicht bei go_router an oder werden doppelt eingelöst (E3 verletzt) | S1; Rückfall 5.4; bei Updates von `supabase_flutter` `_defaultIsAuthCallbackDeeplink` prüfen (Hinweis im PR). Als weitere Option bleibt `detectSessionInUri: false` in `Supabase.initialize`. Wir nutzen weder OAuth noch PKCE-Redirects, die Option wirkt aber auch im Web; sie wird deshalb nur mit eigener Prüfung eingesetzt. |
| R9 | Homelab-Domain (`cshomelabs.work`) ist nicht öffentlich oder hinter einer Zugangssperre | Apple-CDN und Google erreichen die Dateien nicht | C4; APPLINK-05 |
| R10 | Warmer Deep Link ersetzt den Navigationsstack | Nicht gespeicherte Eingaben auf dem vorherigen Screen gehen verloren | Akzeptiert: Mail-Links sind ein bewusster Wechsel; S1 (f) bestätigt das Verhalten |
| R11 | iOS-Debug-Builds starten nicht kalt über einen Link | Fehlalarm beim Testen | Gerätetests mit Profile/Release (6.1) |
| R12 | Bestätigung im mobilen Browser (Fallback) meldet den Nutzer im Browser an, nicht in der App | Nutzer muss sich in der App zusätzlich anmelden | Bestehender Hinweis „Du kannst dich jetzt auch in der App anmelden.“; die Startseite der App bietet den Einstieg (Krit. 1) |
### 9.2 Offene Fragen (Entscheidung Chris)
| # | Frage | Empfehlung | betrifft |
|---|---|---|---|
| F1 | Welche Pfade beansprucht die App? Nur `/auth/confirm` oder alle Web-Adressen? | **Nur `/auth/confirm`.** Erweiterung (z. B. Spieldetail-Links) erst, wenn es teilbare Inhalte gibt und die App die Route kennt. | E2, APPLINK-02, R6 |
| F2 | Passwortspeicher von App und Web verbinden (`webcredentials` iOS, `get_login_creds` Android)? | **Ja für iOS** (`webcredentials`, gleiche Datei, kein Mehraufwand außer Entitlement-Zeile; Passwort-Vorschläge der Web-Anmeldung erscheinen in der App). Android `get_login_creds` optional, nein in #49. | 4.3, 5.2 |
| F3 | Was passiert, wenn ein Link geöffnet wird, während in der App ein anderes Konto angemeldet ist? | **Wie im Web:** Nach Knopfdruck gilt die neue Sitzung, kein zusätzlicher Dialog. Der Knopfdruck ist bereits eine bewusste Handlung; ein Dialog wäre neue UX ohne zweiten Anwendungsfall. | 5.2 |
| F4 | Braucht die App vor KTO-11 ein vorläufiges „Abmelden“ (z. B. für Gerätetests)? | **Nein.** Abmelden ist KTO-11 Kriterium 3; Tester installieren neu oder nutzen den Reset-Abbruch. | 5.1 |
| F5 | Eigene Staging-App-Builds mit Staging-Host (und Debug-Fingerabdruck in der Staging-`assetlinks.json`)? | **In #49 nein.** Ein Host je Build über die zentrale Property (E8); Staging-Builds entscheidet devops mit dem Hosting-Setup. Bis dahin Android lokal per `adb` (6.2) und iOS erst gegen Prod/Staging-Domain mit Entwicklermodus. | E8, APPLINK-01 |
| F6 | Erscheint der Hinweis zur Anmeldung auch im Web? | **Ja.** QUE-09 Kriterium 13 verlangt ihn für App und Web-Adresse; „Web unverändert“ gilt nur für die Mail-Links. | E1 |
| F7 | Wann wird #49 geschlossen: mit dem Merge von PR B oder erst nach der Gerätebestätigung (C6)? | **Nach C6.** PR B referenziert `Refs #49`; Chris schließt #49 nach der Gerätematrix. Alternativ: PR B schließt #49, Gerätetest wird ein eigenes Issue. | 8.2 |
| F8 | Wo liegen die Zuordnungsdateien im Repo? | **`app/web/.well-known/`**, wenn S1 (e) zeigt, dass Build und Hosting sie korrekt ausliefern; sonst `deploy/well-known/` mit Gateway-Regel. | E7, D1a |
| F9 | Für das Produktions-Setup (D1 aus Spec #1, ADR-0002) gibt es kein Forgejo-Issue. Sollen C4/C5 und D1a dort hinein? | **Ja:** Issue „Produktions-Setup nach ADR-0002“ anlegen (requirements-analyst oder devops), Chris-Aufgaben C4/C5 und die Gateway-Vorgaben dort bündeln; #49 verweist darauf. | 8.3 |
| F10 | Übernimmst du die Präzisierungen der Akzeptanzkriterien (§ 2, Vorschläge zu 1–4 und Kriterium 5)? | **Ja**, sie machen Kriterien 2–4 testbar und schreiben das Web-Verhalten fest. Danach Issue-Text anpassen. | § 2 |
## 10. Änderungsprotokoll
| Datum | Änderung |
|---|---|
| 2026-11-03 | Entwurf (architect) |