η-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
160
CLAUDE.md
160
CLAUDE.md
|
|
@ -3,8 +3,9 @@
|
|||
Guidance für Claude Code in diesem Repository.
|
||||
|
||||
> **Wenn du gerade neu bist:** lies zuerst [`PLAN.md`](PLAN.md) und
|
||||
> [`../mana/docs/playbooks/ZITARE_NATIVE_GREENFIELD.md`](../mana/docs/playbooks/ZITARE_NATIVE_GREENFIELD.md).
|
||||
> Dieses CLAUDE.md ist die Konventions- und Cross-Repo-Referenz.
|
||||
> [`docs/NATIVE_LIFT_PLAN.md`](docs/NATIVE_LIFT_PLAN.md). Das ältere
|
||||
> Hybrid-Playbook in `../mana/docs/playbooks/ZITARE_NATIVE_GREENFIELD.md`
|
||||
> ist seit 2026-05-22 **überholt** — nur als historische Referenz lesen.
|
||||
|
||||
## Was dieses Repo ist
|
||||
|
||||
|
|
@ -14,66 +15,74 @@ macOS) für **Zitare**, den öffentlichen Zitat-Korpus des Vereins
|
|||
|
||||
```
|
||||
HTTPS ┌──────────────────┐
|
||||
zitare.com ◄──────────── │ zitare-native │ WKWebView (Lesen)
|
||||
(statisch, public) │ (this repo) │ SwiftUI (Submit)
|
||||
│ ev.mana.zitare │ WidgetKit
|
||||
zitare-api ◄──────────── │ │ SwiftData (Snapshot-Cache)
|
||||
zitare-api.mana.how │ │ CoreSpotlight
|
||||
zitare.com ◄──────────── │ zitare-native │ pure SwiftUI
|
||||
(Snapshot + AASA) │ (this repo) │ SwiftData (voller Korpus)
|
||||
│ ev.mana.zitare │ WidgetKit + ShareExt + Spotlight
|
||||
api.zitare.com ◄──────────── │ │ SafariView (nur Legal)
|
||||
(Read + Submit) │ │
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
## Status
|
||||
|
||||
**Phase ζ-0 — Setup.** Repo-Skelett, `project.yml`, leerer Build im
|
||||
Simulator. Phasen ζ-1 bis ζ-7 in
|
||||
[`../mana/docs/playbooks/ZITARE_NATIVE_GREENFIELD.md`](../mana/docs/playbooks/ZITARE_NATIVE_GREENFIELD.md).
|
||||
**Phase η-0 — De-Hybrid (2026-05-22, in Arbeit).** Hybrid-Strategie
|
||||
(WKWebView für Read-Surfaces) wurde aufgegeben. Plan +
|
||||
Phasen-Übersicht in [`docs/NATIVE_LIFT_PLAN.md`](docs/NATIVE_LIFT_PLAN.md).
|
||||
Status-Spur in [`PLAN.md`](PLAN.md).
|
||||
|
||||
## Leitprinzip: Verteilungs-USP, nicht Funktions-USP
|
||||
## Leitprinzip: pure-native + Offline-first
|
||||
|
||||
Anders als die anderen drei nativen Apps (cards/memoro/manaspur) gibt
|
||||
es bei Zitare **keinen Hardware-Vorteil** gegenüber dem Browser. Die
|
||||
Web-App ist mobile-responsive, statisch prerendered, hat Pagefind-
|
||||
client-Suche. Native bringt:
|
||||
Das Verteilungs-USP-Argument trägt allein nicht; die App soll auch
|
||||
Funktion + Lese-Komfort eigenständig liefern, ohne den Browser-Schwester-
|
||||
Pfad. Native bringt:
|
||||
|
||||
1. **Home-Screen-Widget** „Zitat des Tages"
|
||||
2. **ShareExtension** als Ziel für markierten Text
|
||||
3. **Spotlight-Index** für system-weite Suche
|
||||
4. **Native Submit-View** mit ManaAuthUI
|
||||
1. **Voller Korpus offline** in SwiftData (Snapshot-Sync `index-full.json`).
|
||||
2. **Lokale FTS** + Server-Fallback bei seltenen Queries.
|
||||
3. **Home-Screen-Widget** „Zitat des Tages".
|
||||
4. **ShareExtension** als Ziel für markierten Text.
|
||||
5. **Spotlight-Index** für system-weite Volltext-Suche.
|
||||
6. **Native Submit** mit ManaAuthUI.
|
||||
7. **SafariView** nur für statische Verein-Recht-Seiten (Impressum,
|
||||
Datenschutz, Lizenz) — kein `WKWebView` im Binary.
|
||||
|
||||
Alles andere (Lesen, Filtern, Search, Edit, Moderation) bleibt im
|
||||
`WKWebView` gegen `zitare.com` / `zitare.mana.how`.
|
||||
## Architektonische Invarianten (η)
|
||||
|
||||
## Architektonische Invarianten
|
||||
Beschlossen 2026-05-22. Nicht ohne explizite Diskussion antasten.
|
||||
|
||||
Beschlossen. Nicht ohne explizite Diskussion antasten.
|
||||
|
||||
1. **Hybrid ausnahmsweise.** Lese-Surfaces via `WKWebView`, Native-
|
||||
Surfaces (Widget, ShareExt, Submit, Spotlight) pure SwiftUI. Diese
|
||||
Trennung ist **fest** — keine schleichende Native-Re-Implementation
|
||||
von Read-Routes.
|
||||
2. **Read-only via Web, Submit via SwiftUI.** Submit ist der einzige
|
||||
schreibende Pfad in v1. Edit, Moderation, History bleiben Web.
|
||||
3. **Snapshot lokal gespiegelt für Widget + Spotlight.** Beim Launch
|
||||
`https://zitare.com/index-min.json` pullen, in SwiftData
|
||||
persistieren, App-Group `group.ev.mana.zitare` reicht es an Widget
|
||||
+ ShareExtension durch. **Nicht** für den WebView-Pfad — der lädt
|
||||
live.
|
||||
4. **mana-auth via ManaCore + ManaAuthUI.** Submit-Pfad nutzt
|
||||
1. **Pure SwiftUI für alle Surfaces.** Kein `WKWebView` im App-Binary.
|
||||
`ManaWebShell`-Dep ist 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 ab η-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/`.
|
||||
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.
|
||||
8. **Universal-Links auf `zitare.com` lösen ins native Surface.** AASA
|
||||
bleibt, `onContinueUserActivity` routet auf SwiftUI-Views, nicht
|
||||
in einen WebView-Tab.
|
||||
9. **mana-auth via ManaCore + ManaAuthUI.** Submit + Konto nutzen
|
||||
`AuthClient` und die fertigen Views aus ManaAuthUI. Keine eigene
|
||||
Auth. WebView gegen `zitare.mana.how` bekommt JWT per Cookie-
|
||||
Injection (`mana.access` auf `.mana.how`).
|
||||
5. **Universal-Link-Domain: `zitare.com`.** AASA auf
|
||||
`https://zitare.com/.well-known/apple-app-site-association`.
|
||||
`zitare.mana.how` ist *kein* applinks-Ziel.
|
||||
6. **Theme: `paper`-Variant default.** Werte aus `@mana/themes/paper`,
|
||||
lokal als `ZitareTheme.swift` nachgebaut.
|
||||
7. **Bundle-ID `ev.mana.zitare`.** Reverse-Domain mana-ev.ch.
|
||||
App-Display-Name: „Zitare". Category: `public.app-category.reference`.
|
||||
8. **Pure SwiftUI für Native-Surfaces.** WKWebView ist die einzige
|
||||
UIKit-Bridge.
|
||||
9. **Web gewinnt bei Konflikt.** Funktion wandert zuerst in
|
||||
`zitare/apps/zitare/` oder `zitare/apps/api/`, dann ins WebView-
|
||||
bzw. Native-Surface hier.
|
||||
Auth, keine Cookie-Bridge mehr (entfällt mit `WKWebView`).
|
||||
10. **Universal-Link-Domain: `zitare.com`** (+ `app.zitare.com` als
|
||||
AASA-Redundanz). AASA auf
|
||||
`https://zitare.com/.well-known/apple-app-site-association`.
|
||||
11. **Theme: `paper`-Variant default.** Werte aus `@mana/themes/paper`,
|
||||
lokal als `ZitareTheme.swift` nachgebaut.
|
||||
12. **Bundle-ID `ev.mana.zitare`.** Reverse-Domain mana-ev.ch.
|
||||
App-Display-Name: „Zitare". Category: `public.app-category.reference`.
|
||||
13. **Web gewinnt bei Konflikt im Datenmodell.** Funktion wandert
|
||||
zuerst in `zitare/apps/zitare/` oder `zitare/apps/api/`, dann ins
|
||||
native Surface hier — kein einseitiger Native-Vorlauf, der das Web
|
||||
abhängt.
|
||||
|
||||
## Konventionen
|
||||
|
||||
|
|
@ -202,32 +211,45 @@ swiftlint --strict
|
|||
|
||||
## Phasen-Disziplin
|
||||
|
||||
Jede Phase aus dem Greenfield-Plan hat ein verifizierbares
|
||||
Erfolgskriterium. Nicht in die nächste Phase reinarbeiten, bevor die
|
||||
vorherige abgeschlossen ist:
|
||||
Jede Phase aus dem Lift-Plan hat ein verifizierbares Erfolgskriterium.
|
||||
Nicht in die nächste Phase reinarbeiten, bevor die vorherige
|
||||
abgeschlossen ist. Vollständige Phasen-Tabelle +
|
||||
Erfolgs-Kriterien in [`docs/NATIVE_LIFT_PLAN.md`](docs/NATIVE_LIFT_PLAN.md).
|
||||
|
||||
- ζ-0: leerer Build + ManaCore-Login + Healthz-Probe (**JETZT**)
|
||||
- ζ-1: WebShellView + Universal-Links + Cookie-SSO-Bridge
|
||||
- ζ-2: Snapshot-Sync + DailyQuoteWidget auf realem Gerät
|
||||
- ζ-3: Submit-View nativ mit ManaAuthGate
|
||||
- ζ-4: Spotlight + ShareExtension + App Intents
|
||||
- ζ-5: Polish, Theme-Sync, iPad-Split-Layout, Accessibility
|
||||
- ζ-6: App-Store-Submission
|
||||
Übersicht:
|
||||
|
||||
- ζ-0..ζ-2: erledigt unter Hybrid-Annahme (Setup, WebShell, Snapshot-
|
||||
Stub + Widget-Code). WebShell entfällt jetzt; Widget + Snapshot-
|
||||
Code bleiben, werden in η-1 erweitert.
|
||||
- η-0: De-Hybrid (ManaWebShell raus, RootView vier native Tabs)
|
||||
(**JETZT**)
|
||||
- η-1: Snapshot v2 + volles SwiftData-Schema (Cross-Repo-Blocker:
|
||||
`zitare/apps/zitare/` muss `index-full.json` + 7 Stammdaten-JSONs
|
||||
liefern)
|
||||
- η-2: Read-Core nativ (Heute, Quote-Detail, Author-Detail)
|
||||
- η-3: Read-Browse (Source / Theme / Place / Role / Epoch / Language)
|
||||
- η-4: Explore + Filter
|
||||
- η-5: Search (lokal-first + Server-Fallback)
|
||||
- η-6: Account voll nativ
|
||||
- η-7: Legal-Sheets via SafariView
|
||||
- η-8: Submit / ShareExt / Spotlight an Snapshot v2 anpassen
|
||||
- η-9: Polish (iPad-Split, Accessibility, macOS-Layout)
|
||||
- η-10: TestFlight
|
||||
|
||||
Bei Phasen-Wechsel: PLAN.md aktualisieren + Memory-Eintrag
|
||||
`project_zitare_native.md` nachziehen (sobald angelegt).
|
||||
`project_zitare_native_dehybrid.md` nachziehen.
|
||||
|
||||
## Don't do
|
||||
|
||||
- **Keine Native-Re-Implementation der Read-Routes.** Wenn dir die
|
||||
Web-Quote-Ansicht im WebView nicht gefällt, fixe sie in
|
||||
`../zitare/apps/zitare/src/routes/(read)/`. Nicht hier eine zweite
|
||||
bauen.
|
||||
- **Kein `WKWebView` im App-Binary.** SafariView (SFSafariViewController)
|
||||
ist die einzige zugelassene Browser-Brücke, und nur für statisches
|
||||
Verein-Recht (Impressum, Datenschutz, Lizenz). Wenn du WKWebView
|
||||
brauchen würdest, baust du es als native View nach.
|
||||
- **Kein eigener FSRS-Port, kein eigener Pagefind-Klon.** Existiert
|
||||
beides nicht für Zitare und soll nicht entstehen.
|
||||
beides nicht für Zitare. Lokale FTS in η-5 ist SwiftData-Predicate.
|
||||
- **Keine Push-Notification-Pipeline.** Widget reicht.
|
||||
- **Keine externen UI-Libs / kein Sentry / kein Crash-Reporting-
|
||||
SaaS.** OSLog only (Compliance).
|
||||
- **Keine offline-Volltext-Funktion im WebView.** Wenn Offline-
|
||||
Lesen Use-Case wird → PWA-Pfad (Option A in `zitare/CLAUDE.md`),
|
||||
nicht hier.
|
||||
- **Kein Edit / Moderation / Admin in der App.** Web-Surface
|
||||
(`app.zitare.com`) bleibt SOT für schreibende Pfade jenseits von
|
||||
Submit. Wer das braucht, öffnet Safari.
|
||||
|
|
|
|||
170
PLAN.md
170
PLAN.md
|
|
@ -1,108 +1,118 @@
|
|||
# Plan — zitare-native (SwiftUI Hybrid)
|
||||
# Plan — zitare-native (η = fully native)
|
||||
|
||||
**Stand: 2026-05-14 — Phase ζ-0 Setup.** Repo-Skelett, `project.yml`,
|
||||
`CLAUDE.md`, leerer Build steht aus. Vollständige Phasen-Begründung
|
||||
in [`../mana/docs/playbooks/ZITARE_NATIVE_GREENFIELD.md`](../mana/docs/playbooks/ZITARE_NATIVE_GREENFIELD.md).
|
||||
**Stand: 2026-05-22 — Phase η-0 in Arbeit.** Hybrid-Strategie ist
|
||||
revidiert. Lese-Surfaces werden nativ. Vollständige Phasen-Begründung
|
||||
+ Erfolgs-Kriterien in [`docs/NATIVE_LIFT_PLAN.md`](docs/NATIVE_LIFT_PLAN.md).
|
||||
|
||||
> **SOT:** das Greenfield-Playbook. Dieses File ist nur die App-lokale
|
||||
> **SOT:** der NATIVE_LIFT_PLAN. Dieses File ist nur die App-lokale
|
||||
> Status-Spur.
|
||||
|
||||
## Aktueller Stand
|
||||
## Archiv: Phasen ζ-0 bis ζ-2 (Hybrid, abgeschlossen)
|
||||
|
||||
🚧 **ζ-0 — Setup (2026-05-14, in Arbeit)**
|
||||
Code aus diesen Phasen lebt teils weiter, teils ist er entfernt:
|
||||
|
||||
- [x] Repo-Skelett unter `~/Documents/Code/zitare-native/`
|
||||
- [x] `project.yml` mit Bundle `ev.mana.zitare`, drei Targets
|
||||
(App + Widget + ShareExt), ManaSwiftCore + ManaSwiftUI via
|
||||
`path: ../mana-swift-core` / `path: ../mana-swift-ui`
|
||||
- [x] `.swiftformat`, `.swiftlint.yml`, `.gitignore`
|
||||
- [x] `CLAUDE.md`, `README.md`, `PLAN.md`
|
||||
- [x] Source-Stubs (App, RootView, AppConfig, ZitareAPI, Log,
|
||||
ZitareTheme, Resources)
|
||||
- [x] `xcodegen generate` lokal grün (2026-05-14)
|
||||
- [x] `swiftlint --strict` 0 violations in 14 files
|
||||
- [x] Leerer Build im iOS-Simulator (iPhone 16e, Xcode 26.2 SDK,
|
||||
`xcodebuild ... -destination 'platform=iOS Simulator,name=iPhone 16e'`)
|
||||
- [x] Unit-Tests grün (6/6 AppConfigTests) + UI-Smoke grün
|
||||
- [ ] Leerer Build auf macOS — **blockiert auf Apple-Developer-Portal-
|
||||
Setup.** App-Group `group.ev.mana.zitare` muss im Portal
|
||||
registriert werden, sonst Provisioning-Profile-Fehler. Same
|
||||
Blocker wie cards-native ζ-7 / memoro-native (siehe Memory
|
||||
`project_memoro_native.md` „Apple-Dev-Portal App-Group-
|
||||
Aktivierung nötig vor Test").
|
||||
- [ ] ManaCore-Login mit Founder-Account, JWT im Keychain (manueller
|
||||
Test im Simulator, nicht im CI)
|
||||
- [x] `/healthz`-Probe gegen `zitare-api.mana.how` loggt 200 (live
|
||||
gegen Mac-Mini-API verifiziert 2026-05-14 12:14, HTTP/2 200,
|
||||
OSLog „Healthz: OK")
|
||||
- [ ] AASA auf `https://zitare.com/.well-known/apple-app-site-association`
|
||||
(Aufgabe ans Zitare-Web-Repo, blockiert ζ-1 nicht ζ-0)
|
||||
- [ ] Git-Repo `git.mana.how/till/zitare-native` (push)
|
||||
- ✅ **ζ-0 Setup** — Repo-Skelett, `project.yml`, iOS-Simulator-Build,
|
||||
ManaCore-Login + Healthz-Probe live (2026-05-14).
|
||||
- ✅ **ζ-1 WebShellView + Cookie-Bridge** — gebaut, dann in η-0 (heute)
|
||||
wieder gelöscht. `CookieBridge.swift` weg, `ManaWebShell`-Dep raus
|
||||
aus `project.yml`.
|
||||
- 🚧 **ζ-2 Snapshot-Sync + DailyQuoteWidget (Code-Done)** — Widget-Code
|
||||
+ SwiftData-Stub-Modelle existieren, Endpoint im Web-Repo (Snapshot
|
||||
v1 `index-min.json`) + Apple-Dev-Portal-App-Group fehlen für E2E.
|
||||
Wird in η-1 auf Snapshot v2 erweitert.
|
||||
|
||||
### ζ-0 Verifikations-Log
|
||||
## Aktuell
|
||||
|
||||
```
|
||||
2026-05-14 12:08 xcodebuild iOS Simulator iPhone 16e — BUILD SUCCEEDED
|
||||
(nach Fix: .iso8601withFractional war cards-native-
|
||||
local; in zitare-native auskommentiert, ζ-3 Port-TODO)
|
||||
2026-05-14 12:10 Unit-Tests: 6/6 AppConfigTests passed in 0.03s
|
||||
2026-05-14 12:11 UI-Smoke: test_appLaunches passed in 5.79s
|
||||
(nach Fix: Test suchte "Zitare" das nur im Account-
|
||||
Tab ist, jetzt auf Default-Tab "Lesen" geändert)
|
||||
2026-05-14 12:09 xcodebuild macOS — BUILD FAILED, blockiert auf
|
||||
Apple-Dev-Portal App-Group-Registrierung
|
||||
2026-05-14 12:14 iPhone 16e Simulator: App-Launch + Live-Healthz
|
||||
gegen zitare-api.mana.how → HTTP/2 200, OSLog
|
||||
"[ev.mana.zitare:app] Zitare starting — auth
|
||||
status: signedOut" und "[ev.mana.zitare:api]
|
||||
Healthz: OK" (Fix: AuthenticatedTransport rejecte
|
||||
notSignedIn auf public Endpoint, jetzt direkter
|
||||
URLSession-Call für /healthz)
|
||||
```
|
||||
✅ **η-0 — De-Hybrid (2026-05-22, abgeschlossen)**
|
||||
|
||||
## Phasen-Übersicht
|
||||
- [x] `Sources/Core/WebShell/CookieBridge.swift` gelöscht +
|
||||
WebShell-Verzeichnis entfernt.
|
||||
- [x] `ManaWebShell`-Dep aus `project.yml` raus.
|
||||
- [x] `RootView.swift` auf vier native Tabs umgestellt; Lesen +
|
||||
Erkunden zeigen Platzhalter-Views (η-2/η-4-Hinweis).
|
||||
- [x] DocComments in `DeepLinkRouter.swift`, `AppConfig.swift`,
|
||||
`AccountView.swift`, `SettingsView.swift` von WebView-Verweisen
|
||||
befreit.
|
||||
- [x] `CLAUDE.md` Invarianten ersetzt (Hybrid → η).
|
||||
- [x] `xcodegen generate` + iOS-Simulator-Build grün (iPhone 16e, Xcode
|
||||
26.2 SDK).
|
||||
- [x] `nm ZitareNative` zeigt 0 WKWebView-Referenzen; `otool -L`
|
||||
kein WebKit-Framework-Link. Binary ist WebView-frei.
|
||||
- [x] Tests grün: 20/20 (AppConfigTests + DeepLinkRouterTests +
|
||||
SnapshotSyncTests + SmokeUITests). Pre-existing Keychain-Service-
|
||||
Test auf `ManaSharedKeychainGroup` aktualisiert (war out-of-sync
|
||||
seit Cross-App-SSO-Konsolidierung).
|
||||
- [x] Greenfield-Playbook `mana/docs/playbooks/ZITARE_NATIVE_GREENFIELD.md`
|
||||
als überholt markiert (Banner + status-Header).
|
||||
|
||||
## Phasen-Übersicht (η)
|
||||
|
||||
| Phase | Ziel | Erfolg | Status |
|
||||
|---|---|---|---|
|
||||
| ζ-0 | Setup, leerer Build, Login | iOS-Build ✅, Tests ✅, Healthz Live ✅ | ✅ (Mac + Git-Push offen) |
|
||||
| ζ-1 | WebShellView + Universal-Links | WebView rendert, UL-Routing testbar, Web-Header ausgeblendet | ✅ |
|
||||
| ζ-2 | Snapshot-Sync + DailyQuoteWidget | Code + Tests grün, Endpoint im Web-Repo + Apple-Dev-Portal-App-Group fehlen für E2E | 🚧 (Code-Done) |
|
||||
| ζ-3 | Submit-View nativ | Founder submittet Quote, Draft in /admin/queue | ⏳ |
|
||||
| ζ-4 | Spotlight + ShareExt + App Intents | Spotlight findet, ShareExt POSTet | ⏳ |
|
||||
| ζ-5 | Polish (Theme-Sync, iPad, A11y) | Eine Woche ohne Safari-Tab nutzbar | ⏳ |
|
||||
| ζ-6 | App-Store-Submission | Approved | ⏳ |
|
||||
| η-0 | De-Hybrid | Build grün, kein WKWebView im Binary | ✅ |
|
||||
| η-1 | Snapshot v2 + volles SwiftData-Schema | Cold-Start lädt vollen Korpus, ETag spart Bytes | ⏳ (Web-Repo-Blocker) |
|
||||
| η-2 | Read-Core (Heute, Quote, Author) | Flugmodus + UL aus Mail-App zeigt Quote nativ | ⏳ |
|
||||
| η-3 | Read-Browse (Source/Theme/Place/Role/Epoch/Lang) | Alle Deep-Link-Pattern aus app-manifest.json nativ | ⏳ |
|
||||
| η-4 | Explore + Filter | „Roman + DE + lang" Filter live ohne Server | ⏳ |
|
||||
| η-5 | Search (lokal + Server-Fallback) | Offline-Treffer + Online-Ergänzung | ⏳ |
|
||||
| η-6 | Account nativ | Sign-In/Out + Submissions-Liste nativ | ⏳ |
|
||||
| η-7 | Legal via SafariView | Impressum/Datenschutz/Lizenz als SFSafariViewController | ⏳ |
|
||||
| η-8 | Submit + ShareExt + Spotlight an Snapshot v2 | Spotlight findet via Volltext | ⏳ |
|
||||
| η-9 | Polish (iPad / Accessibility / macOS) | Solo-Nutzung ohne Browser-Tab eine Woche | ⏳ |
|
||||
| η-10 | TestFlight | Approved | ⏳ |
|
||||
|
||||
## Externe Blocker (nicht von Native-Code lösbar)
|
||||
|
||||
- [ ] **Apple-Dev-Portal: App-Group `group.ev.mana.zitare`** registrieren
|
||||
— sonst kein macOS-Build und kein Widget auf realem Gerät. Selbe
|
||||
Aktion wie für cards-/memoro-native nötig.
|
||||
- [ ] **Apple-Dev-Portal: App-Group `group.ev.mana.zitare`**
|
||||
registrieren — sonst kein macOS-Build und kein Widget auf realem
|
||||
Gerät. Selbe Aktion wie für cards/memoro/moodlit/herbatrium nötig.
|
||||
- [ ] **Cloudflare:** DNS-CNAME für `zitare.com` (Apex) auf
|
||||
`1435166a-0e3f-4222-8de6-744f32cea5c9.cfargotunnel.com`
|
||||
proxied. Plus Cleanup des versehentlichen
|
||||
`zitare.com.mana.how`-CNAME. Vollständige Anleitung in
|
||||
`zitare.com.mana.how`-CNAME. Anleitung in
|
||||
[`docs/CLOUDFLARE_TODO.md`](docs/CLOUDFLARE_TODO.md).
|
||||
- [ ] **Forgejo-Repo `git.mana.how/till/zitare-native`** anlegen +
|
||||
Push — ✅ erledigt 2026-05-14, 5 Commits live.
|
||||
|
||||
## Web-Vorbedingungen (Aufgabe an `../zitare/`)
|
||||
|
||||
- [ ] AASA-Eintrag auf `https://zitare.com/.well-known/apple-app-site-association`
|
||||
mit `appID: QP3GLU8PH3.ev.mana.zitare`
|
||||
- [ ] `index-min.json` als versionierter, ETag-versehener Endpoint
|
||||
(oder einigen, dass die Build-Output-Datei stabil bleibt)
|
||||
- [ ] `POST /api/v1/share/receive` mit `mana/text`-Envelope-Handler
|
||||
(Manifest registriert, Code TBD)
|
||||
- [ ] `zitare.com` Cloudflare-Zone-Onboarding (steht im
|
||||
`zitare/STATUS.md` als offen)
|
||||
- [ ] Cookie-SSO-Compat auf `zitare.mana.how` end-to-end testen
|
||||
(Phase 2.G im Web-Repo code-fertig, Live-Test offen)
|
||||
η-1-Blocker — ohne diese Endpoints kein Offline-Lesen:
|
||||
|
||||
- [ ] `https://zitare.com/index-full.json` mit allen Quote-Feldern
|
||||
inkl. Volltext.
|
||||
- [ ] Stammdaten-Collections: `authors.json`, `sources.json`,
|
||||
`themes.json`, `places.json`, `epochs.json`, `roles.json`,
|
||||
`languages.json` auf `zitare.com/`.
|
||||
- [ ] ETag-Header für jeden dieser Endpoints (Cache-Control: 1h).
|
||||
|
||||
η-5-Voraussetzung:
|
||||
|
||||
- [ ] `GET /api/v1/quotes?q=<query>` Volltext-Search bestätigt /
|
||||
ergänzt in `zitare/apps/api/`.
|
||||
|
||||
η-6-Voraussetzung:
|
||||
|
||||
- [ ] `GET /api/v1/me/submissions` (eigene Einreichungen) bestätigt /
|
||||
ergänzt.
|
||||
|
||||
Restliche Web-Vorbedingungen (orthogonal):
|
||||
|
||||
- [ ] AASA auf `https://zitare.com/.well-known/apple-app-site-association`
|
||||
mit `appID: QP3GLU8PH3.ev.mana.zitare`.
|
||||
- [ ] `zitare.com` Cloudflare-Zone-Onboarding (steht in
|
||||
`zitare/STATUS.md` als offen).
|
||||
|
||||
## Verifikations-Lücken
|
||||
|
||||
Wird nach jedem ζ-Schritt befüllt.
|
||||
η-0: keine offenen. Pre-existing 4 swiftlint-strict-Violations in
|
||||
`ZitareNativeApp.swift` + `SubmitQuoteView.swift` bleiben — sind aus
|
||||
ζ-3 (Submit-Phase) und nicht η-0-Scope.
|
||||
|
||||
## Quirks
|
||||
|
||||
Wird im Verlauf befüllt. Format-Vorbild: `../zitare/STATUS.md` Sektion
|
||||
„Quirks".
|
||||
- `swiftformat` läuft aggressiver als die letzte ζ-Phase: entfernt
|
||||
`Sendable`-Konformanz aus `DropRecord`/`QuoteDraft`/`SubmittedQuote`
|
||||
und `self.`-Prefix in `OS_log_t.info(...)`-`@autoclosure`-Calls.
|
||||
Beides führt zu Strict-Concurrency-Errors. In η-0 wurden die
|
||||
Kollateral-Edits zurückgerollt; `swiftformat` muss vor η-1 mit
|
||||
expliziter Exclude-Liste oder einer .swiftformat-Regel-Anpassung
|
||||
gezähmt werden, sonst zerstört der nächste Format-Lauf wieder
|
||||
Submit-Code.
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
import Foundation
|
||||
|
||||
/// Routet sowohl Custom-Scheme- (`zitare://`) als auch Universal-Link-URLs
|
||||
/// (`zitare.com/...`) auf eine konkrete `WebTarget` + Ziel-Tab.
|
||||
/// (`zitare.com/...`) auf eine normalisierte URL + Ziel-Tab.
|
||||
///
|
||||
/// **η-0 (de-Hybrid):** Die URL ist nicht mehr WebView-Target, sondern
|
||||
/// Eingabe für das native `NavigationPath`-Routing in η-2. Path-Struktur
|
||||
/// (`/q/<slug>`, `/a/<slug>`, `/c/<slug>`, `/thema/<slug>` etc.) bleibt
|
||||
/// 1:1 wie im Web-Repo — die Native-Views matchen die gleichen Slugs.
|
||||
///
|
||||
/// Pure-Logic, kein State — easy testbar.
|
||||
enum DeepLinkRouter {
|
||||
/// Mapt eine externe URL auf eine WebShell-URL.
|
||||
/// Normalisiert eine externe URL auf den kanonischen `https://`-Pfad.
|
||||
/// `zitare://quote/x` → `https://zitare.com/q/x`,
|
||||
/// `zitare://author/x` → `https://zitare.com/a/x`,
|
||||
/// `zitare://collection/x` → `https://zitare.com/c/x`.
|
||||
|
|
|
|||
|
|
@ -1,67 +1,29 @@
|
|||
import ManaAuthUI
|
||||
import ManaCore
|
||||
import ManaWebShell
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
private func makeWebShellConfig() -> WebShellConfig {
|
||||
WebShellConfig(
|
||||
allowedHosts: [
|
||||
"zitare.com",
|
||||
"www.zitare.com",
|
||||
"*.mana.how",
|
||||
],
|
||||
userAgent: AppConfig.userAgent,
|
||||
backgroundColor: ZitareTheme.background,
|
||||
progressTint: ZitareTheme.primary,
|
||||
errorBackgroundColor: ZitareTheme.muted,
|
||||
errorForegroundColor: ZitareTheme.foreground,
|
||||
errorIconColor: ZitareTheme.warning,
|
||||
userScripts: [
|
||||
// Syncs System-Dark-Mode in den WebView; zitare-web liest
|
||||
// `localStorage['zitare-mode']` beim First Paint und toggelt
|
||||
// dann `.dark` auf <html>.
|
||||
WebShellScripts.syncDarkMode(localStorageKey: "zitare-mode"),
|
||||
// Versteckt den zitare-web-Header (Brand-Logo + Nav), weil
|
||||
// die native TabBar bereits global navigiert.
|
||||
WebShellScripts.hideElements(
|
||||
selectors: [
|
||||
"header[data-app-nav]",
|
||||
"body header:has(a.brand)",
|
||||
"body > header:first-of-type",
|
||||
"body > div > header:first-of-type",
|
||||
],
|
||||
tagName: "hide-web-header"
|
||||
),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
/// Top-Level-View: TabView mit drei Tabs.
|
||||
/// Top-Level-View: TabView mit vier nativen Tabs.
|
||||
///
|
||||
/// **Phase ζ-1:** Lesen + Erkunden laden `zitare.com` via `WebShellView`.
|
||||
/// Universal-Links auf `zitare.com/q/<slug>` / `/a/<slug>` etc. öffnen
|
||||
/// die App und routen in den passenden Tab.
|
||||
/// **η-0 (de-Hybrid 2026-05-22):** `WebShellView` ist raus. Lesen +
|
||||
/// Erkunden zeigen jetzt Platzhalter-Views; das volle native Read-
|
||||
/// Surface kommt in η-2 (Heute / Quote-Detail / Author-Detail) bzw.
|
||||
/// η-4 (Explore + Filter). Universal-Links werden hier vorerst nur
|
||||
/// geloggt und in den Lesen-Tab gepinnt, bis `NavigationStack`-Routing
|
||||
/// in η-2 steht. Submit + Konto sind schon nativ.
|
||||
struct RootView: View {
|
||||
@Environment(AuthClient.self) private var auth
|
||||
@Environment(ManaAuthGate.self) private var authGate
|
||||
@State private var selectedTab: AppTab = .read
|
||||
@State private var readTarget = WebTarget(url: AppConfig.webBaseURL)
|
||||
@State private var exploreTarget = WebTarget(
|
||||
url: AppConfig.webBaseURL.appendingPathComponent("explore")
|
||||
)
|
||||
@State private var reloadCounter: Int = 0
|
||||
@State private var pendingDeepLink: URL?
|
||||
@State private var healthStatus: HealthStatus = .unknown
|
||||
|
||||
private var webShellConfig: WebShellConfig { makeWebShellConfig() }
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $selectedTab) {
|
||||
WebShellView(target: readTarget, config: webShellConfig)
|
||||
ReadPlaceholderView(pendingDeepLink: pendingDeepLink)
|
||||
.tabItem { Label("Lesen", systemImage: "book") }
|
||||
.tag(AppTab.read)
|
||||
|
||||
WebShellView(target: exploreTarget, config: webShellConfig)
|
||||
ExplorePlaceholderView()
|
||||
.tabItem { Label("Erkunden", systemImage: "sparkle.magnifyingglass") }
|
||||
.tag(AppTab.explore)
|
||||
|
||||
|
|
@ -73,35 +35,31 @@ struct RootView: View {
|
|||
.tabItem { Label("Konto", systemImage: "person.circle") }
|
||||
.tag(AppTab.account)
|
||||
}
|
||||
// Mac-Window-Hintergrund auf Paper-Theme setzen, damit der
|
||||
// TabBar-/Title-Bar-Bereich oben nicht mit dem System-Grau
|
||||
// gegen das Paper-Theme ausreißt. `windowToolbar`-Placement
|
||||
// ist macOS-only.
|
||||
.background(ZitareTheme.background.ignoresSafeArea())
|
||||
#if os(macOS)
|
||||
.toolbarBackground(ZitareTheme.background, for: .windowToolbar)
|
||||
.toolbarBackground(.visible, for: .windowToolbar)
|
||||
#endif
|
||||
.manaBrand(ZitareBrand.manaBrand)
|
||||
.manaAuthGate(authGate) {
|
||||
NavigationStack {
|
||||
ManaLoginView(
|
||||
auth: auth,
|
||||
onSignUpTapped: {},
|
||||
onForgotTapped: {}
|
||||
)
|
||||
.manaBrand(ZitareBrand.manaBrand)
|
||||
.manaBrand(ZitareBrand.manaBrand)
|
||||
.manaAuthGate(authGate) {
|
||||
NavigationStack {
|
||||
ManaLoginView(
|
||||
auth: auth,
|
||||
onSignUpTapped: {},
|
||||
onForgotTapped: {}
|
||||
)
|
||||
.manaBrand(ZitareBrand.manaBrand)
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await probeHealth()
|
||||
}
|
||||
.onOpenURL { url in
|
||||
handle(url: url)
|
||||
}
|
||||
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
|
||||
if let url = activity.webpageURL { handle(url: url) }
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await probeHealth()
|
||||
}
|
||||
.onOpenURL { url in
|
||||
handle(url: url)
|
||||
}
|
||||
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
|
||||
if let url = activity.webpageURL { handle(url: url) }
|
||||
}
|
||||
}
|
||||
|
||||
private func probeHealth() async {
|
||||
|
|
@ -118,31 +76,18 @@ struct RootView: View {
|
|||
}
|
||||
}
|
||||
|
||||
/// Universal-Link- und Custom-URL-Routing. Wird sowohl von
|
||||
/// `onOpenURL` (Custom-Scheme `zitare://...`) als auch von
|
||||
/// `onContinueUserActivity` (Universal-Links auf `zitare.com/...`)
|
||||
/// aufgerufen.
|
||||
/// Deep-Link- + Universal-Link-Routing.
|
||||
///
|
||||
/// Routing-Regeln (gespiegelt zu `app-manifest.json#link_patterns`):
|
||||
/// - `/q/<slug>`, `/a/<slug>`, `/c/<slug>` → Lesen-Tab
|
||||
/// - `/heute`, `/random`, `/feed.rss` → Lesen-Tab
|
||||
/// - `/explore`, `/region/...`, `/thema/...`, `/rolle/...`,
|
||||
/// `/epoche/...`, `/sprache/...`, `/search`, `/t/...` → Erkunden-Tab
|
||||
/// - alles andere unter `zitare.com` → Lesen-Tab, Root-Pfad
|
||||
///
|
||||
/// Custom-Scheme `zitare://quote/<slug>` wird auf
|
||||
/// `https://zitare.com/q/<slug>` umgemappt.
|
||||
/// η-0: Routing-Logik (URL-Normalisierung + Tab-Auswahl) bleibt
|
||||
/// erhalten, das eigentliche Ansteuern einer Quote/Author/Source-
|
||||
/// Detail-View kommt in η-2 mit `NavigationPath`. Solange parken
|
||||
/// wir die URL in `pendingDeepLink`, die Placeholder-View zeigt
|
||||
/// sie zur Diagnose an.
|
||||
private func handle(url: URL) {
|
||||
Log.app.info("Deep-Link empfangen: \(url.absoluteString, privacy: .public)")
|
||||
let routed = DeepLinkRouter.route(url, base: AppConfig.webBaseURL)
|
||||
reloadCounter += 1
|
||||
if routed.isExplore {
|
||||
exploreTarget = WebTarget(url: routed.url, reloadToken: reloadCounter)
|
||||
selectedTab = .explore
|
||||
} else {
|
||||
readTarget = WebTarget(url: routed.url, reloadToken: reloadCounter)
|
||||
selectedTab = .read
|
||||
}
|
||||
pendingDeepLink = routed.url
|
||||
selectedTab = routed.isExplore ? .explore : .read
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -158,3 +103,55 @@ enum HealthStatus {
|
|||
case ok
|
||||
case down
|
||||
}
|
||||
|
||||
/// η-0 Platzhalter — wird in η-2 durch HeuteView + QuoteFeedView ersetzt.
|
||||
private struct ReadPlaceholderView: View {
|
||||
let pendingDeepLink: URL?
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "book")
|
||||
.font(.system(size: 56))
|
||||
.foregroundStyle(ZitareTheme.mutedForeground)
|
||||
Text("Lesen")
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
Text("Native Read-Surface kommt in η-2 (Heute, Quote-Detail, Author-Detail).")
|
||||
.font(.callout)
|
||||
.foregroundStyle(ZitareTheme.mutedForeground)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 32)
|
||||
if let url = pendingDeepLink {
|
||||
Text("Deep-Link wartet auf η-2:\n\(url.absoluteString)")
|
||||
.font(.caption.monospaced())
|
||||
.foregroundStyle(ZitareTheme.mutedForeground)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 32)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(ZitareTheme.background)
|
||||
}
|
||||
}
|
||||
|
||||
/// η-0 Platzhalter — wird in η-4 durch ExploreView (Facet-Filter) ersetzt.
|
||||
private struct ExplorePlaceholderView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "sparkle.magnifyingglass")
|
||||
.font(.system(size: 56))
|
||||
.foregroundStyle(ZitareTheme.mutedForeground)
|
||||
Text("Erkunden")
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
Text("Filter (Sprache, Länge, Thema, Region, Epoche, Rolle) + lokale Suche kommen in η-4 und η-5.")
|
||||
.font(.callout)
|
||||
.foregroundStyle(ZitareTheme.mutedForeground)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(ZitareTheme.background)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ import ManaCore
|
|||
/// App-spezifische Konfiguration für Zitare. Implementiert
|
||||
/// `ManaAppConfig` aus ManaCore und ergänzt die Zitare-eigene
|
||||
/// `apiBaseURL` (api.zitare.com, getrennt von mana-auth) sowie
|
||||
/// `webBaseURL` (zitare.com, für WKWebView und Universal-Links)
|
||||
/// und `appBaseURL` (app.zitare.com, für eingeloggte Pfade).
|
||||
/// `webBaseURL` (zitare.com, für Universal-Links + Snapshot-Pull +
|
||||
/// SafariView auf Legal-Seiten) und `appBaseURL` (app.zitare.com,
|
||||
/// historisch SPA-Surface — η-0+ nicht mehr vom Native-Client geladen).
|
||||
///
|
||||
/// Cutover zu .zitare.com-Subdomains am 2026-05-20. zitare.mana.how
|
||||
/// ist abgeschaltet; zitare-api.mana.how bleibt als Back-Compat-
|
||||
|
|
@ -36,18 +37,19 @@ enum AppConfig {
|
|||
/// Edit, Admin, Me).
|
||||
static let appBaseURL = URL(string: "https://app.zitare.com")!
|
||||
|
||||
/// Default-URL für den WebView (öffentliches Lese-Surface).
|
||||
/// Default-Web-URL (öffentliches Lese-Surface, Snapshot, AASA).
|
||||
/// η-0+: kein WebView mehr, aber bleibt Universal-Link-Domain und
|
||||
/// SafariView-Ziel für Legal-Seiten.
|
||||
static let webBaseURL = publicWebURL
|
||||
|
||||
/// Endpoint für den Korpus-Snapshot (Phase ζ-2). Heute noch nicht
|
||||
/// als statische HTTP-Datei publiziert — Aufgabe im Web-Repo:
|
||||
/// `apps/zitare/static/index-min.json` aus dem Snapshot-Job
|
||||
/// zusätzlich rauskopieren. Bis dahin schlägt der Pull mit 404
|
||||
/// fehl und `SnapshotSync.tryRefresh()` macht fail-soft no-op.
|
||||
/// Endpoint für den Korpus-Snapshot (heute Phase ζ-2: `index-min.json`,
|
||||
/// in η-1 ersetzt durch `index-full.json` + Stammdaten-Collections,
|
||||
/// siehe `docs/NATIVE_LIFT_PLAN.md`). Bis das im Web-Repo gebaut ist,
|
||||
/// schlägt der Pull mit 404 fehl und `SnapshotSync.tryRefresh()` macht
|
||||
/// fail-soft no-op.
|
||||
static let snapshotURL = webBaseURL.appendingPathComponent("index-min.json")
|
||||
|
||||
/// User-Agent-Suffix für WKWebView (ManaWebShell). WKWebView hängt
|
||||
/// das an seinen Standard-UA an, ersetzt ihn nicht.
|
||||
// User-Agent-Suffix für URLSession-API-Calls.
|
||||
#if os(macOS)
|
||||
static let userAgent = "ZitareNative/0.1 (macOS)"
|
||||
#else
|
||||
|
|
|
|||
|
|
@ -1,88 +0,0 @@
|
|||
import Foundation
|
||||
import ManaCore
|
||||
import WebKit
|
||||
|
||||
/// Reicht den mana-auth-JWT als Cookie an den `WKWebView` weiter, sodass
|
||||
/// eingeloggte `(app)`-Routen auf `app.zitare.com` ohne zweiten Login
|
||||
/// erreichbar sind.
|
||||
///
|
||||
/// **Phase ζ-1: Skeleton.** Methoden existieren, werden aber heute
|
||||
/// nicht aufgerufen — bevor sie scharfgeschaltet werden, muss der
|
||||
/// Cookie-SSO-Pfad auf der Web-Seite (`zitare/apps/api/src/auth/`
|
||||
/// und `apps/zitare/src/lib/auth/token-helper.ts`) gegen einen
|
||||
/// *echten* mana-auth-Token End-to-End getestet sein (Verifikations-
|
||||
/// Lücke in `zitare/STATUS.md`).
|
||||
///
|
||||
/// **Cross-Domain-Flow (Cutover 2026-05-20):** Der WKWebView lädt
|
||||
/// `app.zitare.com` (Brand-Domain). Die SvelteKit-App ruft beim
|
||||
/// Boot Cross-Origin `POST auth.mana.how/api/v1/auth/refresh` mit
|
||||
/// `credentials: 'include'` und erwartet einen Refresh-Cookie auf
|
||||
/// `.mana.how`. Der Cookie-Domain-Wert hier (`.mana.how`) ist
|
||||
/// genau richtig — der Browser sendet ihn an das XHR-Ziel
|
||||
/// (auth.mana.how), unabhängig vom Source-Page-Host. Identisches
|
||||
/// Pattern wie im Web-Client (`apps/zitare/src/lib/auth.ts`).
|
||||
///
|
||||
/// **Cookie-Schema** (gespiegelt zu mana-auth `better-auth.config.ts`):
|
||||
/// - Name: `mana.access` (JWT) und optional `mana.refresh` (Opaque)
|
||||
/// - Domain: `.mana.how` (Cookie wird an auth.mana.how-XHR mitgesendet)
|
||||
/// - Path: `/`
|
||||
/// - Secure: true, HTTPOnly: false (WebView muss lesen können)
|
||||
/// - SameSite: **None** — mana-auth setzt für Cross-Subdomain-SSO
|
||||
/// `sameSite: 'none'` (better-auth.config.ts), ohne das wird der
|
||||
/// Cookie bei Cross-Origin-POST nicht mitgesendet. Foundation
|
||||
/// `HTTPCookieStringPolicy` hat dafür keinen Konstanten-Wert →
|
||||
/// `cookieAttributesNone` als Roh-String über die initWithProperties-
|
||||
/// `String`-Variante.
|
||||
enum CookieBridge {
|
||||
/// Setzt den `mana.access`-Cookie im geteilten `WKHTTPCookieStore`,
|
||||
/// wenn der `AuthClient` einen gültigen JWT hält. No-op sonst.
|
||||
@MainActor
|
||||
static func installManaAccess(from auth: AuthClient) async {
|
||||
guard case .signedIn = auth.status, let token = currentAccessToken(from: auth) else {
|
||||
Log.web.debug("CookieBridge: kein signedIn-Token, no-op")
|
||||
return
|
||||
}
|
||||
guard let cookie = makeAccessCookie(token: token) else {
|
||||
Log.web.warning("CookieBridge: konnte Cookie-Properties nicht bauen")
|
||||
return
|
||||
}
|
||||
let store = WKWebsiteDataStore.default().httpCookieStore
|
||||
await store.setCookie(cookie)
|
||||
Log.web.info("CookieBridge: mana.access für .mana.how gesetzt")
|
||||
}
|
||||
|
||||
/// Entfernt den `mana.access`-Cookie wieder — etwa nach Logout.
|
||||
@MainActor
|
||||
static func removeManaAccess() async {
|
||||
let store = WKWebsiteDataStore.default().httpCookieStore
|
||||
let cookies = await store.allCookies()
|
||||
for cookie in cookies where cookie.name == "mana.access" {
|
||||
await store.deleteCookie(cookie)
|
||||
}
|
||||
Log.web.info("CookieBridge: mana.access entfernt")
|
||||
}
|
||||
|
||||
private static func currentAccessToken(from auth: AuthClient) -> String? {
|
||||
// ManaCore hält den JWT im Keychain. In ζ-3 ersetzt durch die
|
||||
// tatsächliche `auth.currentAccessToken()`-API; heute nur
|
||||
// Skelett-Hook, damit Cookie-Setup und API in Reichweite sind.
|
||||
// Linter beruhigen ohne unused warning:
|
||||
_ = auth
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func makeAccessCookie(token: String) -> HTTPCookie? {
|
||||
// SameSite=None: Foundation hat keinen Konstanten-Wert für
|
||||
// "None", aber HTTPCookie akzeptiert beliebige Strings für
|
||||
// .sameSitePolicy. Cross-Origin-POST von app.zitare.com →
|
||||
// auth.mana.how braucht None, sonst kein Cookie-Versand.
|
||||
HTTPCookie(properties: [
|
||||
.name: "mana.access",
|
||||
.value: token,
|
||||
.domain: ".mana.how",
|
||||
.path: "/",
|
||||
.secure: true,
|
||||
.sameSitePolicy: "None"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,10 @@ import ManaCore
|
|||
import SwiftUI
|
||||
|
||||
/// Phase ζ-0 minimal: zeigt Auth-Status und Healthz-Probe-Ergebnis.
|
||||
/// Phase ζ-3 erweitert um Submission-History-Link (via WebShell auf
|
||||
/// `zitare.mana.how/me`). Login-Sheet schon hier, damit Guests einen
|
||||
/// Anmelden-Button finden.
|
||||
/// Phase η-6 erweitert um native Submission-History (eigene
|
||||
/// Submissions-Liste via `GET /api/v1/me/submissions`) + Link auf
|
||||
/// Web-Konto-Verwaltung via SafariView. Login-Sheet schon hier, damit
|
||||
/// Guests einen Anmelden-Button finden.
|
||||
struct AccountView: View {
|
||||
@Environment(AuthClient.self) private var auth
|
||||
@Environment(ManaAuthGate.self) private var authGate
|
||||
|
|
@ -185,13 +186,12 @@ struct AccountView: View {
|
|||
|
||||
private var aboutCard: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Phase ζ-0 — Setup")
|
||||
Text("Phase η-0 — De-Hybrid")
|
||||
.font(.caption)
|
||||
.foregroundStyle(ZitareTheme.mutedForeground)
|
||||
Text(
|
||||
"Diese App ist noch im Aufbau. Web-App live auf "
|
||||
+ "zitare.com und zitare.mana.how. "
|
||||
+ "Plan in mana/docs/playbooks/ZITARE_NATIVE_GREENFIELD.md."
|
||||
"Diese App wird nativ ausgebaut. Web-App weiter live auf "
|
||||
+ "zitare.com. Plan in zitare-native/docs/NATIVE_LIFT_PLAN.md."
|
||||
)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(ZitareTheme.foreground)
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
import SwiftUI
|
||||
|
||||
/// Phase ζ-5 Placeholder.
|
||||
/// Phase η-9 Placeholder.
|
||||
///
|
||||
/// Aufgabenliste in ζ-5:
|
||||
/// Aufgabenliste in η-9:
|
||||
///
|
||||
/// - Theme-Toggle (System / Light / Dark) — propagiert per
|
||||
/// `localStorage['zitare-mode']` an den WebView.
|
||||
/// - Reader-Schriftgröße (S/M/L/XL) — per JS-Bridge an die Web-CSS-
|
||||
/// Variable `--zit-reader-size`.
|
||||
/// - DSGVO-Daten-Export (öffnet `zitare.mana.how/me` Export-Page im
|
||||
/// WebView).
|
||||
/// - About / Impressum / Lizenz (CC-BY-SA-4.0).
|
||||
/// - Theme-Toggle (System / Light / Dark) — direkt nativ via
|
||||
/// `@AppStorage("zitare-mode")` → `ZitareTheme.applyMode`.
|
||||
/// - Reader-Schriftgröße (S/M/L/XL) — `@AppStorage("zitare-reader-size")`,
|
||||
/// propagiert an `QuoteDetailView` (η-2).
|
||||
/// - DSGVO-Daten-Export — SafariView auf `https://app.zitare.com/me/export`.
|
||||
/// - About / Impressum / Lizenz (CC-BY-SA-4.0) — SafariView-Sheets (η-7).
|
||||
struct SettingsView: View {
|
||||
var body: some View {
|
||||
Text("Einstellungen — ζ-5 TODO")
|
||||
Text("Einstellungen — η-9 TODO")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import ManaCore
|
||||
import XCTest
|
||||
@testable import ZitareNative
|
||||
|
||||
/// Phase ζ-0 Basis-Tests: Konfigurations-Konstanten sind konsistent
|
||||
/// mit dem mana-Plattform-Setup (Bundle-ID, Keychain-Service,
|
||||
/// Endpoint-Domains).
|
||||
/// Basis-Tests: Konfigurations-Konstanten sind konsistent mit dem
|
||||
/// mana-Plattform-Setup (Bundle-ID, Keychain-Service, Endpoint-Domains).
|
||||
final class AppConfigTests: XCTestCase {
|
||||
func test_authBaseURL_pointsToManaAuth() {
|
||||
XCTAssertEqual(
|
||||
|
|
@ -12,8 +12,11 @@ final class AppConfigTests: XCTestCase {
|
|||
)
|
||||
}
|
||||
|
||||
func test_keychainService_matchesBundle() {
|
||||
XCTAssertEqual(AppConfig.manaAppConfig.keychainService, "ev.mana.zitare")
|
||||
func test_keychainService_matchesSharedGroup() {
|
||||
// Zitare-native teilt die Keychain-Group mit den 11 anderen
|
||||
// nativen mana-Apps (Cross-App-SSO via
|
||||
// QP3GLU8PH3.ev.mana.session, siehe mana-swift-core).
|
||||
XCTAssertEqual(AppConfig.manaAppConfig.keychainService, ManaSharedKeychainGroup)
|
||||
}
|
||||
|
||||
func test_apiBaseURL_pointsToZitareApi() {
|
||||
|
|
|
|||
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.
|
||||
|
|
@ -42,8 +42,6 @@ targets:
|
|||
product: ManaTokens
|
||||
- package: ManaSwiftUI
|
||||
product: ManaAuthUI
|
||||
- package: ManaSwiftUI
|
||||
product: ManaWebShell
|
||||
- target: ZitareWidgetExtension
|
||||
embed: true
|
||||
platformFilter: iOS
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue