cards/STATUS.md
Till JS 92a1d5804f Phase 12 R4: Marketplace ε — Pull-Requests + Card-Discussions
Pull-Requests (Diff-Modell add/modify/remove, GitHub-style 3-way
merge in der DB-Transaktion):

- POST /decks/:slug/pull-requests (auth) — neuer PR mit
  diff.{add,modify,remove}; previousContentHash für modify identifiziert
  die zu ersetzende Karte by content-hash, type bleibt aus dem alten
  Eintrag (modify ist field-only Replace)
- GET /decks/:slug/pull-requests (optional-auth) — Liste mit Status-
  Filter (open/merged/closed/rejected)
- GET /pull-requests/:id (optional-auth) — Detail
- POST /pull-requests/:id/close (auth) — Author oder Deck-Owner
- POST /pull-requests/:id/reject (auth) — nur Deck-Owner; getrennt
  von close, damit der PR-Author klares Feedback hat
- POST /pull-requests/:id/merge (auth) — nur Deck-Owner; baut neue
  card-list aus latest version + diff (removes weglassen, modifies
  fields-replace, adds anhängen mit re-counted ord), schreibt
  publicDeckVersions + publicDeckCards atomar in einer Drizzle-
  Transaction, bumpt latestVersionId und setzt PR auf merged.
  Default-Semver-Bump: minor (1.0.0 → 1.1.0). Authorenüberschreibbar
  via mergeNote/newSemver-Body-Felder.

Card-Discussions (Threads pro card_content_hash, überleben
Versions-Bumps solange Karten-Inhalt bleibt):

- POST /decks/:slug/cards/:hash/discussions (auth) — neuer Thread
  oder Reply (parent_id muss in derselben card_content_hash-Gruppe
  leben → 422 sonst)
- GET /cards/:hash/discussions (optional-auth) — Liste sichtbarer
  Comments, hidden gefiltert
- GET /decks/:slug/discussions/counts (optional-auth) — Bulk-Count
  pro card_content_hash für Deck-Übersicht (kein N+1)
- POST /discussions/:id/hide (auth) — Soft-Hide (Author oder Deck-
  Owner); kein Delete, Audit-Trail bleibt

Helpers:

- lib/marketplace/semver.ts — bumpMinor, isSemver, semverGreater
  (klein, ohne Range-Logik). Wird von PRs + später vom decks.ts
  publish-Flow konsumiert.

Bug-Fix:

- routes/marketplace/fork.ts hatte r.use('*', authMiddleware) am
  Anfang. An dem /api/v1/marketplace-Mount-Punkt fängt das Wildcard
  alle nachfolgenden Router-Mounts (PRs, Discussions) → anonymer
  GET /pull-requests wurde mit 401 abgelehnt. Refactor auf per-
  route authMiddleware (Pattern wie in subscriptions.ts und
  engagement.ts seit R3). Lessons learned dokumentiert in der
  STATUS.md-Zeile.

Verifikation:

- type-check 0 errors
- 11 neue Semver-Tests, 89 gesamt grün
- E2E-Smoke gegen lokale cards-api durch:
  · Cardecky publisht v1.0.0 (Apatheia, Eudaimonia, Logos)
  · Till's PR: modify Eudaimonia-Back, remove Logos, add Tugendlehre
  · Till's Merge-Versuch → 403 (deck_owner_only)
  · Cardecky merged → v1.1.0 atomar, card_count=3, ord-Reihenfolge:
    [Apatheia, Eudaimonia-mit-neuem-Back, Tugendlehre]
  · Re-Merge → 409 (pr_already_merged)
  · Till's Discussion-Thread + Cardecky-Reply mit parent_id
  · Cross-Card-parent abgelehnt → 422
  · Hide → Comment verschwindet aus Liste, total von 2 auf 1
  · Bulk-Counts liefert {hash → 2}
  · Smart-Merge-Pull v1.0.0→v1.1.0 wertet PR-Merge korrekt aus
    (changed=2 via Eudaimonia + Logos↔Tugendlehre ord-Heuristik)

Verbleibend: R5 Frontend-Routes (/explore, /d/[slug], /u/[slug],
/me/{published,subscribed,forks}), R6 voller UI-E2E.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:50:16 +02:00

513 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Cards — Projekt-Status & Onboarding
**Letztes Update:** 2026-05-08 (Phase 8 + Phase 9 erweiterte Polish-Welle)
**Wenn du gerade neu bist (Mensch oder KI):** dieses Dokument soll dir
in 5 Minuten den vollen Kontext geben. Lies es vor allem anderen.
---
## TL;DR
- **Cards** ist die föderierte Spaced-Repetition-App des Vereins
**mana e.V.** Strategie-B-Greenfield (beschlossen 2026-05-08): kein
Code-Übernahme aus dem alten `mana-monorepo`, sauber neu gebaut —
mit einer dokumentierten Ausnahme für den Anki-Format-Parser
(Phase 8c, standalone Parser-Logik).
- **30+ saubere Commits** auf `main`. Type-check 4/4 grün, **129 Tests
grün** (66 Domain + 56 API + 7 Web), lokaler E2E-Smoke (Postgres →
API → MinIO → Frontend → Cloze + Image-Occlusion + Anki-Import mit
Media → /stats → /me/export → /cards/hashes) durch.
- **🚀 LIVE seit 2026-05-08** auf `https://cardecky.mana.how` +
`https://cardecky-api.mana.how` (Mac Mini, Cloudflare-Tunnel
`1435166a-…`). Container `cards-{postgres,minio,api,web}`.
Forgejo-Remote `git.mana.how/till/cards`. Public-E2E bestätigt:
Deck + Card via API anlegbar, Manifest exposed,
`cards.*`/`cards-api.*` redirecten via nginx-301 zu cardecky.*.
- **Phasen 0, 1, 3, 4, 5, 8 vollständig durch.** **Phase 9 Polish-
Welle teilweise** (Card-Edit, Cloze-Editor, Inbox-Banner, Account-
/DSGVO-Self-Service, Statistik-Dashboard) — i18n + Image-Occlusion
+ Hint-Anzeige bei Cloze stehen noch offen. Phase 2 (Auth-
Föderation) ist auf user-side Pre-Flight blockiert.
- **Pre-Flight teilweise abgeräumt (Sprint 8d):** Verdaccio-Token
produktiv via npm.mana.how, lokaler Protocol-Mirror durch
Re-Exports aus `@mana/shared-share-protocol` ersetzt, Spec-Drift
`mana/url``mana/link` gefixt. Verbleibend Pre-Flight: DNS,
GitHub-Repo, mana-auth-App-Reg, mana-share-Manifest-Reg.
- Cards läuft lokal, ist im Browser benutzbar, hat alle Föderations-
Endpoints aus dem `app-manifest.json` implementiert. Anki-Decks
können importiert werden (Cloze first-class), Karten manuell
editiert, Statistiken angeschaut, Daten via DSGVO-Self-Service
exportiert/gelöscht.
```
┌─────────────────────────────────┐
│ cards/ (this repo) │
│ │
│ apps/web/ SvelteKit + Svelte 5 ← cardecky.mana.how
│ apps/api/ Hono + Bun + Drizzle ← Postgres `cards`
│ packages/cards-domain/ Pure-TS ← FSRS, Schemas, Protocol-Mirror
│ app-manifest.json Föderations-Vertrag
└─────────────────────────────────┘
│ HTTP, JWT, Manifest
┌─────────────────────────────────┐
│ mana/ Plattform │
│ mana-auth, mana-share, │
│ mana-links, mana-mcp, │
│ mana-search, mana-credits, … │
└─────────────────────────────────┘
```
---
## Architektur-Entscheidungen (festgenagelt)
Diese stehen — nicht ohne explizite Diskussion antasten:
1. **Strategie B (Greenfield).** Kein Code aus mana-monorepo.
2. **Server-authoritative MVP.** Keine Dexie, keine eigene Sync-Engine.
Local-First später via mana-sync-Federation, nicht durch eigenen
Stack.
3. **Eigene Postgres-DB `cards`** mit Schema-Isolation `pgSchema('cards')`.
4. **Föderations-Endpoints als Pflicht** — alle aus `app-manifest.json`
implementiert (siehe Phase 5 unten).
5. **Encryption initial AUS.** Nachrüstbar via mana-auth-MK.
6. **MVP-Card-Types `basic` + `basic-reverse` + `cloze` +
`image-occlusion`** (Cloze Phase 8, Image-Occlusion Phase 9l).
Schema vorbereitet auf type-in, audio, multiple-choice.
Vollständiger Plan: [`mana/docs/playbooks/CARDS_GREENFIELD.md`](../mana/docs/playbooks/CARDS_GREENFIELD.md)
(im Plattform-Repo, weil er Verein-übergreifend gilt).
---
## Phasen-Status
| # | Phase | Status | Verifikation |
|---|---|---|---|
| 0 | Read-Day mana-monorepo-Cards-Code lesen | ✅ | `docs/LESSONS_FROM_MANA_MONOREPO.md` |
| 1 | Repo-Skelett (Turbo, pnpm, Bun, Docker, CI) | ✅ | `pnpm install` durch, 136 packages |
| 2 | Auth-Föderation (mana-auth Registrierung, JWT-Verify) | ✅ live 2026-05-08 | App in mana-auth registriert, JWT-Verify additiv mit Dev-Stub-Fallback, E2E gegen `tills95@gmail.com` verifiziert |
| 3 | Domain-Modell + Drizzle + CRUD-API | ✅ | 8 Tabellen, FSRS via ts-fsrs, 46 Tests grün, E2E-Smoke durch |
| 4 | Frontend-Core (SvelteKit, Tailwind 4, Markdown-Editor, Study-View) | ✅ | type-check + build grün, manuell testbar im Browser |
| 5 | Föderations-Endpunkte (share, tools, search, dsgvo) | ✅ | 70 Tests grün, E2E-Smoke (Quote→Inbox→Search→DSGVO-Roundtrip) |
| 6 | Subscriptions/Credits via mana-credits | 🟡 plumbing | Tier-Awareness im JWT-Claim, requireTier-Helper, credits-client. Nicht produktiv aktiv (Cards-MVP ist tier-frei) |
| 7 | AI/MCP-Integration | ✅ live 2026-05-08 | mana-share + mana-mcp deployed (`share.mana.how`, `mcp.mana.how`), Cards-Manifest registriert, `cards.create` + `cards.search` bei mana-mcp upserted (`tools_upserted=2`) |
| 8 | Anki-Import (.apkg-Parser, Cloze-Support) | ✅ | 92 Tests grün, /import-Route benutzbar, Cloze als 3. MVP-Card-Type |
| 9 | Polish (DSGVO-UI, Settings, Account, Statistik, i18n, A11y, Media, Image-Occlusion) | 🟡 weit | Card-Edit + Cloze-Editor + Inbox-Banner + Account/DSGVO + Statistik + Pre-Flight-Swap + i18n DE/EN + A11y-Pass + Cloze-Hint-Anzeige + Anki-Re-Import-Dedupe + MinIO-Media-Upload + Image-Occlusion durch (9a9l). Verbleibend: type-in, audio, multiple-choice (Schema vorbereitet) |
| 10 | Production-Deploy (Mac Mini, Cloudflare-Tunnel) | ✅ live 2026-05-08 | cardecky.mana.how + cardecky-api.mana.how, alte cards.* via nginx-301-Redirect |
| 11 | Decommission Cards-Modul aus mana-monorepo | ✅ 2026-05-08 | apps/cards, services/cards-server, packages/cards-core, mana-app cards-Modul + cross-refs entfernt (4 Commits, type-check 0 errors) |
| 12 | Marketplace-Restore (R0R6) | 🟡 R0+R1+R2+R3+R4 durch | Plan: [`docs/playbooks/MARKETPLACE_RESTORE.md`](docs/playbooks/MARKETPLACE_RESTORE.md). R0 (Doku): ✅. R1 (Schema): ✅. R2 (α+β Authors + Publish): ✅. R3 (γ+δ Discovery + Engagement + Subscribe + Smart-Merge mit FSRS-State-Erhalt): ✅. **R4 (ε Pull-Requests + Card-Discussions): ✅**`POST/GET /decks/:slug/pull-requests` (PR-Erstellung mit add/modify/remove-Diff, public List), `GET /pull-requests/:id`, `POST /pull-requests/:id/{close,reject,merge}` mit Lifecycle-Enforcement (open→merged|closed|rejected), Merge ist Owner-only und erzeugt atomar eine neue Version mit semver-minor-Bump (1.0.0→1.1.0 default), bumpt `latest_version_id`, schreibt PR-Resolution. Card-Discussions: `POST /decks/:slug/cards/:hash/discussions` (auth, Threads keyed auf `card_content_hash` damit sie Versions-Bumps überleben), `GET /cards/:hash/discussions` (public read, hidden filtered), `GET /decks/:slug/discussions/counts` (Bulk pro Karte), `POST /discussions/:id/hide` (Author oder Deck-Owner). 11 neue Semver-Unit-Tests, 89 gesamt grün. **E2E-Smoke**: Cardecky publisht v1.0.0 (Apatheia, Eudaimonia, Logos) → Till submitted PR (modify Eudaimonia-Back, remove Logos, add Tugendlehre) → Till's Merge-Versuch wird mit 403 abgelehnt (deck_owner_only) → Cardecky merged → v1.1.0 entsteht atomar mit korrektem Karten-Mix in Ord-Reihenfolge → re-merge wird mit 409 abgelehnt → Till postet Frage zur Apatheia-Karte → Cardecky antwortet mit parent_id (Threading) → Cross-Card-Parent wird mit 422 abgelehnt → Hide-Operation versteckt vom Read aus → Bulk-Counts korrekt → Smart-Merge-Pull gegen v1.0.0→v1.1.0 zeigt 2 changed (Eudaimonia + Logos↔Tugendlehre über ord-Heuristik), 0 cards_inserted weil bereits-private-via-Fork. Bug-Fix: `r.use('*', authMiddleware)` in fork.ts wäre an dem `/api/v1/marketplace`-Mount-Punkt nachfolgende Router-Mounts (PRs, Discussions) versehentlich gefangen — Refactor auf per-route Middleware. Verbleibend: R5 Frontend-Routes, R6 voller UI-E2E. |
Legende: ✅ erledigt + verifiziert · 🚧 blockiert · ⏸ noch nicht begonnen
---
## Was läuft
### Lokal voll einsatzbereit
**One-Shot (empfohlen):**
```bash
cd /Users/till/Documents/Code/cards
NPM_AUTH_TOKEN=<verdaccio-token> pnpm install # einmalig / nach pull
pnpm dev:full # cards-docker + mana-docker + DB-Push (cards & auth) + dev (cards & mana-auth)
```
Oder von überall via zsh-Alias: `cards-dev` (definiert in `~/.zshrc`,
zeigt auf `pnpm dev:full` im cards-Repo).
`dev:full` greift bewusst in `../mana/` (Plattform-Repo): startet
`mana-postgres` und `mana-auth` (:3001) parallel zu cards-api/-web.
Login lokal komplett testbar — Dev-User `tills95@gmail.com` /
`Aa-123456789` ist in der lokalen `mana_auth`-DB angelegt
(`access_tier=founder`, `email_verified=true`). Cross-Repo-Coupling
ist Trade-off für E2E-Bequemlichkeit; CLAUDE.md erwähnt das nicht
als Architektur-Invariante, sondern Pragma.
**Manuelle Sequenz** (für Debug oder wenn nur Teile gebraucht werden):
```bash
cd /Users/till/Documents/Code/cards
# 1. Dependencies (idempotent)
NPM_AUTH_TOKEN=<verdaccio-token> pnpm install
# 2. Postgres + MinIO-Container (Postgres :5435, MinIO :9100/:9101 —
# kollidiert nicht mit Plattform-:5432/:9000/:9001)
pnpm docker:up
# 3. Drizzle-Schema pushen
cd apps/api
DATABASE_URL='postgresql://cards:cards@localhost:5435/cards' \
pnpm exec drizzle-kit push --force
# 4. API starten (auf :3081)
DATABASE_URL='postgresql://cards:cards@localhost:5435/cards' \
CARDS_API_PORT=3081 \
CARDS_DSGVO_SERVICE_KEY='msk_test_dsgvo_42' \
bun run src/index.ts &
# 5. Web starten (auf :3082)
cd ../web && pnpm dev &
# 6. Browser öffnen
open http://localhost:3082
```
Login mit beliebigem User-ID-String (Dev-Stub speichert via
`sessionStorage`). Für Föderations-Endpunkte (share/receive) muss die
User-ID UUID-formatiert sein, z.B. `00000000-0000-0000-0000-00000000aaaa`.
Aufräumen: `kill %1 %2 && pnpm docker:down` (Daten in
`infrastructure/.volumes/cards-postgres`).
Vollständiger Smoke-Test-Runbook: [`docs/SMOKE_TEST.md`](docs/SMOKE_TEST.md).
### Verifizierte Endpoints
```
GET /healthz → {"status":"ok"}
GET /version → {"app":"cards","version":"…","build":"…"}
GET /.well-known/mana-app.json → Manifest
POST /api/v1/decks User-JWT CRUD
GET /api/v1/decks User-JWT
GET /api/v1/decks/:id User-JWT
PATCH/DELETE /api/v1/decks/:id User-JWT
POST /api/v1/cards User-JWT Create + Auto-Reviews
GET /api/v1/cards?deck_id=… User-JWT
GET/PATCH/DELETE /api/v1/cards/:id User-JWT
GET /api/v1/reviews/due User-JWT Hot-Path
POST /api/v1/reviews/:cardId/:subIndex/grade User-JWT FSRS-Transition
POST /api/v1/share/receive User-JWT Föderations-Inbox
POST /api/v1/tools/:name User-JWT cards.create | cards.search
GET /api/v1/search?q=… User-JWT SearchResultEnvelope
GET /api/v1/dsgvo/export?user_id=… Service-Key
POST /api/v1/dsgvo/delete Service-Key
```
---
## Pre-Flight für Phase 2 + Live-Föderation
Diese Items sind **nicht autonom machbar** — du oder ein Mensch musst
sie freischalten:
| Item | Wer | Status |
|---|---|---|
| DNS für `cardecky.mana.how` reservieren (Cloudflare) | Mensch | offen |
| GitHub-Repo `mana-ev/cards` anlegen + Remote pushen | Mensch | offen |
| Cards in `mana-auth.apps` registrieren (Service-Key + Public-Key) | Mensch oder KI gegen laufende mana-auth | offen |
| `NPM_AUTH_TOKEN` für Verdaccio in `~/.npmrc` setzen | Mensch | offen |
| Cards-Manifest bei mana-share registrieren | KI gegen laufende mana-share | offen |
Sobald `NPM_AUTH_TOKEN` da ist, kann der lokale Protocol-Mirror in
`packages/cards-domain/src/protocol/` durch Re-Exports aus
`@mana/shared-share-protocol` ersetzt werden — das ist eine 1-Liner-
Änderung in `cards-domain/src/index.ts` plus Imports.
---
## Wichtige Pointer
### Konventionen + Stack
- pnpm 9.15.x, Node 20+, Bun für apps/api
- Tabs-Indent, single-quotes, 100-col Prettier (`.prettierrc.json`)
- SvelteKit 2 + Svelte 5 (**runes-only** — kein legacy `let count = 0`)
- Hono + Bun + Drizzle für API
- Drizzle 0.38 / drizzle-kit 0.30 / zod 3 (gleicher Stand wie Mana-Plattform)
- Tailwind 4 via `@tailwindcss/vite` (oklch-Theme + Dark-Mode-Auto)
- Tests: Vitest + Hono `app.request()`, später Playwright für e2e
Volle Konventionen: [`CLAUDE.md`](CLAUDE.md)
### Wichtige Dateien
| Pfad | Zweck |
|---|---|
| [`STATUS.md`](STATUS.md) | dieses Dokument — Single Source of Truth für Status |
| [`CLAUDE.md`](CLAUDE.md) | Konventionen + Architektur-Invarianten + Stack-Decisions |
| [`README.md`](README.md) | Kurz-Anleitung, ein paar Befehle |
| [`docs/SMOKE_TEST.md`](docs/SMOKE_TEST.md) | Reproduzierbarer E2E-Lauf (curl-Sequenz) |
| [`docs/LESSONS_FROM_MANA_MONOREPO.md`](docs/LESSONS_FROM_MANA_MONOREPO.md) | 15 Architektur-Lessons aus dem Read-Day, 5 Kern-Entscheidungen |
| [`app-manifest.json`](app-manifest.json) | Source of Truth für Föderations-Vertrag (v1.0.0, beta-tier) |
| [`packages/cards-domain/src/`](packages/cards-domain/src/) | zod-Schemas SSOT + FSRS-Adapter + Protocol-Mirror |
| [`apps/api/src/`](apps/api/src/) | Hono-Routen, Drizzle-Schemas, Share-Handlers |
| [`apps/web/src/`](apps/web/src/) | SvelteKit-Routes, $lib/api, $lib/auth-Stub |
| [`infrastructure/docker-compose.yml`](infrastructure/docker-compose.yml) | Postgres-Container für lokal-dev |
### Cross-Repo-Dokumente (im Plattform-Repo `mana/`)
| Pfad | Zweck |
|---|---|
| `mana/docs/playbooks/CARDS_GREENFIELD.md` | Master-Playbook (alle Phasen, Pre-Flight, Decommission) |
| `mana/docs/FEDERATION.md` | Föderations-Architektur-Grundlagen |
| `mana/docs/SHARE_PROTOCOL.md` | Manifest- + Envelope-Schema-Spezifikation |
| `mana/docs/MANA_AUTH_FEDERATION.md` | App-Identitäts-Modell (Service-Keys, JWKS) |
| `mana/docs/SHARED_PACKAGES.md` | Versions-Disziplin Klasse A/B/C |
| `mana/docs/PORTS.md` | Port-Allokation (cards-api: **3081**, cards-web: **3082**) |
| `mana/docs/PLAN.md` | Übergreifende mana-e.V.-Roadmap inkl. Phase 6 (Cards-Greenfield) |
---
## Git-Historie
```
39b1791 Phase 9l: Image-Occlusion als 4. MVP-CardType
c9eb0a6 Phase 9k: Media-Upload via MinIO-Container
e7ae93d docs: STATUS.md auf Phase-9-Welle-2-Stand
593d447 Phase 9j: Anki-Re-Import-Dedupe via content_hash
4b451f1 Phase 9i: Cloze-Hint-Anzeige
fd86d96 Phase 9h: A11y-Pass
c25c1d0 Phase 9g: i18n DE/EN über alle Routes
a640594 docs: STATUS.md auf Phase-9-Polish-Stand
6db6dc3 Phase 9f: Statistik-Dashboard
03117d5 Phase 9e: Account-Page mit DSGVO-Self-Service
aff4d95 Phase 9d: Pre-Flight — Protocol-Mirror durch upstream ersetzt
47419b3 Phase 9c: Inbox-Banner auf /decks und /study
35366ed Phase 9b: Cloze-Editor in /cards/new
0a40367 Phase 9a: Card-Edit-Page für alle 3 CardTypes
9da10b3 Phase 8d: STATUS.md auf Phase-8-Stand aktualisiert
2ca09fe Phase 8c: Anki-Import via portiertem Parser
0b609c4 Phase 8b: Cloze-Render im Study-View
553a78d Phase 8a: Cloze als MVP-Card-Type, Cluster-Counter
2bed282 docs: STATUS.md als Single Source of Truth für Cards-Onboarding
0328caa Phase 5: Föderations-Endpunkte — Cards ist föderierter Peer
89a7a92 Phase 4: Frontend-Core MVP — Decks, Cards, Study mit FSRS-Loop
e3b3a2b docs: SMOKE_TEST.md — verifizierter E2E-Lauf gegen lokale Postgres
5f67bd9 Phase 3 follow-up: type-check + tests grün, ts-fsrs v5 API
45a47e0 Phase 3: Domain-Modell + Decks/Cards/Reviews-CRUD
8605b1b Phase 0+1: Repo-Skelett für Cards-Greenfield
```
`git remote -v` ist leer — Repo lebt lokal, GitHub-Remote folgt mit
Pre-Flight (`mana-ev/cards`).
---
## Architektur-Subtilitäten, die nicht offensichtlich sind
### 1. Reviews bleiben PLAINTEXT
Der FSRS-Scheduler quert täglich `due <= now`. Wenn die Reviews
verschlüsselt wären, müsste man jeden Tag N Reviews entschlüsseln nur
um zu wissen welche fällig sind. Geht nicht.
→ Wenn Encryption nachgerüstet wird: nur `cards.fields` (front/back)
und `decks.{name,description}` werden encrypted, Reviews bleiben
plaintext. Pattern aus mana-monorepo bestätigt (`crypto/registry.ts`
hat `cardReviews` plaintext-allowlisted).
### 2. SubIndex-Granularität pro Card-Type
Eine `basic-reverse`-Karte hat **2** Reviews (sub_index 0 = front→back,
sub_index 1 = back→front). Cloze hat 1 Review pro Cluster-Index.
Beim Card-Insert werden alle initialen Reviews in **einer Transaktion**
mit angelegt — siehe `apps/api/src/routes/cards.ts` POST-Handler.
`subIndexCount(type)` in `@cards/domain` ist die SoT für statische
Typen. Für Cloze siehe Subtilität #6`subIndexCountForCloze(text)` ist
die SoT, weil die Anzahl text-abhängig ist.
### 3. Protocol-Mirror auf upstream umgestellt (Sprint 8d, 2026-05-08)
`packages/cards-domain/src/protocol/` war ursprünglich ein lokaler
Mirror, ist seit Sprint 8d ein dünner Re-Export von
`@mana/shared-share-protocol@0.1.0` aus Verdaccio (npm.mana.how). Die
gemeinsamen Schemas (Envelope, Search, Quote/Link/Text-Payloads) kommen
direkt aus dem Föderations-Vertrag, nur die Cards-spezifische
Akzeptanz-Map (`PAYLOAD_SCHEMAS`, `validatePayloadForType`) bleibt
lokal. Drift-Risiko ist damit beseitigt — `pnpm update` zieht
automatisch nach.
**Repo-`.npmrc` zeigt auf `npm.mana.how`** (nicht `pkg.mana.how` wie
zuvor — der Tunnel wurde am 2026-05-07 zurückgerollt). NPM_AUTH_TOKEN
muss als env-var oder im Shell-Profile vor `pnpm install` gesetzt sein.
### 3-historisch. Lokales Protocol-Mirror (vor Sprint 8d)
`packages/cards-domain/src/protocol/` enthielt eine **TEMPORARY**-
Kopie der Schemas aus `@mana/shared-share-protocol`. Solange Verdaccio
nicht offen ist (kein `NPM_AUTH_TOKEN`), halten wir sie hier lokal.
→ Drift-Risiko: bei jedem Update der mana-Spec MUSS diese Datei
nachgezogen werden, bis der Swap erfolgt. Marker-Kommentar oben in
jeder Mirror-Datei.
### 4. Inbox-Deck wird auto-erstellt
Eingehende Shares (über `/share/receive`) landen alle in einem
auto-erstellten "Inbox"-Deck pro User. `ensureInboxDeck(db, userId)`
prüft auf Existenz oder legt es neu an. User kann Karten später in
echte Decks umsortieren.
→ Naming-Hinweis: Wenn ein User schon ein Deck namens "Inbox" hat,
greift unser `ensureInboxDeck` darauf zu. Das ist gewollt (idempotent).
### 5. Dev-Auth via X-User-Id ist EXPLICIT temporär
`apps/api/src/middleware/auth.ts` und `apps/web/src/lib/auth/dev-stub.svelte.ts`
sind beide klar als „Phase 2 ersetzt durch echtes JWT" markiert. Beim
Swap auf mana-auth:
- API: `@mana/shared-hono` `authMiddleware()` mit JWKS-Cache
- Web: `@mana/shared-auth`-Login-Flow gegen `auth.mana.how`
- Beide aus Verdaccio (= NPM_AUTH_TOKEN-blockiert)
### 6. Cloze-Karten haben N Reviews — sub_index pro Cluster
`subIndexCount('cloze')` wirft bewusst, weil die Anzahl text-abhängig
ist. Caller müssen `subIndexCountForCloze(text)` aus `@cards/domain`
nutzen. Cluster werden nach numerischer ID aufsteigend sortiert
(`{{c1::…}}` = sub_index 0, `{{c2::…}}` = 1, …). Der Card-POST-Handler
lehnt `type=cloze` ohne mindestens ein Cluster mit 422 ab — eine Cloze
ohne `{{cN::…}}`-Markup ist sinnlos.
Render-Helpers (`renderClozePrompt` / `renderClozeAnswer`) leben in
`@cards/domain/src/cloze.ts`, sind 12-fach unit-getestet und werden
vom Study-View dünn konsumiert. Hint-Markup (`{{c1::answer::hint}}`)
wird MVP-stumm gedroppt — Hint-Anzeige ist Phase-9-Polish.
### 7. Anki-Parser ist eine bewusste Strategie-B-Ausnahme
`apps/web/src/lib/anki/parse.ts` und `lib/components/AnkiImport.svelte`
sind aus mana-monorepo portiert. Kennzeichnung im Header-Kommentar.
Begründung: Anki-Format-Logik ist standalone Parser-Code (jszip +
sql.js), kein Architektur-Schmuggel — die Kopie spart 2-3 Tage
Re-Implementierung bei null Strategy-Risiko.
`import.ts` wurde NICHT portiert: das Original schreibt gegen Dexie-
Stores und bricht damit Architektur-Invariante #1 (server-authoritative
MVP). Die neue Version ist von Hand geschrieben und nutzt direkt
`$lib/api/{decks,cards}` über HTTP.
### 8. Media-Refs werden beim Anki-Import gedroppt (Phase 8 MVP)
`sanitizeAnkiHtml` strippt `<img>` und `[sound:…]`-Markup ersatzlos.
Späterer Media-Pfad ist additiv — entweder ein lokaler
`POST /api/v1/media/upload` in cards-api oder gegen Plattform-`mana-media`
nach Phase 2. Die Filename→ZIP-Map liegt im `ParsedAnki.mediaByFilename`
weiterhin bereit, sie wird aktuell nur für die Preview-Anzeige
("X Medien werden nicht übernommen") genutzt.
### 9. sql-wasm.wasm liegt unter apps/web/static/
660kB Build-Asset. Wird vom Browser einmal geladen (initSqlJs cache
in parse.ts). Bei Update von `sql.js` muss die Datei neu kopiert
werden: `cp apps/web/node_modules/sql.js/dist/sql-wasm.wasm
apps/web/static/sql-wasm.wasm`.
### 11. MinIO-Media-Storage (Sprint 9k)
`cards-minio`-Container im `infrastructure/docker-compose.yml` auf
9100/9101 (Plattform-MinIO ist 9000/9001 — wir bleiben isoliert).
`apps/api/src/services/storage.ts` ist ein dünner Wrapper um den
`minio`-Client; `ensureBucket()` ist idempotent. ObjectKey-Format
`<userId>/<ulid>.<ext>` ermöglicht Bucket-Prefix-Sweep beim
DSGVO-Delete (kein S3-Cascade).
Konfiguration via `CARDS_S3_*`-env-Vars; Default lokaler Container.
Phase-10-Prod kann gegen denselben Container auf dem Mac Mini laufen
(eigener Bucket) oder gegen Plattform-MinIO. Keine Code-Änderung,
nur env.
### 12. Image-Occlusion (Sprint 9l)
Field-Schema: `image_ref` zeigt auf eine `media_files.id` (Sprint 9k
Storage), `mask_regions` ist ein JSON-Array mit Schema:
`{ id, x, y, w, h, label? }`, alle Coords 0..1 relativ.
`subIndexCount('image-occlusion')` wirft analog zu cloze; Card-POST
nutzt `maskRegionCount(mask_regions)`.
Editor (`ImageOcclusionEditor.svelte`): SVG-Overlay über `<img>`,
Drag-to-create für Masken, Mindestgröße 2% damit Klicks gefiltert
werden. Touch-tauglich via Pointer-Events. Mask-Liste mit Inline-
Label-Edit + Delete.
Study-View (`ImageOcclusionView.svelte`): aktive Maske ist im Prompt
opake schwarz, im Reveal transparent grün mit Label-Text-Overlay
(SVG-Text mit stroke-Outline gegen Bild-Hintergründe). Andere Masken
bleiben dezent gelb-durchsichtig als Lern-Hinweis.
### 10. content_hash auf cards (Sprint 9j)
Jede neue Karte bekommt einen SHA-256-`content_hash` über
`{type, sorted-fields}` — geschrieben automatisch im Card-POST,
genutzt vom Anki-Re-Import-Dedupe (`/api/v1/cards/hashes` lädt
nur die Hash-Liste, der Importer dedupliziert clientseitig).
`cardContentHash()` in `@cards/domain` ist deterministisch und
field-order-invariant. Cluster-Markup, Whitespace und Hint-
Annotationen zählen mit — bewusst, weil zwei Cards mit dem Text
`Paris ist Hauptstadt` und `{{c1::Paris}} ist Hauptstadt` sind
inhaltlich verschieden.
**Backfill-Lücke:** Pre-Sprint-9j-Karten haben `content_hash = NULL`.
Der `/hashes`-Endpoint filtert sie weg, also können sie irrtümlich
beim Re-Import erneut entstehen. Backfill-Skript ist Phase-10-Polish.
### 11. Cards-Modul in mana-monorepo wird nach Live-Gang **gelöscht**
Strategie-B-Konsequenz: nach `cardecky.mana.how` live + 2 Wochen Test
folgt ein Decommission-PR in mana-monorepo, der `apps/mana/.../modules/cards/`,
`packages/cards-core/`, `services/cards-server/` (Marketplace-Backend)
und alle DB-Schemas in `mana_platform.cards.*` entfernt. Keine zwei
Cards-Welten parallel.
→ Diese Entscheidung ist im Greenfield-Playbook festgehalten und im
älteren `CARDS_CUTOVER.md` (jetzt überholt) als Strategie-A-Variante
diskutiert worden.
---
## Wenn du gerade neu bist — Onboarding-Sequenz
1. **Lies dieses Dokument zu Ende** (5 Min).
2. **Lies `CLAUDE.md`** (Konventionen + Stack-Decisions, 3 Min).
3. **Lies `mana/docs/playbooks/CARDS_GREENFIELD.md`** (Master-Plan, 10 Min).
4. **Optional:** `docs/LESSONS_FROM_MANA_MONOREPO.md` für Domain-Verständnis.
5. **Verifiziere lokal:** `docs/SMOKE_TEST.md` durchspielen — wenn
alle 7 Schritte grün, ist die Umgebung in Ordnung.
6. **Memory-Check (KI):** falls du auto-memory hast, prüfe
`memory/project_phasenstand.md` für eventuell neueren Stand.
---
## Was als Nächstes ansteht (Vorschläge)
In Reihenfolge meiner Empfehlung:
1. **Pre-Flight Restklemmen abräumen** — DNS, GitHub-Repo
`mana-ev/cards`, mana-auth-App-Reg (Service-Key + Public-Key),
mana-share-Manifest-Reg. Hängt an User-Aktion + laufender
Plattform.
2. **Phase 6 (Subscriptions)** — braucht laufende mana-credits +
Phase 2 Auth-Föderation.
3. **Phase 7 (AI/MCP)** — braucht laufende mana-mcp.
4. **Phase 10 (Production-Deploy)** — Mac Mini + Cloudflare-Tunnel
(cardecky.mana.how + cards-api.mana.how) + MinIO-Bucket
(eigener Container oder Plattform), nach allen Pre-Flight-Items.
5. **content_hash-Backfill** — Pre-Phase-9j-Karten haben null
content_hash; ein einmaliges Skript würde sie nachziehen, sodass
Re-Import-Dedupe lückenlos funktioniert. Phase 10-Polish, sobald
Live-User da sind.
6. **Weitere CardTypes** — type-in, audio, multiple-choice sind
im Future-Schema vorbereitet, kommen wenn der User-Wunsch klar
ist.
7. **Image-Occlusion-Polish** — Resize/Move existierender Masken
(aktuell: nur Draw + Delete), Mask-Reorder, Mehrere Masken in
einem Cluster (mehrere als ein Review).
Was nicht autonom geht: Phase 2, 6, 7, 10 — alle hängen an Pre-Flight
oder Plattform-Diensten. Phase 9 ist sehr breit ausgebaut.
---
**Wenn du dieses Dokument liest und etwas hier nicht stimmt, ist das
Dokument schuld, nicht der Code. Update es.**