Commit graph

35 commits

Author SHA1 Message Date
Till JS
5859e202c5 feat(cards): deck management UI + production auth portal wiring
Deck schema, API routes, and SvelteKit UI for creating and browsing decks
(DeckStack component, inline creation, floating nav). Production compose
updated with PUBLIC_AUTH_WEB_URL so cards-web redirects to auth.mana.how
for login/register instead of the raw API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:50:27 +02:00
Till JS
3a4523da3e feat(web): UI-Overhaul — Mobile-Nav, Sprachauswahl, 5 Sprachen, Stats-Karten
Mobile-Nav scrollt horizontal und ist auf der Login-Seite ausgeblendet.
Nav-Innere Container entfernt (PillTabGroup → flache Buttons). Sprachauswahl
von der Nav auf die Account-Page verschoben (eigene Karte mit Vollnamen,
vertikales Layout). 5 Locales: DE, EN, FR, IT, ES mit vollständigen
Übersetzungen. Account-Karte erlaubt Namensbearbeitung. Stats-Page komplett
auf Card-Aesthetic umgebaut (ChartBar, Fire, Brain, CalendarDots, Target,
CalendarCheck — keine Emojis). Zwei neue Stats-Karten: Retention-Rate
(lapses/reps) und Fälligkeitsvorschau (nächste 7 Tage). API um
retention_rate, retention_reps, retention_lapses, due_forecast erweitert.
84-Tage-Activity-Grid hinzugefügt. TS-Fehler aus Locale-Erweiterung behoben
(ClozeCardForm number[], decks/new + NewDeckCard Locale-Typ).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 14:20:01 +02:00
Till JS
578a0a41f7 Marketplace-UX: Subscribe=Fork, Deck-Settings-Page, Duplicate/Delete
**Subscribe vereinfacht zu einer Aktion:**
- POST /marketplace/decks/:slug/subscribe forkt automatisch wenn noch
  kein privater Fork für diesen User+Deck existiert (forkDeckForUser
  aus fork.ts extrahiert und von subscriptions.ts importiert).
- GET /subscribe gibt jetzt auch private_deck_id zurück.
- Fork-Button auf /d/[slug] entfernt; stattdessen:
  - Nicht abonniert: "Zu meiner Bibliothek hinzufügen" → subscribe+fork+navigate
  - Abonniert: "✓ In meiner Bibliothek →" Link + "Abo kündigen" Button
- Abonnierte Decks auf /decks (Homepage) navigieren zu /study/{id}
  wenn ein Fork existiert (slug→studyHref via $derived cross-reference).

**Deck-Settings-Page (/decks/[id]/edit) komplett neu:**
- Allgemein: Name, Beschreibung, Farbe, Kategorie-Picker, Sichtbarkeit
- Marketplace (nur für Forks): Link zum Original, Update-ziehen-Banner
- Gefahrenzone: Duplizieren (neue Kopie ohne FSRS-Verlauf) + Löschen

**Neue Backend-Endpoints (apps/api/src/routes/decks.ts):**
- GET /decks/:id/marketplace-source → { slug } des Marketplace-Originals
- POST /decks/:id/duplicate → kopiert Deck + Karten, neues visibility=private

**Domain-Schema:**
- Deck-Schema um forked_from_marketplace_deck_id/_version_id erweitert
  (Backend sendet sie bereits, waren untyped im Frontend).

**Komponenten:**
- MarketplaceDeckStack: optionaler href-Prop überschreibt /d/{slug}
- DeckListGrid: optionaler getHref-Prop gibt href per Slug zurück

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 14:03:49 +02:00
Till JS
26b136a42c test(api): Unit-Tests für makeInitialReviewRows und fetchUrlContent
Some checks are pending
CI / validate (push) Waiting to run
- lib-reviews.test.ts: 5 Tests — subIndex-Count, userId/cardId, leere
  Input-Liste, Initialzustand (reps=0, lapses=0), due ist Date
- lib-url-fetch.test.ts: 6 Tests — mana-search Pfad, Fallback auf
  direktes Fetch, HTML-Stripping, Network-Fehler, leerer Content,
  Truncation auf 8000 Zeichen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:40:30 +02:00
Till JS
dc382a795d feat(api): URL-Kontext auch in /decks/generate + fetchUrlContent extrahieren
- `lib/url-fetch.ts`: fetchUrlContent aus decks-from-image herausgezogen
  — gemeinsam genutzte Logik für mana-search + direktes HTTP-Fetch-Fallback
- `decks-generate.ts`: optionales `url`-Feld im Input-Schema;
  URL-Inhalt wird an den Prompt angehängt wenn vorhanden
- `decks.ts` (web): `generateDeck()` akzeptiert jetzt `url?: string`
- UI: imageUrl wird für Text-KI + Bild-KI als Kontext genutzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:39:39 +02:00
Till JS
c39bacc971 refactor(api): DTO-Helper extrahieren + N+1 in marketplace/decks beheben
- `lib/dto.ts`: `toDeckDto` und `toCardDto` aus routes/decks.ts und
  routes/cards.ts extrahiert — testbar, zentrale Output-Shape-Doku
- `lib/marketplace/dto.ts`: `toPublicDeckDto`, `toOwnerDto`, `toVersionDto`
  aus routes/marketplace/decks.ts extrahiert
- `GET /:slug` in marketplace/decks.ts: Version + Owner parallel per
  `Promise.all` statt sequenziell (2 RTT → 1 RTT)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:30:29 +02:00
Till JS
b182bac2fb refactor(api): review-row-Erstellung extrahieren + QW-Fixes
- makeInitialReviewRows() in lib/reviews.ts: eliminiert 45 Zeilen
  Duplikat aus cards.ts, decks-generate.ts und tools.ts
- /distractors: Query-Param cardId → card_id (snake_case-Konsistenz)
- cards/new: Image-Occlusion-Preview zeigt hochgeladenes Bild statt
  statischen Platzhalter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:12:28 +02:00
Till JS
1f1abf3c4f feat(decks/from-image): URL-Input als Alternative zu Datei-Upload
- API: fetchUrlContent() via mana-search /api/v1/extract (Fallback: direktes Fetch)
- URL-Inhalt wird als Kontext an die LLM-Karten-Generierung übergeben
- Client: url-only Flow sendet JSON statt FormData (Bun-Kompatibilität)
- Deck-Neu-Seite: URL-Eingabefeld neben dem Datei-Upload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:00:04 +02:00
Till JS
2b36990e43 feat(cards): multiple-choice Card-Type mit dynamischen Distractors
- CardTypeSchema: 'multiple-choice' (Felder: front + answer, distractor_pool optional)
- subIndexCount: 'multiple-choice' → 1
- GET /api/v1/decks/:deckId/distractors: N zufällige Feldwerte anderer Karten
  im Deck; field-Allowlist (front/back/answer/question); RANDOM() ORDER; Fallback
  auf distractor_pool wenn Deck < 4 Karten
- fetchDistractors(): Frontend-Client-Funktion
- MultipleChoiceView.svelte: lädt Distractors on mount, shuffelt 4 Optionen,
  zeigt Sofort-Feedback (correct/wrong/neutral), Keyboard 1–4 + Space;
  auto-grade correct→good, wrong→again
- Study-Page: isMultipleChoice + multipleChoiceData derived, Action-Bar
  ausgeblendet, onKey delegiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 15:28:37 +02:00
Till JS
1212b62613 feat(cards): Deck-Generierung aus Bildern und PDFs via Vision-LLM
Neuer Endpoint POST /api/v1/decks/from-image akzeptiert bis zu 5 Bilder
(PNG/JPG/WebP, max 10 MiB je) oder PDFs (max 30 MiB je) als multipart/form-data.
Alle Dateien werden in einem einzigen mana-llm Vision-Call verarbeitet
(mana/vision → llava → Gemini 2.5-flash → GPT-4o Fallback-Chain).

PDFs werden von Gemini nativ verstanden (Layout, Tabellen, Bilder im Dokument)
ohne Zwischenschritt über Text-Extraktion oder Rendering. Der google.py-Provider
reicht den MIME-Type aus dem data:-URI direkt an types.Part.from_bytes() weiter.

- llm-client: chatVisionJson() mit images[]-Array (mehrere Bilder/Dokumente)
- decks-generate: GeneratedDeckSchema + insertGeneratedDeck() exportiert
- decks-from-image: neuer Route-Handler, MIME-Filter für image/* + application/pdf
- index: neue Route gemountet
- client.ts: apiForm() für multipart-Uploads ohne JSON.stringify
- decks.ts: generateDeckFromImage(files, opts)
- NewDeckCard + /decks/new: Dropzone mit Multi-File, Thumbnail-Strip, PDF-Icon

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 15:21:35 +02:00
Till JS
7bf61315b5 feat(decks): Deck-Kategorien über den ganzen Stack
Some checks are pending
CI / validate (push) Waiting to run
- cards-domain: DECK_CATEGORY_IDS, Labels, DeckCategorySchema,
  category-Feld im DeckSchema
- DB-Schema (decks + marketplace/decks): category-Spalte
- API-Routen: category in create/update/list/explore
- Web: DeckCategoryIcon-Komponente, Kategorie-Picker auf Deck-Detail,
  Kategorie-Icon in DeckListGrid (Marketplace)
- Layout: Bottom-Padding für floating Nav-Bar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 20:24:47 +02:00
Till JS
17871ba2a4 Phase 12 G1-G4: Marketplace-Polish — svelte-ignore + Skeleton/Empty-State + Server-Filter + Owner-Info
G1 — svelte-ignore für 5 benigne Init-Capture-Warnings:
- PublishVersionModal: state(latestSemver ? bumpMinor(latestSemver) : '1.0.0')
  ist intentional, weil das Modal pro Click frisch gemountet wird
- SuggestEditModal: state(card.fields.front…) + state({ ...card.fields })
  gleicher Lebenszyklus
Kein Refactor auf $derived, weil das die Bind-Semantik kaputtmachen
würde — Direktive plus ein Kommentar reicht.

G2 — Loading + Empty-States:
- Neue Components SkeletonGrid + EmptyState in lib/components/marketplace/
- /explore: SkeletonGrid statt „Lade Featured + Trending…"-String,
  EmptyState wenn weder Featured noch Trending da
- /me/subscribed + /me/forks: EmptyState statt inline-Box
- Konsistentes Vereins-Vokabular (icon + Title + Description + CTA)

G3 — Server-side Fork-Filter:
- GET /api/v1/decks akzeptiert ?forked_from_marketplace=true
- Drizzle isNotNull-Filter auf decks.forked_from_marketplace_deck_id
- toDeckDto exposed jetzt forked_from_marketplace_{deck,version}_id
  (vorher schwiegen die Spalten, mussten client-side via Cast
  rausgefischt werden)
- /me/forks ruft listDecks({ forkedFromMarketplace: true }) statt
  listDecks() + client-side Filter

G4 — Owner-Author-Info im Deck-Detail-Endpoint:
- GET /api/v1/marketplace/decks/:slug returned jetzt zusätzlich
  owner: { slug, display_name, verified_mana, verified_community,
  pseudonym } — gejoint aus marketplace.authors via deck.owner_user_id
- toOwnerDto-Helper, identisches Shape wie in /authors/:slug
- /d/[slug] verbraucht den neuen owner-Block für AuthorBadge mit
  echtem Profil-Link statt user_id-Slice (vorher: kaputter Link
  /u/<empty-slug> + nur „SEAiKLkPZ…" als Display-Name)

Verifikation:
- API: type-check + 89 Tests grün
- Web: svelte-check 0 errors, 0 warnings (von 5 → 0)
- Live-Smoke: GET /marketplace/decks/r5-stoa-grundlagen liefert
  owner={slug:'cardecky', display_name:'Cardecky', verified_*:false}
- ?forked_from_marketplace=true Filter mit Till's JWT liefert 0
  (weil Till keine Forks hat) — 401 ohne JWT bestätigt

Bewusst nicht angefasst: Header-Nav-Link (WIP-Konflikt), Image-
Occlusion in Marketplace (Player-Side komplex), Auth-Guard im
+layout.svelte (page-level guards reichen), Anki-Import→Marketplace-
Publish-Hook (eigene Welle).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:14:21 +02:00
Till JS
92a1d5804f Phase 12 R4: Marketplace ε — Pull-Requests + Card-Discussions
Pull-Requests (Diff-Modell add/modify/remove, GitHub-style 3-way
merge in der DB-Transaktion):

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

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

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

Helpers:

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

Bug-Fix:

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

Verifikation:

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:50:16 +02:00
Till JS
d45f1c0079 Phase 12 R3: Marketplace γ + δ — Discovery + Engagement + Subscribe + Smart-Merge
Routes (additiv unter /api/v1/marketplace/*):

Discovery (optional-auth, anonymer Read erlaubt):
- GET /explore — featured + trending side-by-side
- GET /decks — browse mit q/tag/language/author/sort/limit/offset
  (sort: recent | popular | trending; trending = star-velocity 7d)
- GET /tags — flacher Tag-Tree

Engagement (auth pro Schreib-Route, optional-auth für GET state):
- POST/DELETE/GET /decks/:slug/star
- POST/DELETE/GET /authors/:slug/follow (cannot-follow-self → 409)

Subscribe + Version-Read + Smart-Merge-Diff:
- POST/DELETE/GET /decks/:slug/subscribe
- GET /me/subscriptions (mit update_available-Indicator)
- GET /decks/:slug/versions/:semver — voller Cards-Payload in ord-
  Reihenfolge
- GET /decks/:slug/diff?from=:semver — computeDiff (added/changed/
  removed/unchanged) basierend auf content_hash + ord-Heuristik für
  "changed an gleicher Position"

Fork + Smart-Merge-Pull (auth):
- POST /decks/:slug/fork — kopiert latest version in privaten
  cards.decks (forked_from_marketplace_* gesetzt) + cards.cards mit
  übernommenem content_hash + frische FSRS-Reviews
- POST /private/:deckId/pull-update — Smart-Merge: existing private
  hashes deduplizieren, nur added/changed cards einfügen (mit fresh
  reviews), unveränderte Karten BEHALTEN inkl. FSRS-State, removed
  cards bleiben lokal (server-authoritative User-Choice). Update der
  forked_from_marketplace_version_id auf latest.

Schema (R3a):
- cards.decks: 2 neue Columns forked_from_marketplace_deck_id +
  forked_from_marketplace_version_id (text, nullable). Drizzle-push
  grün.

Architektur-Highlights:
- @cards/domain.cardContentHash ist die single source of truth für
  Karten-Hashing; marketplace.deck_cards und cards.cards berechnen
  identisch → Smart-Merge ist hash-equality + INSERT-IGNORE statt
  Diff-Replay
- pgSchema-Trennung (marketplace.* vs. cards.*) zahlt sich aus:
  Marketplace-Read-Path (Public + Engagement) und privater Lern-Pfad
  haben separate FK-Welten und können unabhängig versioniert werden
- Hono-Middleware-Pattern: per-route authMiddleware/optionalAuth statt
  Sub-Router-Mount, weil ein Wildcard '*' auf einem Sub-Router via
  r.route('/', sub) sonst die Public-GET-Routes des Parents fängt
  (Hono-Routing-Subtilität, kostete eine Smoke-Iteration)

Verifikation:
- type-check 0 errors
- 6 neue Diff-Heuristik-Tests, 78 gesamt grün
- End-to-End-Smoke gegen lokale cards-api:
  · Cardecky-Author + Deck `r3-stoische-grundbegriffe` v1.0.0 (3 Karten)
  · Till browst (anon → 200), starred, folgt Cardecky, subscribed
  · Till forkt → privates Deck mit 3 Karten + 3 fresh FSRS-Reviews
  · SQL-Manipulation: Apatheia-Review auf state='review',
    stability=10, reps=3 (simuliert "schon gelernt")
  · Cardecky publisht v1.1.0: Apatheia + Eudaimonia unverändert,
    Logos präzisiert (changed), Tugendlehre neu (added)
  · Diff-Endpoint zeigt: unchanged=2, changed=1, added=1, removed=0
  · Till pull-update → cards_inserted=2 (changed.next + added)
  · Verifikation: card_count=5 (war 3), Apatheia-Review **identisch
    erhalten** (state=review, stability=10, reps=3, last_review IS
    NOT NULL), neue Karten state=new — FSRS-State der unveränderten
    Karte überlebt Smart-Merge unverletzt

Verbleibend: R4 ε (PRs + Card-Discussions), R5 Frontend-Routes
(/explore, /d/[slug], /u/[slug], /me/subscribed, /me/forks), R6
voller UI-E2E.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:27:39 +02:00
Till JS
7dbbf63523 Phase 12 R2: Marketplace-Backend α + β — Authors + Deck-Init + Publish
Routes (additiv unter /api/v1/marketplace/*):
- POST/GET /authors/me — eigenes Author-Profil anlegen/updaten/lesen
- GET /authors/:slug — public Profile-Lookup (banned-reason gestrippt)
- POST /decks — Deck-Init (Slug-Validation + Pflicht-Author-Profil +
  CHECK auf paid + Pro-License)
- POST /decks/:slug/publish — Versions-Snapshot mit per-Karte
  cardContentHash aus @cards/domain, per-Version-Hash, AI-Mod-Stub-Log,
  atomarer latest_version_id-Bump in Drizzle-Transaction
- PATCH /decks/:slug — Metadaten-Update (Owner-Only)
- GET /decks/:slug — Public-Detail mit optional-auth-Middleware

Geport aus cards-decommission-base:services/cards-server/, mit
Greenfield-Anpassungen:
- Hashing über @cards/domain.cardContentHash (gemeinsame SoT
  zwischen privatem cards.cards und marketplace.deck_cards), per-
  Version-Hash als SHA-256 über sortierte Karten-Hashes mit Ord-Prefix
- AI-Moderation als R2-Stub (pass+rationale+model='stub'),
  echte mana-llm-Anbindung in späterer Welle
- Auth-Middleware-Shape an Greenfield (userId/tier/authMode in
  c.get(...) statt user-Object), optional-auth als Schwester für
  anonymen Public-Read
- Hono-typing: outer Marketplace-Decks-Router ist Partial<AuthVars>
  weil Public-GET kein JWT braucht; Auth-Subroute ist strict

Lese-Referenz:
- 3331 LOC altes cards-server-Code (routes, services, middleware,
  lib) unter docs/marketplace/archive/code/ archiviert. Read-only,
  nicht im Build-Path.

Verifikation:
- 16 neue Vitest-Tests (Slug + Version-Hash), 72 gesamt grün
- type-check 0 errors
- E2E-Smoke gegen lokale cards-api: Cardecky-Author + Deck
  r2-stoische-ethik mit 3 Karten v1.0.0 (basic + basic + cloze),
  per-Karten-Hashes geschrieben, ai_moderation_log-Row da, semver-409
  + paid-422-Errors verifiziert. Smoke-Daten danach aufgeräumt.

Verbleibend für R3+: Discovery (explore + search), Engagement (stars/
subscribe/fork), Smart-Merge mit FSRS-State-Erhalt; danach R4 PRs +
Card-Discussions, R5 Frontend-Routes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:13:58 +02:00
Till JS
9a7068dd19 Phase 12 R0+R1: Marketplace-Restore-Plan + Schema in marketplace-pgSchema
R0 (Doku):
- Archiv unter docs/marketplace/archive/ aus managarten-Tag
  cards-decommission-base: MARKETPLACE_PLAN (654 Z., Vollvision mit
  mana-credits-Flow, Anti-Patterns), COMPETITORS, GUIDELINES,
  cards-server_CLAUDE.
- docs/playbooks/MARKETPLACE_RESTORE.md mit Schema-Naming-Entscheidung
  (eigenes marketplace-pgSchema), Wellen R0-R6, Cardecky-Skill-
  Integration, Lizenz-Modell.
- CLAUDE.md Invariante 2: Strategie-B gilt nur für Study-/FSRS-/Sync-
  Schicht; Marketplace-Restore ist explizite Ausnahme.
- STATUS.md: Phase 12 R0+R1 durch.

R1 (Schema):
- 16 Tabellen + 5 Enums im neuen marketplace-pgSchema (authors,
  decks, deck_versions, deck_cards, tag_definitions, deck_tags,
  deck_stars, deck_subscriptions, deck_forks, deck_pull_requests,
  card_discussions, deck_reports, ai_moderation_log, deck_purchases,
  author_payouts, author_follows).
- drizzle.config.ts: schemaFilter ['cards', 'marketplace'].
- Greenfield cards-pgSchema unangetastet.
- DB-CHECK decks_price_requires_license verifiziert (paid Deck mit
  CC-BY wirft sauber ab).
- type-check + 56 API-Tests grün, drizzle-kit push idempotent.

Decks dormant (kein Code-Pfad ruft die Tabellen). R2 (Backend α/β:
Author-Profile + Publish + AI-Mod) als nächstes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:05:22 +02:00
Till JS
f11df63e7b Phase 9m: KI-Deck-Generation via mana-llm
Some checks are pending
CI / validate (push) Waiting to run
Server-side AI-Pfad mit atomischer Deck+Cards-Erzeugung:

  POST /api/v1/decks/generate
    body { prompt, language?: 'de'|'en', count?: 3..40 }
  → ruft mana-llm /v1/chat/completions mit `mana/structured`-Alias
    (JSON-Output, hartes zod-Schema)
  → SystemPrompt fixiert das Output-Format (deck_name + cards mit
    front/back), verbietet HTML/Code-Fences, akzeptiert Markdown
  → Validation: zod-strict, halluzinations-resilient
  → Insert: Deck + alle Karten + Reviews in einer DB-Transaction,
    contentHash beim Insert geschrieben (Phase-9j-konform)
  → 502 wenn LLM Schema bricht oder Endpoint timeoutet (90s cap)

Frontend:
  - Neue Route /decks/new-ai mit Prompt-Form, Anzahl-Karten-Slider
    (3-40), Sprach-Wähler (DE/EN, default = aktuelle UI-Sprache).
  - 5 klickbare Beispiel-Prompts als Inspiration.
  - busy-State zeigt "10-60s typisch" (Disclaimer für die LLM-Latenz).
  - " KI-Deck"-Button neben "Neues Deck" auf /decks.
  - error-Display mit role=alert.

apps/api/src/services/llm-client.ts kapselt den Aufruf:
  - mana/structured als Alias (Routing-Layer wählt Provider)
  - response_format json_object
  - 90s-Timeout per AbortController
  - LlmError mit status + body für saubere 502-Mapping
  - Optional CARDS_LLM_API_KEY-Env (für später, wenn mana-llm
    GPU_API_KEY enforce'd)

Auth: aktuell User-JWT via authMiddleware. Tier-Gating bewusst
nicht aktiv — Cards-MVP ist tier-frei. Wenn AI-Generation Credits
kosten soll, kommt requireTier('beta') + creditsClient.reserve()
davor (Phase-6-Plumbing ist da, ein-Liner-Aktivierung).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:10:52 +02:00
Till JS
d7c7c9772e Phase 7a: cards.create-Tool für Cloze + Image-Occlusion + content_hash
Some checks are pending
CI / validate (push) Waiting to run
Tool-Pfad in /api/v1/tools/cards.create war nicht konvergent zum
REST-Pfad in /api/v1/cards POST:
  - subIndexCount(type) crashte bei type='cloze' und 'image-occlusion'
    (beide werfen seit Sprint 8a/9l, weil Sub-Index-Anzahl text-abhängig)
  - content_hash wurde nicht geschrieben (war seit Sprint 9j auf REST-Pfad)

Fix: identische Branching-Logik wie cards.ts POST. Cloze ohne {{cN::…}}
und image-occlusion ohne valides mask_regions-JSON liefern 422.
content_hash wird mit cardContentHash beim Insert geschrieben.

Damit ist der Tool-Pfad voll-konvergent — mana-mcp und Persona-Runner
können jeden Card-Type via cards.create anlegen, sobald die Plattform-
Services (mana-share + mana-mcp) deployed sind.

Phase-7-Plumbing:
  ✓ Cards-Tools (cards.create + cards.search) sind konvergent zum
    REST-Pfad und end-to-end via Bearer-JWT verifiziert
  ✓ App-Manifest deklariert beide Tools (input_schema + output_schema)
  ✓ Service-Key in mana-auth registriert (Phase 2)
  ✗ mana-mcp + mana-share Container sind auf Mac Mini NICHT deployed
    → Tool-Discovery + Routing aus Claude Desktop / Persona-Runner
    bleiben offen, bis die Plattform-Services hochgezogen werden.
    Das ist Plattform-Scope, nicht Cards-Scope.

56 API-Tests grün, type-check sauber.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:48:39 +02:00
Till JS
76d4e9208e Phase 6: Tier-Awareness + mana-credits-Client (Plumbing)
Some checks are pending
CI / validate (push) Waiting to run
auth-middleware setzt jetzt zusätzlich `tier`-Claim aus dem JWT
(public/beta/alpha/founder, default public bei unbekannt) und
`authMode` (jwt|dev-stub). Dev-Stub bekommt founder-Tier — Tests
und der Anki-Importer (X-User-Id) bleiben unbeeinträchtigt.

`requireTier(min)`-Middleware-Helper für künftige Premium-Pfade,
liefert 403 mit `{need, have}` bei Tier-Unterlauf. Aktuell nicht
auf Cards-Endpoints angewendet — der MVP ist tier-frei für jeden
authentifizierten User. Plumbing ist da, sobald Premium-Features
auftauchen (AI-Bulk-Convert, Image-Generation, …).

services/credits-client.ts: schmaler Service-to-Service-Wrapper um
mana-credits (https://credits.mana.how/api/v1/internal/*) für
balance / reserve / commit / refund-reservation. Auth via
X-Service-Key (Cards-App-Key aus mana-auth, env
CARDS_MANA_SERVICE_KEY). Cards-MVP nutzt das nicht produktiv —
ist der Pre-Wire-Layer für später.

56 API-Tests grün, type-check sauber.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:45:08 +02:00
Till JS
506aec3357 Phase 2a: Cards-API JWT-Verify (additiv zum Dev-Stub)
Some checks are pending
CI / validate (push) Waiting to run
cards-api akzeptiert jetzt zwei Auth-Quellen:
  1. Authorization: Bearer <jwt>  — JWT aus mana-auth, gegen
     /api/auth/jwks gecached, sub→userId
  2. X-User-Id: <uuid>            — Dev-Stub, bleibt aktiv

Schalter: CARDS_AUTH_DEV_STUB=false deaktiviert den Dev-Stub
hart (für die spätere Phase-10b-Cutover-Stufe). Default behält
beide Pfade — Tests + Anki-Importer + Sprint-9-User-Smokes
laufen unverändert weiter.

Cards-App ist heute in mana-auth (auth.mana.how) registriert
(app_id='cards', ownership_kind='verein', status='active'),
Service-Key generiert + per direktem DB-Insert in
auth.app_service_keys gelegt (Migrations-Tabellen waren in
prod-DB noch nicht angewendet, jetzt nachgezogen).

56 API-Tests grün, jose als neue Dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:41:09 +02:00
Till JS
f9d7a16ee2 fix(api Dockerfile): COPY app-manifest.json (runtime-import)
Some checks are pending
CI / validate (push) Waiting to run
2026-05-08 20:14:20 +02:00
Till JS
cfdc5822ad fix(dockerfiles): copy tsconfig.base.json into build context
Some checks are pending
CI / validate (push) Waiting to run
cards-domain/tsconfig.json extends ../../tsconfig.base.json — ohne
das File schlägt vite-build im web-Container fehl mit
'failed to resolve extends'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:12:01 +02:00
Till JS
045903b5b9 Phase 10a: Production-Deploy-Stack (Mac Mini)
Some checks are pending
CI / validate (push) Waiting to run
infrastructure/docker-compose.production.yml mit 4 Services:
  - cards-postgres :5436    (Plattform-Postgres :5432, Dev :5435 belegt)
  - cards-minio :9110/9111  (Plattform-MinIO :9000/9001 belegt)
  - cards-api :3091         (alt war :3072 — Cutover via Tunnel-Reroute)
  - cards-web :5181         (alt war :5180)

Persistente Volumes auf /Volumes/ManaData/cards/{postgres,minio} —
außerhalb des Repo-Verzeichnisses (überlebt repo-wipes, gleicher
Pfad wie mana-platform-Daten).

Dockerfiles:
  - apps/api: oven/bun:1.1-alpine, single-stage. pnpm via npm install.
    Verdaccio-Auth via NPM_AUTH_TOKEN-Build-Arg + .npmrc.
  - apps/web: 2-stage node:20-alpine. SvelteKit-build mit
    PUBLIC_CARDS_API_URL als Build-Arg (kommt direkt in den
    Client-Bundle via vite). Runtime startet adapter-node-Bundle
    direkt mit `node build/index.js`.

infrastructure/.env.production.example als committable Skeleton —
echte .env.production bleibt git-ignored. Vier Secrets nötig:
CARDS_DB_PASSWORD, CARDS_S3_SECRET_KEY, CARDS_DSGVO_SERVICE_KEY,
NPM_AUTH_TOKEN.

Hard-Cutover-Plan: alte mana-app-cards-{server,web} bleiben kurz
parallel laufend, Tunnel zeigt nach dem Build/Verify-Cycle auf die
neuen Container, dann werden die alten gestoppt + entfernt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:09:19 +02:00
Till JS
39b1791fb9 Phase 9l: Image-Occlusion als 4. MVP-CardType
Domain: CardTypeSchema öffnet 'image-occlusion'. Neues Modul
@cards/domain/src/image-occlusion.ts mit MaskRegionSchema (zod-strict,
0..1-relative Coords + optionalem Label, max 100 Regionen),
parseMaskRegions (parse + sort-by-id), maskRegionCount, maskForSubIndex.

Field-Schema (cards.fields):
  image_ref:    string  — media_files.id (Phase 9k Storage)
  mask_regions: string  — JSON-Array<MaskRegion>
  note?:        string  — optionale Bildunterschrift

subIndexCount('image-occlusion') wirft analog zu cloze, weil die
Anzahl text-abhängig ist. Card-POST-Handler ruft maskRegionCount
und lehnt 422 ab, wenn das Mask-Array leer / kaputt ist (vor dem
Deck-Lookup).

UI-Komponenten:
  - ImageOcclusionEditor: File-Picker → uploadMedia (Phase 9k),
    SVG-Overlay über das Bild, Drag-to-create-Rectangle (mind. 2%
    Größe, sonst Klick-Filter), Mask-Liste mit Label-Input und
    Delete-Button. Pointer-Events für Touch-Mobile-Support.
  - ImageOcclusionView (Study-Render): Bild + SVG-Overlay; aktive
    Mask ist im Prompt opake schwarz, im Reveal transparent grün
    mit Label-Text; andere Masken bleiben dezent gelb-durchsichtig
    als Lern-Hinweis.

/cards/new + /cards/[id]/edit: Type-Picker um Image-Occlusion
erweitert, Branch-Logik schaltet auf den Editor um. canSave-
Validierung: imageRef gesetzt + mind. 1 Mask. /study/[deckId]
nutzt ImageOcclusionView statt Markdown-Render. /decks/[id]-Liste
zeigt "🖼 image-occlusion · <ref-prefix>" statt "(leer) → (leer)".

i18n DE/EN: type_image_occlusion, toast_image_occlusion,
image_occlusion-Namespace (image_label, draw_hint,
label_placeholder, delete_mask, no_image_selected, etc.).

Tests: 11 neue Domain-Tests für MaskRegion-Schema/Parse/Mapping
(66 Domain ges.), 3 neue API-Tests für 422-Branches und
Validation-vor-Deck-Lookup-Pfad (56 API ges.). 129 Tests grün
ges. (66 + 56 + 7), type-check 384 files 0 errors, prod-Build
sauber.

E2E-Smoke: Image-Occlusion-Card mit 2 Masken (image_ref auf das
Sprint-9k-Test-PNG) → API legt content_hash + 2 Reviews mit
sub_index 0+1 an, reviews/due returnt sie korrekt typisiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:50:45 +02:00
Till JS
c9eb0a6f80 Phase 9k: Media-Upload via MinIO-Container
Eigener cards-minio-Container im docker-compose (9100/9101 — Plattform
auf 9000/9001 bleibt isoliert). cardsadmin/cardsadmin als Dev-Default,
prod via env-Vars (CARDS_S3_*).

apps/api/src/services/storage.ts — schmaler StorageService um den
minio-Client. ensureBucket() ist idempotent (auto-create beim ersten
Upload). removeObjectsByPrefix() implementiert den DSGVO-Bucket-Sweep,
weil die S3-API kein Cascade kennt.

Neue Tabelle media_files in pgSchema('cards'):
  id, user_id, object_key, mime_type, original_filename, size_bytes,
  kind, created_at — kein FK auf cards (ein File kann mehreren Karten
  gehören). objectKey-Format <userId>/<ulid>.<ext> für Bucket-Prefix-
  Sweep beim DSGVO-Delete. Legacy mediaRefs bleibt als Slot.

Neuer Router /api/v1/media:
  POST /upload   — multipart, 25 MiB Default-Limit, image/audio/video
                   only (415 sonst), schreibt media_files-Row + speichert
                   in MinIO unter <userId>/<ulid>.<ext>
  GET  /:id      — streamt aus MinIO mit Cache-Control: private,
                   immutable. Cross-User → 404 (nicht 403, anti-enumeration).
  GET  /         — listet alle eigenen Files

DSGVO-Pfade (Service-Key + /me/delete) räumen jetzt auch media_files
+ MinIO-Bucket-Prefix mit ab. Storage-Sweep ist non-fatal — DB ist erst
konsistent gelöscht, dead bytes wären die schlimmstmögliche Folge.

Anki-Import: parse.ts sanitizeAnkiHtml akzeptiert wieder eine
Filename→URL-Map (war in Phase 8c gedroppt). import.ts lädt vor den
Karten alle referenzierten Media-Files via uploadMedia() in MinIO,
sammelt URLs, ersetzt Anki-Filenames durch /api/v1/media/<id>-Pfade
in `<img>` (Markdown) und `[sound:…]` (HTML <audio>). 4-fache Worker-
Concurrency.

apps/web/src/lib/markdown.ts: DOMPurify lässt jetzt <audio>/<video>/
<source> mit src/controls/preload-Attributen durch — sonst würden die
Audio-Tags aus dem Anki-Import gestrippt.

i18n-Strings (DE/EN) auf Media-Stage erweitert: stage_media,
done_media, what_works_media, dropzone_hint, preview_media.
import.what_skipped_media wird zur Bestätigung dass Media seit
Sprint 9k mit übernommen wird.

Manueller E2E-Smoke gegen lokale MinIO (cards-minio :9100):
- 1×1-PNG hochgeladen → 201 mit ID + URL
- /api/v1/media/<id> streamt 200 image/png 69 bytes (file-Identifikation
  bestätigt)
- Cross-User → 404, ohne X-User-Id → 401, text/plain → 415

53 API-Tests grün (+4 neue media-Auth-Gate-Tests), 7 Web-Tests,
51 Domain-Tests, type-check + svelte-check 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:42:56 +02:00
Till JS
593d4475df Phase 9j: Anki-Re-Import-Dedupe via content_hash
Neuer Domain-Helper cardContentHash({ type, fields }) — SHA-256 über
canonisierten JSON ({type, sorted-fields}), pure Web-Crypto. Field-
Reihenfolge ist invariant; Whitespace + Cloze-Markup zählen mit
(zwei Karten mit identischem Text aber unterschiedlichem
{{c1::…}}-Markup sind verschiedene Karten).

cards-API POST schreibt content_hash automatisch in den schon
existierenden Schema-Slot. Neuer Endpoint GET /api/v1/cards/hashes
liefert die kompakte Hash-Liste des Users (ohne Card-Body) — eine
Anfrage pro Anki-Import statt pro Karte.

apps/web/src/lib/anki/import.ts holt die Hashes vor dem Loop und
prüft pro Karte clientseitig. Duplikate werden gezählt
(cardsSkippedDuplicate) und übersprungen, der Counter erscheint
in der AnkiImport-Done-View. Same-File-Drift (Anki-interne
Doppel-Notes) wird auch erkannt — nach erfolgreichem Insert
landet der Hash sofort im Set.

Fallback: wenn /hashes fehlschlägt (älterer Server), bleibt das
Dedupe-Set leer und Karten werden eingefügt wie zuvor — kein
Hard-Bruch.

Pre-Phase-9j-Karten haben null content_hash (Hashes-Endpoint
filtert sie weg) — sie können also irrtümlich erneut eingespielt
werden, falls noch im Anki-File. Pragmatisch akzeptiert: ein
Backfill-Script wäre Phase-10-Polish, sobald Live-User da sind.

5 neue Domain-Tests, 1 neuer API-Auth-Gate-Test (105 grün ges.:
51 + 49 + 5). svelte-check 380 files 0 errors. E2E gegen lokale
Postgres bestätigt: neue Karte hat content_hash (64-char-hex),
/hashes listet sie.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:29:56 +02:00
Till JS
6db6dc3e42 Phase 9f: Statistik-Dashboard
Neuer Endpoint GET /api/v1/me/stats liefert in einem Aufruf alle
Aggregate für die Stats-UI:
- total_decks / total_cards / total_reviews / due_now
- state_counts pro FSRS-Zustand (new/learning/review/relearning)
- reviewed_per_day für die letzten 7 Tage (Quelle: reviews.last_review,
  via to_char(day, 'YYYY-MM-DD') auf Postgres-Seite gruppiert)
- streak_days (rückwärts ab heute bis zum ersten Tag ohne Review)

study_sessions wird aktuell NICHT befüllt — der Schema-Slot existiert
seit Phase 3, aber der Session-Tracker kommt erst, wenn das Lern-
Flow-Layer ausgebaut wird. last_review reicht für jetzt.

/stats-Page rendert vier KPI-Cards, einen 7-Tage-Säulen-Chart per
CSS-Heights, plus eine FSRS-State-Distribution. Header-Nav um
"Statistik" ergänzt.

E2E-Smoke gegen lokale Postgres bestätigt: bestehender Cloze-User
zeigt 1 Deck, 1 Karte, 2 Reviews, 2 due, alle "new"-State, 0
Streak — passt zum gestern eingespielten Smoke-User.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:06:13 +02:00
Till JS
03117d5869 Phase 9e: Account-Page mit DSGVO-Self-Service
Neuer User-JWT-Pfad GET/POST /api/v1/me/{export,delete} — gespiegelte
DSGVO-Logik aus dem Service-Key-Pfad, aber gegen die eigene User-ID
gated. buildUserExport extrahiert in dsgvo.ts und wird von beiden
Routern geteilt.

/account-Page zeigt User-ID, Logout, JSON-Daten-Export (Download als
Blob), und einen rot-markierten Account-Delete-Knopf mit "LÖSCHEN"-
Confirmation. Logout im Header verlinkt jetzt auf /account statt
direkt clear() — der User sieht zuerst, was an seinem Account hängt.

Andere mana-Apps werden nicht mit gelöscht — der UI-Hinweistext zeigt
auf die spätere Verein-DSGVO-Sammelanfrage über mana-admin.

48 API-Tests grün (+2 neue auth-gate-Tests für /me), web type-check
374 files 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:03:41 +02:00
Till JS
aff4d9536a Phase 9d: Pre-Flight — Protocol-Mirror durch upstream ersetzt
@mana/shared-share-protocol@0.1.0 ist jetzt installierbar (NPM_AUTH_TOKEN
aus claudebot-Verdaccio-Account). Lokaler protocol/-Mirror zeigt jetzt
auf upstream:

- envelope.ts → Re-Export von ShareEnvelopeSchema/Strict, parseEnvelope,
  ENVELOPE_VERSION, ShareEnvelope
- search.ts → Re-Export von SearchHitSchema, SearchResultEnvelopeSchema,
  SEARCH_ENVELOPE_VERSION, SearchHit, SearchResultEnvelope
- payloads.ts → Re-Export der Format-Schemas (Quote/Link/Text);
  Cards-spezifische PAYLOAD_SCHEMAS / validatePayloadForType bleiben
  lokal (Akzeptanz-Liste ist Cards-Layer, nicht Föderation)

Spec-Drift gefixt: der frühere Mirror nutzte MANA_TYPE_URL = 'mana/url',
upstream definiert MANA_TYPE_LINK = 'mana/link'. app-manifest.json,
share-handlers (UrlPayload → LinkPayload, "mana/url" → "mana/link")
und Doku-Kommentare auf den Spec-konformen Namen umgestellt.

DNS-Korrektur in Repo-.npmrc: pkg.mana.how-Tunnel ist Lame-Duck (404),
npm.mana.how ist die produktive Verdaccio-Route nach 2026-05-07-Re-Deploy.
~/.npmrc bleibt unangetastet — Anpassung ist user-side.

Tests + svelte-check 0 errors, 92 Tests grün (41 Domain + 46 API + 5
Web), prod-Build sauber.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:00:56 +02:00
Till JS
553a78d73b Phase 8a: Cloze als MVP-Card-Type, Cluster-Counter
CardTypeSchema öffnet 'cloze' als drittes MVP-Set-Mitglied. Domain-Modul
@cards/domain/src/cloze.ts kapselt die Cluster-Logik (extractClusterIds,
subIndexCountForCloze, clusterIdForSubIndex, renderClozePrompt/Answer)
— Hint-Markup wird MVP-stumm gedroppt.

subIndexCount('cloze') wirft jetzt explizit, statt still auf 1 zu fallen,
weil die Cluster-Anzahl text-abhängig ist und ein silent-default falsch
dimensionierte Review-Tabellen produzieren würde. Card-POST-Handler holt
für Cloze die Anzahl aus subIndexCountForCloze und lehnt 422 ab, wenn
kein {{cN::…}}-Markup vorhanden ist.

12 neue Cloze-Tests, alle Domain- und API-Tests grün (41 + 46).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:35:39 +02:00
Till
0328caa333 Phase 5: Föderations-Endpunkte — Cards ist föderierter Peer
Endpoints (alle Pfade aus app-manifest.json):
- POST /api/v1/share/receive — User-JWT-Auth, ShareEnvelope-Strict-
  Validation (cross-user-forbidden), Recipient-Match, Type-Accept-
  Lookup über Manifest, Payload-Schema-Validation, Handler-Dispatch
- POST /api/v1/tools/:name — User-JWT, dispatch nach `cards.create`
  und `cards.search` mit Tool-Schemas aus @cards/domain
- GET /api/v1/search — User-JWT, ILIKE auf cards.fields jsonb +
  decks.name, baut SearchResultEnvelope für mana-search-Aggregator
- GET /api/v1/dsgvo/export?user_id=… — Service-Key, voll-Bundle aller
  Cards-Daten des Users (decks, cards, reviews, study_sessions, tags,
  media_refs, import_jobs)
- POST /api/v1/dsgvo/delete — Service-Key, kaskadiert via FK-Cascade
  decks → cards → reviews/media_refs/card_tags/tags/study_sessions
  plus separates Cleanup von import_jobs

Share-Handlers (apps/api/src/share-handlers/):
- create_card_from_quote (mana/quote → front=text, back=source)
- save_link_as_card (mana/url → front=title, back=url+description)
- create_card_from_text (mana/text → front=erste-zeile, back=rest)
Alle landen via ensureInboxDeck() in einem auto-erstellten "Inbox"-Deck
pro User, inklusive automatischer FSRS-Reviews-Init in Transaktion.

Lokales Protocol-Mirror in @cards/domain/src/protocol/ (envelope,
payloads, search): TEMPORARY-Markierung mit Swap-Plan auf
@mana/shared-share-protocol via Verdaccio sobald NPM_AUTH_TOKEN da ist.
Spec-strict — UUID für user_id, ULID für share_id, Crockford-Base32.

Service-Key-Middleware mit constant-time-Compare gegen
process.env.CARDS_DSGVO_SERVICE_KEY (Phase F-1: ersetzt durch
mana-auth.app_service_keys-Lookup).

Tests:
- 70 Vitest-Tests grün (27 cards-domain + 43 apps/api):
  - share.test.ts: Auth-Gate, Cross-User-Sperre, User-Mismatch (403),
    Wrong-Recipient (422), Unknown-Type (422), Invalid-Payload (422),
    Wrapped { envelope, delivery_token }-Body akzeptiert
  - tools.test.ts: Auth, Unknown-Tool (404), cards.create-Validation,
    cards.search-Envelope-Shape
  - search.test.ts: Auth, Missing-Query (422), Query-too-long (422),
    Envelope-Version 0.1 + envelope-Felder
  - dsgvo.test.ts: Service-Key-Gate (401), Missing-User-ID (400),
    Export-Bundle-Shape, Delete-Counts, Key-not-configured (500)
- pnpm run type-check  4/4 packages
- E2E-Smoke gegen Postgres: Quote-Share→Inbox-Deck→Karte→Search-Hit→
  DSGVO-Export+Delete-Roundtrip clean (alle 3 Tabellen 0 nach delete)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:10:35 +02:00
Till
89a7a9250b Phase 4: Frontend-Core MVP — Decks, Cards, Study mit FSRS-Loop
Stack:
- Tailwind 4 via @tailwindcss/vite (oklch-Theme + Dark-Mode-Auto)
- marked + DOMPurify für Markdown (sanitized, SSR-Safe)
- Svelte 5 runes durchgängig ($state, $derived, $effect)
- @sveltejs/adapter-node für Production-Build

Infrastruktur:
- $lib/auth/dev-stub.svelte.ts: User-ID via sessionStorage (Phase 2
  ersetzt durch echtes JWT via @mana/shared-auth)
- $lib/api/{client,decks,cards,reviews}.ts: typed Fetch-Wrapper, ruft
  cards-api auf 3081 mit X-User-Id-Header
- $lib/stores/toasts.svelte.ts: Toast-Store mit info/success/warning/error
- $lib/markdown.ts: marked → DOMPurify-Pipeline
- $lib/components/{Header,ToastStack}.svelte: Layout-Shell

Routes:
- / → Dev-Login-Form oder Redirect zu /decks (wenn eingeloggt)
- /decks → Liste mit Color-Dot, Hover-Delete-Button
- /decks/new → Create-Form (Name, Beschreibung, Color-Picker)
- /decks/[id] → Detail mit Cards-Liste + dueCount + "Lernen"-Button
- /cards/new?deck=... → Type-Picker (basic|basic-reverse) +
  Side-by-Side Markdown-Editor mit Live-Preview
- /study → Übersicht aller Decks mit Due-Counts
- /study/[deckId] → Session-View mit Queue-Snapshot, Reveal/Grade,
  Hotkeys (Space/Enter=Reveal & Good, 1-4=Again/Hard/Good/Easy),
  INPUT-Skip im Keyboard-Handler

CORS auf cards-api für localhost-Origins + cardecky.mana.how.

Verifiziert:
- pnpm run type-check  4/4 packages, svelte-check 0 errors
- pnpm build (cards-web)  adapter-node bundle 140 kB server,
  alle Routen bundled
- Tailwind-CSS inlined in SSR-HTML, oklch-Theme korrekt
- CORS-Preflight funktioniert (OPTIONS 204 mit korrekten Allow-*-Headers)
- Live-Smoke-Test gegen localhost:3081 (cards-api) + localhost:3082
  (cards-web): Beide laufen parallel, Web → API CORS-fetch grün

Outside scope (Phase 4):
- Card-Edit-Page (/cards/[id]/edit) — heute nur Create + Delete
- Settings/Account/Credits/DSGVO-Pages — Phase 9 (Polish)
- Anki-Import — Phase 8

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 16:52:31 +02:00
Till
5f67bd9f3e Phase 3 follow-up: type-check + tests grün, ts-fsrs v5 API
- tsconfig.base.json: allowImportingTsExtensions + noEmit (.ts-Imports
  in dev, kein tsc-Output, vitest/bun/vite handhaben Build)
- ts-fsrs v5.3.2 API-Updates:
  - scheduler.next(card, now, grade) statt repeat(card, now)[rating].card
  - Grade-Type für RATING_TO_FSRS (excluded Manual)
  - learning_steps-Feld auf Review (Schema, Drizzle-Column, Adapter,
    DTO-Konverter, Tests)
- apps/web: extends .svelte-kit/tsconfig.json (SvelteKit-Empfehlung),
  test-Script mit --passWithNoTests
- apps/api: dropped types: ['bun-types'] (stale)
- pnpm-lock.yaml committed

Status:
- pnpm run type-check  4/4 packages grün (api, domain, web mit
  svelte-check 0 errors)
- pnpm run test  46 Tests grün (cards-domain: 27, apps/api: 19,
  apps/web: --passWithNoTests)
- pnpm install  136 packages, 8s

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 14:41:04 +02:00
Till
45a47e0ffd Phase 3: Domain-Modell + Decks/Cards/Reviews-CRUD
Domain (@cards/domain):
- zod-Schemas SSOT für Deck, Card, Review, StudySession, FsrsSettings,
  Tools (cards.create + cards.search Input/Output)
- CardType-Discriminated-Union: MVP basic+basic-reverse, Future-Set
  (cloze, type-in, image-occlusion, audio, multiple-choice) für
  Schema-stable-Migration vorbereitet
- validateFieldsForType() Pure-Function pro CardType
- FSRS-Adapter über ts-fsrs v5.3.2: newReview, gradeReview,
  subIndexCount, toFsrsCard/fromFsrsCard ISO↔Date-Roundtrip
- Encryption-Hinweis: reviews bleiben PLAINTEXT (Scheduler quert
  täglich `due <= now`, siehe Lessons §3)

Drizzle-Schemas (apps/api/src/db/schema, alles in pgSchema('cards')):
- decks, cards, card_tags, reviews (PK card_id+sub_index), study_sessions,
  tags (deck-skopiert), media_refs (verweist auf mana-media), import_jobs
- _schema.ts-Pattern um Zirkular-Imports zu vermeiden (Lesson aus
  mana-share/-events während F-0)
- Hot-Path-Index reviews_user_due_idx für Scheduler-Queries

Routes (apps/api/src/routes):
- POST/GET/PATCH/DELETE /api/v1/decks (Deck-CRUD)
- POST/GET/PATCH/DELETE /api/v1/cards (Card-CRUD mit Auto-Reviews-Init:
  beim Card-Insert werden N Reviews via subIndexCount(type) angelegt,
  in einer Transaktion)
- GET /api/v1/reviews/due (Hot-Path, optional deck_id-Filter, Limit 500)
- POST /api/v1/reviews/:cardId/:subIndex/grade (FSRS-State-Transition,
  per-Deck FSRS-Settings)

Auth: Stub-Middleware liest X-User-Id-Header (Phase 2 ersetzt durch
@mana/shared-hono authMiddleware mit JWKS-Cache).

Tests (vitest, Hono app.request()):
- @cards/domain: fsrs.test.ts (newReview, gradeReview Roundtrip,
  Rating-Mapping), schemas.test.ts (zod-strict-Variants, Field-Type-
  Validation, hex-Color)
- apps/api: decks.test.ts + cards.test.ts + reviews.test.ts —
  Auth-Gate + Input-Validation. Volle DB-Integrationstests folgen mit
  pg-mem oder testcontainers in späterer Phase.

Cleanup: types.ts entfernt, zod-Schemas sind SSOT (z.infer für Types).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 14:21:54 +02:00
Till
8605b1b517 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) <noreply@anthropic.com>
2026-05-08 14:08:41 +02:00