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>
**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>
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>
renderClozePrompt zeigt jetzt den Hint im aktiven Cluster anstelle
von „…", wenn der User die `{{c1::Antwort::Hinweis}}`-Syntax nutzt.
Beispiel: Prompt für `{{c1::Paris::Hauptstadt}}` wird "[Hauptstadt]"
statt "[…]". Nicht-aktive Cluster expandieren auf ihre Antwort —
der Hint bleibt unsichtbar, bis sein Cluster dran ist.
Neue Helper-Funktion hintForCluster(text, clusterId) liefert die
erste Hint-Annotation eines Clusters (deterministisches Verhalten
bei mehreren `{{c1::…}}`-Vorkommen mit unterschiedlichen Hints).
5 neue Tests in cloze.test.ts: hintForCluster (4 Cases), erweiterte
renderClozePrompt-Cases. Domain jetzt 46 Tests grün.
cloze_help in i18n DE/EN um die Hint-Syntax-Erklärung erweitert.
Live-Preview im Card-New/Edit nutzt die erweiterte Logik automatisch
(beide rufen renderClozePrompt aus @cards/domain).
svelte-check 379 files 0 errors, API-Tests unverändert grün
(48/9), Web-Tests 5/1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>