zitare-native/docs/NATIVE_LIFT_PLAN.md
Till JS 1d770123f5 η-0: De-Hybrid — WKWebView raus, native Tabs mit Platzhaltern
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>
2026-05-22 12:32:05 +02:00

14 KiB
Raw Blame History

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:

  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/<slug>: Vollansicht, Author- Link, Source-Link, Theme-Chips, Quelle-Metadaten, Lizenz-Hinweis.
  • AuthorDetailView für /a/<slug>: 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/<slug>.
  • ThemeListView, ThemeDetailView fü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

  • 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=<query> (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.

  • 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.