From ef1fddfb728d447036a98e8639137b5c9fbdb9e6 Mon Sep 17 00:00:00 2001 From: cschlaefke Date: Tue, 3 Nov 2026 19:46:25 +0000 Subject: [PATCH] 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 --- docs/specs/49-mobil-anmeldung-app-links.md | 351 +++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 docs/specs/49-mobil-anmeldung-app-links.md diff --git a/docs/specs/49-mobil-anmeldung-app-links.md b/docs/specs/49-mobil-anmeldung-app-links.md new file mode 100644 index 0000000..9dd9b64 --- /dev/null +++ b/docs/specs/49-mobil-anmeldung-app-links.md @@ -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 `/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 `/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://` | Bedingung: Host = Associated Domain (APPLINK-01). Auf Prod setzt das Chris (C5). | +| `GOTRUE_URI_ALLOW_LIST` (AUTH-CFG-02) | `/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 | `.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:///.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": [".work.cshomelabs.shuffle"], + "components": [ + { "/": "/auth/confirm", "comment": "E-Mail-Links (Bestätigung, Reset, später E-Mail-Wechsel)" } + ] + } + ] + }, + "webcredentials": { + "apps": [".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": ["", ""] + } + } +] +``` + +`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:`, 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:///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:///.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/` liefert die aktuelle Datei. +- [ ] Google-Prüfung: Digital Asset Links API (`https://digitalassetlinks.googleapis.com/v1/statements:list?source.web.site=https://&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) |