η-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:
parent
58eb2807c7
commit
1d770123f5
11 changed files with 619 additions and 365 deletions
306
docs/NATIVE_LIFT_PLAN.md
Normal file
306
docs/NATIVE_LIFT_PLAN.md
Normal 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 (Zeilenumbrü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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue