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>
cards-api akzeptiert jetzt zwei Auth-Quellen:
1. Authorization: Bearer <jwt> — JWT aus mana-auth, gegen
/api/auth/jwks gecached, sub→userId
2. X-User-Id: <uuid> — Dev-Stub, bleibt aktiv
Schalter: CARDS_AUTH_DEV_STUB=false deaktiviert den Dev-Stub
hart (für die spätere Phase-10b-Cutover-Stufe). Default behält
beide Pfade — Tests + Anki-Importer + Sprint-9-User-Smokes
laufen unverändert weiter.
Cards-App ist heute in mana-auth (auth.mana.how) registriert
(app_id='cards', ownership_kind='verein', status='active'),
Service-Key generiert + per direktem DB-Insert in
auth.app_service_keys gelegt (Migrations-Tabellen waren in
prod-DB noch nicht angewendet, jetzt nachgezogen).
56 API-Tests grün, jose als neue Dependency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Eigener cards-minio-Container im docker-compose (9100/9101 — Plattform
auf 9000/9001 bleibt isoliert). cardsadmin/cardsadmin als Dev-Default,
prod via env-Vars (CARDS_S3_*).
apps/api/src/services/storage.ts — schmaler StorageService um den
minio-Client. ensureBucket() ist idempotent (auto-create beim ersten
Upload). removeObjectsByPrefix() implementiert den DSGVO-Bucket-Sweep,
weil die S3-API kein Cascade kennt.
Neue Tabelle media_files in pgSchema('cards'):
id, user_id, object_key, mime_type, original_filename, size_bytes,
kind, created_at — kein FK auf cards (ein File kann mehreren Karten
gehören). objectKey-Format <userId>/<ulid>.<ext> für Bucket-Prefix-
Sweep beim DSGVO-Delete. Legacy mediaRefs bleibt als Slot.
Neuer Router /api/v1/media:
POST /upload — multipart, 25 MiB Default-Limit, image/audio/video
only (415 sonst), schreibt media_files-Row + speichert
in MinIO unter <userId>/<ulid>.<ext>
GET /:id — streamt aus MinIO mit Cache-Control: private,
immutable. Cross-User → 404 (nicht 403, anti-enumeration).
GET / — listet alle eigenen Files
DSGVO-Pfade (Service-Key + /me/delete) räumen jetzt auch media_files
+ MinIO-Bucket-Prefix mit ab. Storage-Sweep ist non-fatal — DB ist erst
konsistent gelöscht, dead bytes wären die schlimmstmögliche Folge.
Anki-Import: parse.ts sanitizeAnkiHtml akzeptiert wieder eine
Filename→URL-Map (war in Phase 8c gedroppt). import.ts lädt vor den
Karten alle referenzierten Media-Files via uploadMedia() in MinIO,
sammelt URLs, ersetzt Anki-Filenames durch /api/v1/media/<id>-Pfade
in `<img>` (Markdown) und `[sound:…]` (HTML <audio>). 4-fache Worker-
Concurrency.
apps/web/src/lib/markdown.ts: DOMPurify lässt jetzt <audio>/<video>/
<source> mit src/controls/preload-Attributen durch — sonst würden die
Audio-Tags aus dem Anki-Import gestrippt.
i18n-Strings (DE/EN) auf Media-Stage erweitert: stage_media,
done_media, what_works_media, dropzone_hint, preview_media.
import.what_skipped_media wird zur Bestätigung dass Media seit
Sprint 9k mit übernommen wird.
Manueller E2E-Smoke gegen lokale MinIO (cards-minio :9100):
- 1×1-PNG hochgeladen → 201 mit ID + URL
- /api/v1/media/<id> streamt 200 image/png 69 bytes (file-Identifikation
bestätigt)
- Cross-User → 404, ohne X-User-Id → 401, text/plain → 415
53 API-Tests grün (+4 neue media-Auth-Gate-Tests), 7 Web-Tests,
51 Domain-Tests, type-check + svelte-check 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Strategie-B-Ausnahme: parse.ts (Anki-Format-Parser via JSZip + sql.js)
und AnkiImport.svelte (UI-Stages) sind aus mana-monorepo portiert,
mit Source-Comment-Header dokumentiert. Anki-Format ist standalone
Parser-Logik, kein Architektur-Schmuggel.
Neuer server-authoritative import.ts schreibt direkt gegen die
cards-api ($lib/api/decks + cards) — keine Stores, keine Dexie.
Anki "::"-Hierarchie wird zu " / "-Strings flach. Fallback-Deck
"Anki-Import" für Karten ohne explizites Deck. Cloze-Karten kommen
first-class durch (Sub-Index pro Cluster, Sprint 8a/8b).
Phase-8-MVP-Scope: Bilder + Audio werden gedroppt (Option A) — der
sanitizeAnkiHtml entfernt <img> und [sound:…] ersatzlos. Späterer
Media-Pfad (lokaler Cards-Upload oder mana-media nach Phase 2) ist
additiv.
Neue Route /import + Top-Nav-Link. Hermetic Vitest (5 Cases): baut
zur Laufzeit ein Mini-.apkg via sql.js + JSZip und prüft den
Parser-Output (basic, basic-reverse, cloze, sanitize, dedupe auf
Note-Ebene). svelte-check 0 errors, prod-Build sauber.
sql-wasm.wasm liegt in static/ (660kB) — fix für sql.js 1.14.1, vom
Browser einmal geladen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>