diff --git a/CLAUDE.md b/CLAUDE.md index f3614a3..ebdf22c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,8 +54,16 @@ Diese sind beschlossen. Nicht ohne explizite Diskussion antasten: 1. **Server-authoritative MVP.** Keine Dexie, keine IndexedDB, keine eigene Sync-Engine. Frontend = HTTP-Client zu cards-api. Local-First später via mana-sync-Federation, nicht durch eigenen Stack. -2. **Kein Code aus mana-monorepo kopiert.** Code dort wird gelesen - (Lessons-Doc), nicht übernommen. Sauber neu ab Tag 0. +2. **Kein Code aus mana-monorepo kopiert — für die Study-/FSRS-/Sync- + Schicht.** Diese Architektur (Server-authoritative, kein Dexie, neue + Type-Hierarchie) ist sauber neu ab Tag 0. **Ausnahme: Marketplace- + Restore.** Der ehemalige `services/cards-server/` aus mana-monorepo + war nie auf der Strategie-B-Verbots-Liste — er wurde am 2026-05-08 + nur mit-rausgerissen, weil er an `apps/cards/` gekoppelt war. Bei + einem Restore wird der Marketplace-Code aus dem + `cards-decommission-base`-Tag im managarten-Repo additiv re-import'd, + in eigenes `marketplace`-pgSchema, additiv zur Study-Welt. Plan: + [`docs/playbooks/MARKETPLACE_RESTORE.md`](docs/playbooks/MARKETPLACE_RESTORE.md). 3. **Eigene Postgres-DB `cards`** im geteilten Mana-Cluster, Schema- Isolation via `pgSchema('cards')`. 4. **Föderation über `@mana/shared-app-tpl`.** Pflicht-Endpoints diff --git a/STATUS.md b/STATUS.md index 34f925d..220b968 100644 --- a/STATUS.md +++ b/STATUS.md @@ -98,6 +98,7 @@ Vollständiger Plan: [`mana/docs/playbooks/CARDS_GREENFIELD.md`](../mana/docs/pl | 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 durch | Plan: [`docs/playbooks/MARKETPLACE_RESTORE.md`](docs/playbooks/MARKETPLACE_RESTORE.md). R0 (Doku-Archiv aus `cards-decommission-base` + Restore-Plan + Strategie-B-Klarstellung in CLAUDE.md): ✅. R1 (16 Tabellen + 5 Enums in `marketplace`-pgSchema, drizzle-kit push grün, type-check + 56 Tests grün, CHECK-Constraint `decks_price_requires_license` verifiziert): ✅. Verbleibend: R2 Backend α/β (Author-Profile + Publish + AI-Mod), R3 γ/δ (Discovery + Subscribe + Smart-Merge), R4 ε (PRs + Discussions), R5 Frontend-Routes, R6 E2E-Smoke. | Legende: ✅ erledigt + verifiziert · 🚧 blockiert · ⏸ noch nicht begonnen diff --git a/apps/api/drizzle.config.ts b/apps/api/drizzle.config.ts index 6a23c62..755a390 100644 --- a/apps/api/drizzle.config.ts +++ b/apps/api/drizzle.config.ts @@ -1,13 +1,13 @@ import type { Config } from 'drizzle-kit'; export default { - schema: './src/db/schema/*.ts', + schema: ['./src/db/schema/*.ts', './src/db/schema/marketplace/*.ts'], out: './src/db/migrations', dialect: 'postgresql', dbCredentials: { url: process.env.DATABASE_URL ?? 'postgresql://cards:cards@localhost:5435/cards', }, - schemaFilter: ['cards'], + schemaFilter: ['cards', 'marketplace'], verbose: true, strict: true, } satisfies Config; diff --git a/apps/api/src/db/schema/marketplace/_schema.ts b/apps/api/src/db/schema/marketplace/_schema.ts new file mode 100644 index 0000000..332923c --- /dev/null +++ b/apps/api/src/db/schema/marketplace/_schema.ts @@ -0,0 +1,23 @@ +import { pgSchema } from 'drizzle-orm/pg-core'; + +/** + * Public-Marketplace-Tabellen leben unter einem eigenen + * `marketplace`-pgSchema in derselben `cards`-DB. Bewusste Trennung + * zur privaten Lern-Welt (`cards`-pgSchema): + * + * - sauberer Read-Path: Public-Endpoints sehen nur `marketplace`, + * Greenfield-Code nur `cards`. + * - Backup-Granularität (`pg_dump --schema=marketplace`). + * - RLS-Policies pro Schema möglich (Take-Down-Workflows). + * + * Drizzle-Variablennamen tragen bewusst den `public`-Prefix + * (`publicDecks`, `publicDeckVersions`, …) um in Imports klar + * vom privaten `cards.decks` getrennt zu sein. + * + * Geschichte: 1:1 portiert aus dem alten + * `mana-monorepo/services/cards-server/src/db/schema/_schema.ts` + * (managarten-Tag `cards-decommission-base`, 2026-05-08), nur das + * Schema-Target gewechselt von `cards` auf `marketplace`. Plan: + * `docs/playbooks/MARKETPLACE_RESTORE.md`. + */ +export const marketplaceSchema = pgSchema('marketplace'); diff --git a/apps/api/src/db/schema/marketplace/authors.ts b/apps/api/src/db/schema/marketplace/authors.ts new file mode 100644 index 0000000..9a86896 --- /dev/null +++ b/apps/api/src/db/schema/marketplace/authors.ts @@ -0,0 +1,68 @@ +/** + * Authors — Public-Identität für User, die Decks publishen. + * + * Eine Author-Row pro User, der jemals ein Author-Profil angelegt hat. + * `userId` ist plain-text-Referenz auf `auth.users.id` (cross-DB — + * keine FK auf DB-Ebene; das konsumierende Service validiert JWTs + * von mana-auth und nimmt den `sub`-Claim verbatim). + * + * Verifizierung hat zwei orthogonale Achsen: + * - `verified_mana`: manuell vom Mana-Verein vergeben (Lehrer, + * Pädagog:innen, Ärzt:innen). Nicht erkaufbar. + * - `verified_community`: automatisch berechnet aus Engagement + * (≥ X stars, ≥ Y featured Decks, ≥ Z Subscribers). Periodisch + * re-evaluiert. + * + * Beide Achsen können gleichzeitig true sein → UI zeigt beide Badges. + */ + +import { boolean, index, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'; + +import { marketplaceSchema } from './_schema.ts'; + +export const authors = marketplaceSchema.table( + 'authors', + { + userId: text('user_id').primaryKey(), + slug: text('slug').notNull(), + displayName: text('display_name').notNull(), + bio: text('bio'), + avatarUrl: text('avatar_url'), + joinedAt: timestamp('joined_at', { withTimezone: true }).defaultNow().notNull(), + // Pseudonym-Modus: Klarname versteckt, nur displayName sichtbar. + pseudonym: boolean('pseudonym').default(false).notNull(), + // Verifizierungs-Flags (siehe Header). + verifiedMana: boolean('verified_mana').default(false).notNull(), + verifiedCommunity: boolean('verified_community').default(false).notNull(), + // Soft-Ban: gebannter Author kann nicht mehr publishen, existierende + // Decks bleiben lesbar mit „deactivated"-Badge. + bannedAt: timestamp('banned_at', { withTimezone: true }), + bannedReason: text('banned_reason'), + }, + (t) => ({ + slugIdx: uniqueIndex('authors_slug_idx').on(t.slug), + verifiedIdx: index('authors_verified_idx').on(t.verifiedMana, t.verifiedCommunity), + }) +); + +/** + * Following-Beziehung User → Author. Treibt den persönlichen Activity-Feed. + */ +export const authorFollows = marketplaceSchema.table( + 'author_follows', + { + followerUserId: text('follower_user_id').notNull(), + authorUserId: text('author_user_id') + .notNull() + .references(() => authors.userId, { onDelete: 'cascade' }), + since: timestamp('since', { withTimezone: true }).defaultNow().notNull(), + }, + (t) => ({ + pk: uniqueIndex('author_follows_pk').on(t.followerUserId, t.authorUserId), + authorIdx: index('author_follows_author_idx').on(t.authorUserId), + followerIdx: index('author_follows_follower_idx').on(t.followerUserId), + }) +); + +export type AuthorRow = typeof authors.$inferSelect; +export type AuthorInsert = typeof authors.$inferInsert; diff --git a/apps/api/src/db/schema/marketplace/credits.ts b/apps/api/src/db/schema/marketplace/credits.ts new file mode 100644 index 0000000..7adaa61 --- /dev/null +++ b/apps/api/src/db/schema/marketplace/credits.ts @@ -0,0 +1,68 @@ +/** + * Two-sided Marketplace-Bookkeeping. Das eigentliche Geld lebt in + * mana-credits — wir tracken nur das Deck-Purchase-Event und den + * abgeleiteten Author-Payout, damit Buyer-History, Author-Dashboards + * und Reconcile gegen den mana-credits-Ledger möglich sind. + * + * **Status R1 (Schema-Restore):** Tabellen kommen mit, aber Code-Pfade + * (Routes/Services) bleiben dormant bis nach Live-Validation der + * gratis-Decks. Re-Aktivierung als eigene Welle. + */ + +import { index, integer, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core'; + +import { marketplaceSchema } from './_schema.ts'; +import { authors } from './authors.ts'; +import { publicDecks, publicDeckVersions } from './decks.ts'; + +export const deckPurchases = marketplaceSchema.table( + 'deck_purchases', + { + id: uuid('id').primaryKey().defaultRandom(), + buyerUserId: text('buyer_user_id').notNull(), + deckId: uuid('deck_id') + .notNull() + .references(() => publicDecks.id, { onDelete: 'restrict' }), + // Snapshot der Version zum Kaufzeitpunkt — Buyer behält Lifetime- + // Access auf alle nachfolgenden Versionen. + versionId: uuid('version_id') + .notNull() + .references(() => publicDeckVersions.id, { onDelete: 'restrict' }), + // Snapshot des Preises zum Kaufzeitpunkt. + priceCredits: integer('price_credits').notNull(), + // Pre-computed Split (Sum = priceCredits modulo Rundung). + authorShare: integer('author_share').notNull(), + manaShare: integer('mana_share').notNull(), + // Reference in mana-credits-Ledger. + creditsTransaction: text('credits_transaction'), + purchasedAt: timestamp('purchased_at', { withTimezone: true }).defaultNow().notNull(), + refundedAt: timestamp('refunded_at', { withTimezone: true }), + }, + (t) => ({ + // Ein Kauf pro Buyer pro Deck — covered Lifetime-Access. + uniqueBuyerDeck: uniqueIndex('deck_purchases_buyer_deck_idx').on(t.buyerUserId, t.deckId), + buyerIdx: index('deck_purchases_buyer_idx').on(t.buyerUserId), + deckIdx: index('deck_purchases_deck_idx').on(t.deckId), + }) +); + +export const authorPayouts = marketplaceSchema.table( + 'author_payouts', + { + id: uuid('id').primaryKey().defaultRandom(), + authorUserId: text('author_user_id') + .notNull() + .references(() => authors.userId, { onDelete: 'restrict' }), + sourcePurchaseId: uuid('source_purchase_id') + .notNull() + .references(() => deckPurchases.id, { onDelete: 'restrict' }), + creditsGranted: integer('credits_granted').notNull(), + // Reference in mana-credits-Grant-Ledger. + creditsGrantId: text('credits_grant_id'), + grantedAt: timestamp('granted_at', { withTimezone: true }).defaultNow().notNull(), + }, + (t) => ({ + authorIdx: index('author_payouts_author_idx').on(t.authorUserId), + purchaseIdx: index('author_payouts_purchase_idx').on(t.sourcePurchaseId), + }) +); diff --git a/apps/api/src/db/schema/marketplace/decks.ts b/apps/api/src/db/schema/marketplace/decks.ts new file mode 100644 index 0000000..087b31a --- /dev/null +++ b/apps/api/src/db/schema/marketplace/decks.ts @@ -0,0 +1,138 @@ +/** + * Public-Decks, Versionen, Karten. + * + * Ein Deck ist das langlebige Identitäts-Objekt („Spanish A2 Vocab"), + * zeigt immer auf eine `latest_version_id`. Versionen sind immutable + * Snapshots — einmal published, ändern sie sich nie. Karten sind + * Versions-skopiert und tragen einen per-Karten-Content-Hash, damit + * Subscriber-Smart-Merge den FSRS-State unveränderter Karten über + * Versions-Bumps hinweg erhalten kann. + * + * `price_credits = 0` heißt kostenlos. Alles > 0 erzwingt die + * Cardecky-Pro-Only-1.0-Lizenz (CHECK constraint auf DB-Ebene). + */ + +import { sql } from 'drizzle-orm'; +import { + boolean, + check, + index, + integer, + jsonb, + text, + timestamp, + uniqueIndex, + uuid, +} from 'drizzle-orm/pg-core'; + +import { marketplaceSchema } from './_schema.ts'; +import { authors } from './authors.ts'; + +/** + * Spiegelt `CardType` aus `@cards/domain`. Enum lebt im + * `marketplace`-pgSchema (nicht im default `public`), damit + * drizzle-kit-Push mit `schemaFilter: ['cards', 'marketplace']` + * keine spurious CREATE-TYPE-Versuche macht. + */ +export const cardTypeEnum = marketplaceSchema.enum('card_type', [ + 'basic', + 'basic-reverse', + 'cloze', + 'type-in', + 'image-occlusion', + 'audio', + 'multiple-choice', +]); + +export const publicDecks = marketplaceSchema.table( + 'decks', + { + id: uuid('id').primaryKey().defaultRandom(), + slug: text('slug').notNull(), + title: text('title').notNull(), + description: text('description'), + // ISO-639-1 (z.B. 'de', 'en', 'es'). Nullable für mixed-language. + language: text('language'), + // SPDX-style ID. CC0-1.0, CC-BY-4.0, CC-BY-SA-4.0, + // Cardecky-Personal-Use-1.0 (default für free), Cardecky-Pro-Only-1.0 (paid). + license: text('license').notNull().default('Cardecky-Personal-Use-1.0'), + priceCredits: integer('price_credits').notNull().default(0), + ownerUserId: text('owner_user_id') + .notNull() + .references(() => authors.userId, { onDelete: 'restrict' }), + // Updated bei jedem Publish einer neuen Version. + latestVersionId: uuid('latest_version_id'), + isFeatured: boolean('is_featured').notNull().default(false), + isTakedown: boolean('is_takedown').notNull().default(false), + takedownAt: timestamp('takedown_at', { withTimezone: true }), + takedownReason: text('takedown_reason'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + }, + (t) => ({ + slugIdx: uniqueIndex('decks_slug_idx').on(t.slug), + ownerIdx: index('decks_owner_idx').on(t.ownerUserId), + featuredIdx: index('decks_featured_idx').on(t.isFeatured), + // Paid Decks müssen die Pro-Only-Lizenz tragen. + priceLicense: check( + 'decks_price_requires_license', + sql`price_credits = 0 OR license = 'Cardecky-Pro-Only-1.0'` + ), + }) +); + +export const publicDeckVersions = marketplaceSchema.table( + 'deck_versions', + { + id: uuid('id').primaryKey().defaultRandom(), + deckId: uuid('deck_id') + .notNull() + .references(() => publicDecks.id, { onDelete: 'cascade' }), + // SemVer string — ordering passiert in app code, nicht in der DB. + semver: text('semver').notNull(), + changelog: text('changelog'), + // SHA-256 über die canonicalized Karten-Liste — Clients erkennen + // damit „hat sich was geändert" ohne Payload-Diff. + contentHash: text('content_hash').notNull(), + cardCount: integer('card_count').notNull(), + publishedAt: timestamp('published_at', { withTimezone: true }).defaultNow().notNull(), + // Ältere Versionen bleiben lesbar, neue Subscriber gehen auf latest. + deprecatedAt: timestamp('deprecated_at', { withTimezone: true }), + }, + (t) => ({ + uniqueSemver: uniqueIndex('deck_versions_deck_semver_idx').on(t.deckId, t.semver), + deckIdx: index('deck_versions_deck_idx').on(t.deckId), + hashIdx: index('deck_versions_hash_idx').on(t.contentHash), + }) +); + +export const publicDeckCards = marketplaceSchema.table( + 'deck_cards', + { + id: uuid('id').primaryKey().defaultRandom(), + versionId: uuid('version_id') + .notNull() + .references(() => publicDeckVersions.id, { onDelete: 'cascade' }), + // Spiegelt @cards/domain CardType. + type: cardTypeEnum('type').notNull(), + // Free-form key/value bag. + // basic / basic-reverse / type-in: { front, back } + // cloze: { text, extra? } + fields: jsonb('fields').$type>().notNull(), + ord: integer('ord').notNull(), + // SHA-256 über canonical(type, fields). Subscriber nutzen den + // Hash für per-Karten-Smart-Merge — unveränderte Karten behalten + // ihren FSRS-State über Versions-Pulls hinweg. **Identisch zur + // Berechnung in `@cards/domain` `cardContentHash`** — das ist der + // SoT, dieser Hash ist nur die persistierte Form. + contentHash: text('content_hash').notNull(), + }, + (t) => ({ + uniqueOrd: uniqueIndex('deck_cards_version_ord_idx').on(t.versionId, t.ord), + hashIdx: index('deck_cards_hash_idx').on(t.contentHash), + }) +); + +export type PublicDeckRow = typeof publicDecks.$inferSelect; +export type PublicDeckInsert = typeof publicDecks.$inferInsert; +export type PublicDeckVersionRow = typeof publicDeckVersions.$inferSelect; +export type PublicDeckCardRow = typeof publicDeckCards.$inferSelect; diff --git a/apps/api/src/db/schema/marketplace/discussions.ts b/apps/api/src/db/schema/marketplace/discussions.ts new file mode 100644 index 0000000..c29d71f --- /dev/null +++ b/apps/api/src/db/schema/marketplace/discussions.ts @@ -0,0 +1,81 @@ +/** + * Pull-Requests + Card-Discussions. + * + * Pull-Requests propose card-level Änderungen am Deck; der Author kann + * mergen → cards-server erstellt eine neue Version automatisch. Der + * Diff wird als JSON-Blob (`{ add, modify, remove }`) gespeichert, + * damit eine GitHub-style Review-UI gerendert werden kann ohne aus + * Versions-Diffs neu abzuleiten. + * + * Card-Discussions sind an `card_content_hash` gebunden (nicht an + * `card_id`), damit Threads Versions-Bumps überleben solange die + * Karte selbst unverändert bleibt. + */ + +import { boolean, index, jsonb, text, timestamp, uuid } from 'drizzle-orm/pg-core'; + +import { marketplaceSchema } from './_schema.ts'; +import { publicDecks, publicDeckVersions } from './decks.ts'; + +export const pullRequestStatusEnum = marketplaceSchema.enum('pr_status', [ + 'open', + 'merged', + 'closed', + 'rejected', +]); + +export interface PullRequestDiff { + add: { type: string; fields: Record }[]; + modify: { contentHash: string; fields: Record }[]; + remove: { contentHash: string }[]; +} + +export const deckPullRequests = marketplaceSchema.table( + 'deck_pull_requests', + { + id: uuid('id').primaryKey().defaultRandom(), + deckId: uuid('deck_id') + .notNull() + .references(() => publicDecks.id, { onDelete: 'cascade' }), + authorUserId: text('author_user_id').notNull(), + status: pullRequestStatusEnum('status').notNull().default('open'), + title: text('title').notNull(), + body: text('body'), + diff: jsonb('diff').$type().notNull(), + mergedIntoVersionId: uuid('merged_into_version_id').references(() => publicDeckVersions.id, { + onDelete: 'set null', + }), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + resolvedAt: timestamp('resolved_at', { withTimezone: true }), + }, + (t) => ({ + deckIdx: index('deck_pull_requests_deck_idx').on(t.deckId), + statusIdx: index('deck_pull_requests_status_idx').on(t.deckId, t.status), + authorIdx: index('deck_pull_requests_author_idx').on(t.authorUserId), + }) +); + +export const cardDiscussions = marketplaceSchema.table( + 'card_discussions', + { + id: uuid('id').primaryKey().defaultRandom(), + // Bound to card content_hash, not row id, so der Thread folgt der + // Karte über Versions-Bumps solange Inhalt bleibt. + cardContentHash: text('card_content_hash').notNull(), + deckId: uuid('deck_id') + .notNull() + .references(() => publicDecks.id, { onDelete: 'cascade' }), + authorUserId: text('author_user_id').notNull(), + // Threading: parent_id NULL = root post, NOT NULL = reply. + parentId: uuid('parent_id'), + body: text('body').notNull(), + // Hidden by Author or Mod. Nicht gelöscht — Audit-Trail erhalten. + hidden: boolean('hidden').notNull().default(false), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + }, + (t) => ({ + hashIdx: index('card_discussions_hash_idx').on(t.cardContentHash), + deckIdx: index('card_discussions_deck_idx').on(t.deckId), + parentIdx: index('card_discussions_parent_idx').on(t.parentId), + }) +); diff --git a/apps/api/src/db/schema/marketplace/engagement.ts b/apps/api/src/db/schema/marketplace/engagement.ts new file mode 100644 index 0000000..2108b18 --- /dev/null +++ b/apps/api/src/db/schema/marketplace/engagement.ts @@ -0,0 +1,67 @@ +/** + * Engagement-Primitives — Stars (Bookmarks), Subscriptions (Live- + * Updates), Forks (eigene Kopie mit Lineage). + * + * Alle keyed auf `user_id` text — plain reference auf auth.users.id + * ohne cross-DB-FK. + */ + +import { boolean, index, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core'; + +import { marketplaceSchema } from './_schema.ts'; +import { publicDecks, publicDeckVersions } from './decks.ts'; + +export const deckStars = marketplaceSchema.table( + 'deck_stars', + { + userId: text('user_id').notNull(), + deckId: uuid('deck_id') + .notNull() + .references(() => publicDecks.id, { onDelete: 'cascade' }), + starredAt: timestamp('starred_at', { withTimezone: true }).defaultNow().notNull(), + }, + (t) => ({ + pk: uniqueIndex('deck_stars_pk').on(t.userId, t.deckId), + deckIdx: index('deck_stars_deck_idx').on(t.deckId), + }) +); + +export const deckSubscriptions = marketplaceSchema.table( + 'deck_subscriptions', + { + userId: text('user_id').notNull(), + deckId: uuid('deck_id') + .notNull() + .references(() => publicDecks.id, { onDelete: 'cascade' }), + // Letzte Version, die der User gepullt hat. Smart-Merge vergleicht + // das mit `deck.latest_version_id` für den Diff. + currentVersionId: uuid('current_version_id').references(() => publicDeckVersions.id, { + onDelete: 'set null', + }), + subscribedAt: timestamp('subscribed_at', { withTimezone: true }).defaultNow().notNull(), + notifyUpdates: boolean('notify_updates').notNull().default(true), + }, + (t) => ({ + pk: uniqueIndex('deck_subscriptions_pk').on(t.userId, t.deckId), + deckIdx: index('deck_subscriptions_deck_idx').on(t.deckId), + userIdx: index('deck_subscriptions_user_idx').on(t.userId), + }) +); + +export const deckForks = marketplaceSchema.table( + 'deck_forks', + { + userId: text('user_id').notNull(), + sourceDeckId: uuid('source_deck_id') + .notNull() + .references(() => publicDecks.id, { onDelete: 'cascade' }), + sourceVersionId: uuid('source_version_id') + .notNull() + .references(() => publicDeckVersions.id, { onDelete: 'cascade' }), + forkedAt: timestamp('forked_at', { withTimezone: true }).defaultNow().notNull(), + }, + (t) => ({ + pk: uniqueIndex('deck_forks_pk').on(t.userId, t.sourceDeckId, t.sourceVersionId), + sourceIdx: index('deck_forks_source_idx').on(t.sourceDeckId), + }) +); diff --git a/apps/api/src/db/schema/marketplace/index.ts b/apps/api/src/db/schema/marketplace/index.ts new file mode 100644 index 0000000..6a988d3 --- /dev/null +++ b/apps/api/src/db/schema/marketplace/index.ts @@ -0,0 +1,19 @@ +/** + * Re-Exports für das gesamte Marketplace-Schema. Hält Imports flach — + * downstream code macht + * `import { authors, publicDecks } from '../db/schema/marketplace'`. + * + * **Naming-Konvention:** Variablennamen tragen `public`-Prefix + * (`publicDecks`, `publicDeckVersions`, `publicDeckCards`), damit + * Imports zusammen mit den Greenfield-`cards.decks`/`cards.cards` aus + * `../schema/index.ts` nicht kollidieren. + */ + +export { marketplaceSchema } from './_schema.ts'; +export * from './authors.ts'; +export * from './decks.ts'; +export * from './tags.ts'; +export * from './engagement.ts'; +export * from './discussions.ts'; +export * from './moderation.ts'; +export * from './credits.ts'; diff --git a/apps/api/src/db/schema/marketplace/moderation.ts b/apps/api/src/db/schema/marketplace/moderation.ts new file mode 100644 index 0000000..7ab0efa --- /dev/null +++ b/apps/api/src/db/schema/marketplace/moderation.ts @@ -0,0 +1,83 @@ +/** + * Moderation — User-Reports + AI-First-Pass-Log. + * + * Reports fließen in eine Mana-Admin-Inbox; AI-Mod-Log ist ein Record + * jeder automatisierten Prüfung pro Version, damit wir auditieren / + * re-train können wenn ein bad outcome shipped. + */ + +import { boolean, index, text, timestamp, uuid } from 'drizzle-orm/pg-core'; + +import { marketplaceSchema } from './_schema.ts'; +import { publicDecks, publicDeckVersions } from './decks.ts'; + +export const reportCategoryEnum = marketplaceSchema.enum('report_category', [ + 'spam', + 'copyright', + 'nsfw', + 'misinformation', + 'hate', + 'other', +]); + +export const reportStatusEnum = marketplaceSchema.enum('report_status', [ + 'open', + 'dismissed', + 'actioned', +]); + +export const deckReports = marketplaceSchema.table( + 'deck_reports', + { + id: uuid('id').primaryKey().defaultRandom(), + deckId: uuid('deck_id') + .notNull() + .references(() => publicDecks.id, { onDelete: 'cascade' }), + versionId: uuid('version_id').references(() => publicDeckVersions.id, { + onDelete: 'set null', + }), + // Optional: Report scoped auf eine spezifische Karte by content-hash. + cardContentHash: text('card_content_hash'), + reporterUserId: text('reporter_user_id').notNull(), + category: reportCategoryEnum('category').notNull(), + body: text('body'), + status: reportStatusEnum('status').notNull().default('open'), + resolvedBy: text('resolved_by'), + resolvedAt: timestamp('resolved_at', { withTimezone: true }), + resolutionNotes: text('resolution_notes'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + }, + (t) => ({ + deckIdx: index('deck_reports_deck_idx').on(t.deckId), + statusIdx: index('deck_reports_status_idx').on(t.status), + }) +); + +export const aiModerationVerdictEnum = marketplaceSchema.enum('ai_mod_verdict', [ + 'pass', + 'flag', + 'block', +]); + +export const aiModerationLog = marketplaceSchema.table( + 'ai_moderation_log', + { + id: uuid('id').primaryKey().defaultRandom(), + versionId: uuid('version_id') + .notNull() + .references(() => publicDeckVersions.id, { onDelete: 'cascade' }), + verdict: aiModerationVerdictEnum('verdict').notNull(), + // Categories die das Modell flagged — array weil ein Verdict + // mehrere Kategorien hitten kann (z.B. „spam" + „misinformation"). + categories: text('categories').array(), + model: text('model'), + rationale: text('rationale'), + humanReviewed: boolean('human_reviewed').notNull().default(false), + humanOverrode: boolean('human_overrode').notNull().default(false), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + }, + (t) => ({ + versionIdx: index('ai_moderation_log_version_idx').on(t.versionId), + verdictIdx: index('ai_moderation_log_verdict_idx').on(t.verdict), + }) +); diff --git a/apps/api/src/db/schema/marketplace/tags.ts b/apps/api/src/db/schema/marketplace/tags.ts new file mode 100644 index 0000000..1ff588a --- /dev/null +++ b/apps/api/src/db/schema/marketplace/tags.ts @@ -0,0 +1,48 @@ +/** + * Tag-Taxonomie für den Marketplace. Hierarchisch (parent_id), kuratiert + * vom Mana-Verein für den kanonischen Baum (medizin > anatomie > kardio). + * Authoren picken aus existierenden Tags und können neue über einen + * moderierten Flow vorschlagen (`curated = false` → Admin reviewed). + * + * Bewusst getrennt von der bestehenden `cards.tags`-Tabelle (privat, + * Deck-skopiert) — anderer Use-Case (Discovery vs. Lokal-Organisation), + * andere Lebenszyklen. + */ + +import { boolean, index, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core'; + +import { marketplaceSchema } from './_schema.ts'; +import { publicDecks } from './decks.ts'; + +export const tagDefinitions = marketplaceSchema.table( + 'tag_definitions', + { + id: uuid('id').primaryKey().defaultRandom(), + slug: text('slug').notNull(), + name: text('name').notNull(), + parentId: uuid('parent_id'), + description: text('description'), + curated: boolean('curated').notNull().default(false), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + }, + (t) => ({ + slugIdx: uniqueIndex('tag_definitions_slug_idx').on(t.slug), + parentIdx: index('tag_definitions_parent_idx').on(t.parentId), + }) +); + +export const deckTags = marketplaceSchema.table( + 'deck_tags', + { + deckId: uuid('deck_id') + .notNull() + .references(() => publicDecks.id, { onDelete: 'cascade' }), + tagId: uuid('tag_id') + .notNull() + .references(() => tagDefinitions.id, { onDelete: 'cascade' }), + }, + (t) => ({ + pk: uniqueIndex('deck_tags_pk').on(t.deckId, t.tagId), + tagIdx: index('deck_tags_tag_idx').on(t.tagId), + }) +); diff --git a/docs/marketplace/archive/COMPETITORS_2026-05.md b/docs/marketplace/archive/COMPETITORS_2026-05.md new file mode 100644 index 0000000..377f042 --- /dev/null +++ b/docs/marketplace/archive/COMPETITORS_2026-05.md @@ -0,0 +1,353 @@ +# Cardecky — Konkurrenz-Analyse (Mai 2026) + +> Stand: 2026-05-07. Quellen primär aus offiziellen Pricing-Seiten, G2/Trustpilot/Reddit/HN sowie Wikipedia/Crunchbase. Wo Daten fehlen oder nicht öffentlich sind, ist das explizit vermerkt. Preise schwanken regional/saisonal — die hier genannten Zahlen sind Listenpreise USD, sofern nicht anders angegeben. + +--- + +## 1. Executive Summary + +- **Anki bleibt der unschlagbare technische Gold-Standard**, aber UX-Schwächen (FSRS-„Difficulty Hell", Plugin-Hölle, kein natives Cloud-Sync mit Bildern) und der $25 iOS-Preis sind reale Lücken, in die wir stoßen können. Die Übergabe an AnkiHub im Februar 2026 könnte mittelfristig die Open-Source-Dynamik verändern — Beobachten lohnt. +- **Quizlet hat seine eigene Userbase verärgert**: Trustpilot 1.4/5, massive Beschwerden über Paywalls für Funktionen, die früher gratis waren. Genau dieses Vertrauensvakuum füllen Knowt und potenziell wir. +- **AI-Karten-Generierung ist Tischeinsatz, kein Differenzierer mehr.** Quizlet, Quizgecko, Knowt, RemNote, Wisdolia, sogar Memrise haben es. PDF-Import + KI ist erwartete Baseline. +- **Die „beautiful Anki"-Lücke ist umkämpft**: Mochi (5$/mo), RemNote (8$/mo), Noji (vormals AnkiPro). Cardecky mit _kostenlosem_ Sync sticht heraus — niemand sonst bietet die Kombination Markdown + FSRS + Cloud-Sync gratis. Das ist unsere wichtigste objektive Differenzierung. +- **Brand-Sniping ist real und schädlich**: AnkiPro (jetzt Noji) und AnkiApp (jetzt AlgoApp) haben sich einen Ruf als „Anki-Klone, die täuschen" erarbeitet — inkl. eines 10-tägigen Sync-Outages bei AnkiPro im Mai 2025. Lehre für uns: nie Anki im Namen führen, Kompatibilität sauber kommunizieren. + +--- + +## 2. Vergleichstabelle + +| Konkurrent | USP-Kurz | Lizenz | Free-Tier | Pro-Preis | Bedrohung | +| ------------------------------ | -------------------------------------- | --------------------------- | -------------------------------- | ----------------------------------------------- | -------------------------------- | +| **Anki (Desktop/Web/Android)** | Tech-Gold-Standard, FSRS, Add-ons | AGPL-3.0 | Voll-Funktional gratis | $0 (iOS: $24.99-29.99 lifetime) | **Hoch** | +| **AnkiHub** | Kollaborative Anki-Decks (USMLE-Fokus) | proprietär (auf Anki-Basis) | Trial | $5/mo | Mittel (Power-User) | +| **Quizlet** | Marktführer Volumen + Schule | proprietär | Sehr eingeschränkt, viele Ads | $35.99/Jahr (Plus), ~$45/Jahr (Unlimited) | **Hoch** (Reichweite) | +| **RemNote** | Notes + SR Hybrid | proprietär | Großzügig (3 PDFs, 5 Image-Occ.) | $8/mo annual (Pro) | Mittel | +| **Mochi** | Markdown, Local-First, schickes UI | proprietär | Single-Device | $5/mo (Sync) | **Hoch** (direkter Wettbewerber) | +| **Brainscape** | Confidence-Based-Repetition | proprietär | Limited Decks | ~$19.99/mo, $79.99 lifetime | Gering-Mittel | +| **Memrise** | Sprachen + AI-Buddies | proprietär | Eingeschränkt | $130.99/Jahr, $199.99 lifetime | Gering (Nische Sprachen) | +| **SuperMemo** | Algorithmus-Urvater (SM-20) | proprietär | Monatstrial Mobile | ~9.90$/mo Mobile, ~$66 Desktop perp. | Gering (Nische, sperrige UX) | +| **AnkiPro / Noji** | „Anki-Look" mit modernem UI | proprietär | mit Ads/Limits | nicht öffentlich klar (~$5-10/mo) | Mittel (Brand-Verwirrung) | +| **AnkiApp / AlgoApp** | Cloud-First Closed-Source | proprietär | Limited | Subscription (Details schwammig) | Gering (Reputation kaputt) | +| **Quizgecko** | AI-First (Quizzes, Podcasts) | proprietär | 1 AI-Lesson/Monat | $16/mo (Pro), $29 (Ultra) | Mittel (AI-Side) | +| **Knowt** | „Free Quizlet-Alternative" + AI | proprietär | Sehr großzügig | $9.99/mo (Ultra) | **Hoch** (gleiches Spielfeld) | +| **Wisdolia** | Browser-Ext: Karten aus Webcontent | proprietär | 50 Sets/Monat | $2.50/mo, $25/Jahr | Gering | +| **Mnemosyne** | Open-Source, Forschungs-Datasammlung | GPL | Voll gratis | — | Sehr gering | +| **Traverse** | Mind-Maps + SR (Mandarin Blueprint) | proprietär | Free-Plan | $15/mo Member, $35/User Enterprise | Gering | +| **Cerego** | Enterprise B2B Adaptive Learning | proprietär | — | ab $8.33/mo Indiv., Enterprise on req. | Sehr gering (B2B) | +| **NeuraCache** | Notion/Obsidian-Sync für SR | proprietär | Limited | 14d Trial → Pro (Preis nicht klar dokumentiert) | Gering | + +> Threat-Ranking: nur **Anki, Quizlet, Mochi, Knowt** sind Top-Bedrohungen für Cardeckys Kernzielgruppe. RemNote, Quizgecko, AnkiPro/Noji sind Nebenfront. + +--- + +## 3. Detail-Sektion pro Konkurrent + +### 3.1 Anki (Desktop / AnkiWeb / AnkiDroid / AnkiMobile) + +- **URL:** https://apps.ankiweb.net/ +- **Plattformen:** Windows, macOS, Linux (Desktop), Web (AnkiWeb), Android (AnkiDroid), iOS (AnkiMobile) +- **USP:** Der etablierte technische Standard für Spaced Repetition; mächtig, erweiterbar (Add-ons), FSRS v6 nativ, riesiges Deck-Ökosystem (insbes. Medizin: AnKing). +- **Lizenz:** AGPL-3.0 (Desktop, AnkiDroid, Web). AnkiMobile iOS proprietär (finanziert die Open-Source-Arbeit). +- **Kosten:** Desktop / Web / Android **kostenlos**. AnkiMobile iOS: **$24.99-29.99 einmalig (Lifetime)**. AnkiHub-Cloud-Decks: $5/Monat (separat). +- **User loben:** Mächtig & flexibel; FSRS-Wirksamkeit; freie Decks (insbes. AnKing Step Deck mit 100k+ Studenten); Dauerhaftigkeit (seit 2006). +- **User kritisieren:** Steile UX-Lernkurve; FSRS-„Difficulty Hell" (Karten reifen langsam, Reviews explodieren); Plugin-Brüche zwischen Versionen; iOS-Preis abschreckend; Sync-Setup für Bilder/Audio umständlich. +- **Firma & Geschichte:** Damien Elmes (Australien), gestartet 5.10.2006 ursprünglich für Japanisch-Lernen. Im **Februar 2026** angekündigt, dass AnkiHub (Austin, TX) Business-Operations und Open-Source-Stewardship übernimmt — Anki bleibt Open Source, keine externen Investoren, Versprechen „no enshittification". +- **Bedrohungsgrad: Hoch.** Power-User-Standard, riesiges Decks-Ökosystem, kostenlos. Wir können sie nicht im technischen Spielfeld schlagen — wir müssen über UX, Onboarding und „Anki-Import-Bridge" gewinnen. + +Quellen: [Anki Wikipedia]() · [AnkiMobile App Store](https://apps.apple.com/us/app/ankimobile-flashcards/id373493387) · [Class Central: Anki founder steps back](https://www.classcentral.com/report/anki-founder-steps-back/) · [AnkiHub](https://www.ankihub.net/) · [Difficulty Hell in Anki](https://skerritt.blog/difficulty-hell-in-anki/) · [Anki FSRS-Forum](https://forums.ankiweb.net/c/fsrs/41) + +--- + +### 3.2 Quizlet + +- **URL:** https://quizlet.com/ +- **Plattformen:** Web, iOS, Android. +- **USP:** Massen-Marktführer mit der größten Bibliothek shared decks (Schul-/Hochschul-Vokabeln); inzwischen stark AI-fokussiert (Q-Chat, Magic Notes, Coconote-Akquisition Feb 2026, ChatGPT-Integration März 2026). +- **Lizenz:** Proprietär. +- **Kosten:** Free (sehr eingeschränkt + Werbung). **Plus: $35.99/Jahr (~$2.99/mo)**. **Plus Unlimited: ~$44.99/Jahr** (entfernt Limits wie 3 Practice Tests/Monat, 20 Learn-Runden/Monat). +- **User loben:** Riesige Library shared sets; einfacher Einstieg; Multi-Device gut etabliert; AI-generierte Practice-Tests sind brauchbar. +- **User kritisieren:** **Trustpilot 1.4/5** aus 500+ Reviews. Aggressives Paywalling von Features, die früher gratis waren (Learn-Mode, Test, Lernrunden-Limit); Werbeflut im Free-Tier; Export-Möglichkeiten eingeschränkt (Lock-in); Bugs. +- **Firma & Geschichte:** 2005 gegründet von Andrew Sutherland (damals 15, Albany High School CA) für eigene Französisch-Vokabeln. Bootstrap bis 2015, dann $12M USV/Costanoa. 2020 $30M Series C bei General Atlantic, **$1B Bewertung**. Sitz San Francisco. Insgesamt ~$62M raised. CEO seit 2022 Lex Bayer. **Februar 2026: Akquisition Coconote**. +- **Bedrohungsgrad: Hoch (Reichweite), aber verwundbar.** Das Trustpilot-Desaster ist eine Steilvorlage. Unsere Chance: Quizlet-Refugees mit „so gut wie früher Quizlet, dazu FSRS und ohne Paywall-Gärten" abholen. + +Quellen: [Quizlet Wikipedia](https://en.wikipedia.org/wiki/Quizlet) · [Trustpilot Quizlet (1.4/5)](https://www.trustpilot.com/review/www.quizlet.com) · [Quizlet $1B Bewertung TechCrunch](https://techcrunch.com/2020/05/13/quizlet-valued-at-1-billion-as-it-raises-millions-during-a-global-pandemic/) · [Crunchbase Quizlet](https://www.crunchbase.com/organization/quizlet) · [Navigating Quizlet's Controversial Changes](https://medium.com/@maxtan0626/navigating-quizlets-controversial-changes-afeb97aafd1e) + +--- + +### 3.3 RemNote + +- **URL:** https://www.remnote.com/ +- **Plattformen:** Web, macOS, Windows, iOS, Android. +- **USP:** Hybrid aus Outliner-Notetaking (Roam-/Logseq-ähnlich) und integrierten SR-Karten — Karten entstehen direkt im Notiz-Flow via `::`-Syntax. Plus PDF-Annotation und Image-Occlusion. +- **Lizenz:** Proprietär. +- **Kosten:** Free (3 PDF-Annotationen, 5 Image-Occlusion-Karten). **Pro: $8/Monat annual ($96/Jahr)** oder $10/mo monthly. +- **User loben:** Notes + Cards in _einem_ Workflow; flexible nested Outline-Struktur; PDF-Annotation; AI-Generierung aus Notizen/PDFs. +- **User kritisieren:** Steile Lernkurve („nichts versteht man in 10 Min"); UI als überladen empfunden; **Performance-Probleme** (langsam beim Laden großer Datenbanken, iPad-Stabilität); Bugs nach Beta-Updates; non-English-Support schwach. +- **Firma & Geschichte:** Gegründet 2019 von Martin Schneider (MIT) und Moritz Wallawitsch (Berlin, HTW). Sitz: USA. **$2.8M Seed (Sept 2021)** unter General Catalyst. Hat 2025 ~$2M Revenue mit ~18 Personen erreicht. +- **Bedrohungsgrad: Mittel.** Andere Zielgruppe (PKM-Power-User, Studenten, die Notes wollen). Cardecky ist fokussierter — wir müssen die „nur-Karten"-Nische gegen ihre Hybrid-Erweiterung verteidigen. + +Quellen: [RemNote Pricing](https://www.remnote.com/pricing) · [Crunchbase RemNote](https://www.crunchbase.com/organization/remnote) · [RemNote Reviews Product Hunt](https://www.producthunt.com/products/remnote/reviews) · [RemNote Performance-Forum](https://forum.remnote.io/t/remnote-is-my-dream-pkm-yet-its-too-slow-am-i-doing-something-wrong/10920) · [Latka RemNote $2M ARR](https://getlatka.com/companies/remnote.com) + +--- + +### 3.4 Mochi + +- **URL:** https://mochi.cards/ +- **Plattformen:** macOS (Intel/AS), Windows, Linux Desktop, iOS, Android, Web. +- **USP:** Markdown-First, sauberes minimalistisches UI, Local-First mit Offline-Support; Image-Occlusion und Anki-`.apkg`-Import als First-Class-Feature ohne Plugin-Frickelei. **FSRS seit Mid-2025 unterstützt.** +- **Lizenz:** Proprietär. +- **Kosten:** Free (Single-Device, alle Karten lokal). **Pro: $5/Monat** (Sync, mehrere Geräte, Translations). +- **User loben:** „schönes" UI; intuitiver als Anki; sofortiges Karten-Lernen ohne Onboarding-Friktion; Anki-Import als Plugin-Free-Feature; Markdown-Workflow. +- **User kritisieren:** Sync nur in Pro (Single-Device-Free fühlt sich begrenzt an); algorithm war bis Mid-2025 schwächer als Anki (FSRS-Beta hat das gefixt); kleinere Community → weniger shared decks; Solo-Developer (Bus-Faktor). +- **Firma & Geschichte:** Solo-Projekt von **Matthew Steedman**, eigenfinanziert, Forum auf forum.mochi.cards. Keine externen Investoren öffentlich bekannt. +- **Bedrohungsgrad: Hoch — direktester Wettbewerber.** Praktisch identische Positionierung (Markdown, schickes UI, modern, FSRS, Local-First). Unterschied: Mochi nimmt $5/mo für Sync — **wir bieten Sync gratis**, das ist unsere stärkste objektive Differenzierung gegen Mochi. + +Quellen: [Mochi Cards](https://mochi.cards/) · [Mochi App Store](https://apps.apple.com/us/app/mochi-flashcards-and-notes/id1507775056) · [First Impressions of Mochi (borretti.me)](https://borretti.me/article/first-impressions-mochi) · [Bunpro: If you don't like Anki, try Mochi](https://community.bunpro.jp/t/if-you-dont-like-anki-consider-giving-mochi-a-try/59955) · [Mochi Changelog](https://mochi.cards/changelog/) + +--- + +### 3.5 Brainscape + +- **URL:** https://www.brainscape.com/ +- **Plattformen:** Web, iOS, Android. +- **USP:** „Confidence-Based Repetition" — User raten Selbsteinschätzung auf 1-5-Skala (statt SM-2/FSRS), als wissenschaftlich vermarktetes Schedule-System. Große kuratierte Decks-Library und EDU/Enterprise-Vertrieb. +- **Lizenz:** Proprietär. +- **Kosten:** Free (limitierter Zugang zu Deck-Bibliothek). **Pro: ~$19.99/Monat** (Discounts bei Jahres-/Lifetime-Plan). **Lifetime: $79.99**. +- **User loben:** Kuratierte Content-Bibliothek; klares Lernkonzept; schickes UI; gute Statistiken; Collaboration-Features für Teams. +- **User kritisieren:** Algorithmus weniger anpassbar als Anki/FSRS; Pro-Preis wird als hoch empfunden; Free-Tier-Decks sehr begrenzt; weniger Power-User-Features. +- **Firma & Geschichte:** Gegründet von Andrew Cohen (Idee 2006 Panama-Spanisch-Excel-Macro, später Master's Columbia EdTech). Sitz: New York. Founding Team: Cohen, Andy Lutz, Jay Stramel, Jonathan Thomas, Ron Cadet (2018). >$3M raised bis 2015. +- **Bedrohungsgrad: Gering-Mittel.** Andere Zielgruppe (Pro-Decks-Käufer, EDU-Markt). Wir konkurrieren wenig direkt. + +Quellen: [Brainscape](https://www.brainscape.com/) · [G2 Brainscape Reviews](https://www.g2.com/products/brainscape/reviews) · [Brainscape Wikipedia](https://en.wikipedia.org/wiki/Brainscape) · [How Brainscape Was Born](https://www.brainscape.com/academy/how-brainscape-was-born/) + +--- + +### 3.6 Memrise + +- **URL:** https://www.memrise.com/ +- **Plattformen:** Web, iOS, Android. +- **USP:** Sprachen-Fokus mit Native-Speaker-Videos und seit 2024/25 stark ausgebaute „AI Buddies" (Grammar Buddy, Translator Buddy, Culture Buddy, MemBot Chatbot auf GPT-Basis). +- **Lizenz:** Proprietär. +- **Kosten:** Free (limitiert + Ads). **Monthly $27.99**, **Annual $130.99 (~$11/mo)**, **Lifetime $199.99** (oft Discounts bis 50%). +- **User loben:** Native-Speaker-Video-Clips als Alleinstellungsmerkmal vs Duolingo; AI-Buddies bringen Konversationspraxis; gut für Vokabel-Aufbau. +- **User kritisieren:** Schwach in Grammatik; nicht für Fortgeschrittene; AI-Buddies hinter Paywall; teure Subscription verglichen mit Konkurrenten; legendäre community-„mems"-Funktion wurde entfernt (alte Community vergrätzt). +- **Firma & Geschichte:** Gegründet 2010 von **Ed Cooke** (Grand Master of Memory), **Ben Whately** und **Greg Detre** (Princeton-Neurowissenschaftler). Oxford-Trio. Sitz London. **$25.3M raised** über 7 Runden / 10 Investoren. Profitabel seit Ende 2016. **72M registrierte User (2024)**. +- **Bedrohungsgrad: Gering.** Sprach-Lerner-Nische, kaum Überlappung mit unserer generischen SR-Zielgruppe. + +Quellen: [Memrise](https://www.memrise.com/) · [Memrise Wikipedia](https://en.wikipedia.org/wiki/Memrise) · [Crunchbase Memrise](https://www.crunchbase.com/organization/memrise) · [Business of Apps: Memrise Statistics 2026](https://www.businessofapps.com/data/memrise-statistics/) + +--- + +### 3.7 SuperMemo + +- **URL:** https://www.supermemo.com/ (Web/Mobile) · https://supermemo.store/ (Desktop) +- **Plattformen:** Windows Desktop (Premium-Version), Web, iOS, Android, Browser-API. +- **USP:** Originator des Spaced-Repetition-Konzepts (1985 ff.) — Algorithmen SM-2 bis aktuell **SM-20 (2026)**. Die Desktop-Version hat Funktionen, die andere SR-Tools nicht haben (Incremental Reading, Concept Maps). +- **Lizenz:** Proprietär. +- **Kosten:** Mobile/Web: 1 Monat free, danach **~9.90 USD/EUR pro Monat**. Desktop SuperMemo 19 (Windows): **~$66 perpetual** (Käufer März 2026 bekommen kostenloses Upgrade auf SuperMemo 20). API: Early Access, 100 Repetitions/Tag gratis. +- **User loben:** Algorithmus-Tiefe; Incremental Reading; SM-20 als state-of-the-art; Hardcore-Power-User-Tool. +- **User kritisieren:** UI „aus den 1990ern"; sperrige Bedienung; Desktop-only für viele Features; Mobile-App stark eingeschränkt; Preis vs Anki nicht zu rechtfertigen für 95% der User. +- **Firma & Geschichte:** SuperMemo World Sp. z o.o., gegründet **5. Juli 1991** in Poznań, Polen, von Krzysztof Biedalak und **Piotr Wozniak** (mit Tomasz Kuehn, Janusz Murakowski, Marczello Georgiew). Wozniak begann SuperMemo 1.0 schon 13.12.1987. +- **Bedrohungsgrad: Gering.** Nische für Algorithmus-Enthusiasten und Incremental-Reading-Fans. Keine reale UX-Bedrohung für uns. + +Quellen: [SuperMemo Wikipedia](https://en.wikipedia.org/wiki/SuperMemo) · [SuperMemo Store](https://supermemo.store/products/supermemo-19-for-windows) · [Algorithm SM-18](https://supermemo.guru/wiki/Algorithm_SM-18) · [Piotr Wozniak](https://supermemo.guru/wiki/Piotr_Wozniak) · [SuperMemo iOS App Store](https://apps.apple.com/us/app/supermemo-effective-learning/id982498980) + +--- + +### 3.8 AnkiPro / Noji + +- **URL:** https://noji.io/ (vormals ankipro.net) +- **Plattformen:** iOS, Android, Web. +- **USP:** „Anki-Look-and-Feel" mit modernem UI und Cloud-Sync — verkauft sich aktiv als „die einfachere Anki-Variante". Nicht kompatibel mit echtem Anki (auch nicht mit `.apkg`-Decks ohne Workarounds). +- **Lizenz:** Proprietär. +- **Kosten:** Free mit Werbung/Limits. Pro-Subscription, Preise nicht prominent — nach Reports im Bereich **$5-10/mo** oder Jahresplan. +- **User loben:** Schickes Mobile-UI; einfacher Onboarding-Flow; Cross-Device-Sync „out of the box"; community Decks. +- **User kritisieren:** **Brand-Verwirrung** (User dachten, sie laden „echtes" Anki herunter); **10-Tage-Sync-Outage Mai 2025** mit Datenverlust für viele User; Lock-in (Export-Tools wurden vom Anbieter blockiert, ein Migrations-Tool erhielt einen **Rickroll-Response** von AnkiPro); offizielles Anki-Team distanziert sich. +- **Firma & Geschichte:** Anki Pro UAB; Co-Founder **Maksim Abramchuk** (im Crunchbase) und **Andrew Bond** (LinkedIn). 2021 gestartet, 2024/25 Rebrand zu **Noji**. Sitz nicht eindeutig öffentlich (LinkedIn-Indikatoren UK/Osteuropa). +- **Bedrohungsgrad: Mittel.** Nicht weil sie technisch besser sind, sondern weil Anki-Suchende auf sie reinfallen. **Lehre für Cardecky: Brand-Hygiene**. Wir sind „Cardecky" — nie „Anki" im Marketing, klare Trennung kommunizieren, Anki-Import sauber als Bridge dokumentieren. + +Quellen: [Anki knockoffs (offizielle Anki FAQ)](https://faqs.ankiweb.net/anki-knockoffs.html) · [AnkiPro Ripoff Forum](https://forums.ankiweb.net/t/ankipro-another-ripoff-anki-app/11791) · [Anki Users Get Rickrolled](https://broderic.blog/post/anki-users-get-rickrolled/) · [Noji App Store](https://apps.apple.com/us/app/noji-flashcards-anki-method/id1573585542) · [Crunchbase Anki Pro](https://www.crunchbase.com/organization/anki-pro) · [Speakada: Official Anki vs Fake Apps](https://speakada.com/official-anki-vs-fake-apps-the-critical-mistake-costing-language-learners-hours/) + +--- + +### 3.9 AnkiApp / AlgoApp + +- **URL:** https://www.algoapp.ai/ (vormals ankiapp.com) +- **Plattformen:** iOS, Android, Web, Desktop. +- **USP:** Closed-Source Cloud-First Karten-App, die seit Jahren den Namen „Anki" ausnutzt. **In manchen Regionen (z. B. japanischer App Store) firmiert sie weiterhin als „AnkiApp"**. +- **Lizenz:** Proprietär. +- **Kosten:** Free + Subscription-Tiers (Details vage, oft als „Trial-Trap" kritisiert). +- **User loben:** Funktioniert auf allen Plattformen; Cloud-Sync inkludiert; einfaches UI. +- **User kritisieren:** **Komplette Brand-Täuschung**; kein Import/Export zu echtem Anki; aggressive Subscription-Walls; Reviews mit „nichts mit echtem Anki zu tun" als wiederkehrendes Muster; Reputation in der Community unter null. +- **Firma & Geschichte:** AlgoApp Inc., gegründet **2021**, Sitz **San Mateo, CA**. Vor Kurzem von AnkiApp zu AlgoApp umbenannt (Anki-Brand-Druck wurde zu groß), aber teils noch unter altem Namen aktiv. +- **Bedrohungsgrad: Gering.** Reputation kaputt; informierte User meiden sie aktiv. Hauptthema für uns ist nicht Wettbewerb, sondern Brand-Hygiene-Lehre (siehe AnkiPro). + +Quellen: [Anki knockoffs FAQ](https://faqs.ankiweb.net/anki-knockoffs.html) · [AlgoApp on Anki Forum](https://forums.ankiweb.net/t/algoapp-still-using-ankiapp-name-in-japanese-app-store/69103) · [Crunchbase AlgoApp](https://www.crunchbase.com/organization/algoapp) · [Pitchbook AlgoApp](https://pitchbook.com/profiles/company/495884-44) + +--- + +### 3.10 Quizgecko + +- **URL:** https://quizgecko.com/ +- **Plattformen:** Web, iOS, Android. +- **USP:** AI-First-Workflow: aus PDF / Text / URL → Quizzes + Karten + Notizen + **Audio-Podcasts** (Notebook-LM-ähnlich). SR ist sekundär. +- **Lizenz:** Proprietär. +- **Kosten:** **Basic Free (1 AI-Lesson/Monat)**. **Pro $16/mo** (annual). **Ultra $29/mo** (50 Podcasts/mo, Custom Prompts). Business $32/mo (API + Branding). +- **User loben:** Vielseitige Output-Formate (Quiz/Karten/Podcast); guter PDF-Parser; multi-Question-Types. +- **User kritisieren:** SR ist „mitgeliefert" aber nicht der Fokus; Free-Tier sehr eng (1 Lesson); Pro-Preis hoch verglichen mit dedicated AI-Card-Tools. +- **Firma & Geschichte:** Privates Startup, kleinere Bekanntheit, keine prominente Funding-Information öffentlich. +- **Bedrohungsgrad: Mittel (in der AI-Front).** Wir konkurrieren am AI-Generierungs-Feature. Für reines SR-Lernen ist Quizgecko keine Bedrohung; für „ich habe ein Skript und will lernen" schon. Unser Konter: AI-Generierung ist bei uns „free with sync" und dann _dauerhaft_ in einem echten SR-System. + +Quellen: [Quizgecko](https://quizgecko.com/) · [Quizgecko Pricing](https://quizgecko.com/pricing) · [Toosio Quizgecko Review 2026](https://toosio.com/tool/quizgecko-ai-quiz-flashcard-podcast-generator) + +--- + +### 3.11 Knowt + +- **URL:** https://knowt.com/ +- **Plattformen:** Web, iOS, Android. +- **USP:** Positioniert sich explizit als **„free Quizlet alternative"**. Importiert Quizlet-Sets direkt, hat ähnliche Study-Modes (Learn, matching, practice tests, „Knowt Play") plus AI-Generierung aus Notizen/PDFs. +- **Lizenz:** Proprietär. +- **Kosten:** **Sehr großzügiges Free-Tier** (unlimited Karten, alle Study-Modes, basic AI mit monatlichen Limits). **Ultra: $9.99/mo annual** (Snap & Solve, unlimited AI). Manche Listen nennen einen $12.50/mo Premium. +- **User loben:** „Endlich Quizlet ohne Paywall"; Quizlet-Import funktioniert; AI-Note-zu-Karten brauchbar; Free-Tier wirklich nutzbar. +- **User kritisieren:** Hauptsächlich Schüler-/US-Highschool-Zielgruppe (für Erwachsene weniger durchdacht); AI-Limits im Free-Tier; SR-Algorithmus weniger ausgereift als Anki/FSRS. +- **Firma & Geschichte:** US-Startup, primär Studenten-Zielgruppe, keine prominente Funding-Information öffentlich verfügbar. +- **Bedrohungsgrad: Hoch (gleiches Spielfeld).** Beide Apps positionieren „free + AI + bessere UX als Quizlet". Unsere Differenzierung: **FSRS v6, Markdown, echtes Local-First-PWA-Modell, Anki-Import inkl. Bilder/Audio**. Knowt ist webbasiert, wir sind installierbar offline-first. + +Quellen: [Knowt](https://knowt.com/) · [Knowt vs Quizlet (StudyGenie 2026)](https://studygenie.io/blog/knowt-vs-quizlet) · [Best Quizlet Alternatives 2026](https://kvistly.com/blog/best-quizlet-alternatives) + +--- + +### 3.12 Wisdolia + +- **URL:** https://www.wisdolia.com/ (vorrangig als Chrome-Extension) +- **Plattformen:** Chrome Extension; Karten-Export zu Anki möglich. +- **USP:** Generiert Karten aus _jeder Webseite, PDF oder YouTube-Video_ in Sekunden — sehr fokussiert auf den „Capture beim Browsen"-Use-Case. +- **Lizenz:** Proprietär. +- **Kosten:** **Free: 50 Sets/Monat** (Limit: 15 PDF-Seiten, 12 Min YouTube). **Pro: $2.50/mo oder $25/Jahr** (unlimited). +- **User loben:** Spielerisch billig; Browser-Extension-Workflow ist reibungsarm; Anki-Export als Bridge. +- **User kritisieren:** Kein eigenes SR-System mit eigener Tiefe (eher Generator als Lern-App); Browser-only beschränkt. +- **Firma & Geschichte:** Kleines indie-Projekt; keine prominente Funding-Information öffentlich. +- **Bedrohungsgrad: Gering.** Komplementäres Tool eher als Wettbewerber — wer Wisdolia nutzt, exportiert oft _zu Anki_ (oder zu uns, wenn wir Wisdolia-Export sauber importieren). + +Quellen: [Wisdolia (Findmyaitool)](https://findmyaitool.com/tool/wisdolia) · [Wisdolia Plain English Walkthrough](https://plainenglish.io/artificial-intelligence/wisdolia-ai-generate-flashcards-anywhere-on-the-web-with-google-chrome-extension) + +--- + +### 3.13 Mnemosyne + +- **URL:** https://mnemosyne-proj.org/ +- **Plattformen:** Windows, macOS, Linux Desktop; Android (eingeschränkt). +- **USP:** Open-Source-Alternative zu Anki mit explizitem **Forschungs-Fokus**: Nutzer können (opt-in) anonyme Lerndaten beitragen, die seit 2006 zur Untersuchung von Langzeitgedächtnis gesammelt werden. +- **Lizenz:** GPL. +- **Kosten:** Komplett gratis. Kein Sync. +- **User loben:** Sauber, leichtgewichtig, ehrlich akademisch; gut für Forschung; lange Geschichte (>20 Jahre). +- **User kritisieren:** UI veraltet; Mobile-Support schwach (Android-App OK, iOS quasi nichts); kleine Community; weniger Decks als Anki. +- **Firma & Geschichte:** Community-Projekt um Peter Bienstman (Belgien). Letzte Release März 2026 — aktiv aber langsam. +- **Bedrohungsgrad: Sehr gering.** Akademisches Nischen-Tool, andere Zielgruppe. + +Quellen: [Mnemosyne Wikipedia]() · [Mnemosyne Project](https://mnemosyne-proj.org/) · [GitHub Mnemosyne](https://github.com/mnemosyne-proj/mnemosyne) + +--- + +### 3.14 Traverse + +- **URL:** https://traverse.link/ +- **Plattformen:** Web, iOS, Android. +- **USP:** Kombiniert Mind-Mapping + Note-Taking + SR-Karten in einer App; offizielle Integration mit „Mandarin Blueprint" (Chinesisch-Lernkurs). +- **Lizenz:** Proprietär. +- **Kosten:** Free, **Member $15/mo**, **Enterprise $35/User/mo**. +- **User loben:** Mind-Map + Karten kombiniert ist konzeptionell stark für Sprachen/komplexe Domains; Mandarin-Community schätzt es. +- **User kritisieren:** Member-Preis hoch; relativ kleine Bekanntheit außerhalb Mandarin-Sub-Community; nicht so viel feature parity mit Anki. +- **Firma & Geschichte:** Indie-Startup, primär Bootstrap; keine prominente Funding-Information öffentlich. +- **Bedrohungsgrad: Gering.** Andere Zielgruppe (visuelles Lernen, Sprachen). Keine direkte Konkurrenz. + +Quellen: [Traverse.link](https://traverse.link/) · [Traverse.link Capterra 2026](https://www.capterra.com/p/234102/Traverse/) + +--- + +### 3.15 Cerego + +- **URL:** https://www.cerego.com/ +- **Plattformen:** Web (B2B-Plattform). +- **USP:** Enterprise-Adaptive-Learning mit Versprechen „4-5× schnelleres Lernen, 90% Retention"; AI/ML-basierte Personalisierung. **Verkauft sich an Unternehmen, nicht Endkunden**. +- **Lizenz:** Proprietär. +- **Kosten:** Indiv. ab **$8.33/mo**, Enterprise ab 500 Seats individuell verhandelt (nicht öffentlich). +- **User loben:** Solide Lerneffekte in Enterprise-Trainings; gutes Reporting; sauberes UI. +- **User kritisieren:** Nicht für Selbstlerner gemacht; teuer für Einzelne; deck-Erstellungs-Workflow umständlich für Privatuser. +- **Firma & Geschichte:** US-Firma, in der Vergangenheit mehrfach pivotiert (B2C → B2B). Keine aktuelle Funding-Info. +- **Bedrohungsgrad: Sehr gering.** B2B, andere Welt. + +Quellen: [Cerego](https://www.cerego.com/) · [Cerego G2](https://www.g2.com/products/cerego/reviews) · [Cerego Capterra 2026](https://www.capterra.com/p/169739/Cerego/) + +--- + +### 3.16 NeuraCache + +- **URL:** https://neuracache.com/ +- **Plattformen:** iOS, Android. +- **USP:** SR-Karten **synchronisiert mit Notion / Obsidian / Logseq / Roam / Evernote / OneNote**, automatisches Extrahieren markierter Notizen → Karten. „Bridge"-Tool für PKM-Nutzer. +- **Lizenz:** Proprietär. +- **Kosten:** 14-Tage-Trial Pro. Pro-Subscription oder One-Time Lifetime; konkrete 2026-Preise nicht klar dokumentiert auf der öffentlichen Seite. +- **User loben:** Notion-/Obsidian-Sync ist die Killer-Funktion; spart Doppelarbeit für PKM-Power-User. +- **User kritisieren:** Klein, indie; UI weniger poliert als Mochi; Pricing intransparent; eher Mobile-only. +- **Firma & Geschichte:** Indie-Developer, geringe öffentliche Sichtbarkeit. +- **Bedrohungsgrad: Gering.** PKM-Nische; keine Überlappung mit unserer Generalist-Zielgruppe. + +Quellen: [NeuraCache](https://neuracache.com/) · [NeuraCache App Store](https://apps.apple.com/us/app/neuracache-spaced-repetition/id1450923453) · [NeuraCache AlternativeTo](https://alternativeto.net/software/neuracache/about/) + +--- + +## 4. Schluss-Empfehlung: 3 Differenzierungs-Hebel für Cardecky + +### Hebel 1: **„Free Sync" konsequent ausspielen** + +Niemand sonst bietet die Kombination, die wir liefern — _Markdown + FSRS + Multi-Device-Cloud-Sync inkl. Bilder/Audio + PWA + AI-Generierung_, alles im Free-Tier. Konkurrenten wollen für Sync Geld: + +- Mochi: $5/mo +- AnkiMobile iOS: $25-30 einmalig +- Quizlet: Sync ja, aber Features paywallen +- RemNote: Pro-Limit (3 PDFs) +- Brainscape: $20/mo + +**Action:** Marketing-Hauptbotschaft auf Pricing-Seite und Landingpage explizit machen: _„Sync gratis, immer. Karten gehören dir, lokal und in der Cloud."_ Gegen Mochi besonders direkt vergleichen. Wenn wir später monetarisieren, sollte Sync NIE in den Pro-Tier wandern — unser Reputations-Anker. + +### Hebel 2: **Anki-Migration als First-Class-Feature, ohne Brand-Sniping** + +Anki bleibt Power-User-Standard, aber Anki-User klagen über UX, FSRS-Tweaking und iOS-Preis. Sie sind die wertvollste Migrations-Zielgruppe (lange Lern-Historie, 100k+ Karten). Wir importieren bereits inkl. Bilder/Audio — das ist Gold. + +**Action:** + +- Eine dezidierte Landingpage `cardecky.com/from-anki` mit ehrlichem Vergleich (was wir besser machen, was Anki noch besser kann), Migrationsanleitung, und expliziter Distanzierung von AnkiPro/AnkiApp/Noji. +- Eine ehrliche Story dazu („Wir sind nicht Anki. Wir sind Cardecky. Aber wir respektieren deine Anki-Karten."). Das positioniert uns als seriöse Alternative gegen die Brand-Sniper. +- Für Bonus-Punkte: Imports von Mochi-Decks und Quizlet-Sets ebenfalls anbieten — Knowt lebt davon, wir können das auch. + +### Hebel 3: **„Local-First PWA" als Tech-Identität, nicht nur Implementierungsdetail** + +Cardeckys Local-First + PWA-Architektur ist konzeptionell anders als Quizlet/Knowt (Web-First) und besser als Mochi auf iOS (App-Store-Friktion). Wir sind installierbar, offline-funktional, ohne App Store. Das schlägt mehrere Fliegen: + +- Kein iOS-30%-Tax (vs AnkiMobile-Modell, das deshalb $25 kostet) +- Kein Vendor-Lock-in (Daten bleiben im Browser/lokal nutzbar) +- Kein Werbe-Modell nötig (vs Quizlet) +- Schnelles Auto-Update (vs Anki-Plugin-Brüche) + +**Action:** Konsequent „Local-First PWA" in Tech-Marketing nutzen (HN, Reddit /r/Anki, /r/medicalschool, indie-hacker-Communities). Genau dort sitzen Quizlet-Wechsler und Anki-frustrierte Med-Studenten, die diesen technischen Pitch verstehen. + +--- + +## Bonus: Was wir _nicht_ tun sollten + +- **Nicht „Anki" im Namen führen** — siehe AnkiPro/AnkiApp Reputation. „Cardecky" ist neutral, freundlich, und distanziert sich klar. +- **Nicht die SR-Algorithmus-Race spielen** — FSRS v6 reicht. SuperMemo SM-20 ist kein Marketing-Argument für 99% der User. +- **Nicht in Sprach-Lernen pivotieren** — Memrise und Duolingo besitzen das Feld, andere Mechaniken nötig. +- **Nicht alle AI-Features paywallen** — Knowt zeigt: ein großzügiges Free-Tier mit AI ist der Hebel gegen Quizlet. +- **Nicht Sync paywallen** — siehe Hebel 1. Das ist unser Anker-Wert. + +--- + +## Methodische Hinweise + +- Recherche durchgeführt 2026-05-07 via WebSearch (offizielle Pricing-Seiten, G2, Trustpilot, Capterra, Crunchbase, Wikipedia, Reddit, Anki-Forums, Hacker News). +- Einige Konkurrenten (NeuraCache, Quizgecko, Traverse, kleinere Indie-Tools) haben begrenzt öffentlich verfügbare Daten zu Funding/Team — wo Daten fehlen, ist „nicht öffentlich bekannt" eingetragen statt Spekulation. +- AnkiPro/Noji ist besonders intransparent (eigene Pricing-Seite versteckt klare Tier-Liste, Zahlen aus Reviews); wir sollten das im Auge behalten, wenn wir gegen sie konkurrieren. +- Quizlet-Bewertung mit „verwundbar" basiert real auf dem **Trustpilot-1.4/5** und der breiten Reddit-Stimmung — das ist eine echte Marktchance, kein Wunschdenken. diff --git a/docs/marketplace/archive/GUIDELINES.md b/docs/marketplace/archive/GUIDELINES.md new file mode 100644 index 0000000..38f8096 --- /dev/null +++ b/docs/marketplace/archive/GUIDELINES.md @@ -0,0 +1,367 @@ +# Cardecky — Projekt-Leitlinien + +Verbindliche Regeln für den Spinoff. Ziel: in wenigen Wochen ein +ausspielbares Web-MVP, das ausschließlich seinen *Core Gameloop* +beherrscht und alles andere von zentralen Mana-Bausteinen erbt. + +**Status:** Planungsphase, noch kein Code. +**Name:** Cardecky. +**App-Domain:** `cardecky.mana.how` (Subdomain unter `*.mana.how`, SSO über mana-auth). +**Marketing-Landing:** `cardecky.com` (eigene Domain, statisch, SEO/Akquise — keine Auth, leitet auf `cardecky.mana.how` für die App). +**Zugang:** offen für jeden eingeloggten Mana-User (`requiredTier: 'public'`, kein Beta-Gate). + +## 1. Mission in einem Satz + +Die schönste, einfachste Karteikarten-App mit Spaced Repetition — +zuerst nur Web, später Mobile, KI-Generierung als Phase 2. + +## 2. Game-Dev-Prinzip: zuerst nur der Core Gameloop + +Wie bei einem Spielprototyp gilt: alles, was nicht zum Loop gehört, +wird zurückgestellt. Erst wenn der Loop sich gut anfühlt und Nutzer ihn +freiwillig wiederholen, wird gebaut, was drumherum gehört. + +### Der Core Gameloop von Cardecky + +``` +Start + │ + ▼ +"Du hast N Karten heute fällig" ─────► (wenn 0: "Alles gelernt — komm später wieder") + │ + ▼ +[Lernen starten] + │ + ▼ +Vorderseite zeigen ──► User denkt ──► Tap/Space ──► Rückseite zeigen + │ + ▼ +Selbst-Bewertung: 1=nochmal · 2=schwer · 3=gut · 4=leicht + │ + ▼ +FSRS rechnet next-due ──► nächste Karte (oder Session-Ende) + │ + ▼ +Session-Ende: "X Karten gelernt, nächste in Y Stunden" + │ + └─► zurück zum Start +``` + +Sekundäre Loops (Karten erstellen, Decks verwalten) werden gebaut, sind +aber UI-arm. **Tertiäre Loops (KI-Generierung, Voice, Sharing) sind +Phase 2 und werden in Phase 1 nicht angefasst.** + +### Was Phase 1 enthält + +- Decks anlegen / löschen / umbenennen +- Karten manuell erstellen (Markdown-Inhalt) +- **Kartentypen:** Basic, Basic + Reverse, Cloze, Type-In (siehe §6) +- Lernsession mit FSRS v6, **inklusive per-User-Parameter-Tuning** +- "Heute fällig"-Übersicht + Streak-Zähler +- Tags auf Decks (das Modul hat sie ohnehin schon, raus wäre Mehrarbeit) +- PWA-installierbar, offline-fähig +- Auth via mana-auth, Sync via mana-sync + +### Was Phase 1 absichtlich NICHT enthält + +- KI-Generierung von Karten (kein PDF-Upload, keine Bild→Karte) +- Voice/TTS-Lernen +- Anki-Import / Export +- Statistik-Dashboards (nur Streak + Tagessumme) +- Public Decks / Marktplatz / Sharing +- Stripe / Bezahlung +- Mobile-App (PWA-tauglich aber kein Expo) +- Eigene Domain & Marketing-Landing +- Mehrsprachigkeit über Deutsch hinaus +- Bilder / Audio in Karten +- Image-Occlusion-Karten, Audio-Karten, Multiple-Choice +- Custom Card-Templates / WYSIWYG-Editor +- Erweiterte Suche + +Jede dieser Features ist legitim — aber nur, wenn der Loop steht. + +## 3. Goldene Regeln + +1. **Simpel schlägt vollständig.** Wenn ein Feature nicht zum Core Gameloop gehört, kommt es in einen Phase-2-Backlog, nicht in den Code. +2. **Open Source only.** Jede Library, jedes Tool, jeder Dienst muss eine OSI-konforme Lizenz haben (MIT, Apache 2.0, BSD, MPL, AGPL akzeptabel). Keine Closed-Source-SDKs, keine proprietären APIs als Pflichtabhängigkeit. +3. **Bevorzugt was im Verein schon läuft.** Neue Technologie nur einführen, wenn ein konkreter Engpass es verlangt und kein vorhandenes Tool es löst. +4. **Zentrale Mana-Dienste statt Eigenbau.** Auth, Sync, Analytics, Notifications, Media usw. werden NICHT neu gebaut — siehe §5. +5. **Local-First wie der Rest des Verein-Stacks.** IndexedDB als Quelle der Wahrheit, Sync nach Postgres im Hintergrund. +6. **`cardecky.mana.how` als Subdomain unter `*.mana.how`.** Kein eigenes Auth-System, kein eigenes Hosting-Setup — Eintrag in `PRODUCTION_TRUSTED_ORIGINS` + Cloudflare-Tunnel-Route reichen. +7. **Eine UI-Schicht, ein Theme.** Wir verwenden `@mana/shared-theme(-ui)` und `@mana/shared-ui` so weit es geht — kein paralleles Design-System. +8. **Erweiterbare Daten, simples UI.** Das Datenmodell denkt zukünftige Kartentypen mit (siehe §6), das UI zeigt in Phase 1 nur die vier definierten Typen. + +## 4. Tech-Stack (Phase 1) + +Alles bereits im Verein verwendet, alles OSI-Open-Source. + +### Frontend +| Schicht | Wahl | Lizenz | +|---|---|---| +| Framework | SvelteKit 2 | MIT | +| UI-Sprache | Svelte 5 (Runes) | MIT | +| Sprache | TypeScript 5 | Apache-2.0 | +| Styling | Tailwind CSS 4 | MIT | +| Build/Dev | Vite | MIT | +| PWA | `@vite-pwa/sveltekit` (über `@mana/shared-pwa`) | MIT | +| Icons | über `@mana/shared-icons` | MIT | +| Markdown-Render | `marked` + `DOMPurify` | MIT | + +### Datenhaltung (Client) +| Schicht | Wahl | Lizenz | +|---|---|---| +| Local Store | IndexedDB via Dexie | Apache-2.0 | +| Local-Store-Wrapper | `@mana/local-store` (intern) | — | +| Verschlüsselung | AES-GCM-256 via `@mana/shared-crypto` (Phase 2 — Hooks bereits an allen Schreib-/Lese-Pfaden, Wirkung deferred bis Vault-Server-Roundtrip steht; siehe `src/lib/data/crypto.ts`) | — | + +### Spaced Repetition +| Schicht | Wahl | Lizenz | +|---|---|---| +| Algorithmus | FSRS v6 (Free Spaced Repetition Scheduler) | BSD-3 | +| TS-Implementation | `ts-fsrs` (offizielle Portierung, mit Optimizer) | MIT | +| Per-User-Tuning | `ts-fsrs`-Optimizer, läuft client-seitig nach ≥ 50 Reviews | MIT | + +### Deployment +| Schicht | Wahl | Lizenz | +|---|---|---| +| Adapter | `@sveltejs/adapter-node` | MIT | +| Container | Docker, hinter Cloudflare Tunnel | Apache-2.0 | +| Host | Mac mini (siehe `docker-compose.macmini.yml`) | — | + +### Tooling +| Schicht | Wahl | Lizenz | +|---|---|---| +| Paket-Manager | pnpm 9 | MIT | +| Monorepo-Orchestrierung | Turborepo (vorhanden) | MPL-2.0 | +| Linting | ESLint (`@mana/eslint-config`) | MIT | +| Formatierung | Prettier | MIT | +| Tests (Unit) | Vitest | MIT | +| Tests (E2E) | Playwright | Apache-2.0 | +| TS-Config | `@mana/test-config`, `@mana/shared-vite-config` | — | + +### Backend in Phase 1: keiner + +Phase 1 braucht **keinen eigenen Service**. Lese-/Schreibpfad geht +ausschließlich über IndexedDB → `mana-sync` (existiert) → Postgres. + +Erst wenn KI-Generierung (Phase 2) dazukommt, entsteht +`services/cards-server` (Hono + Bun, analog zu allen anderen +Verein-Services). + +## 5. Zentrale Mana-Bausteine (Pflicht in Phase 1) + +### Services (laufen bereits, nur konsumieren) +| Service | Port | Wofür in Cardecky | +|---|---|---| +| `mana-auth` | 3001 | SSO, JWT, Sessions, Tier-Claims. Cardecky-Origin in `PRODUCTION_TRUSTED_ORIGINS` eintragen. | +| `mana-sync` | 3050 | Sync der `cards`-AppId-Daten (Decks, Karten, Reviews, StudyBlocks). | +| `mana-user` | 3062 | Profilinfos / Settings. | +| `mana-analytics` | 3064 | Page-Views, Loop-Events (siehe §11). | +| `mana-events` | 3115 | Domain-Events für Streak-Logik. | +| `mana-notify` | 3040 | "Du hast X Karten fällig"-Push (Phase 1.5). | +| `mana-credits` | 3061 | **Erst Phase 2** (KI-Generierung). | +| `mana-subscriptions` | 3063 | **Erst Phase 2** (Pro-Tier). | +| `mana-llm`, `mana-stt`, `mana-tts` | – | **Erst Phase 2.** | +| `mana-media` | 3015 | **Erst wenn Bilder in Karten erlaubt sind.** | + +### Workspace-Pakete (`@mana/*`) +| Paket | Wofür in Cardecky | +|---|---| +| `@mana/shared-auth` | Client-seitiger Auth-Hook (SSO-Flow, JWT-Handling). | +| `@mana/shared-auth-ui` | Login/Logout-Komponenten. | +| `@mana/shared-hono` | (sobald cards-server existiert) Auth-/Health-/Error-Middleware. | +| `@mana/shared-branding` | App-Registry-Eintrag (Tier=`public`, Branding, Subdomain). | +| `@mana/shared-types` | Geteilte TS-Typen. | +| `@mana/shared-utils` | Utility-Funktionen. | +| `@mana/shared-ui` | UI-Komponenten. | +| `@mana/shared-theme`, `@mana/shared-theme-ui` | Theme-Tokens, Dark/Light. | +| `@mana/shared-tailwind` | Tailwind-Preset. | +| `@mana/shared-i18n` | Übersetzungsfundament (Phase 1: nur DE registriert). | +| `@mana/shared-icons` | Icon-Set. | +| `@mana/shared-privacy` | Visibility-Enum für Decks (Sharing erst Phase 2, aber Feld vorbereitet). | +| `@mana/shared-crypto` | AES-GCM-256 für sensible Felder. | +| `@mana/shared-pwa` | Manifest, Service-Worker, Install-Prompt. | +| `@mana/shared-vite-config` | Vite-Defaults. | +| `@mana/shared-error-tracking` | Error-Reporting. | +| `@mana/shared-logger` | Strukturiertes Logging (Server-Seite, sobald relevant). | +| `@mana/shared-stores` | Geteilte Local-Store-Helpers. | +| `@mana/shared-tags` | Tags auf Decks. | +| `@mana/local-store` | Dexie-Setup, Sync-Hooks. | +| `@mana/eslint-config` | Lint-Regeln. | +| `@mana/test-config` | Vitest-Defaults. | +| `@mana/feedback` | In-App-Feedback-Widget. | +| `@mana/help` | Hilfe-Overlay. | + +**Erst Phase 2 oder später:** `@mana/shared-llm`, `@mana/shared-ai`, +`@mana/local-llm`, `@mana/local-stt`, `@mana/credits`, `@mana/qr-export`, +`@mana/wallpaper-generator`, `@mana/website-blocks`, +`@mana/shared-research`, `@mana/shared-uload`, `@mana/shared-storage`. + +### Datenpfad + +Cardecky übernimmt 1:1 das Mana-Datenpfad-Pattern: + +``` +User-Aktion → Store → encryptRecord → Dexie → Hooks (_pendingChanges) + → mana-sync → Postgres (mana_platform.cards.*) → andere Clients +``` + +appId = `cards`. Tabellen: `cardDecks`, `cards`, `cardReviews`, +`cardStudyBlocks`, `deckTags`. + +## 6. Datenmodell — erweiterbar gedacht + +Heutiges Modul kennt nur `front`/`back`. Damit weitere Kartentypen +ohne Schema-Bruch dazukommen, wechseln wir auf ein **Felder-Map + +Typ-Diskriminator**: + +```ts +type CardType = + | 'basic' // Phase 1: front/back + | 'basic-reverse' // Phase 1: erzeugt zwei Lernrichtungen aus einer Karte + | 'cloze' // Phase 1: Lückentext, eine Subkarte pro Cluster + | 'type-in' // Phase 1: User tippt Antwort, exact-match-Vergleich + | 'image-occlusion' // Phase 2 + | 'audio' // Phase 2 + | 'multiple-choice' // ggf. Phase 2 + +interface LocalCard extends BaseRecord { + deckId: string + type: CardType + fields: Record // basic: { front, back } · cloze: { text, extra? } + // FSRS-State liegt nicht hier, sondern in cardReviews (1:N pro Subkarte) + order: number +} + +interface LocalCardReview extends BaseRecord { + cardId: string + subIndex: number // basic-reverse → 0|1, cloze → c1, c2, … + stability: number // FSRS + difficulty: number // FSRS + due: string // ISO + reps: number + lapses: number + state: 'new' | 'learning' | 'review' | 'relearning' + lastReview?: string +} + +interface LocalCardStudyBlock extends BaseRecord { + date: string // YYYY-MM-DD + cardsReviewed: number + durationMs: number +} +``` + +**Cloze-Syntax:** Anki-kompatibel: `{{c1::Wort}}`, `{{c1::Wort::Hinweis}}`. +Eine Cloze-Karte mit Cluster `c1`+`c2` erzeugt 2 Reviews +(`subIndex 1`, `subIndex 2`). + +**Markdown:** `marked` + `DOMPurify` rendern Front/Back. Cloze-Tags +werden vor dem Markdown-Parser zu HTML-Spans umgewandelt, damit sie im +Render erhalten bleiben. + +**Migration aus dem Bestand:** existierende `front`/`back`-Karten werden +beim ersten Schema-Upgrade auf `type='basic'` mit +`fields={front, back}` migriert. Alte Spalten bleiben für eine +Übergangsversion lesbar (siehe `docs/DATABASE_MIGRATIONS.md`). + +## 7. Daten-Contract mit dem mana-Modul + +Wichtig: das **bestehende `cards`-Modul in der Mana-Web-App bleibt +erhalten**. Cardecky und das mana-Modul schreiben in dieselben +Postgres-Tabellen. + +Daher gilt: +- Schema-Änderungen werden **gemeinsam** im mana-Modul und im + Cardecky-Code rolled out (nie nur auf einer Seite). +- Encryption-Registry-Einträge müssen in beiden Frontends identisch + sein (Field-Allowlist). +- Migrationen über `docs/DATABASE_MIGRATIONS.md`. + +**Reihenfolge:** Phase 0 (mana-Modul um neue Tabellen + Kartentyp-Felder ++ FSRS erweitern) wird **vor** dem Standalone-Build durchgezogen. So +gibt es nie zwei Wahrheiten zur Datenstruktur. + +## 8. Definition of Done für Phase 1 + +Phase 1 ist fertig, wenn: + +1. Ein eingeloggter Mana-User kann auf `cardecky.mana.how` + - mindestens ein Deck anlegen, + - Karten manuell hinzufügen (Basic, Basic+Reverse, Cloze, Type-In), + - Markdown im Front/Back nutzen (Bold, Listen, Code, Links), + - eine Lernsession starten und mit FSRS-Bewertung durchspielen, + - die App schließen und am nächsten Tag die richtigen fälligen Karten wiederfinden. +2. FSRS-Per-User-Tuning läuft automatisch nach ≥ 50 Reviews und überschreibt die Default-Parameter. +3. Die App ist als PWA installierbar und offline-bedienbar (Karten lernen ohne Netz). +4. Auth läuft komplett über mana-auth (kein Eigen-Login). +5. Daten landen in Postgres und sind im bestehenden mana-Modul sichtbar (gleiche Datenquelle, kein Drift). +6. `pnpm validate:all` grün. +7. Mindestens drei Smoke-E2E-Tests (Playwright): + - „Login → Deck anlegen → Basic-Karte → Lernsession → bewerten" + - „Cloze-Karte mit zwei Clustern → erzeugt zwei Subkarten" + - „Type-In: korrekte Antwort = grün, falsche = rot" +8. Container baut & läuft auf dem Mac mini hinter Cloudflare Tunnel (`cardecky.mana.how`). + +Alles andere ist Phase 2. + +## 9. Repo-Struktur (Phase 1) + +``` +apps/cards/ +├── apps/ +│ └── web/ # SvelteKit-App, einziges Surface in Phase 1 +│ ├── src/ +│ │ ├── lib/ +│ │ │ ├── data/ # Dexie + Sync-Anbindung +│ │ │ ├── fsrs/ # ts-fsrs-Wrapper + Optimizer-Hook +│ │ │ ├── cards/ # Kartentyp-Renderer (basic, cloze, type-in) +│ │ │ ├── stores/ # Decks, Cards, Reviews, StudyBlocks +│ │ │ └── ui/ # Komponenten (DeckList, CardEditor, Session) +│ │ └── routes/ +│ │ ├── +layout.svelte +│ │ ├── +page.svelte # Heute fällig + Decks +│ │ ├── decks/[id]/+page.svelte # Deck-Detail + Karten +│ │ └── learn/[deckId]/+page.svelte # Lernsession +│ ├── package.json +│ ├── svelte.config.js +│ └── vite.config.ts +├── GUIDELINES.md # ← dieses Dokument +└── README.md +``` + +`apps/cards/apps/mobile/` und `apps/cards/apps/landing/` sind erst +Phase 2/3. + +## 10. PR-Checkliste + +Bei jedem Pull-Request gefragt: + +- Gehört die Änderung zum Core Gameloop? +- Wenn nein: rechtfertigt sie sich aus einer Pflicht (Auth, Sync, Build)? +- Wird ein bestehendes `@mana/*` Paket genutzt statt neu zu bauen? +- Ist jede neue Dependency Open-Source und im Verein bereits in Verwendung? +- Sind Datenmodell-Änderungen mit dem mana-Modul konsistent? +- Bricht die Änderung das Versprechen "Erweiterbare Daten, simples UI"? + +## 11. Analytics-Events (Mindestumfang Phase 1) + +Über `mana-analytics`: + +- `cards_session_started` — `{ deckId, dueCount }` +- `cards_card_rated` — `{ cardId, type, grade (1–4), elapsedMs }` +- `cards_session_completed` — `{ deckId, cardCount, durationMs }` +- `cards_deck_created` — `{ deckId }` +- `cards_card_created` — `{ deckId, type }` +- `cards_fsrs_optimized` — `{ reviewCount, paramsHash }` +- `cards_pwa_installed` — Standard-PWA-Event + +Reicht für die Core-Loop-Validierung. Mehr Events erst, wenn eine +konkrete Frage entsteht, die Daten beantworten sollen. + +## 12. Hinweis im mana-Modul + +Sobald `cardecky.mana.how` live ist, bekommt das mana-Modul einen +**dezenten** Hinweis (z.B. ein Banner oder Badge über der ListView): +"Cardecky gibt es jetzt auch als eigenständige App". Kein Pop-up, kein +forcierter Redirect — User entscheiden selbst. diff --git a/docs/marketplace/archive/MARKETPLACE_PLAN_2026-05-07.md b/docs/marketplace/archive/MARKETPLACE_PLAN_2026-05-07.md new file mode 100644 index 0000000..d753bc7 --- /dev/null +++ b/docs/marketplace/archive/MARKETPLACE_PLAN_2026-05-07.md @@ -0,0 +1,654 @@ +# Cardecky-Marktplatz — Plan + +> **Status**: Plan, kein Code. Stand 2026-05-07. +> **Goal-Setting**: Vollvision, kein MVP-Druck. Wir bauen die optimale Lösung. +> **Alignment**: User hat folgende Eckpunkte gesetzt: +> - Versionierte Decks + Live-Updates + Pull-Requests = ja, volle Vision +> - mana-credits zentral, sowohl für User-Käufe als auch Author-Verdienst +> - „Verified" zweigleisig: Mana-Verein-Kuration UND Community-Schwellen, mit unterschiedlichen Badges +> - Co-Learn-Sessions explizit **nicht** für Phase 1 — auf Phase 2 verschoben +> - Mobile-App auch später + +--- + +## 1. Mission + +**Die Karteikarten-Plattform mit der besten Lern-Community im Netz.** Wo qualitativ hochwertige Decks entstehen, gepflegt, geteilt und gelernt werden — und wo Lernende einander helfen. + +## 2. Was wir gegen die Konkurrenz aufbieten + +(verdichtet aus `apps/cards/COMPETITORS_2026-05.md`) + +| Differenzierer | Wir | Wer noch | +|---|---|---| +| Free Cloud-Sync | ✓ | niemand | +| Versionierte Decks mit Live-Updates | ✓ | nur AnkiHub (paywalled, Medizin-only) | +| Pull-Requests auf Decks | ✓ | niemand | +| Card-Discussions (inline pro Karte) | ✓ | niemand | +| AI-Karten + AI-Moderation + AI-Tags | ✓ | fragmentiert bei anderen | +| Open Source PWA | ✓ | nur Anki/Mnemosyne (Desktop) | +| Anki-Migration mit Bildern/Audio | ✓ (vorhanden) | niemand vollständig | +| Author-Followings + Activity-Feed | ✓ | niemand | +| Bezahlte Decks mit Author-Erlös via mana-credits | ✓ | nur Brainscape (eigenes Closed-Pricing) | +| Pseudonym + verifiziert kombinierbar | ✓ | niemand klar | + +## 3. Architektur-Prinzipien + +1. **API ist `/v1` ab Tag 1** — OpenAPI-Spec als Quelle der Wahrheit, Versionierungs-Bewusstsein eingebaut. +2. **Public-Decks leben separat** vom Local-First-Sync-Pfad (eigene Postgres-Tabellen, eigene Service, eigene RLS-Policies). Kein Vermischen mit `mana_sync.sync_changes`. +3. **Subscribed Decks sind unidirektional**: Author → Subscribers. Updates fließen einseitig. Wer ändern will, forkt. +4. **Content-Hash überall.** Jede Karte und jede Version bekommt einen deterministischen SHA-256 → Trust + Cache + Diff kostenlos. +5. **Lizenzen sind explizit + maschinen-lesbar** (SPDX-IDs: `CC0-1.0`, `CC-BY-4.0`, `CC-BY-SA-4.0`, plus eigener `Cardecky-Personal-Use-1.0` für Default-Käufe und `Cardecky-Pro-Only-1.0` für paid Decks). +6. **AI ist Moderator, nicht Gatekeeper** — KI-First-Pass + Human-Review-Eskalation. Niemals KI-allein-Take-down. +7. **Search ist von der DB entkoppelt** — Read-Only-Index, asynchron befüllt. Bricht der Search-Service, läuft der Marktplatz weiter. +8. **mana-credits ist die einzige Geld-Schnittstelle** — niemals Stripe direkt im cards-server. Alles geht über `/api/v1/credits/use`, `/credits/grant`, `/credits/reservations/*`. +9. **Anonymisiertes Lern-Verhalten**: aggregierte Stats sichtbar (z.B. „1.200 Lernende"), individuelles Lernverhalten nie öffentlich ohne explizites Opt-in. +10. **Keine Drittanbieter-Tracker.** Telemetrie ausschließlich über mana-analytics, opt-out möglich. + +## 4. Datenmodell + +Neues Schema `cards` in `mana_platform`. Alle Tabellen über `pgSchema('cards').table(...)` (Mana-Konvention). + +### 4.1 Authoren + +```sql +public_authors ( + user_id uuid PRIMARY KEY REFERENCES auth.users(id), + slug text UNIQUE NOT NULL, -- @anna-lang + display_name text NOT NULL, + bio text, + avatar_url text, + joined_at timestamptz DEFAULT now(), + pseudonym boolean DEFAULT false, -- true = klarname versteckt + verified_mana boolean DEFAULT false, -- vom Verein verliehen + verified_community boolean DEFAULT false, -- automatisch ab Schwelle + banned_at timestamptz, -- soft-ban + banned_reason text +) +``` + +Drei Verifizierungs-Stufen mit unterschiedlichen Badges in der UI: + +| Status | Badge | Wer / wie | +|---|---|---| +| `verified_mana = true` | 🛡️ **Mana Verifiziert** | Manuell vom Mana-Verein vergeben (Lehrer, Profis, Sprachschulen, Ärzte). Nicht erkaufbar. | +| `verified_community = true` | ⭐ **Community Verifiziert** | Automatisch bei: ≥ 500 Stars über alle Decks ODER ≥ 3 featured Decks ODER ≥ 200 aktive Subscribers über alle Decks. Periodisch neu evaluiert. | +| beides | 🛡️⭐ Beide Badges | Mana + Community zusammen. | + +### 4.2 Decks + Versionen + +```sql +public_decks ( + id uuid PRIMARY KEY, + slug text UNIQUE NOT NULL, -- /decks/anna-lang/spanish-a2-vocab + title text NOT NULL, + description text, + language text, -- ISO-639-1 + license text NOT NULL, -- SPDX + price_credits integer DEFAULT 0, -- 0 = kostenlos + owner_user_id uuid NOT NULL REFERENCES public_authors(user_id), + latest_version_id uuid, -- → public_deck_versions + is_featured boolean DEFAULT false, + is_takedown boolean DEFAULT false, + takedown_at timestamptz, + takedown_reason text, + created_at timestamptz DEFAULT now(), + CONSTRAINT price_requires_license CHECK (price_credits = 0 OR license = 'Cardecky-Pro-Only-1.0') +) + +public_deck_versions ( + id uuid PRIMARY KEY, + deck_id uuid NOT NULL REFERENCES public_decks(id), + semver text NOT NULL, -- 1.0.0, 1.1.0, 2.0.0 + changelog text, + content_hash text NOT NULL, -- SHA-256 of canonicalized cards + card_count integer NOT NULL, + published_at timestamptz DEFAULT now(), + deprecated_at timestamptz, + UNIQUE (deck_id, semver) +) + +public_deck_cards ( + id uuid PRIMARY KEY, + version_id uuid NOT NULL REFERENCES public_deck_versions(id), + type text NOT NULL, -- basic, basic-reverse, cloze, type-in + fields jsonb NOT NULL, -- {front, back} oder {text, extra} + ord integer NOT NULL, + content_hash text NOT NULL, -- per Karte: ermöglicht Smart-Merge + UNIQUE (version_id, ord) +) +``` + +### 4.3 Tags + Discovery + +```sql +tag_definitions ( + id uuid PRIMARY KEY, + slug text UNIQUE NOT NULL, + name text NOT NULL, + parent_id uuid REFERENCES tag_definitions(id), -- Hierarchie + description text, + curated boolean DEFAULT false -- vom Mana-Verein gepflegt +) + +deck_tags ( + deck_id uuid REFERENCES public_decks(id), + tag_id uuid REFERENCES tag_definitions(id), + PRIMARY KEY (deck_id, tag_id) +) +``` + +### 4.4 Engagement (Stars, Subscribes, Forks) + +```sql +deck_stars ( + user_id uuid REFERENCES auth.users(id), + deck_id uuid REFERENCES public_decks(id), + starred_at timestamptz DEFAULT now(), + PRIMARY KEY (user_id, deck_id) +) + +deck_subscriptions ( + user_id uuid REFERENCES auth.users(id), + deck_id uuid REFERENCES public_decks(id), + current_version_id uuid REFERENCES public_deck_versions(id), + subscribed_at timestamptz DEFAULT now(), + notify_updates boolean DEFAULT true, + PRIMARY KEY (user_id, deck_id) +) + +deck_forks ( + user_id uuid REFERENCES auth.users(id), + source_deck_id uuid REFERENCES public_decks(id), + source_version_id uuid REFERENCES public_deck_versions(id), + forked_at timestamptz DEFAULT now(), + PRIMARY KEY (user_id, source_deck_id, source_version_id) +) + +author_follows ( + follower_user_id uuid REFERENCES auth.users(id), + author_user_id uuid REFERENCES public_authors(user_id), + since timestamptz DEFAULT now(), + PRIMARY KEY (follower_user_id, author_user_id) +) +``` + +### 4.5 Pull-Requests + Discussions + +```sql +deck_pull_requests ( + id uuid PRIMARY KEY, + deck_id uuid REFERENCES public_decks(id), + author_user_id uuid REFERENCES auth.users(id), + status text NOT NULL, -- open, merged, closed, rejected + title text NOT NULL, + body text, + diff jsonb NOT NULL, -- {add: [...], modify: [...], remove: [...]} + merged_into_version uuid REFERENCES public_deck_versions(id), + created_at timestamptz DEFAULT now(), + resolved_at timestamptz +) + +card_discussions ( + id uuid PRIMARY KEY, + card_content_hash text NOT NULL, -- bindet sich an Karte, nicht an version + deck_id uuid REFERENCES public_decks(id), + author_user_id uuid REFERENCES auth.users(id), + parent_id uuid REFERENCES card_discussions(id), + body text NOT NULL, + hidden boolean DEFAULT false, + created_at timestamptz DEFAULT now() +) +``` + +### 4.6 Moderation + +```sql +deck_reports ( + id uuid PRIMARY KEY, + deck_id uuid REFERENCES public_decks(id), + version_id uuid REFERENCES public_deck_versions(id), + card_content_hash text, -- optional: Karte spezifisch + reporter_user_id uuid REFERENCES auth.users(id), + category text NOT NULL, -- spam, copyright, nsfw, misinformation, other + body text, + status text DEFAULT 'open', -- open, dismissed, actioned + resolved_by uuid, + resolved_at timestamptz, + resolution_notes text, + created_at timestamptz DEFAULT now() +) + +ai_moderation_log ( + id uuid PRIMARY KEY, + version_id uuid REFERENCES public_deck_versions(id), + verdict text NOT NULL, -- pass, flag, block + categories text[], -- spam, csam, hate, nsfw, ... + model text, -- "claude-3-5-sonnet" etc + rationale text, + human_reviewed boolean DEFAULT false, + human_overrode boolean DEFAULT false, + created_at timestamptz DEFAULT now() +) +``` + +### 4.7 mana-credits Integration + +```sql +deck_purchases ( + id uuid PRIMARY KEY, + buyer_user_id uuid REFERENCES auth.users(id), + deck_id uuid REFERENCES public_decks(id), + version_id uuid REFERENCES public_deck_versions(id), + price_credits integer NOT NULL, -- Snapshot zum Zeitpunkt des Kaufs + author_share integer NOT NULL, -- nach Verein-Cut + mana_share integer NOT NULL, + credits_transaction text, -- mana-credits ID + purchased_at timestamptz DEFAULT now(), + refunded_at timestamptz, + UNIQUE (buyer_user_id, deck_id) -- einmal Kauf reicht für Lifetime + alle Versionen +) + +author_payouts ( + id uuid PRIMARY KEY, + author_user_id uuid REFERENCES public_authors(user_id), + source_purchase_id uuid REFERENCES deck_purchases(id), + credits_granted integer NOT NULL, + credits_grant_id text, -- mana-credits grant ID + granted_at timestamptz DEFAULT now() +) +``` + +## 5. mana-credits Integration (Detail) + +Zwei-seitiger Marktplatz. mana-credits ist Single-Source-of-Truth fürs Geld. + +### 5.1 Kauf-Flow (Buyer) + +1. User klickt „Kaufen" auf paid Deck (Preis: z.B. 50 Credits) +2. cards-server checkt: Hat User schon dieses Deck? (deck_purchases) → wenn ja, sofort Zugriff +3. cards-server reserviert Credits via `POST mana-credits/api/v1/credits/reservations` (2-phase) +4. cards-server erstellt deck_purchases-Row (committed) +5. cards-server commit-released die Reservation → Credits abgebucht +6. cards-server erstellt author_payouts-Row → ruft `POST mana-credits/api/v1/internal/credits/grant` für den Author-Anteil +7. User bekommt sofortigen Zugriff: Deck wird in private Liste verschoben (User hat eine eigene Lokal-Kopie als Author-Subscription) + +**Was passiert wenn Author gebannt nach Kauf?** → Refund-Path (Phase γ Implementation): Admin kann Refund triggern → mana-credits → Reverse-Grant → User behält das Deck nicht mehr. + +### 5.2 Author-Auszahlungs-Modell + +- **Standard-Cut**: 80 % Author / 20 % Mana-Verein (Server-, Hosting-, Moderations-Kosten) +- **Verifizierte Authoren** (verified_mana): 90 % / 10 % +- **Mindestauszahlung**: keine — Credits werden direkt im mana-credits-Account gebucht, von dort kann der Author sie selbst nutzen oder per Stripe-Payout (mana-credits-Feature, falls vorhanden) abheben +- **Pricing-Range**: Free (0 Credits), oder 10–500 Credits (entspricht ungefähr 1–50 € — exakte Conversion siehe mana-credits packages) + +### 5.3 Käufer-Lebenszyklus + +- Einmal gekauft = Lifetime-Zugriff auf alle künftigen Versionen +- Bei major Version (e.g. 1.x → 2.0.0) **kein** zweiter Kauf nötig — Author behält die Verbesserungs-Pflicht +- Refund-Window: 30 Tage, automatisch verfügbar wenn ≤ 10 % der Karten gelernt wurden (Quizlet hat das, ist Best-Practice) + +### 5.4 Buyer-Protection bei Take-Down + +- Wenn Deck per Take-Down entfernt wird, behält Buyer Zugriff auf das letzte gesehene Snapshot (DSGVO-konform) +- Refund automatisch wenn Take-Down innerhalb 90 Tagen nach Kauf + +## 6. Service-Architektur + +### 6.1 `cards-server` (neu) + +- **Stack**: Hono + Bun (Mana-Konvention) +- **Port**: 3072 +- **Deps**: PostgreSQL (`mana_platform.cards.*`), Redis (Job-Queue für Indexing/Notifications) +- **Auth**: JWT via JWKS (mana-auth) +- **Routes**: siehe §7 + +### 6.2 `cards-search` (neu, später) + +- Eigene PostgreSQL-Instance mit pg_trgm + tsvector + pgvector +- Async-Indexer hört auf cards-server-Events („deck-published", „deck-updated") +- Optional: Meilisearch wenn Postgres FTS nicht reicht + +### 6.3 mana-llm (existierend, erweitert) + +- Embeddings für semantic search (jeden Deck-Description + Karte → 1536-dim Vector) +- Moderation-First-Pass (Klassifikation in spam/csam/hate/nsfw/etc.) +- Auto-Tag-Suggestions +- Auto-Summary für Deck-Beschreibungen + +### 6.4 mana-credits (existierend, erweitert) + +- Bestehende `/credits/use` und `/credits/reservations/*` für Kauf +- Bestehender `/internal/credits/grant` für Author-Auszahlung +- Vermutlich keine API-Erweiterung nötig + +### 6.5 mana-notify (existierend, erweitert) + +- Push-Notifications für Subscribe-Updates, neue Subscribers, neue Discussions/Replies, neue Stars (vom User konfigurierbar) + +### 6.6 mana-media (existierend) + +- Bilder/Audio in published Decks landen wie heute auch +- Pro Author-Tier ein Soft-Quota: Free 100MB, Verified 1GB, Mana 5GB + +## 7. API-Endpoints (Auswahl) + +OpenAPI-Spec wird die Quelle der Wahrheit; hier die wichtigsten Routes: + +### 7.1 Authoren + +``` +POST /v1/authors/me — Profil anlegen/updaten (slug, displayName, bio, avatar, pseudonym) +GET /v1/authors/:slug — Public Profile + Decks-Liste + Stats +GET /v1/authors/me/dashboard — Eigene Stats: Subscriber, Erlöse, Mod-Inbox +POST /v1/authors/:slug/follow — Folgen +DELETE /v1/authors/:slug/follow — Entfolgen +GET /v1/authors/me/feed — Personal Activity-Feed +``` + +### 7.2 Decks + +``` +POST /v1/decks — Deck als public registrieren (Init-Flow) +GET /v1/decks/:slug — Public Deck mit latest version +GET /v1/decks/:slug/versions — Versionsliste mit Changelogs +GET /v1/decks/:slug/versions/:semver — Specific Version + alle Karten +PATCH /v1/decks/:slug — Metadaten (title, description, license, price) + +POST /v1/decks/:slug/publish — Neue Version publishen (body: cards[], semver, changelog) + → triggert AI-Mod-Pass + → setzt latest_version_id + +POST /v1/decks/:slug/star — Star setzen +DELETE /v1/decks/:slug/star — Star entfernen + +POST /v1/decks/:slug/subscribe — Subscribe (lädt + sync'd Karten in lokale DB) +DELETE /v1/decks/:slug/subscribe — Unsubscribe + +POST /v1/decks/:slug/fork — Fork (lokale Kopie + Author-Lineage) + +POST /v1/decks/:slug/buy — Paid Deck kaufen (mana-credits-Flow) +POST /v1/decks/:slug/refund — Refund anfragen +``` + +### 7.3 Pull-Requests + +``` +GET /v1/decks/:slug/pull-requests — Liste +POST /v1/decks/:slug/pull-requests — Neuer PR (body: title, body, diff) +GET /v1/pull-requests/:id — Details +POST /v1/pull-requests/:id/merge — Author merged → erstellt neue Version +POST /v1/pull-requests/:id/close — Author schließt +POST /v1/pull-requests/:id/comments — Diskussion auf PR-Ebene +``` + +### 7.4 Discussions + +``` +GET /v1/cards/:contentHash/discussions — Threads für eine Karte (über Versionen hinweg) +POST /v1/cards/:contentHash/discussions — Neuer Thread / Reply +POST /v1/discussions/:id/hide — Author/Mod versteckt +``` + +### 7.5 Discovery + Search + +``` +GET /v1/explore — Featured + Trending + Categories (curated) +GET /v1/search?q=…&tag=…&lang=…&sort=… — Volltextsuche (FTS + semantic) +GET /v1/tags — Tag-Hierarchie +GET /v1/decks?author=…&tag=…&sort=…&p=… — Filtered Browse +``` + +### 7.6 Reports + Moderation + +``` +POST /v1/decks/:slug/report — User reportet Deck +POST /v1/cards/:contentHash/report — User reportet Karte +GET /v1/admin/reports — Admin-Inbox (verifizierte Mana-Mods only) +POST /v1/admin/decks/:slug/takedown — Admin entfernt Deck +POST /v1/admin/authors/:slug/ban — Admin sperrt Author +POST /v1/admin/authors/:slug/verify-mana — Mana-Verein-Badge vergeben +``` + +### 7.7 Notifications + +``` +GET /v1/notifications — Unread + recent +POST /v1/notifications/:id/read — Mark read +PATCH /v1/notifications/preferences — Settings (welche Events triggern Push) +``` + +## 8. UI / Routes (Cardecky-Frontend) + +``` +/explore — Featured + Trending + Tag-Tree + Search-Bar +/explore/search?q=… — Search-Result-Page +/explore/tag/:slug — Tag-Page + +/u/:slug — Author-Profil (Public) +/u/:slug/follow — Follow-Button im Header + +/d/:slug — Public-Deck-Detail-View + (Description, Stats, Latest-Karten-Preview, Subscribe/Fork/Star/Buy, Discussions) +/d/:slug/v/:semver — spezifische Version +/d/:slug/discussions — Alle Discussions zum Deck +/d/:slug/pull-requests — PRs +/d/:slug/pull-requests/:id — PR-Detail mit Diff-View + +/me/decks — Eigene private Decks (heute existiert) +/me/published — Eigene published Decks + Stats +/me/subscribed — Abonnierte Decks (mit Update-Indikator) +/me/forks — Geforkte Decks +/me/dashboard — Author-Dashboard (Erlöse, Subscriber-Wachstum) + +/feed — Personal Activity-Feed (Following-Activity + Updates) + +/admin/reports — Admin-Inbox (verified-mana-only) +/admin/decks — Take-Down-UI +/admin/authors — Verify + Ban +``` + +Zusätzlich: einige bestehende Komponenten erweitern (DeckDetail bekommt Subscribe-Button etc.). + +## 9. Cold-Start-Strategie + +Marktplatz ohne Decks ist nutzlos. Drei parallele Hebel: + +1. **Verein-Seed-Decks**: 50 hochwertige Decks selbst erstellen — sprachen (Top-3000 Vokabeln pro Sprache), Geschichte (TimeLine-Karten), Allgemeinwissen, Programmierung. Vom Mana-Team published, alle mit `verified_mana`-Badge. +2. **Anki-Top-100-Import-Service**: Wir bieten an, populäre Anki-Web-Decks (mit korrekter CC-BY-Lizenz) zu importieren und mit Original-Author-Attribution als Public-Decks anzulegen. Original-Author bekommt das `verified_mana`-Badge wenn er sich registriert. +3. **Influencer-Outreach**: Direkte Ansprache von 10-20 Anki-Power-Authoren (AnKing, etc.) mit dem Angebot eines verified-Status + sehr Author-freundlichem Cut. Wenn 1-2 wechseln, kommt ein Lawineneffekt. + +## 10. Risiken + Mitigationen + +| Risiko | Mitigation | +|---|---| +| Cold-Start (Marktplatz leer) | Seed + Anki-Import + Influencer (siehe §9) | +| Spam / Junk-Decks | AI-Mod-First-Pass + Report-System + Author-Ban-Flow | +| Copyright-Klagen (Lehrbuch-Karten) | Lizenz-Pflichtangabe + DMCA-Process + Take-Down-Workflow | +| Server-Kosten (Storage von Bildern/Audio) | Soft-Quotas pro Author-Tier (§6.6) + lossy compression im mana-media | +| AnkiHub als Konkurrent (Live-Updates Medizin) | „Alle Fachgebiete + gratis" als Counter; Med-Decks aktiv akquirieren | +| Mana-Credits-Verein-Cut zu hoch oder zu niedrig | A/B-Test verschiedener Cut-Verhältnisse; Best-Practice: ~80/20 für Standard, ~90/10 für Verified | +| Author-Frustration über fehlende Mobile-App | Klarer Roadmap-Hinweis + Mobile-Push-Notifications via PWA (heute geht das schon) | +| Discussions werden Toxic | Author-Owns-Their-Discussions (kann hide); Community-Mod (Verified-User können flaggen); klar dokumentierte Community-Guidelines | +| Mining/Scraping der Decks | Rate-limit auf API + Auth-Required für full-content; offene Snippets aber paywall am Voll-Inhalt | + +## 11. Phasenplan + +> **Co-Learn explizit ausgeklammert.** Mobile-App auch. + +### Phase α — Daten-Skelett (cards-server v0.1) + +- `services/cards-server/` SvelteKit-style Service-Setup, Hono + Bun + Drizzle +- Alle Schema-Tabellen + Migrationen (§4) +- API-Routes (CRUD-Niveau): Authoren, Decks, Versionen, Stars, Subscriptions +- OpenAPI-Spec +- Integration-Tests (Drizzle + Vitest) +- mana-auth-JWT-Middleware (`@mana/shared-hono`) +- Container in `docker-compose.macmini.yml` +- Cloudflare-Tunnel-Route `cardecky-api.mana.how` → `:3072` + +### Phase β — Author-Workflow ✅ shipped + +- ✅ „Author werden"-Flow im Frontend (Profil anlegen, slug claimen) +- ✅ „Publish"-Aktion auf Deck-Detail-Seite + - ✅ Lizenz-Picker (SPDX-Auswahl) + - ✅ Optional: Preis in Credits + - ⏳ Tags: Picker fehlt im Publish-Flow; Server-Schema steht +- ✅ Versioning: semver-Eingabe (Auto-Suggest pre-fill folgt in θ) +- ✅ Changelog-Editor +- ✅ AI-First-Pass-Moderation (mana-llm classify, Verdict im Publish-Result) +- ⏳ Author-Dashboard mit Subscriber-Counts: Erlöse jetzt unter `/me/purchases`, restliche Stats fehlen + +### Phase γ — Discovery-Frontend ✅ shipped (FTS minimal) + +- ✅ `/explore`-Seite mit Featured + Trending +- 🟡 Volltext-Suche: einfaches `ILIKE` über Title/Description; tsvector-Upgrade in Phase ι +- 🟡 Tag-Hierarchie: flach implementiert; baumartige Eltern-Kind-Navigation offen +- ✅ Author-Profile (`/u/`) + Follow-Button +- ⏳ Activity-Feed (wer hat was published / merged): nicht gebaut +- ✅ Star-System + +### Phase δ — Subscribe + Updates + Smart-Merge ✅ shipped + +- ✅ „Abonnieren"-Button → lädt aktuelle Version in lokale Cardecky-DB +- 🟡 Update-Detection: Polling beim Öffnen der Deck-Page; **kein** WebSocket-Push (kommt in θ/ι) +- ✅ **Smart-Merge**: Diff zwischen Versionen → unveränderte Karten behalten FSRS-State; geänderte erben FSRS-State über Ord-Pairing-Heuristik; neue + entfernte werden korrekt behandelt +- ✅ Diff-View „+N · ~N · −N" mit Apply-Button auf der Deck-Page +- ⏳ Push-Notifications für Subscribe-Updates via mana-notify: PR-/Verkaufs-Mails sind drin (ε.3, ζ.1), Update-Mail noch nicht + +### Phase ε — Pull-Requests + Discussions ✅ shipped + +- ✅ PR-Erstellen-UI: „✏️ Verbessern" auf `/learn/[id]` für Karten aus abonnierten Decks (modify oder remove) +- ✅ PR-Diff-Preview (flach, alle drei Blöcke `add` / `modify` / `remove`) +- ✅ Author-Merge-Workflow → erstellt neue Version atomar, bumped semver-Minor by default +- ✅ Inline-Discussion-Threads: in `/learn` (Toggle) + auf `/d/` (Karten-Liste mit Comment-Counts) +- ✅ Notify: Author bei neuem PR; PR-Author bei Merge/Reject (deterministische ExternalIDs für Dedup) +- ⏳ Mention-System (@username): nicht gebaut; Schema-Änderung später trivial +- 🟡 PR-Merge ist „stale-blind": kein Rebase / Konflikt-Detection (siehe §13a) + +### Phase ζ — mana-credits Marketplace 🟡 ζ.1 shipped, ζ.2 offen + +- ✅ Paid-Deck-Workflow End-to-End: 4-step Pipeline `reserve → INSERT purchase → commit → grant author + INSERT payout`, idempotent über `(buyer, deck)` +- ✅ Author-Auszahlungs-Pipeline: 80/20 Standard, 90/10 für `verifiedMana`-Authoren, kommt aus `config.authorPayout` (Basis-Punkte) +- ✅ Buyer-Dashboard `/me/purchases` mit Käufen + Author-Auszahlungs-Historie +- ⏳ **Refund-Workflow**: bewusst out-of-scope für ζ.1 (Author-Clawback ist konzeptuell heikel — siehe §13a) +- ⏳ **Reconciler**: bei Commit-/Grant-Failure nach Schritt 2 bleibt eine Purchase-Row mit `creditsTransaction = null` bzw. ohne Payout. Code logged, niemand fegt nach. Cron-Sweep in ζ.2 +- ⏳ Author-Payouts-CSV-Export für Steuern + +### Phase η — Moderation + Trust 🟡 η.1 shipped, η.2/η.3 offen + +- ✅ Report-Buttons auf Deck (`/d/`) + Discussion-Kommentare +- ✅ Admin-Inbox-UI (`/admin/reports`) mit Abweisen / Deck-Takedown / Author-Bann +- ✅ Take-Down-Workflow: transaktional, auto-closed parallele Reports + offene PRs auf demselben Deck, Mail an Author +- 🟡 Verified-Badge-Vergabe via API (`POST /v1/admin/authors/:slug/verify`); kein dediziertes UI +- ⏳ **Community-Verified Auto-Calculation**: Schema + Schwellwerte da; Cron-Job fehlt (η.2) +- ⏳ **Public Take-Down-Changelog**: Plan erwähnt das, nicht gebaut +- ⏳ **Verified-Mana-only Mods**: aktuell nur `role === 'admin'`; Plan-Vision ist „verified-mana darf auch resolven" — feiner Cut, später +- ⏳ Author-Ban-Process: Ban kaskadiert auf Decks ✅, aber kein Self-Service-Appeal-Flow für Author +- ⏳ Report-Spam-Schutz (Rate-Limit pro User+Deck): nicht da + +### Phase θ — Deep AI + +- Auto-Tag-Suggestions beim Publish (mana-llm) +- Auto-Summary für Decks (mana-llm Markdown-Render-tauglich) +- Audio-Vertonung mit mana-tts (Author opt-in: alle Karten als Audio generieren) +- Semantic-Search via Embeddings (mana-llm + pgvector) +- Personalized-Discovery („Empfohlen für dich" basierend auf Lern-Historie) + +### Phase ι — Optimierung + Skalierung + +- Search-Service als separater Pod (Meilisearch wenn Postgres FTS limitiert) +- CDN für public-deck-content (Cache + Geo-Distribution) +- Rate-Limiting + Anti-Scraping +- Real-time-Stats-Aggregation (Materialized Views) + +### Phasen die später kommen (explizit nicht in diesem Plan) + +- **Phase λ — Co-Learn-Sessions**: WebSocket-Multiplayer, gemeinsam lernen, Sehen-was-andere-machen +- **Phase μ — Mobile-Apps**: Expo-App (Cardecky-Standalone-Mobile) +- **Phase ν — Author-Tools**: Bulk-Edit-UI für Authoren mit großen Decks, Style-Templates, Author-Analytics-Deep-Dive +- **Phase ξ — Lern-Battles**: Asynchroner Wettkampf-Modus + +## 12. Konkrete Differenzierungs-Hebel — was geht wirklich nur bei uns + +1. **Gratis Cloud-Sync + Live-Updates auf abonnierte Decks**. Niemand sonst hat beides ohne Paywall. +2. **Pull-Requests auf Decks**. AnkiHub erlaubt das nicht so flüssig, andere gar nicht. „Lerne und verbessere mit" als Modus. +3. **Card-Discussions inline** — wenn ich beim Lernen eine Karte unverständlich finde, kann ich direkt fragen / ergänzen. Anki hat Plugin dafür, RemNote auch nicht. +4. **Authoren verdienen via mana-credits** — wir behandeln Authoren als 1st-Class-Konstrukt mit Erlös-Möglichkeit. Quizlet macht das nicht, AnkiWeb macht das nicht, Brainscape paywalled stattdessen die User. +5. **Open Source PWA** mit klarer Roadmap-Transparenz — Vertrauensvorsprung vs. Quizlet (closed, Trustpilot 1.4/5) und gegenüber AnkiPro/AnkiApp (closed-source, Brand-Sniper). +6. **Doppelte Verifizierungs-Stufen** mit unterschiedlichen Badges — Anki-Foren machen das ad-hoc; wir formalisieren es. +7. **AI als Moderator + Generator + Indexer** ohne Paywall — wir haben den eigenen mana-llm-Stack, Konkurrenten zahlen OpenAI per Call. + +## 13. Was wir NICHT tun + +- **Kein Decks-Bewertungssystem mit 1-5 Sternen**. Stars (Bookmarks) ja, Bewertungen nein — die werden gegamed (Quizlet-Erfahrung), und führen zu Author-Frust + Review-Bombing. +- **Kein Reddit-Style-Voting auf Karten / PRs / Discussions**. Wirkt cool, ruiniert die Community (Hacker-News-Effekt). Lieber „helpful"-Reactions in begrenzten Kategorien. +- **Kein „Karten der Woche" allein-algorithmisch**. Editorial-Pick (Mana-Verein) + Trending-Liste, aber niemals nur Algo, das landet immer beim niederschwelligsten Content. +- **Kein Anki-Bashing im Marketing**. Anki ist OSS, ehrlich, und wir wollen nicht ihre Audience entfremden — wir wollen sie ergänzen. Bridge nicht Burning. +- **Keine Pflicht-Klarnamen**. Pseudonyme bleiben gleichberechtigt. Verifizierung ist Bonus, nicht Pflicht. +- **Kein Marketplace-Cut über 30 %**. Apple-App-Store-Hass ist real, wir bleiben fair. + +## 13a. Bekannte Limitierungen / „macht später" + +**Phase ε (Pull-Requests + Discussions)** + +- **PR-Merge ist stale-blind**: `merge()` baut die neue Version aus `currentCards` zusammen, indem es Removes anwendet, dann Modifies-by-Hash, dann Adds. Wenn der Author zwischen PR-Open und Merge selbst eine Karte geändert hat, deren `previousContentHash` der PR matched, gewinnt **stumm** der PR — kein Konflikt-Hinweis. Akzeptabel solange wir wenige PRs/Tag haben; später entweder (a) PR-rebase mit `status=stale` bei Konflikt, oder (b) optimistic locking via `baseVersionId` auf der PR-Row mit Reject bei Mismatch. +- **Keine Multi-Card-Diff-Visualisierung**: PR-Diff-Preview zeigt jeden Block (`add` / `modify` / `remove`) flach. Bei großen PRs mit 50+ Karten unübersichtlich — Side-by-side-Vergleich pro modify wäre nett. +- **Discussion-Threading ist 1-Level**: Server speichert schon `parent_id`, aber das UI rendert flach. Bei Bedarf später ein Antworten-Button + visuelle Einrückung — kein Schema-Change nötig. +- **Card-Preview-Heuristik ist roh**: `` zieht `front` → `text` → erstes nicht-leeres Feld, strippt HTML, capt bei 140 Zeichen. Bei Cloze-Karten sieht der Leser den Roh-Text mit `{{c1::…}}`-Markern statt der maskierten Lern-Form. Kein Showstopper; später kann der Server eine `searchPreview`-Spalte schreiben. + +**Phase ζ (Paid Decks)** + +- **Refunds**: bewusst weggelassen. Author-Clawback ist konzeptuell heikel, weil der Author seinen Anteil nach Grant schon ausgegeben haben kann (→ 402 beim Reverse-Charge). Empfohlene ζ.2-Variante: Admin-only Refund, Buyer kriegt vollen Preis zurück, Author-Clawback nur best-effort, AGB-Klausel über Author-Cut-Risiko bei Refund. +- **Reconciler fehlt**: Wenn `commit` oder `grant` nach Schritt 2 fehlschlägt, bleibt eine Purchase-Row mit `creditsTransaction = null` bzw. ohne `author_payout`. Code logged das, aber niemand fegt nach. Cron-Sweep in ζ.2. +- **Buyer hat keinen Refund-Self-Service**: kein 30-Tage-Window-Knopf in der UI. Plan §5.3 sieht ihn vor; warten auf ζ.2. +- **CSV-Export für Steuern**: nicht drin. Easy add-on, sobald Verein die Steuerklärung 2026 vorbereitet. + +**Phase η (Moderation)** + +- **Verified-Mana-only Mods**: Admin-Gate ist aktuell `role === 'admin'`. Plan §11 sieht vor, dass auch verified-mana-Authoren Reports abarbeiten dürfen (mit eingeschränkten Aktionen). Würde nach den ersten 50 Reports sinnvoll, vorher over-engineered. +- **Community-Verified Cron**: Schema + Schwellwerte (`COMMUNITY_VERIFY_STARS=500`, `_FEATURED=3`, `_SUBSCRIBERS=200`) sind im config, aber kein Job berechnet `verified_community`. Add-on: ein Cron-Endpoint im internal API + SystemD-Timer auf Mac mini. +- **Public Take-Down-Changelog**: Plan erwähnt eine `/transparency`-Page — nicht gebaut. Bringt Trust, niedrige Priorität. +- **Appeal-Self-Service**: Author hat keinen Self-Service-Knopf für Restore. Bewusste Entscheidung — Appeals sollen menschlich sein, kein Self-Restore. +- **Report-Spam-Schutz**: ein User kann unbegrenzt Reports gegen ein Deck filen. Rate-Limit (max 1/User+Deck+Tag) wäre billig; kommt mit Phase ι. + +**Querschnittsthemen** + +- **Disk-Space auf der Build-Maschine** (Mac mini): aktuell ~6.7 GB frei. `pnpm store prune` als nächste Notbremse, falls cards-web-Builds enge Container-Layer brauchen. + +## 14. Offene Punkte die später entschieden werden müssen + +- **Mobile-Push-Notifications** für Subscribe-Updates: native PWA-Push reicht aktuell, aber Browser-API ist hin- und her — könnte Phase ι in einen eigenen Push-Service auslagern müssen. +- **Slack/Discord-Bots für Author-Updates**: nice-to-have, irgendwann. +- **Embed-Widget**: „Lerne dieses Deck auf meiner Webseite" mit IFrame — könnte Reichweite stark boosten. +- **API-Public**: API-Keys für Drittentwickler die eigene Tools rund um Cards bauen. +- **Backup für Subscriber**: Wenn ein Author published-Deck depubliziert, behalten Subscriber das letzte Snapshot (DSGVO-pflicht eh). +- **Internationalisierung der UI** (heute nur DE): nötig fürs internationale Publikum. + +## 15. Aktueller Stand 2026-05-07 + +| Phase | Status | Was läuft | Was fehlt | +|-------|--------|-----------|-----------| +| α — Skelett | ✅ | cards-server lebt auf 3072, Schema gepushed, JWT-Auth, Container in `docker-compose.macmini.yml`, Tunnel-Route `cardecky-api.mana.how` | — | +| β — Author-Workflow | ✅ | Profil-Claim, Publish, Lizenz, Preis, AI-Mod-Verdict | Tag-Picker im Publish, Author-Dashboard-Stats | +| γ — Discovery | ✅ | `/explore`, Stars, Follows, Author-Profile, Trending | tsvector-FTS, Tag-Tree, Activity-Feed | +| δ — Subscribe + Smart-Merge | ✅ | Pull, Smart-Merge mit FSRS-State-Erhalt, Diff-View | WebSocket-Push, Update-Mails | +| ε — PRs + Discussions | ✅ | PR-Erstellen / List / Merge / Reject / Close, Discussions auf `/learn` + `/d/`, Notify-Mails | Mention-System, PR-Rebase, Multi-Card-Diff-View, Discussion-Threading | +| ζ — Paid Decks | 🟡 ζ.1 | Buy-Flow, Author-Payout, Buyer-Dashboard | Refund, Reconciler, CSV-Export | +| η — Moderation | 🟡 η.1 | Reports, Admin-Inbox, Takedown, Ban-Cascade, Verify-API | Community-Verified-Cron, Public-Changelog, Verified-Mana-Mod-Permissions, Rate-Limit | +| θ — Deep AI | ⏳ | — | Auto-Tags, Auto-Summary, TTS, Embeddings, Personalized-Discovery | +| ι — Optimierung | ⏳ | — | Search-Service, CDN, Rate-Limiting, Materialized Views | +| λ / μ / ν / ξ | ⏳ | — | später (Co-Learn, Mobile, Author-Tools, Lern-Battles) | + +**Live-Domains**: `cardecky.mana.how` (Web) · `cardecky-api.mana.how` (API). + +**Nächste sinnvolle Schritte (Empfehlung)**: + +1. **ζ.2 Reconciler + minimaler Admin-Refund** — schließt das größte operative Loch im Paid-Flow. +2. **η.2 Community-Verified-Cron** — Plan-Vision der „doppelten Verifizierung" ist sonst nur halb umgesetzt; Cron ist klein. +3. **Update-Mail in δ.4** — Subscriber bekommen sonst nichts mit, wenn Author published. Dann ist die Notify-Story rund (PR-Open + PR-Merged + PR-Rejected + Verkauf + Takedown + Update). +4. **Phase θ starten** — Auto-Tags + Auto-Summary beim Publish via mana-llm: kostet wenig Code, viel Discovery-Hebel. + +--- + +*Plan erstellt: 2026-05-07. Owner: @till. Letzter Stand-Update: 2026-05-07 nach η.1.* diff --git a/docs/marketplace/archive/cards-server_CLAUDE.md b/docs/marketplace/archive/cards-server_CLAUDE.md new file mode 100644 index 0000000..521be64 --- /dev/null +++ b/docs/marketplace/archive/cards-server_CLAUDE.md @@ -0,0 +1,110 @@ +# cards-server + +Cardecky Marketplace + Community backend. Owns the published-deck side +of the Cardecky product (the standalone app at `cardecky.mana.how` is +the client). Phase α is the data skeleton — schema + bootstrap + JWT +auth in place; routes land progressively in Phase β onwards. + +For the full design rationale, phasing, and contract decisions see +**[`apps/cards/docs/MARKETPLACE_PLAN.md`](../../apps/cards/docs/MARKETPLACE_PLAN.md)**. + +## Tech Stack + +| Layer | Tech | +|-------|------| +| Runtime | Bun | +| Framework | Hono | +| Database | PostgreSQL (`mana_platform.cards.*` schema) + Drizzle ORM | +| Auth | JWT via JWKS from mana-auth (EdDSA, jose) | +| Money | mana-credits — never Stripe directly | + +## Port: 3072 + +## Quick Start + +```bash +# Schema push (writes to local mana_platform DB) +bun run db:push + +# Dev server with watch +bun run dev + +# Type check +bun run type-check +``` + +## Database + +Schema: **`cards`** inside the shared `mana_platform` DB. 17 tables across +six logical groups (matching the source files in `src/db/schema/`): + +| File | Tables | +|------|--------| +| `authors.ts` | `cards.authors`, `cards.author_follows` | +| `decks.ts` | `cards.decks`, `cards.deck_versions`, `cards.deck_cards` | +| `tags.ts` | `cards.tag_definitions`, `cards.deck_tags` | +| `engagement.ts` | `cards.deck_stars`, `cards.deck_subscriptions`, `cards.deck_forks` | +| `discussions.ts` | `cards.deck_pull_requests`, `cards.card_discussions` | +| `moderation.ts` | `cards.deck_reports`, `cards.ai_moderation_log` | +| `credits.ts` | `cards.deck_purchases`, `cards.author_payouts` | + +`co_learn_sessions` (Phase λ) is intentionally not yet in the schema. +Every table is created via `pgSchema('cards')` per the Mana convention. + +## Auth model + +Three middleware: + +- `jwtAuth(authUrl)` — validates Bearer tokens via JWKS. Sets + `c.set('user', { userId, email, role })`. Used on every user-facing + `/v1/*` route. +- `serviceAuth(serviceKey)` — `X-Service-Key` check for service-to- + service calls (e.g. mana-credits-webhook → cards-server). +- (planned) `optionalAuth` — for routes that should respond + differently when the caller is signed-in but never reject anonymous. + +## Phasing (per MARKETPLACE_PLAN §11) + +| Phase | What lands | Where | +|-------|-----------|-------| +| **α** | Skeleton + schema + JWT + health | now | +| β | Author publish flow + AI-mod-first-pass | next | +| γ | Discovery (browse, search, tags, follow) | | +| δ | Subscribe + smart-merge | | +| ε | Pull-requests + discussions | | +| ζ | mana-credits marketplace | | +| η | Moderation + trust | | +| θ | Deep AI (auto-tags, embeddings, audio) | | +| ι | Optimisation + scale | | + +## Environment Variables + +```env +PORT=3072 +DATABASE_URL=postgresql://mana:devpassword@localhost:5432/mana_platform +MANA_AUTH_URL=http://localhost:3001 +MANA_CREDITS_URL=http://localhost:3061 +MANA_LLM_URL=http://localhost:3025 +MANA_MEDIA_URL=http://localhost:3015 +MANA_NOTIFY_URL=http://localhost:3040 +MANA_SERVICE_KEY=dev-service-key +CORS_ORIGINS=http://localhost:5173,http://localhost:5180 + +# Author payout splits (basis points). Defaults: 80/20 standard, +# 90/10 verified-mana. +AUTHOR_PAYOUT_STANDARD_BPS=8000 +AUTHOR_PAYOUT_VERIFIED_BPS=9000 + +# Community-verified auto-thresholds. +COMMUNITY_VERIFY_STARS=500 +COMMUNITY_VERIFY_FEATURED=3 +COMMUNITY_VERIFY_SUBSCRIBERS=200 +``` + +## Critical Rules + +- **Never call Stripe directly.** All money flows through mana-credits. +- **`/v1` is the public contract** — additive-only changes within v1, breaking changes go to `/v2`. +- **Content-hash everything.** Per-card and per-version SHA-256s drive smart-merge, cache invalidation, and trust. +- **Subscribed Decks are unidirectional.** Author → Subscriber. Forks for the bidirectional case. +- **Verification is binary, not numeric.** Two flags (`verified_mana`, `verified_community`), the UI shows badges. Never invent a "trust score". diff --git a/docs/playbooks/MARKETPLACE_RESTORE.md b/docs/playbooks/MARKETPLACE_RESTORE.md new file mode 100644 index 0000000..6ea13b0 --- /dev/null +++ b/docs/playbooks/MARKETPLACE_RESTORE.md @@ -0,0 +1,312 @@ +# Marketplace-Restore — Playbook + +> **Status:** Plan, R0+R1 in Arbeit. Stand 2026-05-09. +> **Vorgänger:** das alte `services/cards-server/` aus `managarten/` (mana- +> monorepo) wurde am 2026-05-08 zusammen mit `apps/cards/` dekommissioniert, +> weil beides eine Kopplung war — siehe Decommission-Commits +> `bc158cb0b` (cards-server), `9cd871749` (apps/cards), `dd1bab09d` +> (cards-core). Rollback-Tag: `cards-decommission-base` im managarten-Repo. +> +> Dieser Plan dokumentiert den **Restore** des Marketplace-Backends in +> die Standalone-Cards-App, additiv zur bestehenden Greenfield-API. + +## TL;DR + +- **Strategie-B-Klarstellung:** „Kein Code aus mana-monorepo" galt der + Study-/FSRS-/Sync-Schicht (Dexie raus, server-authoritative). Marketplace + war nie davon betroffen — er wurde nur mit-rausgerissen, weil er an + `apps/cards` gekoppelt war. +- **Restore vs. Neubau:** Restore. ~13.000 Zeilen reifer Code, 8 Phasen + in Produktion gelaufen, Plan-Doku in Goldstandard-Qualität (654 Zeilen), + bekannte Limitierungen sauber dokumentiert. Neubau wäre 4–6 Wochen, + Restore ~14 Tage. +- **Schema-Naming-Entscheidung:** **Eigenes `marketplace`-pgSchema** + in derselben `cards`-DB. Begründung: saubere Read/Write-Trennung, + Backup-Granularität (`pg_dump --schema=marketplace`), RLS-Policies + pro Schema möglich, keine Kollisionen mit den existierenden + `cards.{decks,cards,reviews,…}`-Tabellen. +- **Service-Topologie:** Single-Service. Marketplace-Routen kommen unter + `/api/v1/marketplace/*` in den bestehenden `cards/apps/api`. Kein + zweiter Hono-Prozess, kein zweiter Container. YAGNI bei deinem Volumen. +- **Frontend:** alte Routes 1:1 nach `cards/apps/web/src/routes/` — + `/explore`, `/d/[slug]`, `/u/[slug]`, `/me/{published,subscribed,forks,purchases}`, + `/admin/reports`. Imports auf Verdaccio-Pakete umstellen, Theming- + Bridge-Aliase greifen automatisch. +- **Restore-Reihenfolge:** R0 (Doku) → R1 (Schema) → R2 (Auth + Routes + α/β) → R3 (γ + δ) → R4 (ε) → R5 (Frontend) → R6 (Smoke + erste + Cardecky-Decks publishen). +- **Was nicht im ersten Wurf:** Paid Decks (ζ.1) und Moderation-UI + (η.1). Schema-Tabellen kommen mit, aber Code-Pfade bleiben dormant + bis nach erstem Live-Test. + +--- + +## Was die alte Implementation konnte (Phase α–η.1) + +Inhalt der archivierten Dokumente — **Original-Wahrheit** unter +`cards/docs/marketplace/archive/`: + +| Datei | Was drin | +|---|---| +| `MARKETPLACE_PLAN_2026-05-07.md` | 654-Zeilen-Vollvision: Datenmodell, mana-credits-Flow, Cold-Start-Strategie, Anti-Patterns, Phasen-Status | +| `COMPETITORS_2026-05.md` | 353-Zeilen-Konkurrenz-Analyse: Quizlet, AnkiHub, Brainscape, AnkiPro, AnkiApp, RemNote, Mnemosyne | +| `GUIDELINES.md` | 367 Zeilen Community-Guidelines + Lizenz-Modell (SPDX + Cardecky-Personal-Use-1.0 + Cardecky-Pro-Only-1.0) | +| `cards-server_CLAUDE.md` | 110 Zeilen Tech-Stack-Doku des Original-Service | + +Phasen-Status zum Zeitpunkt der Decommission (2026-05-08): + +| Phase | Status | Was lief produktiv | +|---|---|---| +| α — Skelett | ✅ live | 17-Tabellen-Schema, JWT-Auth, Container, Tunnel `cardecky-api.mana.how` | +| β — Author-Workflow | ✅ live | Profil-Claim, Publish, Lizenz-Picker (SPDX), Preis-Eingabe, AI-Mod-First-Pass | +| γ — Discovery | ✅ live | `/explore`, Stars, Follows, Author-Profile, Trending, Search (ILIKE) | +| δ — Subscribe + Smart-Merge | ✅ live | Pull, Diff-View „+N · ~N · −N", FSRS-State erhalten über Karten-Hash-Diff | +| ε — PRs + Discussions | ✅ live | „✏️ Verbessern" auf jeder Karte, Author-Merge, Inline-Threads, Notify-Mails | +| ζ.1 — Paid Decks | ✅ live | 4-Schritt-Reserve→Purchase→Commit→Grant, 80/20-Split (90/10 für `verified_mana`) | +| η.1 — Moderation | ✅ live | Reports, Admin-Inbox, Takedown-Workflow (kaskadiert auf PRs), Author-Ban | + +Bekannte Limitierungen (siehe `MARKETPLACE_PLAN_2026-05-07.md` §13a): +PR-Merge-stale-blind, Reconciler-Lücke bei Paid-Pipeline, Mention-System +fehlt, Discussion-Threading 1-Level, kein Refund-Self-Service. Alles +dokumentiert, nichts unbekannt. + +--- + +## Architektur-Anpassungen für den Restore + +### 1. DB-Topologie: eigenes `marketplace`-pgSchema + +**Alt** (in `mana_platform`-DB, geteilt mit allen mana-Services): +``` +mana_platform.cards.{authors, decks, deck_versions, …} — 17 Tabellen +``` + +**Neu** (in standalone `cards`-DB des Standalone-Repos): +``` +cards.cards.{decks, cards, reviews, media_files, tags, imports} — Greenfield, bleibt +cards.marketplace.{authors, decks, deck_versions, deck_cards, …} — Restore, neu +``` + +Begründung für ein **eigenes pgSchema** statt Tabellen-Prefix +(`published_decks`, `published_deck_versions`, …): +- **Sauberer Read-Path.** Public-Endpoints sehen nur das `marketplace`- + Schema. Greenfield-Code (private Decks/Karten/Reviews) sieht + ausschließlich `cards`. Kein Risiko, dass ein `SELECT * FROM decks` + versehentlich beide Welten mischt. +- **Backup-Granularität.** `pg_dump --schema=marketplace` exportiert + nur den Marketplace-Stand für Compliance/Recovery. Privater Lern- + Stand der User bleibt unangetastet. +- **RLS-Policies pro Schema** — falls wir je Row-Level-Security + einführen für public-decks-take-down-Workflows, ist das pro Schema + konfigurierbar. +- **Drizzle-kit-Push-Disziplin.** `schemaFilter: ['cards', 'marketplace']` + hält beide Pushes sauber. Schema-Drift fängt sich auf Schema-Ebene. + +Drizzle-Variablennamen halten den `public`-Prefix aus dem alten Code: +`publicDecks`, `publicDeckVersions`, `publicDeckCards`. So bleibt das +intent klar, und Imports kollidieren nicht mit `cards.decks` aus dem +Greenfield. + +### 2. Auth-Modell: identisch, kein Refactor + +Alter cards-server: JWKS-Cache gegen `mana-auth`. Greenfield-cards-api: +JWKS-Cache gegen `mana-auth`. Identisch. Service-Key-Auth (`X-Service-Key`) +für Mana-Webhooks ebenfalls 1:1 übernommen. + +Optional-Auth-Middleware für Public-Endpoints (`/explore`, `/d/:slug`) +muss aus dem alten Code mitkommen — der erlaubt anonymen Read und +gibt zugleich personalisierte Daten (z.B. „Bist du Subscriber?") +wenn ein Bearer mit-übermittelt ist. + +### 3. Service-Topologie: Single-Service + +Alle Marketplace-Routen unter `/api/v1/marketplace/*` in +`cards/apps/api/src/routes/marketplace/`: + +``` +cards/apps/api/src/routes/marketplace/ +├── authors.ts +├── decks.ts (publish, list, version reads) +├── engagement.ts (stars, subscribe, fork) +├── discussions.ts (card-discussions threads) +├── pull-requests.ts +├── moderation.ts (reports + admin) +├── purchases.ts (paid decks — dormant in R3, aktiv ab späterer Welle) +└── explore.ts (discovery + search) +``` + +Hauptserver-Mount in `cards/apps/api/src/index.ts`: +```ts +app.route('/api/v1/marketplace/authors', authorsRouter()) +app.route('/api/v1/marketplace/decks', decksRouter()) +// … +``` + +Vorteile: ein Prozess, ein Container, ein Tunnel-Endpoint +(`cardecky-api.mana.how`), eine JWT-Validierung, eine Drizzle-DB-Connection. + +### 4. Frontend: additive Routes, gleicher Stack + +`cards/apps/web/` ist SvelteKit + Svelte 5 (runes-only). Alter +`apps/cards/apps/web/` war es auch. Routen werden 1:1 übernommen, +mit drei Anpassungen: + +- **Imports:** `@mana/shared-*` kommt heute aus Verdaccio (npm.mana.how). + `pnpm add @mana/shared-ui@^0.1.1 @mana/shared-share-protocol …`. +- **Theming:** alte Components nutzten alte Token. Greenfield-Bridge- + Aliase in `app.css` mappen die meisten alten Token aufs 12er-Mana- + Vokabular. Test im Browser, ggf. Anpassungen. +- **Auth-Hook:** `dev-stub.svelte.ts` bleibt für Phase-2-Lücke. Sobald + echte mana-auth-Login-Flow ausgerollt ist (siehe `STATUS.md`), + weicht der Stub für `@mana/shared-auth`. + +### 5. Hash-Implementierung: bestehende `@cards/domain` benutzen + +Alter `cards-server/src/lib/hash.ts`: eigenständige SHA-256-Implementierung. +Greenfield: `cardContentHash` in `@cards/domain` (Web-Crypto, deterministisch). + +Beim Restore: `lib/hash.ts` **nicht** mit-übernehmen, sondern +Marketplace-Code auf `cardContentHash` aus `@cards/domain` umbiegen. +Eine Hash-Definition für die ganze App. + +### 6. mana-Service-Calls: identische Pattern + +Alter cards-server rief mana-credits, mana-llm, mana-media, mana-notify +über `MANA_*_URL`-env-Vars. Greenfield-cards-api macht das genauso. +Code 1:1 übernehmen. + +--- + +## Wellen-Plan + +| Welle | Zustand | Was passiert | Blocker | +|---|---|---|---| +| **R0** | 🟡 in Arbeit | Doku-Restore: Archive aus cards-decommission-base + dieser Plan + Strategie-B-Klarstellung | — | +| **R1** | ⏸ pending | Schema-Restore: 7 Schema-Files in `cards/apps/api/src/db/schema/marketplace/`, drizzle-kit push grün gegen lokale `cards`-DB | R0 | +| **R2** | ⏸ pending | Backend Phase α + β: Author-Profile + Publish + AI-Mod-Stub | R1 | +| **R3** | ⏸ pending | Backend Phase γ + δ: Discovery + Subscribe + Smart-Merge | R2 | +| **R4** | ⏸ pending | Backend Phase ε: Pull-Requests + Card-Discussions | R3 | +| **R5** | ⏸ pending | Frontend-Routes: `/explore`, `/d/[slug]`, `/u/[slug]`, `/me/{published,subscribed,forks}` | R4 | +| **R6** | ⏸ pending | E2E-Smoke: erstes Cardecky-Deck publishen, von Till's Account subscriben, Smart-Merge testen | R5 | + +**Aufwand-Schätzung gesamt: ~14 Tage Real-Arbeit.** + +Bewusst aus dem ersten Restore-Wurf rausgelassen: + +- **ζ.1 Paid Decks** — Schema-Tabellen kommen in R1 mit, aber Routes/UI + bleiben dormant. Re-Aktivierung als eigene Welle nach Live-Validation. + Begründung: mana-credits-Integration ist heikel (4-step-Pipeline mit + reservation-commit-grant), Author-Erlöse sind ein Verein-Compliance- + Thema (Steuern, AGB, Refund-Policy), und solange Cardecky synthetic + Decks publisht, gibt's keinen Need. +- **η.1 Moderation-UI** — Schema-Tabellen + API-Endpoints kommen mit, + Admin-Frontend (`/admin/reports`) wird ausgelassen bis erste echte + User da sind. Take-Down via SQL für die ersten Wochen. +- **θ Deep AI** (Auto-Tags, Embeddings, TTS) — bleibt explizit später- + Phase, war im Original-Plan auch nicht für den ersten Wurf vorgesehen. + +--- + +## Cardecky-Skill-Integration + +Der `/cards-deck`-Skill (siehe `~/.claude/skills/cards-deck/SKILL.md`) +produziert Decks unter dem Cardecky-Plattform-User. Beim Marketplace- +Restore wird Cardecky **automatisch zum Marketplace-Author**: + +1. Bei R2 wird ein Init-Skript einen `marketplace.authors`-Row für + Cardecky-User-ID anlegen (`slug='cardecky'`, `display_name='Cardecky'`, + `pseudonym=false`, `verified_mana=true` — der Verein vergibt das + Badge an seinen eigenen KI-Author). +2. Skill-Stufe 5 (Publish) wird erweitert um einen optionalen Schritt: + nach Anlegen des privaten Decks kann der Skill ein `POST + /api/v1/marketplace/decks/:id/publish` mit `semver=1.0.0` und einem + auto-generierten Changelog hinterher schicken — dann ist das Deck + sofort im `/explore` sichtbar. +3. Default bleibt aber „nur privat anlegen", weil: + - das Validate-Stage (Stufe 4) eine menschliche Sichtung verdient, + bevor 30 Karten öffentlich werden; + - Reviewer-Stops nach Stufe 3 sind zwingend. + +Der Skill braucht keine Architektur-Änderung — er addiert nur einen +optionalen 6. Schritt. + +--- + +## Lizenz-Modell (aus dem Original übernommen) + +Aus `MARKETPLACE_PLAN_2026-05-07.md` §3 + `GUIDELINES.md`: + +| SPDX-ID | Erlaubt | Wann | +|---|---|---| +| `CC0-1.0` | alles, kein Attribution-Pflicht | für Public-Domain-fähige Karten | +| `CC-BY-4.0` | alles, mit Attribution | meist gewählt für Wissens-Decks | +| `CC-BY-SA-4.0` | alles, ShareAlike + Attribution | für Wikipedia-derivierte Decks | +| `Cardecky-Personal-Use-1.0` | nur persönlicher Lern-Use, kein Re-Publish | **Default für kostenlose Decks** | +| `Cardecky-Pro-Only-1.0` | nur via Kauf, kein Re-Publish, kein Fork | **Pflicht für paid Decks** (DB-CHECK enforced) | + +Der DB-CHECK auf `decks.price_credits = 0 OR license = 'Cardecky-Pro-Only-1.0'` +ist im Schema beibehalten — Code-Bug kann nicht stillschweigend ein +Paid-Deck mit CC-Lizenz publishen. + +--- + +## Cold-Start-Strategie (aus dem Original) + +Kommt im ersten Restore-Wurf nicht aktiv zum Tragen, aber der Original- +Plan §9 hat drei Hebel definiert, die bei Re-Launch greifen: + +1. **Verein-Seed-Decks** — 50 hochwertige Cardecky-published Decks + (Sprachen, Geschichte, Allgemeinwissen, Programmierung). Der + `/cards-deck`-Skill ist genau das Werkzeug dafür. +2. **Anki-Top-100-Import-Service** — populäre CC-BY-Anki-Decks mit + Original-Author-Attribution importieren, Original-Author bekommt + `verified_mana`-Badge bei Registrierung. +3. **Influencer-Outreach** — 10–20 Anki-Power-Authoren (AnKing & + Konsorten) gezielt ansprechen, sehr Author-freundlicher Cut. + +Hebel 2 + 3 sind Wachstumsmaßnahmen, nicht Phase-1. Hebel 1 ist +sofort umsetzbar mit dem bestehenden Skill. + +--- + +## Anti-Patterns aus dem Original-Plan §13 (gelten weiter) + +- **Kein 1-5-Sterne-Rating-System.** Stars (Bookmark) ja, Bewertungen nein. +- **Kein Reddit-Style-Voting** auf Karten/PRs/Discussions. Hacker-News-Effekt. +- **Kein „Karten der Woche" allein-algorithmisch.** Editorial + Trending- + Liste, aber niemals reiner Algo-Feed. +- **Kein Anki-Bashing im Marketing.** Bridge nicht Burning. +- **Keine Pflicht-Klarnamen.** Pseudonyme bleiben gleichberechtigt. +- **Kein Marketplace-Cut über 30 %.** Standard 80/20, verified 90/10. + +--- + +## Offene Punkte + +- **PR-Merge-stale-blind aus dem Original.** Bekannte Limitierung: wenn + Author zwischen PR-Open und Merge selbst eine Karte ändert, deren + `previousContentHash` der PR matched, gewinnt stumm der PR. Im + ersten Restore-Wurf so übernehmen wie war; späterer Fix via + optimistic locking auf `baseVersionId` der PR-Row mit Reject bei + Mismatch. +- **Reconciler-Cron für Paid-Pipeline-Inkonsistenzen.** Original §13a + beschreibt das Loch: bei Commit-/Grant-Failure nach Schritt 2 bleibt + eine Purchase-Row mit `creditsTransaction = null`. Beim Restore + initial nicht aktiv (Paid-Decks dormant), aber sobald Phase ζ + reaktiviert wird, ist Reconciler ein muss-haben. +- **`cards-decommission-base`-Tag im managarten-Repo.** Falls jemand + managarten löscht oder das Tag nochmal entfernt, geht der Restore- + Pfad verloren. Empfehlung: Schema-Files + ausgewählte Code-Snippets + einmal nach `cards/docs/marketplace/archive/` kopieren (Doks sind + schon drin, Code-Files folgen optional bei R1+R2). +- **Schema-Migrations-Pfad bei späterer Drizzle-Version.** Greenfield- + Cards plant Migration auf Drizzle 0.45/zod-4 mit der Plattform mit + (`mana/docs/MIGRATION_DRIZZLE_ZOD.md`). Marketplace-Schema kommt mit + Drizzle 0.38 — sollte mit upgradeen, idealerweise atomar. +- **Karten-Hash-Konsistenz zwischen Greenfield und Marketplace.** + Greenfield-`@cards/domain` `cardContentHash` ist die SoT. Marketplace- + `deck_cards.content_hash` muss mit demselben Algorithmus berechnet + werden — sonst funktioniert Smart-Merge nicht. Beim R2-Port-Pass + testen, dass `cardContentHash({type,fields})` aus `@cards/domain` + byte-identisch ist mit dem alten `cards-server/src/lib/hash.ts`. Wenn + nicht: alten Code anpassen, nicht `@cards/domain` brechen.