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

306 lines
14 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/<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.
### η-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.