commit 8605b1b517419e21cf08127a9e8ed4d6ecc13000 Author: Till Date: Fri May 8 14:08:41 2026 +0200 Phase 0+1: Repo-Skelett für Cards-Greenfield Strategie B (beschlossen 2026-05-08): Cards wird als eigenständige föderierte App neu gebaut, ohne Code-Übernahme aus mana-monorepo. Skelett enthält: - apps/api: Hono+Bun mit /healthz, /version, Manifest-Endpoint, leere pgSchema('cards'), Drizzle-Config, erstem Vitest - apps/web: SvelteKit 2 + Svelte 5 (runes), Vite auf 3082 - packages/cards-domain: Pure-TS, CardType-Discriminated-Union, SubIndex-Granularität für Reviews, Future-CardType-Set vorbereitet - infrastructure/docker-compose.yml: Postgres 16 auf 5435 - app-manifest.json: v1.0.0, Verein-owned, beta-tier - .github/workflows/ci.yml - docs/LESSONS_FROM_MANA_MONOREPO.md (Read-Day-Output, 15 Lehren) Pre-Flight für Phase 2 (Auth-Föderation): DNS cardecky.mana.how, GitHub-Repo mana-ev/cards, Cards-App-Registrierung in mana-auth, NPM_AUTH_TOKEN für Verdaccio. Plan: mana/docs/playbooks/CARDS_GREENFIELD.md Co-Authored-By: Claude Opus 4.7 (1M context) diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e46684c --- /dev/null +++ b/.env.example @@ -0,0 +1,36 @@ +# Cards-Greenfield — Local Dev Defaults +# Copy to .env (git-ignored) and adjust as needed. + +# === API + DB === +NODE_ENV=development +DATABASE_URL=postgresql://cards:cards@localhost:5435/cards +CARDS_API_PORT=3081 +CARDS_WEB_PORT=3082 + +# === Plattform-Endpunkte === +# Lokal mit Mana-Plattform-Stack auf Default-Ports laufend +MANA_AUTH_URL=http://localhost:3001 +MANA_CREDITS_URL=http://localhost:3061 +MANA_MEDIA_URL=http://localhost:3065 +MANA_SHARE_URL=http://localhost:3072 +MANA_LINKS_URL=http://localhost:3073 +MANA_EVENTS_URL=http://localhost:3074 +MANA_MCP_URL=http://localhost:3069 +MANA_SEARCH_URL=http://localhost:3076 + +# === App-Identität (von mana-auth ausgegeben) === +# Cards-Service-Key für interne Cross-Service-Calls. +# Production: aus mana-auth.apps-Tabelle bei App-Onboarding. +CARDS_APP_SERVICE_KEY=msk_dev_change_me + +# === JWT-Validation === +# JWKS-URL für User-JWT-Verification (5-Min-Cache). +MANA_AUTH_JWKS_URL=http://localhost:3001/.well-known/jwks.json + +# === Verdaccio (für pnpm install) === +# Lokal: npm login --registry=https://pkg.mana.how +# Production: GitHub Secret +NPM_AUTH_TOKEN= + +# === MinIO (über mana-media) === +CARDS_MEDIA_BUCKET=cards-storage diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b159dd9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9.15.9 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + + - name: Setup Bun (für apps/api) + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: pnpm install --frozen-lockfile + env: + NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} + + - name: Type-check + run: pnpm run type-check + + - name: Test + run: pnpm run test + + - name: Format check + run: pnpm run format:check + + - name: Validate manifest + run: pnpm run validate:manifest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee3f08a --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +# Dependencies +node_modules/ +.pnpm-store/ + +# Build outputs +dist/ +build/ +.turbo/ +.svelte-kit/ + +# Environment files +.env +.env.local +.env.*.local +.env.secrets +!.env.development +!.env.example +!.env.secrets.example + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +logs/ +*.log +npm-debug.log* +pnpm-debug.log* + +# Testing +coverage/ +.nyc_output/ +playwright-report/ +test-results/ + +# TypeScript +*.tsbuildinfo + +# Drizzle compiled config +drizzle.config.js +drizzle.config.d.ts + +# Cache +.cache/ +.eslintcache +.prettiercache + +# Package manager locks (keep only pnpm) +package-lock.json +yarn.lock + +# Mac Mini deploy +.env.macmini +ssh-key-command.txt + +# Volumes for local docker-compose +.volumes/ +.local/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..5ce0b4c --- /dev/null +++ b/.npmrc @@ -0,0 +1,7 @@ +# Cards consumes @mana/* shared packages from the Verein's private +# Verdaccio registry (pkg.mana.how on Mac Mini). +# Local dev: run `npm login --registry=https://pkg.mana.how` once; +# the resulting ~/.npmrc token is read via $NPM_AUTH_TOKEN substitution. +# Production CI: set NPM_AUTH_TOKEN in workflow secrets. +@mana:registry=https://pkg.mana.how/ +//pkg.mana.how/:_authToken=${NPM_AUTH_TOKEN} diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..cd537ec --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,15 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "es5", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ddf62e8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,133 @@ +# CLAUDE.md — cards repo + +Guidance for Claude Code when working in this repository. + +## Was dieses Repo ist + +**Cards** — eigenständige föderierte Spaced-Repetition-App des +Vereins **mana e.V.** Greenfield-Build (Strategie B, beschlossen +2026-05-08). Cards ist eine **Peer-App** im mana-Ökosystem: redet über +mana-share/-mcp/-search/-events/-credits mit Memoro, Who und der +Mana-Successor-App, hat aber eigene DB, eigenes Frontend, eigene +Deployment-Pipeline. + +``` + HTTP/JWT ┌──────────────┐ + mana e.V. ◄─────────────── │ cards/ │ Postgres `cards` + Plattform │ (this repo) │ cardecky.mana.how + (auth/credits/share/etc.) └──────────────┘ +``` + +## Status + +**Phase 0 — Repo-Skeleton.** Vollständiger Plan in +`mana/docs/playbooks/CARDS_GREENFIELD.md` (im Plattform-Repo). +Nächste konkrete Schritte: Read-Day, dann Phase 1 (Auth-Föderation). + +## Konventionen + +- **pnpm 9.15.x**, Node 20+, Turborepo 2.x +- **Tabs** für Indent, **single quotes**, 100-col Prettier + (`.prettierrc.json` ist die Source of Truth) +- **Stack:** SvelteKit 2 + Svelte 5 (runes-only) für Web, + Hono + Bun + Drizzle für API, Postgres mit `pgSchema('cards')` +- **Drizzle 0.38 / drizzle-kit 0.30 / zod 3** — bewusst gleich wie + mana-Plattform; Migration auf 0.45/0.31/zod-4 läuft mit Plattform- + Migrations-Pfad mit (`mana/docs/MIGRATION_DRIZZLE_ZOD.md`) +- **JWT-Validation** lokal via JWKS-Cache (5 min), niemals Live-Call + zu mana-auth pro Request +- **Service-to-Service**-Calls via `X-Service-Key` (`CARDS_APP_SERVICE_KEY`) + oder mana-auth-issued Bearer-Tokens +- **Tests:** Vitest für Unit/Integration, Playwright für e2e +- **Formatierung:** `pnpm run format` vor jedem Commit + +## Architektonische Invarianten + +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. +3. **Eigene Postgres-DB `cards`** im geteilten Mana-Cluster, Schema- + Isolation via `pgSchema('cards')`. +4. **Föderation über `@mana/shared-app-tpl`.** Pflicht-Endpoints + (`/.well-known/mana-app.json`, `/healthz`, `/api/v1/share/receive`, + `/api/v1/tools/:name`, `/api/v1/search`, `/api/v1/dsgvo/...`) + kommen aus dem geteilten Boilerplate. +5. **Keine externe SaaS, wo vermeidbar.** Erlaubte Ausnahmen kommen + über mana-Plattform: Stripe (mana-credits), Anthropic/OpenAI + (mana-llm), Cloudflare-Tunnel (Edge-Routing). +6. **Encryption initial AUS.** Nachrüstbar, wenn sensibles Feld auftaucht. + +## Repo-Struktur + +``` +cards/ +├── apps/ +│ ├── web/ SvelteKit-Frontend (cardecky.mana.how) +│ └── api/ Hono+Bun-Backend +├── packages/ +│ └── cards-domain/ Pure-TS: FSRS, Card-Types, Schemas, Anki-Parser +├── infrastructure/ docker-compose, tunnel-config +├── docs/ DOMAIN.md, ANKI_IMPORT.md, LESSONS_FROM_MANA_MONOREPO.md, … +├── app-manifest.json Source of Truth für Föderation +└── .github/workflows/ ci.yml, deploy.yml +``` + +## @mana/* Konsumation + +Cards zieht alle Shared-Pakete aus Verdaccio (`pkg.mana.how`): + +- `@mana/shared-share-protocol` — Manifest, Envelope-Schemas +- `@mana/shared-app-tpl` — Pflicht-Endpoint-Boilerplate +- `@mana/shared-hono` — Auth-Middleware, Health-Routes, Error-Handler +- `@mana/shared-auth` — Client-SDK für Login-Flow +- `@mana/shared-types` — Common Types +- `@mana/shared-icons`, `@mana/shared-i18n`, `@mana/shared-theme`, + `@mana/shared-tailwind` — UI-Building-Blocks (kein Code-Duplikat + aus mana-monorepo) + +Alle als reguläre `dependencies`. Versions-Disziplin ist Klasse-A +(siehe `mana/docs/SHARED_PACKAGES.md`). + +## Wichtige Cross-Repo-Doks + +- `mana/docs/playbooks/CARDS_GREENFIELD.md` — der vollständige Plan +- `mana/docs/FEDERATION.md` — Architektur-Grundlagen +- `mana/docs/SHARE_PROTOCOL.md` — Manifest- und Envelope-Schemas +- `mana/docs/MANA_AUTH_FEDERATION.md` — App-Identitäts-Modell +- `mana/docs/SHARED_PACKAGES.md` — Versionierungs-Disziplin +- `mana/docs/PORTS.md` — Port-Allokation (cards-api: TBD, cards-web: TBD) + +## Wenn ein neuer Pflicht-Endpoint dazukommt + +1. `app-manifest.json` aktualisieren (Endpoint-Pfad, Auth-Mode) +2. Handler in `apps/api/src/routes/` schreiben +3. Tests in `apps/api/tests/` (Vitest) +4. `pnpm run validate:manifest` lokal grün +5. CI-Workflow zieht Manifest-Validation automatisch + +## Wenn ein neues Tool/Share/Accept dazukommt + +1. Schema in `packages/cards-domain/src/schemas/` definieren (zod) +2. JSON-Schema-Export für mana-mcp/mana-share generieren +3. Handler im Manifest registrieren (`tools[].name` oder `accepts[].handler`) +4. Implementierung in `apps/api/src/routes/tools.ts` bzw. + `apps/api/src/share-handlers/` +5. Test-Coverage: zod-parse + Handler-Logic getrennt + +## Lokal entwickeln + +```bash +pnpm install # @mana/* aus pkg.mana.how holen +pnpm docker:up # Lokales Postgres +pnpm db:push # Drizzle-Schema aktualisieren +pnpm dev # api + web parallel +``` + +Voraussetzungen: +- mana-Plattform-Stack lokal laufend (mana-auth, optional die + Föderations-Services für E2E-Tests) +- Verdaccio-Token in `~/.npmrc` (`npm login --registry=https://pkg.mana.how`) diff --git a/README.md b/README.md new file mode 100644 index 0000000..109a73b --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# Cards + +**Eigenständige Spaced-Repetition-App des Vereins mana e.V.** + +Cards ist eine föderierte Peer-App im mana-Ökosystem. Sie verwaltet +Karteikarten, plant Wiederholungen mit dem FSRS-Algorithmus und +empfängt Inhalte aus anderen Verein-Apps (z.B. Zitate aus Memoro, +Notizen aus Mana, Web-Schnipsel aus dem Browser-Plugin). + +→ Live (geplant): + +## Stack + +- **Frontend:** SvelteKit 2 + Svelte 5 (runes-only) +- **Backend:** Hono + Bun + Drizzle ORM +- **Datenbank:** Postgres mit Schema-Isolation (`pgSchema('cards')`) +- **Auth:** föderiert über mana-auth (EdDSA JWT, JWKS-Cache) +- **Subscriptions:** mana-credits (zentral pro Verein-Account) +- **AI-Tools:** über mana-mcp Claude Desktop / persona-runner verfügbar +- **i18n:** DE / EN / FR / ES / IT +- **Build:** Turborepo + pnpm 9 + +## Status + +Phase 0 (Repo-Skeleton) — siehe `mana/docs/playbooks/CARDS_GREENFIELD.md` +für den vollständigen Plan. + +## Lokal entwickeln + +```bash +pnpm install +pnpm docker:up # Postgres in Docker +pnpm db:push # Drizzle-Schema +pnpm dev # api + web parallel +``` + +→ API auf `http://localhost:3081`, Web auf `http://localhost:3082` (oder Vite-Dev-Default `5173`). + +Voraussetzung: Mana-Plattform-Stack (mana-auth, evtl. Föderations-Services) +muss lokal laufen, sonst greift Auth-Login nicht. + +## Lizenz + +Mana-Verein-intern, MIT (siehe `mana/docs/COMPLIANCE.md` für Details +zur Verein-Lizenzpolitik). diff --git a/app-manifest.json b/app-manifest.json new file mode 100644 index 0000000..3700a1e --- /dev/null +++ b/app-manifest.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://pkg.mana.how/@mana/shared-share-protocol/0.1/manifest.json", + "schema_version": "0.1", + "id": "cards", + "name": "Cardecky", + "version": "1.0.0", + "homepage": "https://cardecky.mana.how", + "icon": "https://cardecky.mana.how/icon-512.png", + "description": "Spaced-repetition flashcards with FSRS scheduling.", + "description_de": "Lernkarten mit Spaced-Repetition (FSRS).", + "ownership": { "kind": "verein" }, + "tier_required": "beta", + "endpoints": { + "base_url": "https://cardecky.mana.how", + "health": "/healthz", + "dsgvo_export": "/api/v1/dsgvo/export", + "search": "/api/v1/search", + "share_receive": "/api/v1/share/receive", + "tool_invoke": "/api/v1/tools/:name", + "deep_link_scheme": "cards://", + "link_patterns": [ + { + "pattern": "card/(?[a-z0-9_-]+)", + "template": "https://cardecky.mana.how/c/{id}" + }, + { + "pattern": "deck/(?[a-z0-9_-]+)", + "template": "https://cardecky.mana.how/d/{id}" + } + ] + }, + "shares": [{ "type": "mana/card", "schema_ref": "/payload-schemas/card.json" }], + "accepts": [ + { "type": "mana/quote", "handler": "create_card_from_quote" }, + { "type": "mana/url", "handler": "save_link_as_card" }, + { "type": "mana/text", "handler": "create_card_from_text" } + ], + "tools": [ + { + "name": "cards.create", + "description": "Erzeugt eine Lernkarte für den authentifizierten User.", + "input_schema": { "$ref": "/payload-schemas/create-card-input.json" }, + "output_schema": { "$ref": "/payload-schemas/card.json" }, + "auth": "user_token" + }, + { + "name": "cards.search", + "description": "Sucht in den Lernkarten des authentifizierten Users.", + "input_schema": { "$ref": "/payload-schemas/search-input.json" }, + "output_schema": { "$ref": "/payload-schemas/search-output.json" }, + "auth": "user_token" + } + ], + "search": { + "types": ["card", "deck"], + "languages": ["de", "en", "fr", "es", "it"], + "max_results": 30, + "timeout_ms": 3000 + }, + "data": { + "personal_data_categories": ["card_content", "review_history"], + "encrypted_at_rest": false, + "storage_location": "EU" + } +} diff --git a/apps/api/drizzle.config.ts b/apps/api/drizzle.config.ts new file mode 100644 index 0000000..6a23c62 --- /dev/null +++ b/apps/api/drizzle.config.ts @@ -0,0 +1,13 @@ +import type { Config } from 'drizzle-kit'; + +export default { + schema: './src/db/schema/*.ts', + out: './src/db/migrations', + dialect: 'postgresql', + dbCredentials: { + url: process.env.DATABASE_URL ?? 'postgresql://cards:cards@localhost:5435/cards', + }, + schemaFilter: ['cards'], + verbose: true, + strict: true, +} satisfies Config; diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..0b48a36 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,32 @@ +{ + "name": "@cards/api", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "Cards-API — Hono+Bun-Backend für die Greenfield-Cards-App. Spricht mana-Plattform-Services über HTTP.", + "scripts": { + "dev": "bun run --hot src/index.ts", + "start": "bun run src/index.ts", + "build": "tsc -p tsconfig.json --noEmit", + "type-check": "tsc -p tsconfig.json --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "lint": "echo 'lint configured later (eslint flat-config)'", + "clean": "rm -rf dist .turbo coverage", + "drizzle:generate": "drizzle-kit generate", + "drizzle:push": "drizzle-kit push --force", + "drizzle:studio": "drizzle-kit studio" + }, + "dependencies": { + "@cards/domain": "workspace:*", + "hono": "^4.6.0", + "drizzle-orm": "0.38", + "postgres": "^3.4.0", + "zod": "3", + "zod-to-json-schema": "^3.23.0" + }, + "devDependencies": { + "drizzle-kit": "0.30", + "vitest": "^2.1.0" + } +} diff --git a/apps/api/src/db/schema/index.ts b/apps/api/src/db/schema/index.ts new file mode 100644 index 0000000..e84f525 --- /dev/null +++ b/apps/api/src/db/schema/index.ts @@ -0,0 +1,13 @@ +// Drizzle-Schemas für die `cards`-Datenbank. +// +// Phase-3-Aufgabe (siehe CARDS_GREENFIELD.md): hier landen +// `decks`, `cards`, `reviews`, `study_sessions`, `tags`, `media_refs`, +// `import_jobs` als pgSchema('cards').table(...) Definitionen. +// +// Schema-Skizze in mana/docs/playbooks/CARDS_GREENFIELD.md §"Drizzle-Schema-Skizze". +// Card-Type-Granularität (subIndex pro Karte) aus +// docs/LESSONS_FROM_MANA_MONOREPO.md mitnehmen. + +import { pgSchema } from 'drizzle-orm/pg-core'; + +export const cardsSchema = pgSchema('cards'); diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts new file mode 100644 index 0000000..88bd064 --- /dev/null +++ b/apps/api/src/index.ts @@ -0,0 +1,26 @@ +import { Hono } from 'hono'; + +import { manifestRoute } from './routes/manifest.ts'; +import { healthRoute } from './routes/health.ts'; + +const app = new Hono(); + +app.route('/', healthRoute); +app.route('/.well-known/mana-app.json', manifestRoute); + +app.get('/', (c) => + c.json({ + app: 'cards', + version: process.env.CARDS_API_VERSION ?? '0.0.0', + see: '/.well-known/mana-app.json', + }) +); + +const port = Number(process.env.CARDS_API_PORT ?? 3081); + +console.log(`[cards-api] listening on http://localhost:${port}`); + +export default { + port, + fetch: app.fetch, +}; diff --git a/apps/api/src/routes/health.ts b/apps/api/src/routes/health.ts new file mode 100644 index 0000000..d5dff0b --- /dev/null +++ b/apps/api/src/routes/health.ts @@ -0,0 +1,27 @@ +import { Hono } from 'hono'; + +export const healthRoute = new Hono(); + +healthRoute.get('/healthz', (c) => c.json({ status: 'ok' })); + +healthRoute.get('/healthz/details', (c) => + c.json({ + status: 'ok', + app: 'cards', + version: process.env.CARDS_API_VERSION ?? '0.0.0', + uptime_s: Math.floor(process.uptime()), + mana_packages: { + // In Phase 5 mit @mana/shared-app-tpl ersetzen. + '@mana/shared-share-protocol': 'TBD', + '@mana/shared-app-tpl': 'TBD', + }, + }) +); + +healthRoute.get('/version', (c) => + c.json({ + app: 'cards', + version: process.env.CARDS_API_VERSION ?? '0.0.0', + build: process.env.CARDS_BUILD_SHA ?? 'dev', + }) +); diff --git a/apps/api/src/routes/manifest.ts b/apps/api/src/routes/manifest.ts new file mode 100644 index 0000000..75857b7 --- /dev/null +++ b/apps/api/src/routes/manifest.ts @@ -0,0 +1,7 @@ +import { Hono } from 'hono'; + +import manifest from '../../../../app-manifest.json' with { type: 'json' }; + +export const manifestRoute = new Hono(); + +manifestRoute.get('/', (c) => c.json(manifest)); diff --git a/apps/api/tests/health.test.ts b/apps/api/tests/health.test.ts new file mode 100644 index 0000000..f496e73 --- /dev/null +++ b/apps/api/tests/health.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from 'vitest'; +import { Hono } from 'hono'; +import { healthRoute } from '../src/routes/health.ts'; + +describe('health routes', () => { + const app = new Hono(); + app.route('/', healthRoute); + + it('GET /healthz returns ok', async () => { + const res = await app.request('/healthz'); + expect(res.status).toBe(200); + const body = (await res.json()) as { status: string }; + expect(body.status).toBe('ok'); + }); + + it('GET /version returns app + version + build', async () => { + const res = await app.request('/version'); + expect(res.status).toBe(200); + const body = (await res.json()) as { app: string; version: string; build: string }; + expect(body.app).toBe('cards'); + expect(body.version).toBeTruthy(); + expect(body.build).toBeTruthy(); + }); +}); diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..9825b3c --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "types": ["bun-types"], + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*", "tests/**/*", "drizzle.config.ts"], + "exclude": ["node_modules", "dist", ".turbo"] +} diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..1c478b9 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,29 @@ +{ + "name": "@cards/web", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "Cards-Web — SvelteKit 2 + Svelte 5 Frontend für cardecky.mana.how.", + "scripts": { + "dev": "vite dev --port 3082 --host", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "test": "vitest run", + "lint": "echo 'lint configured later (eslint flat-config)'", + "clean": "rm -rf .svelte-kit build .turbo" + }, + "dependencies": { + "@cards/domain": "workspace:*" + }, + "devDependencies": { + "@sveltejs/adapter-node": "^5.2.0", + "@sveltejs/kit": "^2.8.0", + "@sveltejs/vite-plugin-svelte": "^4.0.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "vite": "^5.4.0", + "vitest": "^2.1.0" + } +} diff --git a/apps/web/src/app.d.ts b/apps/web/src/app.d.ts new file mode 100644 index 0000000..743f07b --- /dev/null +++ b/apps/web/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/apps/web/src/app.html b/apps/web/src/app.html new file mode 100644 index 0000000..da72501 --- /dev/null +++ b/apps/web/src/app.html @@ -0,0 +1,13 @@ + + + + + + + Cards + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/apps/web/src/lib/index.ts b/apps/web/src/lib/index.ts new file mode 100644 index 0000000..88c24d6 --- /dev/null +++ b/apps/web/src/lib/index.ts @@ -0,0 +1,6 @@ +// Re-exports for $lib/... +// +// Phase 4: api-clients, auth, components, stores, i18n. +// Heute: Stub. + +export const APP_NAME = 'cards'; diff --git a/apps/web/src/routes/+layout.svelte b/apps/web/src/routes/+layout.svelte new file mode 100644 index 0000000..aa9bcde --- /dev/null +++ b/apps/web/src/routes/+layout.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
+ + diff --git a/apps/web/src/routes/+page.svelte b/apps/web/src/routes/+page.svelte new file mode 100644 index 0000000..f718c14 --- /dev/null +++ b/apps/web/src/routes/+page.svelte @@ -0,0 +1,16 @@ + + +

Cards

+

+ Karteikarten-App des Vereins mana e.V. + — Phase 0, Repo-Skelett. +

+

+ Plan: + CARDS_GREENFIELD.md + +

diff --git a/apps/web/static/robots.txt b/apps/web/static/robots.txt new file mode 100644 index 0000000..90c1178 --- /dev/null +++ b/apps/web/static/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Disallow: / + +# Cards ist eine private Verein-App, kein Index für Suchmaschinen. diff --git a/apps/web/svelte.config.js b/apps/web/svelte.config.js new file mode 100644 index 0000000..3082778 --- /dev/null +++ b/apps/web/svelte.config.js @@ -0,0 +1,15 @@ +import adapter from '@sveltejs/adapter-node'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter(), + alias: { + $lib: 'src/lib', + }, + }, +}; + +export default config; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..993de98 --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + }, + "include": [ + ".svelte-kit/ambient.d.ts", + ".svelte-kit/non-ambient.d.ts", + ".svelte-kit/types/**/$types.d.ts", + "vite.config.ts", + "src/**/*.js", + "src/**/*.ts", + "src/**/*.svelte", + "tests/**/*" + ], + "exclude": ["node_modules", ".svelte-kit/output", "build"] +} diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts new file mode 100644 index 0000000..42d7f26 --- /dev/null +++ b/apps/web/vite.config.ts @@ -0,0 +1,10 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()], + server: { + port: Number(process.env.CARDS_WEB_PORT ?? 3082), + host: true, + }, +}); diff --git a/docs/LESSONS_FROM_MANA_MONOREPO.md b/docs/LESSONS_FROM_MANA_MONOREPO.md new file mode 100644 index 0000000..bc9d048 --- /dev/null +++ b/docs/LESSONS_FROM_MANA_MONOREPO.md @@ -0,0 +1,203 @@ +# Lessons aus mana-monorepo für Cards-Greenfield + +**Stand:** 2026-05-08, Read-Day-Output +**Quelle:** `mana-monorepo/{packages/cards-core,apps/mana/.../modules/cards,services/cards-server}` +**Verwendung:** Architektonische Lehren für den Greenfield-Build. Kein Code wird übernommen, nur Designs. + +## TL;DR + +- **Domain-Library separieren** (`@cards/domain`, framework-agnostic) +- **Card-Type als discriminated union** mit `fields: Record` + `subIndex`-Granularität +- **FSRS via `ts-fsrs` v5.3.2** in dünnem Adapter, Reviews bleiben PLAINTEXT +- **Markdown-Editor**, kein Rich-Text/WYSIWYG +- **Keyboard-driven Study-View** (Space=Reveal, 1–4=Grade) +- **Marketplace ist separate Concern** (eigener Server in mana-monorepo, NICHT im Cards-MVP) +- **Encryption-Registry zentral planen**, MVP `enabled: false` +- **AI-Tools sind dünn** in mana-monorepo (nur Draft `create_card`); wir können sauber neu definieren + +--- + +## 1. Domain-Library als eigener Workspace-Package + +mana-monorepo hat `packages/cards-core/` mit Pure-TS: +- Card-Types, Cloze-Parser, FSRS-Wrapper, Markdown-Render-Helpers +- Keine Dexie-, Sync-, oder UI-Abhängigkeiten +- Konsumiert von App-UI-Modul UND von `services/cards-server` + +→ **Bei uns:** `packages/cards-domain/` analog. Bewusst kein `@cards/core` (Name-Konflikt), sondern `@cards/domain`. Steht. + +## 2. Card-Type-Modell + +mana-monorepo unterstützt: `'basic' | 'basic-reverse' | 'cloze' | 'type-in' | 'image-occlusion' | 'audio' | 'multiple-choice'`. + +Datenstruktur: +- `fields: Record` als generischer Slot (statt `front`/`back`-Spalten) +- Pro Type unterschiedliche Field-Sets: + - `basic`/`basic-reverse`: `{ front, back }` + - `cloze`: `{ text, extra? }` + - `type-in`: `{ question, expected }` + +**SubIndex-Granularität:** Pro Karte gibt es N Reviews (mit `subIndex`): +- `basic`: 1 Review (subIndex 0) +- `basic-reverse`: 2 Reviews (front→back UND back→front) +- `cloze`: 1 Review pro Cluster-Index (`{{c1::...}}`, `{{c2::...}}`) + +→ **Bei uns:** MVP nur `basic` + `basic-reverse`. Aber `Card`-Schema gleich für die volle CardType-Future-Union vorgesehen, damit Schema-Migration klein bleibt. SubIndex-Pattern in Reviews-Tabelle übernehmen. + +## 3. FSRS-Library + Adapter-Pattern + +- `ts-fsrs` v5.3.2 — dependency-free, Date-basierte API +- mana-monorepo wraps in `LocalCardReview` ↔ `ts-fsrs.Card` Adapter (ISO-Strings ↔ Date) +- Funktionen: `newReview()` (init), `gradeReview(review, rating)` (next state) + +→ **Bei uns:** Genau gleicher Wrapper. Phase 3. + +**Wichtige Regel:** Reviews bleiben **PLAINTEXT** in der DB, weil der Scheduler täglich auf `due <= now` quert. Encryption müsste täglich N Reviews entschlüsseln — geht nicht. mana-monorepos `crypto/registry.ts` listet `cardReviews` explizit als plaintext-allowlisted. + +→ **Bei uns:** Schema-Doku wird das erwähnen. MVP-Encryption-OFF macht das ohnehin moot, aber Future-Proofing wichtig. + +## 4. Cloze-Parser: Token-basiert, nicht Regex + +mana-monorepo hat `packages/cards-core/src/cloze.ts` mit: +- Anki-kompatible Syntax: `{{c1::answer}}` oder `{{c1::answer::hint}}` +- `tokenize()` → `Array<{ kind: 'text' | 'cluster', ... }>` +- `clusters()` gruppiert pro Cluster-Index +- `renderCloze(source, hideIndex)` → `{ front, back, answer }` + +**Warum Token-basiert:** Ein Cluster kann mehrfach im Text vorkommen (`{{c1::Berlin}} … {{c1::Berlin}}`). Beide müssen synchron geblankt werden. Token-Parser macht das trivial; Regex-Replace nicht. + +→ **Bei uns:** Cloze ist Phase 8+. Wenn implementiert: Token-basiert, Anki-kompatible Syntax. Library-Wahl: gleich wie ts-fsrs eigene mini-Library schreiben (klein), oder `@anki/cloze-parser` falls existent (TBD). + +## 5. Local-First-Stack ist nicht trivial + +mana-monorepos Cards-Modul nutzt: +- Dexie + 5 Tabellen (`cardDecks`, `cards`, `cardReviews`, `cardStudyBlocks`, `deckTags`) +- mana-sync (Go) für Server-Sync mit Field-Level-LWW +- `__fieldMeta` pro Record für Konflikt-Detection +- `_updatedAtIndex` Shadow-Column für orderBy +- Encryption-Layer mit Master-Key aus mana-auth + +**Lessons:** +- Sync-Engine ist nicht trivial (siehe `apps/mana/.../data/DATA_LAYER_AUDIT.md`) +- Quota-Recovery, Backoff, RLS, Encryption-Rollout sind ihre eigenen Features +- "Schnell mal was Local-First bauen" ist eine Illusion + +→ **Bei uns:** **Server-authoritative MVP, Local-First erst via mana-sync-Federation** (Variante III in CARDS_GREENFIELD.md). Damit überspringen wir das gesamte Komplexitäts-Paket. Wenn später Local-First nötig: Cards bekommt `appId=cards` in mana-sync, statt eigenen Stack zu bauen. + +## 6. Encryption-Registry zentral + +mana-monorepo hat `apps/mana/.../data/crypto/registry.ts`: +- Cards: `{ enabled: true, fields: ['front', 'back', 'fields'] }` (ein-Tabelle) +- CardDecks: `{ enabled: true, fields: ['name', 'description'] }` +- cardReviews + cardStudyBlocks: explizit plaintext (Performance-Gründe oben) + +Regel: Plaintext = IDs/Timestamps/Enums/Sortkeys/Indizes. Encrypt = User-Typed-Content. + +→ **Bei uns:** Encryption initial **AUS** (CARDS_GREENFIELD.md). Wenn nachgerüstet: gleiche Aufteilung. Felder-Allowlist in `packages/cards-domain/src/crypto/registry.ts` zentral, von `apps/api/src/db/...` konsumiert. + +## 7. Card-Editor: Markdown statt Rich-Text + +mana-monorepo nutzt: +- Stateless `CardFace.svelte` (`card`, `subIndex`, `showBack`, Callbacks) +- Render-Logik: + - Basic: `renderMarkdown(card.fields.front/back)` + - Cloze: `renderCloze(card.fields.text, subIndex)` + extra-hint + - Type-In: Input-Feld + case-insensitive Vergleich gegen `expected` + +**Anti-Pattern (vermieden):** Rich-Text-Editor mit Toolbar/Undo/Bold-Hotkeys. + +→ **Bei uns:** Phase 4: Markdown-Editor. Library: vermutlich `marked` + `DOMPurify`. CodeMirror oder Monaco wäre Overkill für Karten — wir brauchen kein Syntax-Highlighting. + +## 8. Study-View: Keyboard-driven + +mana-monorepos `/cards/learn/[deckId]/+page.svelte`: +- **Space/Enter:** Antwort aufdecken +- **1/2/3/4:** Grade (Again/Hard/Good/Easy → ts-fsrs Rating-Enum) +- **Queue snapshot am Session-Start** — verhindert Mid-Session-Verschiebung neu fälliger Karten +- **Skip-Logic:** Wenn `INPUT` focused, ignoriere Hotkeys + +→ **Bei uns:** Phase 4. Dasselbe Pattern. Hotkey-Handler-Helper ``-Component oder Effect mit `addEventListener`. + +## 9. Marketplace: separater Service, NICHT im MVP + +mana-monorepo hat **`services/cards-server/`** (Hono+Bun, Port 3072 — Konflikt mit unserer mana-share-Plattform!) als Marketplace-Backend: +- Tabellen: `decks` (public, slug-indexed), `deck_versions` (immutable snapshots), `deck_cards` (versionId, type, fields JSON, contentHash) +- Smart-Merge via per-card SHA-256-Hashes: Subscriber pullen neue Versionen ohne FSRS-State zu verlieren +- Routes: POST `/decks`, GET `/:slug`, POST `/:slug/publish` + +**Wichtig:** Das ist ein **eigenes Feature**, kein Teil des Cards-Frontend-Moduls. Cards-Web bringt Decks lokal, Marketplace ist read-only-Browse + Publish. + +→ **Bei uns:** **Nicht im MVP.** Phase 12+. Wenn nachgerüstet: eigenes Backend (oder dedizierter Endpoint in cards-api), nicht in MVP-Schema. Smart-Merge-Pattern (content-hash) ist ein gutes Design — übernehmen, wenn es so weit ist. + +**Decommission-Konsequenz:** Wenn Cards-Greenfield live ist, muss `mana-monorepo/services/cards-server/` ebenfalls weg (siehe CARDS_GREENFIELD.md → Decommission). Steht. + +## 10. AI-Tools sind dünn + +mana-monorepos `lib/modules/cards/tools.ts` hat: +- 1 Tool: `create_card` (name, deckId, front, back) — als Draft, NICHT in `AI_TOOL_CATALOG` registriert +- Keine `list_decks`, `get_deck_stats`, `update_card`-Tools + +→ **Bei uns:** Phase 7 entscheidet Scope. Vermutlich `cards.create` + `cards.search` für MVP. Mehr Tools wenn Persona-Runner-Use-Cases erscheinen. + +## 11. Routing-Pattern + +mana-monorepos `(app)/cards/`-Routen: +- `/cards/decks` — ListView +- `/cards/decks/[id]` — DetailView (EditName, EditDescription, EditColor, VisibilityPicker) +- `/cards/learn/[deckId]` — Study-Session-View +- `/cards/explore` — Marketplace-Browse +- `/cards/progress` — Stats, Streak, Heatmap + +→ **Bei uns:** Cards ist Standalone, nicht `(app)/cards/...`-Sub-Tree. Routing flacht aus zu `/decks`, `/decks/:id`, `/study/:deckId`. `/explore` und `/progress` sind Polish (Phase 9). + +## 12. Visibility: einfacher Enum reicht + +mana-monorepo nutzt `@mana/shared-privacy` mit `VisibilityLevel = 'private' | 'space' | 'public'`. Bei Änderungen wird `visibilityChangedAt` + `visibilityChangedBy` getrackt. + +→ **Bei uns:** Gleiches Enum als TypeScript-Type in `@cards/domain`. Audit-Felder optional ab Phase 9 (DSGVO-Polish). + +## 13. Tests mit fake-indexeddb + +mana-monorepo nutzt `fake-indexeddb` für Unit-Tests gegen Dexie ohne echte DB. + +→ **Bei uns:** **Nicht relevant** — wir sind server-authoritative, keine Dexie. Vitest + Hono `app.request()` (wie in mana-Plattform) für API-Tests, Playwright für e2e. + +## 14. Domain-Events + Analytics früh + +mana-monorepo emittiert `CardCreated` u.ä. an einen domain-event-Bus + ruft `CardsEvents.cardCreated()` für Analytics. + +→ **Bei uns:** Domain-Events gehen über mana-events (`card.created`, `card.studied`, `deck.completed`). Analytics ggf. später. Phase 5 macht die Event-Anbindung. + +## 15. Hash-Everything-Early + +mana-monorepos Marketplace hat content-hash auf jeder Karte + jedem Deck-Version-Snapshot. Ermöglicht Smart-Merge ohne Diffing. + +→ **Bei uns:** Hash-Spalte auf `cards` und `decks` schon im MVP-Schema einplanen, auch wenn Marketplace nicht da ist. Cheap to add, expensive to retrofit. + +--- + +## Anti-Patterns aus mana-monorepo, die wir vermeiden + +- **Rich-Text-Editor mit Toolbar:** Markdown reicht für Karten-Inhalte +- **Real-time-Collaboration:** Cards sind privat, async-Sync genügt +- **Geteilte Dexie für 27 Module:** entfällt, Cards hat eigene Postgres +- **`updatedAt` als synced data field:** mana-monorepo musste das nachträglich auf `__fieldMeta`-deriviert umbauen (siehe `mana-monorepo/CLAUDE.md` §"Conflict-Detection 2026-04-26"). **Wir machen es ab Tag 1 richtig:** `updatedAt` wird beim Read aus `max(__fieldMeta[*].at)` deriviert, falls wir je eine ähnliche Architektur brauchen — oder bleibt einfach eine normale Column im Server-Modell. (MVP: Column reicht.) + +## 5 Kern-Entscheidungen für unser Greenfield + +1. **`@cards/domain` als Pure-TS-Workspace-Package** — keine Framework-Bindings +2. **Card-Type discriminated union mit `fields`-Slot + `subIndex`-Granularität** — auch wenn MVP nur basic-Karten hat +3. **`ts-fsrs` v5.3.2 hinter dünnem Adapter** — Reviews plaintext, indiziert auf `due` +4. **Cloze als Token-Parser, wenn implementiert** — Anki-kompatible Syntax +5. **Encryption planen, aber MVP-OFF** — Felder-Allowlist von Tag 1 vorsehen + +## Was nicht aus mana-monorepo kommt + +- Marketplace-Backend (eigenständige Phase 12+) +- Local-First-Sync-Stack (mana-sync-Federation oder gar nicht) +- Mobile-App (PWA reicht) +- Komplexe AI-Workbench-Integration + +--- + +**Verbindlich:** Phase 1 (Repo-Skelett) ist abgeschlossen. Phase 0 (dieser Read-Day) ist mit diesem Dokument erledigt. Phase 2 (Auth-Föderation) ist als Nächstes dran. diff --git a/infrastructure/docker-compose.yml b/infrastructure/docker-compose.yml new file mode 100644 index 0000000..0081417 --- /dev/null +++ b/infrastructure/docker-compose.yml @@ -0,0 +1,28 @@ +# Lokales Cards-Dev-Setup. Postgres-Container auf 5435, +# damit der mana-Plattform-Stack auf 5432 ungestört weiterläuft. +# +# Start: pnpm docker:up (vom Repo-Root) +# Logs: pnpm docker:logs +# Stop: pnpm docker:down +# +# Daten persistieren in `infrastructure/.volumes/cards-postgres`, +# git-ignored. + +services: + cards-postgres: + image: postgres:16-alpine + container_name: cards-postgres + restart: unless-stopped + environment: + POSTGRES_USER: cards + POSTGRES_PASSWORD: cards + POSTGRES_DB: cards + ports: + - '5435:5432' + volumes: + - ./.volumes/cards-postgres:/var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U cards -d cards'] + interval: 5s + timeout: 3s + retries: 10 diff --git a/package.json b/package.json new file mode 100644 index 0000000..6d581c2 --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "cards", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "Cards — eigenständige föderierte Spaced-Repetition-App des mana e.V. Greenfield-Build (Strategie B), redet über mana-share/-mcp/-search/-events mit den anderen Verein-Apps.", + "packageManager": "pnpm@9.15.9", + "engines": { + "node": ">=20", + "pnpm": "^9" + }, + "scripts": { + "dev": "turbo run dev", + "build": "turbo run build", + "test": "turbo run test", + "lint": "turbo run lint", + "type-check": "turbo run type-check", + "clean": "turbo run clean", + "format": "prettier --config .prettierrc.json --write \"**/*.{ts,tsx,svelte,js,jsx,json,md}\"", + "format:check": "prettier --config .prettierrc.json --check \"**/*.{ts,tsx,svelte,js,jsx,json,md}\"", + "validate:manifest": "pnpm exec validate-manifest app-manifest.json", + "validate:fast": "pnpm run type-check && pnpm run validate:manifest", + "validate:all": "pnpm run type-check && pnpm run validate:manifest && pnpm run test", + "docker:up": "docker compose -f infrastructure/docker-compose.yml up -d", + "docker:down": "docker compose -f infrastructure/docker-compose.yml down", + "docker:logs": "docker compose -f infrastructure/docker-compose.yml logs -f", + "db:push": "pnpm --filter @cards/api drizzle:push" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^22.10.2", + "prettier": "^3.3.3", + "prettier-plugin-svelte": "^3.2.6", + "turbo": "^2.3.0", + "typescript": "^5.6.3" + } +} diff --git a/packages/cards-domain/package.json b/packages/cards-domain/package.json new file mode 100644 index 0000000..c2d8412 --- /dev/null +++ b/packages/cards-domain/package.json @@ -0,0 +1,31 @@ +{ + "name": "@cards/domain", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "Cards-Domain — Pure-TS-Modell: Card-Types, FSRS-Adapter, Cloze-Parser, Anki-Import-Helpers, zod-Schemas. Keine DB-, Framework- oder Hono/SvelteKit-Abhängigkeiten.", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts", + "./types": "./src/types.ts", + "./fsrs": "./src/fsrs.ts", + "./cloze": "./src/cloze.ts", + "./schemas": "./src/schemas/index.ts" + }, + "scripts": { + "build": "tsc -p tsconfig.json --noEmit", + "type-check": "tsc -p tsconfig.json --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "lint": "echo 'lint configured later'", + "clean": "rm -rf dist .turbo coverage" + }, + "dependencies": { + "ts-fsrs": "^5.3.2", + "zod": "3" + }, + "devDependencies": { + "vitest": "^2.1.0" + } +} diff --git a/packages/cards-domain/src/index.ts b/packages/cards-domain/src/index.ts new file mode 100644 index 0000000..e2a9367 --- /dev/null +++ b/packages/cards-domain/src/index.ts @@ -0,0 +1,8 @@ +// @cards/domain — public API +// +// Pure-TS, keine DB/Framework-Abhängigkeiten. Wird vom apps/api +// (Drizzle-Schemas) und apps/web (UI) gleichermaßen konsumiert. + +export * from './types.ts'; +// export * from './fsrs.ts'; // Phase 3: FSRS-Adapter (ts-fsrs) +// export * from './cloze.ts'; // Phase 8 oder später: Cloze-Parser diff --git a/packages/cards-domain/src/schemas/index.ts b/packages/cards-domain/src/schemas/index.ts new file mode 100644 index 0000000..af4d78b --- /dev/null +++ b/packages/cards-domain/src/schemas/index.ts @@ -0,0 +1,7 @@ +// Phase-3-Aufgabe: zod-Schemas für API-Inputs/Outputs. +// +// Konvention: ein zod-Schema pro Domain-Type (DeckSchema, CardSchema, +// ReviewSchema). API-Routen rufen `Schema.parse()` für Input-Validation, +// und `zod-to-json-schema` generiert die JSON-Schemas für mana-mcp/-share. + +export {}; diff --git a/packages/cards-domain/src/types.ts b/packages/cards-domain/src/types.ts new file mode 100644 index 0000000..6d5d4b2 --- /dev/null +++ b/packages/cards-domain/src/types.ts @@ -0,0 +1,87 @@ +// Domain-Typen für Cards. +// +// Modellierung folgt den Lessons aus mana-monorepo +// (siehe docs/LESSONS_FROM_MANA_MONOREPO.md): +// +// - CardType ist eine discriminated union. +// - Card hat `fields: Record` als generischen Slot. +// - Pro Karte gibt es N Reviews mit `subIndex` (basic = 1, basic-reverse = 2, +// cloze = 1 pro Cluster). +// - cardReviews bleiben PLAINTEXT, weil der Scheduler täglich auf `due` +// filtert. + +/** Phase-1-MVP-Set; Cloze + Image-Occlusion in Phase 8+. */ +export type CardType = 'basic' | 'basic-reverse'; + +/** Voll geplantes Set (für Schemas vorbereitet, MVP nicht alle implementiert). */ +export type CardTypeFuture = + | 'basic' + | 'basic-reverse' + | 'cloze' + | 'type-in' + | 'image-occlusion' + | 'audio' + | 'multiple-choice'; + +export type CardFields = Record; + +export type Deck = { + id: string; + user_id: string; + name: string; + description?: string; + color?: string; + visibility: 'private' | 'space' | 'public'; + fsrs_settings: FsrsSettings; + created_at: string; + updated_at: string; +}; + +export type Card = { + id: string; + deck_id: string; + user_id: string; + type: CardType; + fields: CardFields; + tags: string[]; + media_refs: string[]; + created_at: string; + updated_at: string; +}; + +/** + * Pro `(card_id, sub_index)` ein Review-Eintrag. Der Scheduler quert auf + * `due <= now` täglich — das Feld bleibt deshalb plaintext und ist indiziert. + */ +export type Review = { + card_id: string; + sub_index: number; + user_id: string; + due: string; + stability: number; + difficulty: number; + elapsed_days: number; + scheduled_days: number; + reps: number; + lapses: number; + state: 'new' | 'learning' | 'review' | 'relearning'; + last_review: string | null; +}; + +export type StudySession = { + id: string; + user_id: string; + deck_id: string; + started_at: string; + finished_at: string | null; + cards_reviewed: number; + cards_correct: number; +}; + +/** Default-Konstanten aus ts-fsrs; per-Deck-Overrides möglich. */ +export type FsrsSettings = { + requestRetention?: number; + maximumInterval?: number; + w?: number[]; + enableFuzz?: boolean; +}; diff --git a/packages/cards-domain/tsconfig.json b/packages/cards-domain/tsconfig.json new file mode 100644 index 0000000..398b130 --- /dev/null +++ b/packages/cards-domain/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*", "tests/**/*"], + "exclude": ["node_modules", "dist", ".turbo"] +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..0a5ab0e --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,11 @@ +packages: + - 'apps/*' + - 'packages/*' + +# Akzeptierte Peer-Drift mit Mana-Plattform-Stack (siehe mana/pnpm-workspace.yaml). +# Cards startet auf gleichem Stand und migriert mit der Plattform mit. +peerDependencyRules: + allowedVersions: + drizzle-orm: '0.38' + drizzle-kit: '0.30' + zod: '3' diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..ca2dd03 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ESNext"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": false + } +} diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..0878f61 --- /dev/null +++ b/turbo.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://turbo.build/schema.json", + "concurrency": "5", + "globalEnv": [ + "NODE_ENV", + "DATABASE_URL", + "MANA_AUTH_URL", + "MANA_SHARE_URL", + "MANA_LINKS_URL", + "MANA_MCP_URL", + "MANA_SEARCH_URL", + "MANA_EVENTS_URL", + "MANA_CREDITS_URL", + "MANA_MEDIA_URL", + "MANA_SERVICE_KEY", + "CARDS_APP_SERVICE_KEY" + ], + "tasks": { + "dev": { + "cache": false, + "persistent": true + }, + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", "build/**", ".svelte-kit/**"] + }, + "lint": { + "dependsOn": ["^lint"], + "outputs": [] + }, + "type-check": { + "dependsOn": ["^type-check", "^build"], + "outputs": [] + }, + "test": { + "dependsOn": ["^build"], + "outputs": ["coverage/**"] + }, + "clean": { + "cache": false + } + } +}