Commit graph

123 commits

Author SHA1 Message Date
Till JS
1743fe6453 docs(status): Title cards → Wordeck
Some checks are pending
CI / validate (push) Waiting to run
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 23:25:41 +02:00
Till JS
2473ccd65d fix(dockerfiles): packages/cards-domain → wordeck-domain
Some checks are pending
CI / validate (push) Waiting to run
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:40:31 +02:00
Till JS
372832d266 refactor(big-bang): cards → wordeck im gesamten Code-Layer
Some checks are pending
CI / validate (push) Waiting to run
Phase 2 des cards→wordeck Big-Bang-Rebrand:

- 4 package.json: @cards/* → @wordeck/*
- packages/cards-domain/ → packages/wordeck-domain/
- 41+12 Files: from '@cards/domain' → '@wordeck/domain'
- pgSchema('cards') → pgSchema('wordeck') (Drizzle-Schema)
- 17 Files: process.env.CARDS_* → process.env.WORDECK_*
- docker-compose Service-Names: cards-* → wordeck-*
- docker-compose Volume: /Volumes/ManaData/cards → wordeck
- env-vars in compose: CARDS_DB_PASSWORD/_API_VERSION/_DSGVO_SERVICE_KEY etc. → WORDECK_*
- Log-Prefixes + Error-Strings + manifest-id 'cards' → 'wordeck'
- CORS-Origin cardecky.mana.how → wordeck.com
- .env.production.example umbenannt + S3-Key entfernt (kein MinIO mehr)

Type-Check 0 Errors in api+domain+web, 51/51 Domain-Tests grün.

DB-Rename + Container/Volume-Rename auf mana-server folgen in nächstem
Commit nach Verzeichnis-Rename.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:39:42 +02:00
Till JS
c77100e85a chore: stale Cardecky-Refs entfernt + apps/landing/ gelöscht
Some checks are pending
CI / validate (push) Waiting to run
- apps/landing/ entfernt (Cardecky-Marketing-Astro-Site, war nie
  deployed — der nginx-Block in landings.conf zeigte auf einen
  Pfad ohne Inhalt)
- Stale Doc-Kommentare in api+web: Cardecky → Wordeck wo passend
- fsrs.ts subIndexCount: image-occlusion + audio-front Cases raus
  (CardType-Enum hat sie nicht mehr — der throw wäre toter Code)
- fork.ts: image-occlusion-Sonderfall in subIndexCountFor weg
- cards-domain tests: schemas-Test prüft jetzt Wordeck-Typen + dass
  image-occlusion/audio-front ABGELEHNT werden
- cards-domain tests: image-occlusion-throw-Test entfernt
- api tests/cards.test.ts: 3 image-occlusion-Cases zu 1 negative
  Tested (422 expected, weil Schema sie verbietet)

51 Tests grün (cards-domain).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:58:52 +02:00
Till JS
9b37243b1d docs: STATUS auf Wordeck-LIVE-Stand
Some checks are pending
CI / validate (push) Waiting to run
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:27:12 +02:00
Till JS
17bf6f4d66 fix(migration): 0004 droppt auch cards.media_refs (separate Tabelle, nicht nur Spalte)
Some checks are pending
CI / validate (push) Waiting to run
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:26:27 +02:00
Till JS
e3c84a9249 feat(text-only): Wordeck-Cutoff für Image-Occlusion + Audio + MinIO
Some checks are pending
CI / validate (push) Waiting to run
Ω-1: Text-Only-Architektur ist scharfgestellt.

Code-Cleanup:
- 4 Components gelöscht: ImageOcclusionEditor, ImageOcclusionView,
  AudioFrontView, AudioUploadField
- 3 API-Module gelöscht: routes/media.ts, services/storage.ts,
  db/schema/media.ts (mediaRefs + mediaFiles), routes/decks-from-image.ts
- packages/cards-domain: image-occlusion.ts + Tests entfernt,
  CardTypeSchema reduziert auf basic/basic-reverse/cloze/typing/multiple-choice
- 3 Web-Routes (study/[deckId], cards/new, cards/[id]/edit) bereinigt:
  Image-Occlusion- und Audio-Front-Code-Pfade raus
- anki/import.ts text-only: kein Media-Upload mehr, img/sound werden
  ersatzlos gestrippt
- 21 weitere Files bereinigt: dto, health, me, dsgvo, tools, cards,
  decks, share-handlers, marketplace/decks, marketplace/fork,
  marketplace/pull-requests, AnkiImport.svelte

DB-Migrationen (noch nicht gerannt, idempotent):
- 0004_wordeck_text_only.sql: DELETE image-occlusion/audio (0 betroffene
  Rows), media_files-Tabelle DROP, media_refs-Spalte DROP, CHECK
  cards.type IN (basic, basic-reverse, cloze, type-in, multiple-choice)
- 0005_wordeck_license_rename.sql: Cardecky-Personal-Use-1.0 →
  Wordeck-Personal-Use-1.0, Cardecky-Pro-Only-1.0 → Wordeck-Pro-Only-1.0,
  Default + CHECK + Backfill

Infrastruktur:
- docker-compose.production.yml: cards-minio-Service raus, MinIO-Envs
  aus cards-api raus, CARDS_PUBLIC_URL + PUBLIC_CARDS_API_URL auf
  wordeck.com / api.wordeck.com
- App-Manifest schon vorher auf wordeck umgestellt

Type-Check grün (api, domain, web — alle 3 Sub-Pakete).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:23:30 +02:00
Till JS
1228eb4692 fix(web): app.html default-title Cards → Wordeck
Some checks are pending
CI / validate (push) Waiting to run
Vergessener Branding-Stelle aus Bulk-Rename. Der app.html-Title
greift bei Seiten ohne expliziten <svelte:head><title>.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:49:15 +02:00
Till JS
39fe22a018 feat: Wordeck-Web-Branding + Manifest-Cutover
Some checks are pending
CI / validate (push) Waiting to run
Cards-zu-Wordeck-Umbennung im Web-Layer komplett:

- app-manifest.json: id, name, homepage, icon, base_url, deep_link_scheme,
  link_patterns alle auf wordeck. tools[].name auch (cards.create →
  wordeck.create — bei mana-mcp wird das beim re-upsert konsumiert).
- 5 i18n-Files (de/en/fr/es/it): App-Beschreibung von Cardecky-zu-Wordeck-
  Wording, USP "text-first" wo es passt.
- 11 Routes mit Page-<title>: Cardecky → Wordeck.
- CSP connect-src ergänzt um api.wordeck.com (cardecky-api bleibt während
  Übergang).
- AASA exposed jetzt BEIDE Bundle-IDs (ev.mana.cardecky alt + ev.mana.wordeck
  neu) — die alte Native bleibt während der Ω-3 Bauphase funktional.
- UI-Slug-Vorschau (PublishDeckModal, +page, me/published): wordeck.com.

DNS für wordeck.com + api.wordeck.com + www.wordeck.com sind als
Cloudflare-Tunnel-CNAMEs angelegt; cloudflared-Ingress auf mana-server
patched + reloaded. wordeck.com antwortet HTTP 200 mit Cards-Container
(Branding-Update hier macht das jetzt zu Wordeck).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:46:10 +02:00
Till JS
22927fbe59 feat: Wordeck-Vorarbeiten — Datenschutz-Seite + Text-Only-Migration
Some checks are pending
CI / validate (push) Waiting to run
Privacy/+page.svelte: Cards → Wordeck, MinIO-Erwähnung entfernt
(text-only, kein Object-Storage), Bilder/Audio explizit als
"speichern wir NICHT" markiert, Bundle ev.mana.wordeck.

0004_wordeck_text_only.sql: Migration für CardType-Cutoff
(image-occlusion/audio raus, CHECK-Constraint, media_files-DROP).
Vor-Audit gegen prod hat 0 betroffene Karten gezeigt — die Migration
ist defensiv und idempotent, kann ohne Daten-Verlust gefahren werden.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:36:20 +02:00
Till JS
29351f881d docs(content-plan): Author-Slug von cardecky zu wordeck migriert
Some checks are pending
CI / validate (push) Waiting to run
Big-Bang Author-Slug-Migration vollzogen am 2026-05-17:
- /u/wordeck/<slug> → 200 (alle 43 Decks)
- /u/cardecky/<slug> → 404 (URL-Bruch akzeptiert)
- Login-Email cardecky@mana.how bleibt (technische Identität)
- owner_user_id der Decks unverändert (Join läuft über User-ID)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:32:59 +02:00
Till JS
4c0eb0a7b1 docs: Cards-Rebrand zu Wordeck angekündigt
Some checks are pending
CI / validate (push) Waiting to run
Brand-Wechsel zu Wordeck (wordeck.com) ist beschlossen (2026-05-17).
Wordeck wird text-only — image-occlusion und audio fallen weg, neuer
iOS-Bundle ev.mana.wordeck, Pre-Cutover-Export für betroffene User.

- STATUS.md: Banner am Anfang mit Verweis auf das Playbook
- CLAUDE.md: neue Architektur-Invariante 0 (Text-only)
- CONTENT_PLAN.md: Owner-Slug cardecky bleibt, Brand-Wechsel-Notice

Vollständiger Plan: mana/docs/playbooks/WORDECK_REBRAND.md (eigener
Commit im mana-Repo).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:54:47 +02:00
Till JS
f3445a8f9d docs(content-plan): Phase-1-Seed komplett — 20/20 Tier-A live + 5 Tier-B-Bonus
Some checks are pending
CI / validate (push) Waiting to run
Tracking-Update nach der Cardecky-Content-Welle vom 2026-05-17. Alle
Phase-1-Tier-A-Decks aus §8 (Naturwissenschaften, Sprache, Mathe,
Geografie/Geschichte) sind jetzt im Marketplace . Dazu 5 neue
Tier-B-Bonus-Decks (B-16..B-20): Schweizer Kantone, Helvetismen,
Informatik Sek 1, Italienisch A2 (534), Latein-Grundwortschatz (199).

Marketplace-Stand: 43 Decks · 5572 Karten · 100 % kategorisiert
(language/science/math/history/technology). Audit-Trail pro Deck
liegt außerhalb des Repos in ~/Documents/cards-drafts/ (laut
Cardecky-Skill-Konvention).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:42:04 +02:00
Till JS
ac05afa93e devlog: 6 Tage geschrieben (Greenfield → Hardening + Cardecky-Native-Vorbereitung)
Some checks failed
CI / validate (push) Has been cancelled
Tag 1: Phase 0–10c Marathon, Live-Cut auf cardecky.mana.how.
Tag 2: Marketplace-Restore (Phase 12 R0–R5 + G1–G4).
Tag 3: Karten-Typ-Vollausbau (Periodensystem, audio, typing,
multiple-choice, Vision-LLM-Deck-Generation).
Tag 4: Mobile-Nav + 5 Sprachen + CSV/PDF + Astro-Landing.
Tag 5: Security-Hardening (fail-secure, CSP, DSGVO-Audit,
rate-limit) + Leech-Detection + AASA.
Tag 6: Recovery + Undo + FSRS-Slider + Streak + Stats-Charts +
Blog + Marketplace-Report + Privacy/Help.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:23:29 +02:00
Till JS
ff00c7d961 feat(marketplace): Deck-Report + Author-Block + me/decks-Endpoints
Some checks failed
CI / validate (push) Has been cancelled
Cardecky-Marketplace bekommt die App-Store-Guideline-5.1.1(v)-
Pflicht-Komponenten für User-Generated-Content: User können einzelne
Decks melden und Authors blockieren. Plus `GET /me/decks` für den
Native-Re-Publish-Flow.

Schema (Migration 0003)
- Neue Tabelle `marketplace.author_blocks (blocker_user_id,
  blocked_user_id, created_at)` mit Unique-Index auf dem Tupel
- `deckReports` lag schon im Schema, jetzt erstmals durch Routes
  erreichbar

Routes
- POST /api/v1/marketplace/decks/:slug/report — auth, 10/min Rate-
  Limit, Kategorie-Enum (spam, copyright, nsfw, misinformation, hate,
  other), optional `body` ≤ 1000 Zeichen. Idempotent pro (deck,
  reporter, category): doppeltes Melden liefert `already_reported:
  true` ohne Fehler. Owner darf eigenes Deck nicht melden.
- POST /api/v1/marketplace/authors/:slug/block — idempotent
  (onConflictDoNothing). Self-Block geht 422.
- DELETE /api/v1/marketplace/authors/:slug/block
- GET /api/v1/marketplace/me/blocks — eigene Block-Liste mit
  display_name + blocked_at
- GET /api/v1/marketplace/me/decks — eigene Marketplace-Decks mit
  latest_version (semver, card_count, published_at). Native nutzt das
  für die „Neue Version"-Auswahl im Publish-Flow

Listing-Filter
- explore.ts: `browseImpl` nimmt `signedInUserId?` und filtert
  blockierte Author-Decks per `NOT EXISTS`. Wirkt auf /explore +
  /decks (Browse mit Filtern)
- decks.ts: `GET /:slug` returnt 404 wenn der Viewer den Author
  blockiert hat — bewusst 404 statt 403, UGC-Block soll ohne Hinweis
  auf den Block wirken

Mount: zwei neue Router auf /api/v1/marketplace (moderation) und
/api/v1/marketplace/me. 104/104 Vitest-Tests grün.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 02:04:54 +02:00
Till JS
eb39faddb3 feat(web): /auth/reset + /auth/verify als Fallback-Pages
Some checks are pending
CI / validate (push) Waiting to run
Wenn der Reset-Link aus der Email auf einem Gerät ohne installierte
Cardecky-iOS-App geöffnet wird (Desktop, Android, iOS-ohne-App),
fängt Apple den Universal-Link nicht ab — der Browser landet auf
cardecky.mana.how/auth/reset. Heute = 404.

Diese minimalen Brücken redirecten den Token an die jeweilige
auth.mana.how-Surface, damit der Reset/Verify-Flow trotzdem durchläuft:

  cardecky.mana.how/auth/reset?token=X
    → auth.mana.how/reset-password?token=X (Web-Reset-Formular)

  cardecky.mana.how/auth/verify?token=X
    → auth.mana.how/api/auth/verify-email?token=X (Better-Auth-Endpoint)

iOS mit installierter App: Universal-Link greift, Browser-Page wird
nie gerendert.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:18:44 +02:00
Till JS
8c7c8c9c98 feat(aasa): /auth/* in Universal-Link-Paths
Reset- und Verify-Mails aus mana-auth zeigen jetzt direkt auf
cardecky.mana.how/auth/reset?token=… — damit der iOS-Universal-Link
greift, muss /auth/* in der AASA-Paths-Liste sein.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:16:58 +02:00
Till JS
4d905bb4cd fix(api): 0002_decks_archived_at — schließt Schema-Drift
Some checks are pending
CI / validate (push) Waiting to run
Production-DB hatte cards.decks.archived_at nicht, obwohl 0000_baseline
die Spalte im CREATE TABLE hat. Ursache: Schema-Datei wurde nach
initialer DB-Provisionierung um archived_at erweitert, ohne separate
ADD-COLUMN-Migration zu generieren. Resultat: cards-native
DeckListView triggert GET /api/v1/decks → isNull(decks.archivedAt) →
PostgresError → HTTP 500.

Fix:
- 0002_decks_archived_at.sql mit ALTER TABLE IF NOT EXISTS (idempotent)
- _journal.json updaten
- Auf Production manuell schon angewandt 2026-05-13 — diese Migration
  ist nur für fresh Setups + Migration-State-Konsistenz

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:32:25 +02:00
Till JS
c6488c0b83 feat(web): /privacy + /help Stubs für App-Store-Submission
Some checks are pending
CI / validate (push) Waiting to run
Apple verlangt für jede App-Store-Submission verlinkte Privacy-Policy
und Support-Page. Beide jetzt als SvelteKit-Routes mit Verein-Content.

/privacy:
- Was wir speichern (Account/Inhalte/Reviews/Server-Logs)
- Was wir NICHT machen (kein Ad-Tracking, kein SaaS-Crashreporter)
- Welche Dienste (Cloudflare-Tunnel, Eigenhosting)
- DSGVO-Rechte (Export, Löschung)
- Native-App-Spezifika (SwiftData-Cache, Keychain)

/help:
- Kontakt kontakt@mana-ev.ch
- FAQ (FSRS, Anki-Import, Offline, Marketplace, Mitmachen)
- Bug-Report-Anleitung

Beide nutzen die 12-Token-CSS-Vars (--color-foreground etc.) für
Theme-Konsistenz.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:10:45 +02:00
Till JS
abf493aeec feat(cards): recovery mode, undo, FSRS slider, streak header, stats charts, blog
Some checks are pending
CI / validate (push) Waiting to run
Study-View:
- Graceful Backlog Recovery: Banner bei >30 fälligen Karten, Recovery-Queue
  sortiert nach Stability aufsteigend (25er-Batch, ?recovery=true)
- Undo letzte Bewertung: 5s-Toast mit RAF-Fortschrittsbalken, Ctrl/Cmd+Z,
  prevSnapshot-Spalte in reviews (Migration 0001, Prod deployed)
- FSRS-Tooltip nach Reveal: State / Stability / Difficulty als Popover

Deck-Edit:
- Neuer Abschnitt „Lern-Algorithmus" mit request_retention-Slider (50–99 %)

Header:
- Streak-Pill (🔥 N) + fällige-Karten-Pill via GET /api/v1/me/summary

Stats-Page:
- Difficulty-Distribution (5 Buckets, Farb-Bars)
- Deck-Fortschritt (Mastery % = stability>21, max 6 Decks)

API:
- GET /me/summary: streak_days + due_now (leichtgewichtiger Header-Endpoint)
- GET /reviews/due: ?recovery=true → stability-sort, Limit 25
- POST /reviews/:cardId/:subIndex/undo: prevSnapshot-Restore, 409 wenn leer
- /me/stats: difficulty_distribution + deck_mastery

Landing:
- 5 Blog-Artikel (Quizlet-Paywall, FSRS, Datenschutz, Anki, Lernkarten-Tipps)
- BlogTeaser-Komponente auf Startseite, Footer-Spalte „Artikel"

i18n: 11 neue Schlüssel in DE/EN/FR/IT/ES

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 13:37:03 +02:00
Till JS
21ec535173 fix(web): AASA bundleId ev.mana.cards → ev.mana.cardecky
Some checks are pending
CI / validate (push) Waiting to run
App-ID im Apple-Developer-Portal heißt ev.mana.cardecky (analog
zur Brand cardecky.mana.how). AASA-appID nachgezogen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:26:43 +02:00
Till JS
e68d53bfbb feat(infra): PUBLIC_APPLE_TEAM_ID für AASA-Endpoint
Some checks are pending
CI / validate (push) Waiting to run
mana e.V. Apple-Developer-Team-ID QP3GLU8PH3 ins cards-web-Service-
Environment in docker-compose.production.yml. Wird zur Runtime von
$env/dynamic/public aufgelöst und in
/.well-known/apple-app-site-association eingesetzt — Apple matched
das mit dem applinks:cardecky.mana.how-Entitlement in cards-native
(Bundle ev.mana.cards).

Aktivierung: docker compose -f docker-compose.production.yml up -d cards-web
auf mana-server. Verify nach Deploy:
  curl https://cardecky.mana.how/.well-known/apple-app-site-association
  → "appID":"QP3GLU8PH3.ev.mana.cards" statt XXXXXXXXXX.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:17:27 +02:00
Till JS
bdce9c98b6 feat(web): AASA-Endpoint für cards-native Universal-Links
Some checks are pending
CI / validate (push) Waiting to run
SvelteKit-Server-Route unter /.well-known/apple-app-site-association
mit Content-Type application/json. Apple holt diese Datei beim ersten
App-Launch und matched gegen das applinks:cardecky.mana.how-Entitlement
in cards-native (siehe Tag v0.8.0 in git.mana.how/till/cards-native).

- paths: /d/* (Public-Deck-Detail) + /u/* (Author-Profile)
- Team-ID via env PUBLIC_APPLE_TEAM_ID, Default Platzhalter XXXXXXXXXX
- Cache-Control: public, max-age=3600

Production-Deploy braucht echte Team-ID — bis dahin akzeptiert Apple
die AASA nicht. Details: cards-native/docs/RELEASE_CHECKLIST.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 01:56:36 +02:00
Till JS
9f019d8e2f feat(cards): leech detection in /me/stats + Stats-Page-Sektion
Some checks are pending
CI / validate (push) Waiting to run
Karten mit ≥4 Lapses werden im Stats-Endpoint als `leech_cards`
geliefert (mit Front-Snippet, Deck-Name, Lapses-Count, sortiert
desc, max 20). Stats-Page zeigt eine rote „Schwierige Karten"-
Sektion mit Link in den Card-Editor.

* apps/api/src/routes/me.ts: GROUP BY card → SUM(lapses) ≥ 4
  Filter, frontSnippetFor()-Helper für alle 6 Card-Types
  (basic, basic-reverse, cloze, image-occlusion, audio-front,
  typing, multiple-choice). Cloze-Markup wird gestrippt damit
  der Snippet UI-tauglich ist.
* apps/web Stats-Page: neue CardSurface mit Warning-Icon, scrollbare
  Liste mit Front + Deck + Lapses-Badge, Empty-State.
* i18n in DE/EN/FR/IT/ES (5 Strings + plural-Form).

104/104 Tests grün (kein neuer Test — bestehende /me/stats-Tests
covern die Aggregations-Form), web check 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 19:01:48 +02:00
Till JS
4bb1390180 db(cards): baseline migration + drizzle-tracking bootstrap script
Some checks are pending
CI / validate (push) Waiting to run
Schließt die Ops-Lücke „kein versioniertes Schema-Tracking" aus
FEATURE_IDEAS.md.

* apps/api/src/db/migrations/0000_baseline.sql — Drizzle-generierte
  Baseline-Migration, 355 Zeilen, 25 Tabellen + 5 Enums (cards- und
  marketplace-Schema). Eingefrostet auf den Live-Stand 2026-05-12.
* apps/api/scripts/bootstrap-drizzle-tracking.ts — neues Script,
  markiert die Baseline in einer bestehenden DB als „bereits
  angewandt", ohne SQL erneut auszuführen. Verwendet sha256 wie
  drizzle-orm/migrator (Hash 312d67ba1aeb…), idempotent.
* package.json: drizzle:migrate + drizzle:bootstrap-tracking
  npm-scripts.
* docs/playbooks/DRIZZLE_MIGRATIONS_BOOTSTRAP.md — Hand-Over für
  Prod (Bootstrap einmalig, dann normaler Workflow:
  schema → generate → commit → migrate, kein push --force mehr).

Lokal verifiziert: 17/104 Tests grün, bootstrap idempotent,
drizzle-kit migrate erkennt die Baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:53:52 +02:00
Till JS
5a29dd9a8c security(cards): CSP report-only + service-key rotation playbook
Some checks are pending
CI / validate (push) Waiting to run
Folge-Hardening zu e1ddbf3, Cluster A2+A3 aus FEATURE_IDEAS.

* hooks.server.ts: restriktive CSP im Report-Only-Modus
  (default-src 'self', script-src 'self', connect-src whitelist
  auf cardecky-api/auth.mana.how/share/mcp). CARDS_CSP_ENFORCE=true
  flippt auf den scharfen Header.
* docs/playbooks/SERVICE_KEY_ROTATION.md: 5-Schritt-Rotation für
  CARDS_DSGVO_SERVICE_KEY bis Phase F-1 (mana-auth-managed Keys).

Forensik der Bypass-Periode 2026-05-08 → 2026-05-12 ist abgeschlossen:
nur 2 user_ids in der Cards-DB, beide legitim (tills95@gmail.com +
Smoke-Test-Sentinel c1a5, letztere via DSGVO-Endpoint aufgeräumt).
Kein ausgenutzter Bypass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:40:29 +02:00
Till JS
e1ddbf34b3 security(cards): fail-secure dev-stub, headers, rate-limit, dsgvo audit
Some checks are pending
CI / validate (push) Waiting to run
Behebt live verifiziertes Auth-Bypass auf cardecky-api.mana.how
(X-User-Id → founder-Tier) und zieht im selben Patch das fehlende
Operations-/Compliance-Fundament nach.

* Auth-Middleware fail-secure: opt-in via CARDS_AUTH_DEV_STUB="true"
  (war opt-out, Default true). Compose-Default flipped auf "false",
  NODE_ENV="production" für cards-api ergänzt, env-Template
  dokumentiert. vitest.config.ts + tests/setup.ts aktivieren den
  Stub gezielt für Test-Suiten.
* Security-Headers: Hono secureHeaders() in apps/api,
  SvelteKit hooks.server.ts mit X-Frame/X-Content-Type/Referrer/
  HSTS in apps/web. CSP bewusst ausgelassen — eigener Sprint.
* CORS-localhost-Whitelist nur außerhalb Prod.
* Rate-Limiting (in-memory sliding window, dependency-frei) auf
  share.receive 60/min/IP, media.upload 30/min/user,
  decks.generate + decks.from-image 10/min/user, dsgvo.* 10/min/IP.
* Health-Endpoint mit echter DB- und MinIO-Probe; /healthz bleibt
  Liveness, /healthz/details ist Readiness mit 503 bei Failure.
* DSGVO-Honesty: storage_ok + storage_error im Response (statt
  schluckend console.warn), Account-UI zeigt Fehler-Toast.
* Audit-Log: strukturierte JSON-Zeile (kind: "audit") auf stdout
  für /dsgvo/export, /dsgvo/delete, /me/export, /me/delete.
* Bug-Fix: duplizierte case "multiple-choice"-Clause in fsrs.ts.

Verifiziert: apps/api 17 Files / 104 Tests grün, apps/web check
0 errors. Deploy auf Mac Mini steht noch aus.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:56:03 +02:00
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
7116bd66b4 chore: pnpm-lock.yaml nach landing-Deps-Install aktualisiert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:41:45 +02:00
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