Lift von Hybrid (WKWebView für Lesen/Erkunden) auf fully-native ist beschlossen. Diese Phase entfernt die WebShell-Infrastruktur; das volle native Read-Surface folgt in η-2..η-5 nach docs/NATIVE_LIFT_PLAN.md. - ManaWebShell-Dep raus aus project.yml - Sources/Core/WebShell/CookieBridge.swift gelöscht - RootView auf vier native Tabs (Lesen + Erkunden = Platzhalter, Submit + Konto unverändert nativ) - DocComments in DeepLinkRouter / AppConfig / Account / Settings von WebView-Verweisen befreit - CLAUDE.md Invarianten von Hybrid auf η umgestellt (13 Invarianten, pure SwiftUI + Offline-first + SafariView-Ausnahme für Legal) - PLAN.md auf η-0 + Phasenübersicht η-0..η-10 - AppConfigTests.test_keychainService_matchesSharedGroup auf ManaSharedKeychainGroup aktualisiert (war drift seit Cross-App-SSO) Verifikation: - xcodebuild iOS-Simulator iPhone 16e: BUILD SUCCEEDED - nm ZitareNative | grep WKWebView: 0 Referenzen - otool -L: kein WebKit-Framework-Link - 20/20 Tests grün Cross-Repo-Follow-up (η-1 Blocker): - zitare/apps/zitare/ muss index-full.json + 7 Stammdaten-JSONs liefern - zitare/apps/api/ Volltext-Search-Endpoint bestätigen/ergänzen Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
14 KiB
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
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/<slug> |
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/<page> |
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:
- Pure SwiftUI für alle Surfaces. Kein
WKWebViewmehr im App- Binary.ManaWebShell-Dep raus ausproject.yml. - Offline-first Lesen. SwiftData ist Primary-Store, API ist Fallback bei Cache-Miss + Sync-Quelle. Flugmodus-Test ist Akzeptanz- Kriterium für η-2.
- Quote-Korpus ist Snapshot. Beim Launch + Background-Refresh
wird
index-full.jsongepullt, ETag-aware, in SwiftData persistiert, per App-Group an Widget + ShareExt + Spotlight durchgereicht. - 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. - Search lokal-first. Lokale FTS aus SwiftData; bei < 3 Treffern
oder leerem Result Server-Fallback (
GET /api/v1/quotes?q=). - 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. - 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. - Universal-Links auf
zitare.comlösen ins native Surface. AASA bleibt, aberonContinueUserActivityroutet auf SwiftUI-Views, nicht in einen WebView-Tab. - 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.mdInvarianten 1, 2, 8 ersetzen durch die η-Liste oben.ManaWebShell-Dep ausproject.ymlentfernen.Sources/Core/WebShell/CookieBridge.swiftlöschen.RootView.swiftauf 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.swiftvon WebView-Verweisen befreien (Settings-„Hilfe → öffne Browser" bleibt, aber alsSafariView).- 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.jsonmit 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-Matchschicken.
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.QuoteDetailViewfür Deep-Link/q/<slug>: Vollansicht, Author- Link, Source-Link, Theme-Chips, Quelle-Metadaten, Lizenz-Hinweis.AuthorDetailViewfür/a/<slug>: Bio (Name, Native-Name, Places, Epochs, Roles), Quote-Liste des Authors.DeepLinkRouterumverdrahten: keine WebShell-Targets mehr, sondernNavigationPath-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.
SourceDetailViewfür/c/<slug>.ThemeListView,ThemeDetailViewfür/thema/<slug>.PlaceListView(Regionen) für/region/<slug>.- 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
ExploreViewals 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-
@Querymit dynamischemPredicate. - 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, inSDQuote.searchTokensals[String](oder alsStringmit Space-Separator) speichern. - Query-Splitting: Whitespace + Diakritika-Normalisierung.
- SwiftData-Predicate mit
containsübersearchTokens.
- Bei Snapshot-Import: pro Quote
- Server-Fallback:
- Wenn lokaler Result-Count < 3 ODER Query-Länge < 3 Token:
parallel
GET /api/v1/quotes?q=<query>(Endpoint muss in zitare-api vorhanden sein — prüfen, ggf. ergänzen). - Server-Results mit Marker „Online-Treffer" einsortieren, nicht persistieren.
- Wenn lokaler Result-Count < 3 ODER Query-Länge < 3 Token:
parallel
- UI:
SearchViewmitsearchable(), 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)
AccountViewheute 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:UIViewControllerRepresentableumSFSafariViewController(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.jsonBuild-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 + PGto_tsvectoroder einfachesilikefür Anfang). Search-Quality-Diskussion in η-5.GET /api/v1/me/submissionsfü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.zitareregistrieren — 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 (Zeilenumbrüche, Hervorhebung)? Falls ja: in η-2 mit
AttributedStringrendern. 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.comUniversal-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.comselbst. 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 inzitare/STATUS.mddiskutieren.
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.