η-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>
This commit is contained in:
Till JS 2026-05-22 12:32:05 +02:00
parent 58eb2807c7
commit 1d770123f5
11 changed files with 619 additions and 365 deletions

306
docs/NATIVE_LIFT_PLAN.md Normal file
View file

@ -0,0 +1,306 @@
# 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.