# Native-Lift Plan — zitare-native (ζ → η) **Stand: 2026-05-22 — Planung.** Drei Antworten von Till liegen vor (Offline-first + lokal-first-Suche + SafariView für Legal). Dieser Plan ersetzt die Hybrid-Strategie aus dem Greenfield-Playbook [`../../mana/docs/playbooks/ZITARE_NATIVE_GREENFIELD.md`](../../mana/docs/playbooks/ZITARE_NATIVE_GREENFIELD.md) für die Read-Surfaces. Submit / ShareExt / Widget / Spotlight bleiben in ihrer schon-nativen Form. > SOT für die Status-Spur: dieses File + `PLAN.md`. Die ζ-Phasen sind > historisch (ζ-0/ζ-1/ζ-2 erledigt), die η-Phasen sind der Lift. ## Was sich ändert vs. Hybrid | Bereich | Vorher (ζ) | Nachher (η) | |---|---|---| | Lesen-Tab | `WebShellView` auf `zitare.com` | `QuoteFeedView` (SwiftUI), Daten aus SwiftData | | Erkunden-Tab | `WebShellView` auf `zitare.com/explore` | `ExploreView` (SwiftUI) mit Facet-Filter | | Quote-Detail | Web-Route `/q/` | `QuoteDetailView` (nativ), Deep-Link-Target | | Author/Source/Thema | Web-Route | Native Detail-Views | | Search | `Pagefind` im Web | SwiftData-FTS lokal + API-Fallback | | Account | (heute schon Stub) | `AccountView` voll nativ, ManaAuthUI | | Impressum/Datenschutz/Lizenz | Web-Routen | `SFSafariViewController`-Sheet auf `zitare.com/` | | `ManaWebShell`-Dep | benutzt | **wird komplett entfernt** | | `CookieBridge.swift` | benötigt | **entfällt** (kein WebView, kein Cookie-SSO mehr) | | Cookie-SSO `mana.access` → WebView | Phase 2.G | **gestrichen** | | AASA `applinks` | `zitare.com` + `app.zitare.com` | bleibt; Routen jetzt nativ | | Snapshot `index-min.json` | nur Meta (11 Quotes, Slugs+Tags) | **erweitert** auf Volltext + Author-/Source-/Themen-Stammdaten | | Datenmodell | nur `SnapshotQuote` (Stub) | volles SwiftData-Schema gespiegelt zu zitare-DB | ## Architektonische Invarianten (η) Diese ersetzen die alten Hybrid-Invarianten in `CLAUDE.md`: 1. **Pure SwiftUI für alle Surfaces.** Kein `WKWebView` mehr im App- Binary. `ManaWebShell`-Dep raus aus `project.yml`. 2. **Offline-first Lesen.** SwiftData ist Primary-Store, API ist Fallback bei Cache-Miss + Sync-Quelle. Flugmodus-Test ist Akzeptanz- Kriterium für η-2. 3. **Quote-Korpus ist Snapshot.** Beim Launch + Background-Refresh wird `index-full.json` gepullt, ETag-aware, in SwiftData persistiert, per App-Group an Widget + ShareExt + Spotlight durchgereicht. 4. **Server gewinnt bei Schema-Konflikt.** SwiftData-Modelle sind eine abgeleitete Spiegelung des Drizzle-Schemas in `zitare/apps/api/`. Bei Drift: Server-Schema, dann Snapshot-Format, dann Native-Modell. 5. **Search lokal-first.** Lokale FTS aus SwiftData; bei < 3 Treffern oder leerem Result Server-Fallback (`GET /api/v1/quotes?q=`). 6. **Legal-Seiten via SafariView.** Impressum / Datenschutz / Lizenz öffnen `SFSafariViewController`. Das ist die **einzige** zugelassene Browser-Brücke und nur für statisches Verein-Recht-Material. 7. **Schreiben bleibt Submit-only in v1.** Edit / Moderation / Flags- Verwaltung kommen NICHT in die App. Bleiben Web (`app.zitare.com`). Wer das braucht, öffnet Safari. 8. **Universal-Links auf `zitare.com` lösen ins native Surface.** AASA bleibt, aber `onContinueUserActivity` routet auf SwiftUI-Views, nicht in einen WebView-Tab. 9. **Keine Web-Re-Implementation von Web-Funktion.** Wenn eine Read- Ansicht in der App fehlt, die im Web existiert: erst Web im SOT prüfen, dann nativ nachziehen. Kein einseitiger Native-Vorlauf, der das Web abhängt. ## Phasen-Plan Erfolgskriterium ist je **eine** Sache. Nicht in die nächste Phase arbeiten, bevor die vorherige geschlossen ist. ### η-0 — De-Hybrid (Vorbereitung) - `CLAUDE.md` Invarianten 1, 2, 8 ersetzen durch die η-Liste oben. - `ManaWebShell`-Dep aus `project.yml` entfernen. - `Sources/Core/WebShell/CookieBridge.swift` löschen. - `RootView.swift` auf vier native Tabs umstellen (Lesen / Erkunden / Einreichen / Konto) — Lesen + Erkunden zeigen erstmal Platzhalter („wird in η-2/η-4 implementiert"). - `Features/Settings/SettingsView.swift` + `Features/Account/AccountView.swift` von WebView-Verweisen befreien (Settings-„Hilfe → öffne Browser" bleibt, aber als `SafariView`). - Submit / ShareExt / Widget bleiben unangetastet. **Erfolg:** Build grün auf iOS-Simulator, kein `WKWebView`-Symbol im Binary (`nm | grep WKWebView` leer), App startet, alle vier Tabs sichtbar, Lesen + Erkunden zeigen Platzhalter-View. ### η-1 — Snapshot v2 + SwiftData-Schema **Cross-Repo: zitare-web muss liefern, bevor η-2 startet.** Web-Repo-Aufgaben (`../zitare/apps/zitare/`): - Build-Script erweitern: `index-full.json` mit allen Quote-Feldern inkl. `text`, `language`, `lengthBucket`, `form`, `createdAt`, `author`, `source`, `tags[]`, `themes[]`, plus Author-/Source-/Theme-/Place-/ Epoch-/Role-Stammdaten als eigene Collections (`authors.json`, `sources.json`, `themes.json`, `places.json`, `epochs.json`, `roles.json`). - ETag-Header für jeden dieser Endpoints (Cache-Control: 1h). - Stabile URL: `https://zitare.com/index-full.json` (Apex), nicht über mana.how. - Größen-Budget: Korpus < 5 MB unkomprimiert für die nächsten ~5000 Quotes (Pagination dann in η-5 nachgedacht, heute 11 Quotes). Native-Aufgaben: - SwiftData-Modelle in `Sources/Core/Snapshot/Models/`: `SDQuote`, `SDAuthor`, `SDSource`, `SDTheme`, `SDPlace`, `SDEpoch`, `SDRole`, `SDTag`. Relations 1:1 mit Drizzle. - `SnapshotSync.swift`: lädt parallel die sieben Collections, diffed gegen lokalen Store, persistiert atomar. - App-Group-Container statt nur-App-Container, damit Widget + ShareExt + Spotlight denselben Store lesen. - ETag im UserDefaults persistieren, `If-None-Match` schicken. **Erfolg:** Nach Cold-Start lädt App den vollen Korpus, SwiftData- Store hat erwartete Row-Counts, ETag spart Bytes beim Reload. ### η-2 — Read-Core nativ (Heute, Quote-Detail, Author-Detail) - `QuoteCard`-Komponente (Text, Author, Theme-Chips, lengthBucket). - `HeuteView`: deterministisches Tages-Quote (Hash über Datum + Korpus- Count). Match-Logik mit Widget abgleichen. - `QuoteDetailView` für Deep-Link `/q/`: Vollansicht, Author- Link, Source-Link, Theme-Chips, Quelle-Metadaten, Lizenz-Hinweis. - `AuthorDetailView` für `/a/`: Bio (Name, Native-Name, Places, Epochs, Roles), Quote-Liste des Authors. - `DeepLinkRouter` umverdrahten: keine WebShell-Targets mehr, sondern `NavigationPath`-Push. **Erfolg:** Flugmodus, frische Install, App zeigt Heute-Quote + Quote-Detail nach Universal-Link aus Mail-App. ### η-3 — Read-Browse (Source, Theme, Place, Role, Epoch, Language) - Detail-Views je Entity mit Listing der zugehörigen Quotes. - `SourceDetailView` für `/c/`. - `ThemeListView`, `ThemeDetailView` für `/thema/`. - `PlaceListView` (Regionen) für `/region/`. - Analog Roles / Epochs / Languages. - Konsistente `EntityListSection`-Komponente (Title, Count, Chip-Grid). **Erfolg:** Alle Web-Deep-Link-Pattern aus `app-manifest.json` rufen die richtige native View auf. ### η-4 — Explore + Filter - `ExploreView` als kombinierte Filter-Oberfläche: Sprache, Length-Bucket, Form, Theme, Region, Epoche, Role. - Filter-State als `@Observable`-Klasse, persistierbar in UserDefaults. - Live-Result-Counter über SwiftData-`@Query` mit dynamischem `Predicate`. - iPad: NavigationSplitView mit Filter-Sidebar + Result-List. **Erfolg:** Filter „Roman + DE + lang" zeigt korrekte Subset live, ohne Server-Call. ### η-5 — Search (lokal + Server-Fallback) - Lokale FTS: - Bei Snapshot-Import: pro Quote `normalizedText` (lowercase, diakritika-frei) ableiten, in `SDQuote.searchTokens` als `[String]` (oder als `String` mit Space-Separator) speichern. - Query-Splitting: Whitespace + Diakritika-Normalisierung. - SwiftData-Predicate mit `contains` über `searchTokens`. - Server-Fallback: - Wenn lokaler Result-Count < 3 ODER Query-Länge < 3 Token: parallel `GET /api/v1/quotes?q=` (Endpoint muss in zitare-api vorhanden sein — prüfen, ggf. ergänzen). - Server-Results mit Marker „Online-Treffer" einsortieren, nicht persistieren. - UI: `SearchView` mit `searchable()`, Result-List mit Source-Marker (lokal vs. online), Empty-State. **Erfolg:** Begriff der existiert lokal kommt offline; seltener Begriff bringt Online-Treffer dazu, der vom Sync nicht abgedeckt war. ### η-6 — Account nativ (vollständige Auth-Surface) - `AccountView` heute hat Health-Status + Sign-In-Button als Stub. - Voll bauen: Profil-Info, Gerätesitzungen, Sign-Out-Button, Link auf Web-Konto-Verwaltung via SafariView. - Eigene Submissions-Liste (`GET /api/v1/me/submissions`, falls vorhanden — sonst Cross-Repo-Aufgabe). - Keine WebView-Verlinkung mehr. **Erfolg:** Sign-In + Sign-Out vollständig nativ, JWT im Keychain verifizierbar. ### η-7 — Legal via SafariView - `LegalSheet.swift`: `UIViewControllerRepresentable` um `SFSafariViewController` (iOS) / `NSWorkspace.open` (macOS). - Drei Links im Settings-Tab: Impressum, Datenschutz, Lizenz. - macOS: öffnet System-Browser, kein SafariView dort. **Erfolg:** Tap auf „Impressum" öffnet `https://zitare.com/impressum` im SFSafariViewController-Sheet, nicht im App-WebView (gibt's nicht mehr). ### η-8 — Submit + ShareExt + Spotlight an Snapshot v2 anpassen - Submit-Author-Autocomplete: heute über `ZitareAPI.searchAuthors` (live API). Jetzt erstmal lokal aus Snapshot (instant), bei < 3 Treffern Server-Fallback. - ShareExt: sollte schon nativ sein, prüfen ob es WebShell-Imports hat (`grep WebShell ShareExtension/`). - Spotlight: jetzt mit Volltext indexieren statt nur Meta — `searchableItem.contentDescription = quote.text.prefix(200)`. **Erfolg:** Spotlight findet Quote nach Volltext-Fragment, nicht nur nach Author/Theme. ### η-9 — Polish (iPad-Split, Accessibility, macOS-Layout) - iPad: NavigationSplitView für Quote-Listen. - macOS: Window-Layout, Sidebar-NavSplit statt TabBar. - Dynamic Type, VoiceOver-Labels für QuoteCard, Theme-Sync mit System-Dark-Mode (ohne JS-Bridge, weil ohne WebView). - Haptics für Heute-Quote-Refresh. **Erfolg:** Eine Woche Solo-Nutzung ohne offene Browser-Tabs zu Zitare-Inhalten. ### η-10 — TestFlight - App-Store-Connect: Build hochladen, Review-Notes, Screenshots. - Apple-Dev-Portal-Blocker (App-Group) ist hier vermutlich längst erledigt; falls nicht: nachziehen vor Upload. **Erfolg:** TestFlight-Tester können Korpus lesen + Quote einreichen. ## Cross-Repo-Aufgaben ### `../zitare/apps/zitare/` (Web-Build) - [ ] `index-full.json` Build-Script + Endpoint (η-1 Blocker) - [ ] Stammdaten-Collections: `authors.json`, `sources.json`, `themes.json`, `places.json`, `epochs.json`, `roles.json`, `languages.json` (η-1 Blocker) - [ ] ETag-Header auf allen Snapshot-Endpoints - [ ] AASA bleibt wie heute (kein Change) ### `../zitare/apps/api/` (API) - [ ] `GET /api/v1/quotes?q=...` Volltext-Search prüfen — wenn nicht vorhanden, ergänzen (Drizzle + PG `to_tsvector` oder einfaches `ilike` für Anfang). Search-Quality-Diskussion in η-5. - [ ] `GET /api/v1/me/submissions` für AccountView prüfen, ggf. ergänzen (η-6). - [ ] Wire-Format-Check: Native-Codable-Structs gegen Hono-zod- Response in η-2/η-3 abgleichen, Test-Fixtures aus echten Server-Responses einchecken. ### `../mana/docs/playbooks/ZITARE_NATIVE_GREENFIELD.md` - [ ] Status-Banner ergänzen: Read-Surface-Strategie revidiert 2026-05-22, jetzt fully-native; SOT für laufende Arbeit ist dieses File. ### Apple Developer Portal - [ ] App-Group `group.ev.mana.zitare` registrieren — falls noch offen aus ζ-0/ζ-2. Selbe Blocker-Lage wie cards/memoro/moodlit. ## Offene Punkte - **Snapshot-Größe-Skalierung.** Heute 11 Quotes, Plan ist < 5 MB unkomprimiert. Ab welcher Korpus-Größe kippt das? Wahrscheinlich > 5000 Quotes. Antwort: Pagination + Sync-Deltas in η-5+. Heute irrelevant. - **Multilingual FTS.** Lokale Search splittet nach Whitespace, ist diakritika-frei. Stemming (dt + en + andere) ist nicht im Plan. Falls Such-Qualität schwach: in η-5 nachschärfen oder als „kennen wir, später" akzeptieren. - **Quote-Text-Formatierung.** Hat ein Quote-Text Markdown-artige Zeichen (Zeilen­umbrüche, Hervorhebung)? Falls ja: in η-2 mit `AttributedString` rendern. Web-SOT prüfen. - **macOS-Layout.** Bisher steht macOS auf Apple-Dev-Portal-Blocker. Sobald entblockt: SplitView statt TabBar. In η-9. - **Submit-Pfad und Online-Pflicht.** Submit braucht Auth + API. Wenn offline: queue + retry — ist heute schon so vorgesehen, prüfen, ob die Queue noch passt nach SwiftData-Schema-Lift. - **Schema-Versions-Tag im Snapshot.** SwiftData-Migrationen für künftige Schema-Änderungen brauchen einen Versions-Marker. In η-1 mit eingeplant (`SchemaV1.swift`), aber Migrations-Strategie ist „lokal löschen + neu sync" für v0.x → v1.0. - **Was passiert mit `/feed.rss`?** Deep-Link-Pattern existiert. Native-Antwort: SafariView oder „nicht in App"-Toast? In η-2 entscheiden. - **`app.zitare.com` Universal-Link.** Heute AASA-Ziel. Wenn die SPA dort wegfällt für native Nutzer, brauchen wir AASA dort weiterhin (für Web-Fallback ja, für native Routing — als identisch zu zitare.com behandeln)? In η-2 routen wir beide Domains gleich, AASA bleibt redundant gepflegt. - **Web-App auf `app.zitare.com` selbst.** Wenn native der primäre Read-Pfad wird, wozu existiert die SPA dann noch? Antwort: Browser-Nutzer + nicht-iOS/macOS-User + Moderation/Admin. Aber Frage rechtfertigt sich, ob die SPA aktiv weiterentwickelt wird oder einfriert. Nicht hier entscheiden, aber in `zitare/STATUS.md` diskutieren. ## Was außerhalb bleibt - **Push-Pipeline.** Widget reicht. - **Eigener Pagefind-Klon.** Lokale FTS ist SwiftData-Predicate, nicht Web-Pagefind portiert. - **FSRS oder Lernen.** Nicht Zitare-Scope. - **Crash-Reporting-SaaS.** OSLog only (Compliance). - **Admin / Moderation / Edit in der App.** Bleiben Web.