G1 — svelte-ignore für 5 benigne Init-Capture-Warnings:
- PublishVersionModal: state(latestSemver ? bumpMinor(latestSemver) : '1.0.0')
ist intentional, weil das Modal pro Click frisch gemountet wird
- SuggestEditModal: state(card.fields.front…) + state({ ...card.fields })
gleicher Lebenszyklus
Kein Refactor auf $derived, weil das die Bind-Semantik kaputtmachen
würde — Direktive plus ein Kommentar reicht.
G2 — Loading + Empty-States:
- Neue Components SkeletonGrid + EmptyState in lib/components/marketplace/
- /explore: SkeletonGrid statt „Lade Featured + Trending…"-String,
EmptyState wenn weder Featured noch Trending da
- /me/subscribed + /me/forks: EmptyState statt inline-Box
- Konsistentes Vereins-Vokabular (icon + Title + Description + CTA)
G3 — Server-side Fork-Filter:
- GET /api/v1/decks akzeptiert ?forked_from_marketplace=true
- Drizzle isNotNull-Filter auf decks.forked_from_marketplace_deck_id
- toDeckDto exposed jetzt forked_from_marketplace_{deck,version}_id
(vorher schwiegen die Spalten, mussten client-side via Cast
rausgefischt werden)
- /me/forks ruft listDecks({ forkedFromMarketplace: true }) statt
listDecks() + client-side Filter
G4 — Owner-Author-Info im Deck-Detail-Endpoint:
- GET /api/v1/marketplace/decks/:slug returned jetzt zusätzlich
owner: { slug, display_name, verified_mana, verified_community,
pseudonym } — gejoint aus marketplace.authors via deck.owner_user_id
- toOwnerDto-Helper, identisches Shape wie in /authors/:slug
- /d/[slug] verbraucht den neuen owner-Block für AuthorBadge mit
echtem Profil-Link statt user_id-Slice (vorher: kaputter Link
/u/<empty-slug> + nur „SEAiKLkPZ…" als Display-Name)
Verifikation:
- API: type-check + 89 Tests grün
- Web: svelte-check 0 errors, 0 warnings (von 5 → 0)
- Live-Smoke: GET /marketplace/decks/r5-stoa-grundlagen liefert
owner={slug:'cardecky', display_name:'Cardecky', verified_*:false}
- ?forked_from_marketplace=true Filter mit Till's JWT liefert 0
(weil Till keine Forks hat) — 401 ohne JWT bestätigt
Bewusst nicht angefasst: Header-Nav-Link (WIP-Konflikt), Image-
Occlusion in Marketplace (Player-Side komplex), Auth-Guard im
+layout.svelte (page-level guards reichen), Anki-Import→Marketplace-
Publish-Hook (eigene Welle).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
25 KiB
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-Tunnel1435166a-…). Containercards-{postgres,minio,api,web}. Forgejo-Remotegit.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-protocolersetzt, Spec-Driftmana/url→mana/linkgefixt. 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.jsonimplementiert. 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:
- Strategie B (Greenfield). Kein Code aus mana-monorepo.
- Server-authoritative MVP. Keine Dexie, keine eigene Sync-Engine. Local-First später via mana-sync-Federation, nicht durch eigenen Stack.
- Eigene Postgres-DB
cardsmit Schema-IsolationpgSchema('cards'). - Föderations-Endpoints als Pflicht — alle aus
app-manifest.jsonimplementiert (siehe Phase 5 unten). - Encryption initial AUS. Nachrüstbar via mana-auth-MK.
- 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
(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 (9a–9l). 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 (R0–R6) | 🟡 R0+R1+R2+R3+R4+R5+G1-G4 durch | Plan: docs/playbooks/MARKETPLACE_RESTORE.md. R0–R4 (Backend): ✅. R5 (Frontend-Routes): ✅. G1-G4 (Polish-Pass): ✅ — G1 svelte-ignore für 5 benigne Modal-Init-Capture-Warnings (Modals werden pro Click gemountet, nicht-reactive ist gewollt), G2 Loading-Skeleton + EmptyState als Shared Components in /explore und /me/{subscribed,forks} (statt nackter „Lade…"-Strings), G3 Server-side Filter GET /api/v1/decks?forked_from_marketplace=true (vorher client-side filtering — funktional bei <100 Decks egal, jetzt sauber), G4 Owner-Author-Info im Deck-Detail-Endpoint (GET /api/v1/marketplace/decks/:slug returned jetzt owner.{slug, display_name, verified_mana, verified_community, pseudonym}, /d/[slug] zeigt korrekt verlinkten AuthorBadge statt user-id-prefix). svelte-check: 4019 Files, 0 errors, 0 warnings. 89 API-Tests grün. Bewusst nicht angefasst: Header-Nav-Link auf /explore (Header.svelte ist in Tills uncommitted WIP), Image-Occlusion/Audio in Marketplace (Image-Occlusion-Schema ja, Player-Side später), Auth-Guard im +layout.svelte (page-level guards in /me/*-Pages reichen). Verbleibend: R6 voller UI-E2E im Browser (Cardecky-Publish + Till-Subscribe + Till-Fork + Till-PR + Cardecky-Merge + Till-Pull-Update mit FSRS-Erhalt-Verifikation), Anki-Import→Marketplace-Publish-Hook (eigene Welle). |
Legende: ✅ erledigt + verifiziert · 🚧 blockiert · ⏸ noch nicht begonnen
Was läuft
Lokal voll einsatzbereit
One-Shot (empfohlen):
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):
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.
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
Wichtige Dateien
| Pfad | Zweck |
|---|---|
STATUS.md |
dieses Dokument — Single Source of Truth für Status |
CLAUDE.md |
Konventionen + Architektur-Invarianten + Stack-Decisions |
README.md |
Kurz-Anleitung, ein paar Befehle |
docs/SMOKE_TEST.md |
Reproduzierbarer E2E-Lauf (curl-Sequenz) |
docs/LESSONS_FROM_MANA_MONOREPO.md |
15 Architektur-Lessons aus dem Read-Day, 5 Kern-Entscheidungen |
app-manifest.json |
Source of Truth für Föderations-Vertrag (v1.0.0, beta-tier) |
packages/cards-domain/src/ |
zod-Schemas SSOT + FSRS-Adapter + Protocol-Mirror |
apps/api/src/ |
Hono-Routen, Drizzle-Schemas, Share-Handlers |
apps/web/src/ |
SvelteKit-Routes, $lib/api, $lib/auth-Stub |
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-honoauthMiddleware()mit JWKS-Cache - Web:
@mana/shared-auth-Login-Flow gegenauth.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
- Lies dieses Dokument zu Ende (5 Min).
- Lies
CLAUDE.md(Konventionen + Stack-Decisions, 3 Min). - Lies
mana/docs/playbooks/CARDS_GREENFIELD.md(Master-Plan, 10 Min). - Optional:
docs/LESSONS_FROM_MANA_MONOREPO.mdfür Domain-Verständnis. - Verifiziere lokal:
docs/SMOKE_TEST.mddurchspielen — wenn alle 7 Schritte grün, ist die Umgebung in Ordnung. - Memory-Check (KI): falls du auto-memory hast, prüfe
memory/project_phasenstand.mdfür eventuell neueren Stand.
Was als Nächstes ansteht (Vorschläge)
In Reihenfolge meiner Empfehlung:
- 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. - Phase 6 (Subscriptions) — braucht laufende mana-credits + Phase 2 Auth-Föderation.
- Phase 7 (AI/MCP) — braucht laufende mana-mcp.
- 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.
- 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.
- Weitere CardTypes — type-in, audio, multiple-choice sind im Future-Schema vorbereitet, kommen wenn der User-Wunsch klar ist.
- 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.