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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
**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>
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>
- `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>
- 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>
- /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>
- 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>
- 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>
- 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>
- 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>
- 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>
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>
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>
- 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>
- /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>
@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>
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>
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>
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>
Bug: Browser-Client requested localhost:3081 statt cardecky-api.mana.how
nach Login. Ursache: API_BASE und authBaseUrl() lasen die Variable
über import.meta.env.PUBLIC_*, was unter SvelteKit nicht zuverlässig
inlined wird (Vite-direct, ohne SvelteKit-Wrapper-Hook).
Fix: \$env/dynamic/public liest die env zur Runtime aus den Node-
Server-Variablen (adapter-node) — Browser bekommt sie über den
SSR-Init-Snapshot. Damit muss die Variable nur als runtime-env am
Container hängen, nicht als Build-Arg.
docker-compose.production.yml: PUBLIC_CARDS_API_URL und
PUBLIC_MANA_AUTH_URL aus build.args nach environment verschoben.
Build-Pipeline: cards-web muss neu gebaut werden, sonst greift der
Wechsel von static→dynamic env nicht.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mana-auth's /api/v1/auth/login schluckt das Set-Cookie der signedSession
(es nutzt die Cookie nur intern um den JWT zu minten) — der Browser
bekommt nur einen JSON-Body ohne Cookie. Damit funktioniert der spätere
/refresh-Endpoint, der auf Cookie-Auth basiert, nicht.
Two-step-Login:
1. POST /api/auth/sign-in/email (Better-Auth-native) — setzt das
__Secure-mana.session_token Cookie auf Domain=.mana.how. Liefert
User-Profil im Body.
2. POST /api/v1/auth/refresh mit credentials:include → accessToken.
Effekt: spätere Auto-Refreshs, Cross-App-SSO und 401-Retries laufen
korrekt. Memo für die Plattform-Seite: /api/v1/auth/login könnte das
Set-Cookie weiterreichen — dann wäre der zweite Call überflüssig.
Heute: aktiver Workaround.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cards-Web läuft jetzt auch über die 15min-JWT-TTL hinaus weiter:
- Login schickt credentials:'include' → SSO-Cookie auf .mana.how
wird gesetzt (mana-auth-Standard).
- tryRefresh() ruft mana-auth POST /api/v1/auth/refresh mit Cookie-
Auth, holt frischen accessToken, legt ihn in localStorage. Multi-
Caller werden zu einer Promise gecoalesced (kein Refresh-Storm).
- ensureFreshToken() prüft beim Konstruktor + vor jedem API-Request,
ob der Token noch ≥60s gültig ist. Wenn nicht, proaktiver Refresh.
- API-Client: 401 → tryRefresh() → exactly-once Retry. Erst wenn das
auch 401 gibt, wird die Session lokal geleert (führt User zurück
auf /).
- uploadMedia (multipart) parallel mit demselben Pattern.
- Logout ruft mana-auth /logout mit Cookie-Auth, damit das SSO-Cookie
für andere *.mana.how-Apps auch weg ist (best-effort, nicht-fatal).
- Boot-Pfad: bei abgelaufenem Token im localStorage behalten wir das
User-Profil temporär und versuchen einen still-Refresh — User sieht
beim Reload kein Login-Flackern, wenn die Cookie-Session noch lebt.
svelte-check 384 files 0 errors, 7 Web-Tests grün, prod-Build sauber.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>