Commit graph

94 commits

Author SHA1 Message Date
Till JS
8a56d0dcff feat(landing): Astro-Landingpage für Cardecky
Neue statische Astro 5-App in apps/landing/ (Port 4380).
Sektionen: Nav, Hero, Kartentypen-Grid (6 Typen), How-it-works,
Features, mana-e.V.-Pitch, CTA, Footer.

Stack: Astro 5 + Tailwind 3, kein MDX (overkill für MVP), keine
externen Abhängigkeiten. Forest-grüne Farbpalette passend zum App-
Theme, Serif-Headings im mana-e.V.-Stil.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:40:51 +02:00
Till JS
3669a86599 feat(web): audio-front Upload-Widget + typing Aliases-Feld + Edit-Fixes
audio-front:
- AudioUploadField.svelte: Datei-Upload statt rohem media_ref-Textfeld;
  ruft uploadMedia() auf, zeigt Dateiname nach Upload + Replace-Button
- Karten-Erstellungsseite: AudioUploadField ersetzt das unbrauchbare Textfeld
- Edit-Seite: audio-front wird jetzt korrekt geladen (audio_ref + back statt
  dem falschen basic-else-Zweig) und gespeichert

typing:
- Aliases-Feld in Erstellungs- und Edit-Seite; kommagetrennte Alternativ-
  antworten werden in fields.aliases gespeichert und von checkTypingAnswer
  ausgewertet
- Edit-Seite: typing wird jetzt korrekt geladen (front + answer + aliases)

i18n: alle 5 Sprachen mit audio_upload_btn/uploading/failed/replace,
typing_aliases_label/hint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:36:28 +02:00
Till JS
926ff685c7 feat(web): CSV-Import/Export, Tab-Format-Import, PDF-Druckansicht
- CSV-Import: Dropzone für .csv-Dateien, unterstützt 2-spaltig (front,back)
  und 3-spaltig (type,front,back) inkl. cloze; Dedupe via contentHash
- CSV-Export: Button auf Deck-Detail-Seite, lädt type,front,back als .csv
- Tab-Format-Import (ehem. Quizlet): Textarea für tab-getrennte Zeilen;
  funktioniert mit Excel, Google Sheets, Notion und Quizlet-Extension;
  Anleitung erklärt Quizlet-Paywall-Workaround (Quizlet Exporter Extension)
- PDF-Druckansicht: Route /decks/[id]/print, A6-Karten mit alternierenden
  Vorder-/Rückseiten, CSS @page { size: A6 landscape } für Browser-Druck
- Import-Seite: Tab-Bar Anki | CSV | Tab-Format
- i18n: alle 5 Sprachen (DE/EN/FR/ES/IT) vollständig
- docs/FEATURE_IDEAS.md: strukturierte Feature-Liste als Planungsgrundlage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:27:39 +02:00
Till JS
9839737049 feat(web): multiple-choice — explanation-Feld, Edit-Bug-Fix, State-Reset
- MultipleChoiceCardForm: optionales `explanation`-Feld (Erklärung wird
  nach Auswahl angezeigt); `field-optional`-Style ergänzt
- MultipleChoiceView: `explanation`-Prop; zeigt Erklärungsbox nach
  Auswahl (grün bei richtig, neutral bei falsch); `{#key card_id}`-Block
  erzwingt Remount bei Kartenwechsel — behebt State-Leak zwischen Karten
- edit/+page.svelte: MC-Edit-Bug behoben — Karten wurden fälschlich mit
  `{front, back}` gespeichert und haben `answer`/`distractor_pool`
  überschrieben; `MultipleChoiceCardForm` importiert und verdrahtet;
  `canSave` und `onSubmit` handhaben MC korrekt; lädt `answer` +
  `distractor_pool` beim Öffnen zurück in `mcOptions`-Array
- new/+page.svelte: `mcExplanation`-State an Form gebunden und beim
  Speichern als `fields.explanation` gesetzt
- study/+page.svelte: `explanation` aus Card-Fields extrahiert und
  an MultipleChoiceView durchgereicht
- scripts/migrate-factfulness-to-mc.ts: einmalige Migration — 13
  Factfulness-Quiz-Karten von `basic` (A/B/C in Freitext) auf
  `multiple-choice` mit strukturierten Feldern konvertiert; Deck auf
  `visibility=public` gesetzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:24:18 +02:00
Till JS
41ecec16c3 fix(web): SkeletonGrid padding an DeckListGrid angleichen — kein Layout-Sprung mehr
padding-block: 0 1rem → 1.25rem 2.5rem (identisch zu DeckListGrid/.deck-row),
plus ::after-Spacer ergänzt. Verhindert den Höhensprung beim Übergang
Skeleton → echte Decks auf /decks und /explore.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 14:45:31 +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
333581c5ef fix(web): body stream already read — Text zuerst lesen, dann JSON parsen
Some checks failed
CI / validate (push) Has been cancelled
res.json() konsumiert den Body-Stream auch bei SyntaxError, danach
schlägt res.text() mit 'body stream already read' fehl. Fix: text()
einmalig lesen, dann JSON.parse() versuchen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 17:07:24 +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
595f1f9cb6 refactor(web): ClozeCardForm + MultipleChoiceCardForm extrahieren + Import-Bug fixen
- `ClozeCardForm.svelte`: Lückentext-Formular-Sektion aus cards/new herausgezogen
- `MultipleChoiceCardForm.svelte`: MC-Options-Builder (inkl. 85 Zeilen MC-CSS)
  aus cards/new herausgezogen — cards/new: 1010 → 856 Zeilen
- Import-Bug in 9 Dateien behoben: Python-Skript hatte apiErrorMessage-Import
  in mehrzeilige import-Blöcke eingefügt (Syntaxfehler)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:35:01 +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
f2f752e9ee feat(web): apiErrorMessage-Utility + MultipleChoice-Fallback
- Neue Utility `apiErrorMessage()` in `$lib/api/error.ts`: liest `body.detail`
  / `body.error` aus ApiError-Responses statt generischer "(err as Error).message"
  — 22 Dateien auf die Utility umgestellt, keine rohen Type-Casts mehr
- MultipleChoiceView: Fallback-UI wenn < 1 Distractor verfügbar — zeigt
  Antwort direkt + Nochmal/Gewusst-Buttons statt kaputter 1-Option-Auswahl

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:27:19 +02:00
Till JS
f3a148171a refactor(account): Profil-Karte, Meta-Grid, Action-Karten
- Avatar mit Initialen, Name, E-Mail, Tier-Badge, Logout-Button
- Meta-Grid: User-ID (gekürzt, full on hover) + Tier
- Export- und Danger-Zone als saubere Action-Karten mit Icon
- Einheitliche Btn-Styles (outline, primary, danger)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:18:57 +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
a883ba87b6 refactor(me/published): UX-Fix — Anzeige-Name zuerst, Auto-Slug, einspaltiges Layout
- Anzeige-Name als erstes Feld, Slug wird automatisch daraus abgeleitet
  (slugify: Umlaute, Sonderzeichen, Leerzeichen → url-safe)
- Slug kann manuell überschrieben werden (slugManuallyEdited-Flag)
- Alle Felder einspaltung untereinander (kein sm:grid-cols-2 mehr)
- Seitentitel dynamisch: "Author-Profil anlegen" vs "Meine Veröffentlichungen"
- Untertitel zeigt @slug + Anzahl Decks wenn Profil existiert
- Deck-Liste überarbeitet mit meta-Zeile und "Ansehen →"-Link

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:11:57 +02:00
Till JS
608b385c05 feat(web): decks-page auf Explore-Layout migriert + Subscriptions sichtbar
- /decks zeigt jetzt zwei horizontal scrollbare Abschnitte (wie Explore):
  "Eigene Decks" (DeckStack + NewDeckCard) und "Abonniert" (MarketplaceDeckStack)
- Subscriptions werden über getMySubscriptions() + getMarketplaceDeck() geladen
  und als vollwertige DeckListEntry-Objekte dargestellt
- DeckListGrid: padding-block-start 0→1.25rem, padding-block-end 1rem→2.5rem
  damit Hover-Schatten (translateY-2px + box-shadow 0 12px 28px) nicht abgeschnitten wird
- Eigene Decks verwenden identisches Scroll-CSS wie DeckListGrid (visuell einheitlich)
- Beide Sektionen laden parallel, je mit SkeletonGrid-Platzhalter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:08:21 +02:00
Till JS
c1a87a4f88 feat(publish): Deck direkt aus der Detail-Seite veröffentlichen
- PublishDeckModal: Author-Check, Slug-Eingabe mit Live-Exists-Check,
  Titel/Beschreibung/Lizenz (nur für neue Decks), Semver auto-gebumpt,
  Karten automatisch aus privatem Deck übernommen (kein JSON-Paste)
- Deck-Detail-Seite: "↑ Veröffentlichen"-Button im Header, öffnet Modal,
  leitet nach Erfolg auf /d/:slug (Marketplace-Seite) weiter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:07:55 +02:00
Till JS
b761cd52c9 fix(decks/from-image): kontextbewusste Statusmeldungen für URL-only-Generierung
🌐-Emoji und angepasste Texte wenn nur URL (keine Bilder) verarbeitet wird.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:02:21 +02:00
Till JS
0c68186563 refactor(marketplace): UI-Verbesserungen, MarketplaceDeckStack, Explore-Icons
- DeckListGrid: überarbeitetes Layout
- EmptyState + SkeletonGrid: aufgeräumt
- Neuer MarketplaceDeckStack für Marketplace-Karten-Darstellung
- Explore: Icons (Fire, Star, MagnifyingGlass, Books) + Header-Cleanup
- me/forks + me/subscribed: kleinere Korrekturen
- docs/deck_ideas: initiale Ideen-Sammlung

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:00:11 +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
731481ffe3 refactor(deck-detail): Redesign mit Kategorie-Picker, Card-Menü, Markdown
- Deck-Header mit Farb-Dot, Titel, Aktions-Buttons (Lernen / Neue Karte)
- Kategorie-Picker (eingeklappt, inline wie /decks/new)
- Card-List mit kontextuellem 3-Punkte-Menü (Edit / Löschen)
- Karten-Vorschau mit Front/Back via marked
- NewDeckCard: Kategorie-Icon-Größe korrigiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 15:59:56 +02:00
Till JS
03ec7e7b3e feat(decks): Edit-Icon auf Deck-Karten + Deck-Edit-Page
- DeckStack: Pencil-Icon absolut unten-rechts, erscheint beim Hover
  (z-index über Card, ausserhalb des <a>-Links zur Detail-Page)
- Neuer Route /decks/[id]/edit: Form für Name, Beschreibung, Farbe
- i18n deck_edit keys (de + en)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 15:57:37 +02:00
Till JS
9754718157 feat(cards/new): Live-Kartenvorschau neben dem Formular
- 2-Spalten-Layout: Formular links, sticky Preview rechts
- CardSurface (hero, raised) als Preview-Container
- Typ-spezifische Vorschau:
  - basic/basic-reverse: Flip-Toggle Vorderseite ↔ Rückseite
  - multiple-choice: Frage + Option-Buttons mit grüner Markierung
  - typing: Frage + deaktiviertes Eingabefeld
  - audio-front: Play-Button-Mockup + Rückseite nach Flip
  - cloze: Erste-Cluster-Vorschau live
  - image-occlusion: Platzhalter-Hinweis
- Preview aktualisiert sich reaktiv beim Tippen
- Responsive: auf < 800px Preview über Formular

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 15:50:57 +02:00
Till JS
b5d3a29335 refactor(cards/new): UI-Redesign + Multiple-Choice Option-Builder
- Section-Cards statt flaches Form-Layout
- Typ-Beschreibungszeile unter dem Dropdown
- Multiple-Choice: 4-Options-Builder mit Radio-Auswahl für richtige
  Antwort; Distractors werden aus den anderen Optionen extrahiert
- Typing: Alias-Hinweis im Formular
- Audio-Front: Hinweis zum media_ref-Flow
- Einheitliche Input-/Button-Styles mit Theme-Tokens

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 15:45:21 +02:00
Till JS
a612ad05d6 feat(cards/new): typing, multiple-choice, audio-front im Erstellungs-UI
- Dropdown um 3 neue Types erweitert
- typing/multiple-choice: front + answer Felder (korrekte field-Namen)
- multiple-choice: optionaler distractor_pool (Fallback für kleine Decks)
- audio-front: audio_ref Text-Input + back Antworttext
- canSave + onSubmit korrekt pro Type
- i18n de + en vollständig

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 15:36:17 +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
0791436107 feat(cards): typing Card-Type mit Fuzzy-Match
- typing.ts: checkTypingAnswer (exact / close / wrong) + Levenshtein-
  Impl; close = Distanz ≤ max(1, floor(len * 0.2)); Alias-Support via
  komma-separiertes aliases-Feld
- CardTypeSchema: 'typing' ergänzt; validateFieldsForType: front+answer required
- subIndexCount: 'typing' → 1
- TypingView.svelte: Input-Feld + Submit + Result-Badge + Antwort-Markdown +
  kontext-spezifische Grade-Buttons (correct: Weiter; close: Nochmal/War richtig;
  wrong: volle 4 Buttons); svelte:window für Keyboard
- Study-Page: TypingView eingebunden, Action-Bar bei typing ausgeblendet,
  onKey delegiert zu TypingView

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 15:23:58 +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
170a2825a4 feat(cards): audio-front Card-Type
- CardTypeSchema: 'audio-front' als vollwertiger Type (fields: audio_ref + back + front?)
- subIndexCount: 'audio-front' → 1
- AudioFrontView.svelte: custom Play/Pause-Button, audio via /api/v1/media/:id,
  optionaler Hint-Text; Antwort-Markdown läuft über bestehenden answerHtml-Pfad
- Study-Page: isAudioFront + audioFrontData derived, AudioFrontView eingebunden

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 15:18:41 +02:00
Till JS
598acb410d fix(study): kein Layout-Sprung beim Wechsel Reveal ↔ Grade-Buttons
grade-row bleibt immer im Flow und setzt die Höhe der action-bar.
reveal-row liegt absolut darüber. Wechsel via visibility statt {#if},
sodass die Karte beim Aufdecken nicht springt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 14:47:15 +02:00
Till JS
d9532ed9c3 fix(study): Hint-Text entfernt, kein vertikales Scrollen
- grade-hint Paragraph und CSS entfernt (Keyboard-Shortcuts sichtbar
  in den Buttons selbst via kbd-Tags)
- min-height: 100vh → height: 100vh + overflow: hidden

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 14:40:17 +02:00
Till JS
e2b493d528 feat(study): Periodensystem-Karten mit Kategorie-Farben und Eigenschaften-Tabelle
- build_cards.py: Atommasse, Kategorie, Periode/Gruppe für alle 118 Elemente
- front: "# {Symbol}" → großes zentriertes Heading im Study-View
- back: Name + Eigenschaften-Tabelle (Z, Atommasse, Kategorie, P/G)
- fields.color: Kategorie-Farbe (10 Kategorien, je eigene Hex-Farbe)
- Study-Page: cardColor liest fields.color → linker Streifen wechselt pro Element
- CSS: h1 (4rem, zentriert), table (Header hidden, Label gedimmt)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 14:38:48 +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
5876f95d85 refactor(web): vereinfachte Navigation und inline Deck-Erstellung
- /study-Listing entfernt, Route redirectet auf /decks
- Study-Tab aus Nav entfernt; /study/*-Pfade highlighten Decks-Tab
- /decks/new und /decks/new-ai zusammengeführt: ein Formular mit
  zwei Buttons (Anlegen +  Mit KI generieren), new-ai redirectet
- Inline NewDeckCard: Formular klappt in der Kachel auf (gleiche
  Größe, scrollbar), Fach als Toggle-Picker, alle Felder volle Breite
- DeckStack: Text linksbündig, Beschreibung ohne Clamp, Titel weiter oben

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 20:17:58 +02:00
Till JS
9a07454b75 seed: 3 Cardecky-Decks v1.0.0 + Audit-Trail im Repo
Drei Cardecky-Decks live im lokalen Marketplace, mit komplettem
Audit-Trail unter docs/marketplace/seed/:

| Deck                          | Karten | Lizenz    |
|-------------------------------|--------|-----------|
| geografie-welt-top30          |    30  | CC0-1.0   |
| english-a2-grundwortschatz    |   500  | CC-BY-4.0 |
| periodensystem-elemente       |   118  | CC0-1.0   |

648 Karten gesamt = 1296 FSRS-Reviews. Alle drei via /cards-deck-
Skill 5-Stage-Pipeline (Plan, Recherche, Design, Validate, Publish).
Bulk-Mode mit Python-Heredoc-Generator, Server-Side atomic-Insert
in <1s pro Deck.

Pro Deck im Audit-Trail:
- plan.md (Subtopic-Boundaries, Streitfälle vorab)
- research/sources.md (3-8 nummerierte Quellen)
- research/notes.md (Recherche-Notes, Streitfall-Auflösungen)
- design/build_cards.py (deterministischer Generator mit Sanity-
  Checks gegen Front-Duplikate)
- design/cards.jsonl (atomic Output, 1 Karte/Zeile)
- design/outline.md (Subtopic-Counts + F-Range)
- validate/report.md (5 Standard-Checks alle ✓)
- publish/deck.json + cards.log (Server-Response + Round-Trip)

CONTENT_PLAN §8 Phase-1-Seed-Liste: 3/20 done.

README.md erklärt die seed/-Konvention für künftige Cardecky-Decks
sowie das Update-Protokoll bei PR-Merges aus dem Marketplace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:49:05 +02:00
Till JS
d7f3b93996 feat(deps): migrate Header from @mana/shared-ui@0.1.x to shared-ui-2
Some checks are pending
CI / validate (push) Waiting to run
@mana/shared-ui-2@0.1.0 wurde heute publiziert. Cards' einziger
shared-ui-Konsument war PillTabGroup im Header (Routen-Nav +
DE/EN-Switcher). Drop-in-Migration:

- apps/web/package.json: @mana/shared-ui + @mana/shared-icons raus
  (letzteres war nur shared-ui-Transitive, in Cards-Code nirgends
  direkt importiert), @mana/shared-ui-2 ^0.1.0 rein.
- Header.svelte: Import wechselt von @mana/shared-ui auf
  @mana/shared-ui-2. primaryColor-Prop entfernt — shared-ui-2
  PillTabGroup nutzt --color-primary direkt aus dem 12-Token-Set.

Bridge-Aliase in app.css bleiben — Cards' Eigen-Komponenten
(Header-Logo, Modals, Marketplace, Routen) nutzen historisch
--color-card, --color-popover, --color-accent etc. Diese Tokens
existieren im 12-Token-Mana-Set NICHT, aber die Aliase mappen sie
weiter aufs 12er-Set. Aliase-Kommentar präzisiert: nicht mehr für
shared-ui@0.1.x (raus), sondern für Cards-eigenen Code, bis Cards
in eigenem Refactor-Sprint auf das 12er-Vokabular umzieht.

Type-check-Pipeline schrumpft drastisch: von 3994 Files (mit
shared-ui@0.1.x's 176 Quellfiles + transitive) auf 439 Files (nur
Cards + shared-ui-2's kleinerer Surface). Build sauber, weniger
JS im Output-Bundle.

Cards ist damit die erste Vereins-App, die operativ auf
shared-ui-2 läuft — End-to-End-Beweis dass das System trägt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:27:24 +02:00
Till JS
e4cf124cb7 docs(status): Cardecky-Skill auf Marketplace + 2 Decks live
Some checks are pending
CI / validate (push) Waiting to run
Marketplace-Stack ist komplett (R0-R5 + Polish), und Cardecky hat
seine ersten beiden Decks im Marketplace:

- /d/geografie-welt-top30 — 30 Karten (CC0-1.0)
- /d/english-a2-grundwortschatz — 500 Karten (CC-BY-4.0), atomic
  publish in <1s

Cardecky-Skill (~/.claude/skills/cards-deck/SKILL.md) wurde
upgegradet:

- Default-Target: marketplace-local statt private-local
- Cardecky-Author-Bootstrap-Step in Stage 5
- Bulk-Mode-Section dokumentiert (>100 Karten via Python-Heredoc,
  Sampling-Reviewer-Stop, Server-Side atomic publish)
- Beispiel-Run geografie-welt-top30 als Proof-of-Concept inline

Nicht im Repo (audit-trail intentional außerhalb):
~/Documents/cards-drafts/{geografie-welt-top30,english-a2-grundwortschatz}/
mit plan.md, research/, design/build_cards.py, validate/, publish/.

CONTENT_PLAN §8 Phase-1-Seed-Liste: 2/20 done.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:16:23 +02:00
Till JS
9626200616 chore: seed-test-decks browser-console snippet
Some checks are pending
CI / validate (push) Waiting to run
Lokales Test-Daten-Tool: 7 Decks gemischt (verschiedene Farben +
Karten-Counts inkl. Empty-Stack für Empty-State-Test) anlegbar
via Browser-Console-Paste.

Auth liest TOKEN/STUB-User-ID aus localStorage, hits cards-api
auf localhost:3081 (lokal) oder cardecky-api.mana.how (live).

Wiederholtes Ausführen erstellt zusätzliche Decks — keine
Unique-Constraint auf Deck-Name. Sauberer Reset über die UI oder
docker exec ... TRUNCATE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:02:15 +02:00
Till JS
870e2aea85 feat(decks): card-stack visualization + direct-launch study mode
Decks fühlen sich jetzt wie echte Karten an, statt flacher Boxen.
Eine zentrale CardSurface-Komponente trägt die Karten-Optik
(Border, Radius 0.875rem, Shadow, Color-Stripe links) — drei
Sizes (md/lg/hero), drei Konsumenten, ein visuelles Familien-Set.

Komponenten:
- CardSurface.svelte   Foundation für jede Card-Erscheinung in Cards
- DeckStack.svelte     5:7-Stapel mit 3 deterministisch tilted
                       Hintergrund-Layern (cyrb53-Hash der Deck-ID,
                       reproduzierbar pro Mount), Color-Stripe-Akzent,
                       Title/Description/Card-Count/Due-Badge
- DeckGrid.svelte      Auto-fill responsive Grid; selectedId-Prop
                       triggert Stufe-1-Animation (others fade-out,
                       selected scale-up)
- DeckFan.svelte       Auffächer-Detail-View (Hand of Cards) mit
                       Top-7-Karten als Spread, Mount-Animation
                       gestapelt → fanned via doppeltem rAF +
                       cubic-bezier-Transition
- utils/deck-tilt.ts   cyrb53 + stackLayers für Pseudo-Random-Tilts

Routing-Wechsel: Klick auf Deck-Stack → 220ms Stufe-1-Fade →
goto('/study/<id>') direkt in den Lernmodus. Detail-View
(/decks/<id>) bleibt erreichbar über "Karten verwalten →"-Link
in der Study-Sidebar.

Lernmodus visuell als Karte:
- Globale Header (Logo + Nav + Sprach-Switcher) im Lernmodus
  ausgeblendet (routes/+layout.svelte detektiert /study/<deckId>
  per Regex), volle Konzentration auf die Karte
- Lern-Karte ist CardSurface size="hero" mit aspect-ratio 5:7
  (Portrait, gleiches Verhältnis wie Deck-Stacks und Fan-Karten)
- Color-Stripe links zeigt die Deck-Farbe — visuelle Bindung an
  Herkunft
- Sidebar oben links absolute-positioniert: ← Decks, Deck-Name,
  Fortschritt, Karten verwalten → — kompakter UI-Block, der die
  Karten-Zentrierung NICHT beeinflusst (Karte bleibt geometrisch
  Bildschirm-Mitte)
- Reveal-Button + 4er-Grade-Grid (Wieder/Schwer/Gut/Leicht) als
  Aktions-Leiste UNTER der Karte — keine weiteren Karten, ein
  Review = eine Karte

Mobile (≤720px): Sidebar wird zur horizontalen Zeile oben, Karte
rückt darunter durch padding-top: 6.5rem.

Reduced-motion durchgängig respektiert (keine Tilts, keine Hover-
Lifts, keine Fan-Spread-Animation, kein Card-Transition).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:02:04 +02:00
Till JS
19a0036b82 feat(theming): forest variant from @mana/themes (sprint 9m)
Cards is the first app on the new 12-token mana-vereinsweite
theming system (mana/docs/THEMING.md). Forest-Variant aus
@mana/themes/variants/forest.css konsumiert via app.css-Import,
data-theme="forest" in app.html.

Token-Welt umgestellt — 158 renames + 304 hsl-wraps in 17 Files
(Python-Refactor, BSD-sed war zu unzuverlässig):
- --color-bg          → --color-background
- --color-fg          → --color-foreground
- --color-muted       → --color-muted-foreground
- --color-primary-fg  → --color-primary-foreground
- --color-danger      → --color-error
- bare var(--color-X) → hsl(var(--color-X)) durchgängig

Bridge-Aliase in app.css mappen die shared-ui@0.1.x-Erwartungen
(card, accent, surface-elevated-*, …) auf das 12er-Set. Mit
shared-ui@2.0-Refactor entfällt diese Sektion. --brand-cards-forest
als App-Identitäts-Hex separiert von Theme-Tokens.

Header konsumiert PillTabGroup aus @mana/shared-ui@0.1.1 für die
Routen-Navigation (Decks/Lernen/Library/Import/Stats) und den
DE/EN-Sprach-Switcher — visuell konsistent mit Vereins-Standard.

Cards' primary-Grün wurde dabei von 142 76% 36% (alter Live-Stand)
auf 142 76% 28% verdunkelt, damit primary-foreground/primary-
Kontrast WCAG-AA-konform (≥4.5) ist. Der alte Live-Stand hatte
Ratio 3.35.

i18n: deck_stack.aria_label, deck_detail.fan_aria, deck_detail.
card_open, decks.card_count_more, study_session.manage_link.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:01:37 +02:00
Till JS
404ddec62d docs(marketplace): CONTENT_PLAN — bilingualer CH/DE-Lehrplan-Korridor + 20 Phase-1-Seed-Decks
Strategie-Dokument für Cardecky-Editorial. Recherche-Synthese aus:

- Lehrplan 21 (CH) — 6 Fachbereiche + 2 Module, 3 Zyklen, 21
  deutschsprachige Kantone (Quelle: zh.lehrplan.ch +
  zh.ch/.../volksschule-schulinfo-unterricht/fachbereiche-und-module)
- KMK-Bildungsstandards (DE) — Primar 2022, Sek 1 HSA/MSA 2022/2023/
  2024, Sek 2 Abi 2012/2020. Bundesländer-Hoheit über Lehrpläne
- SRS-Forschungs-Konsens (Kang 2016, PMC) — Vokabeln + Fakten +
  Definitionen Goldstandard, kreatives Schreiben falsches Werkzeug
- Empirische Anki/Quizlet/Brainscape-Top-Domänen — Medizin (US),
  Sprachen (Japanisch riesig), Programmierung, Geografie, Geschichte

Kernfindung: Cardecky targetet die deutschsprachige Schul-Lücke
zwischen CH-Sek-1 und DE-Sek-1 (Klasse 7–9 / Zyklus 3), wo es heute
keine kuratierte SRS-Bibliothek gibt. Hot-Five-Schnittmenge:
Deutsch-Grammatik, Mathe, Englisch-Vokabeln, Naturwissenschaften,
Geografie.

Inhalt:

- Domain-Matrix mit SRS-Eignung × CH-Lehrplan × DE-KMK ×
  Beliebtheit, Tier A/B/C/D/E
- Stufen-Mapping pro Schulstufe (Zyklus 1 Skip → Sek 1 Kerngeschäft)
- Phase-1-Seed: 20 konkrete Tier-A-Decks (~3.100 Karten gesamt)
  mit Slugs, Karten-Counts, Lizenzen (CC-BY-4.0 default, CC0 für
  gemeinfreie Inhalte wie 1×1)
- Cold-Start-Hebel 1+2+3: Cardecky-Eigenproduktion (via /cards-deck-
  Skill mit menschlichem Reviewer-Stop), Anki-Top-Decks-Import mit
  Original-Author-Attribution + verified_mana-Badge, Lehrkräfte-
  Outreach
- Anti-Patterns: keine Medizin-Decks (anderer Markt), kein
  Religiös-Konfessionelles ohne Editorial-Linie, keine politischen
  Fakten ohne Konsens, kein Anki-Bashing, kein nationalistisches
  Framing
- Lizenz-Hygiene: Lehrplan-21-Inhalte selbst ist CC-BY-NC-ND, KMK-
  Material gemischt — wir formulieren Karten eigenständig neu auf
  Basis der Kompetenz-Bereiche, mit Quellen-Belegen pro Karte
- Offene Punkte: CH-Französisch-vs-DE-Englisch-Track, Schweizer
  Helvetismen, Jahrgangsstufen-Tagging-Schema-Erweiterung, Editorial-
  Gate für Featured, Anki-Importe-Lizenz-Audit, Image-Occlusion-
  Verifikation für Bio-Organe + Geografie-Decks

Owner: @till + Cardecky-Skill. Erste Welle: 5 priorisierte Decks
(Englisch A2/B1, Französisch A2, Mathe Sek 1, Geografie Welt).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 17:16:22 +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
40861710bf Phase 12 R5: Marketplace-Frontend — /explore + /d + /u + /me/{published,subscribed,forks}
Routes:
- /explore — Featured + Trending side-by-side, Browse mit Suche
  (Title/Description ILIKE), Sprachfilter, Sort (recent/popular/
  trending), load-more-Pagination
- /d/[slug] — Public-Deck-Detail mit Star/Subscribe/Fork-Buttons
  (Star + Subscribe sind toggle, Fork erstellt private cards.decks-
  Kopie und navigiert dorthin), Karten-Liste mit Discussion-Counts +
  Click-to-expand-Thread + Suggest-Edit-Modal, PR-Liste mit
  Owner-Merge/Reject + PR-Author-Close, Publish-Modal für Owner
- /u/[slug] — Author-Profil mit Verified-Badges (Mana/Community),
  Follow-Button, Decks-Liste
- /me/published — Author-Profil-CRUD (Slug + Display-Name + Bio +
  Pseudonym-Toggle), Liste eigener veröffentlichter Decks
- /me/subscribed — Abos mit prominentem update_available-Banner
- /me/forks — Geforkte Decks mit „Update ziehen"-Button →
  Smart-Merge-Pull (FSRS-State unveränderter Karten bleibt erhalten)

Components (apps/web/src/lib/components/marketplace/, eigener
Namespace ohne Konflikt zu Tills WIP-DeckGrid.svelte/DeckFan/
DeckStack):
- AuthorBadge — Display-Name + Verified-Symbole + Link aufs Profil
- DeckListGrid — 3-spalt Grid mit Author-Badge, Karten-/Star-/
  Subscriber-Counts, Sprache, Featured-Tag
- PublishVersionModal — SemVer-Eingabe (Default-Bump 1.0.0→1.1.0),
  Changelog, Karten als JSON-Array
- SuggestEditModal — Modify- oder Remove-Mode pro Karte, ergibt
  einen Pull-Request via /api/v1/marketplace/.../pull-requests
- DiscussionThread — Liste sichtbarer Comments inkl. Reply-Threading
  (parent_id), Hide-Button für Author oder Deck-Owner, Post-Form
- PullRequestList — Status-Filter, Diff-Summary +N ~M −R, per-PR
  Merge/Reject/Close-Buttons je nach Owner/Author-Permission

API-Client (apps/web/src/lib/api/marketplace.ts, ~440 Z.):
- Authors (CRUD + public lookup)
- Discovery (explore + browse + tags)
- Public Deck-Read + Init/Publish/Patch
- Engagement (Stars + Follows mit own-state-Endpoints)
- Subscribe + Fork + Pull-Update
- Pull-Requests (Lifecycle + List + Detail)
- Card-Discussions (Post + List + Counts + Hide)

Verifikation:

- svelte-check: 4017 Files, 0 errors, 5 Svelte-5-rune-Warnings
  (benigne — Modals capturen Init-Values von Props bewusst, weil sie
  pro Klick frisch gemountet werden; nicht-reactive ist gewollt)
- SSR-Smoke: /explore, /d/r5-stoa-grundlagen, /u/cardecky,
  /me/published liefern alle 200 — Routes mounten, Pages rendern
  initial mit Titles + Containern; API-Calls laufen client-side
  beim Mount
- Live-Daten: Test-Decks r5-stoa-grundlagen (Stoische Grundbegriffe,
  4 Karten v1.0.0) + r5-deutsche-historie (2 Karten) bewusst in
  lokaler cards-DB liegen gelassen, damit Browser sofort Inhalt hat

Bewusst nicht angefasst:

- Header.svelte ist in Tills uncommitted WIP — Header-Nav-Link auf
  /explore wird beim Theming-WIP-Commit nachgezogen. Marketplace-
  URLs sind aktuell direkt erreichbar via URL-Bar.
- type-check-Warnings nicht silencet — die 5 sind benign und das
  Refactoren auf $derived würde keine Verhaltens-Änderung bringen.

Verbleibend: R6 voller UI-E2E gegen das ganze System (Cardecky-
Deck-Publish + Till-Subscribe + Till-Fork + Till-Suggest-PR +
Cardecky-Merge + Till-Pull-Update — alles im Browser, manuell oder
Playwright). Polish (Empty-States, Loading-Skeletons, Pagination-
Edge-Cases) sammelt sich auf für eine separate Welle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:04:40 +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
e596199ba0 dev: dev:full + cards-dev-Alias + lokale mana-auth-Pipeline
Some checks are pending
CI / validate (push) Waiting to run
- pnpm dev:full chained: docker:up --wait → db:push → concurrently dev + dev:auth
- vite.config: ssr.noExternal + optimizeDeps.exclude für @mana/* aus Verdaccio
  (Raw-.ts-Sourcen brechen Node-22-Type-Stripping in node_modules)
- README + STATUS auf neuen Setup-Umfang aktualisiert (mana-auth läuft mit)
- concurrently als devDep, dev:auth/docker🆙auth/db:push:auth Sub-Scripts

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:38:51 +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