diff --git a/.env.development b/.env.development index 4f9c72b9c..3dc68bcad 100644 --- a/.env.development +++ b/.env.development @@ -23,6 +23,10 @@ PUBLIC_GLITCHTIP_DSN= # Mana Core Auth Service MANA_AUTH_URL=http://localhost:3001 +# Auth-Portal-UI (Login/Register/Reset, getrennt vom Auth-API-Service). +# In Prod identisch mit MANA_AUTH_URL (nginx splittet /api/* zu mana-auth, +# Rest zu mana-auth-web), lokal aber eigener Port (mana-auth-web :3002). +MANA_AUTH_WEB_URL=http://localhost:3002 # Mana Credits Service MANA_CREDITS_URL=http://localhost:3061 # Mana Media Service (CAS, thumbnails, Photos gallery) diff --git a/.env.macmini.example b/.env.macmini.example index 960fdc0cb..d500a7d03 100644 --- a/.env.macmini.example +++ b/.env.macmini.example @@ -21,7 +21,7 @@ # Compose project name (pinned, do not change) # ============================================ # All Mac Mini containers were originally created under this project -# name, which mismatches the current directory name (mana-monorepo). +# name, which mismatches the current directory name (managarten). # Pinning the project name here means anyone running 'docker compose ...' # from the repo root automatically lands in the same project as the # already-running containers, instead of silently spawning a duplicate diff --git a/.github/workflows/cd-macmini.yml b/.github/workflows/cd-macmini.yml index 84b544d70..8a75ff396 100644 --- a/.github/workflows/cd-macmini.yml +++ b/.github/workflows/cd-macmini.yml @@ -55,7 +55,7 @@ concurrency: cancel-in-progress: false # Don't cancel in-progress deploys env: - PROJECT_DIR: /Users/mana/projects/mana-monorepo + PROJECT_DIR: /Users/mana/projects/managarten COMPOSE_FILE: docker-compose.macmini.yml ENV_FILE: .env.macmini DOCKER_BUILDKIT: 1 @@ -339,12 +339,17 @@ jobs: # If a service has no Drizzle config or no schema diff this is # a fast no-op. We must source POSTGRES_PASSWORD from the env # file because the workflow env doesn't carry it. + # + # `. "$ENV_FILE"` (bash source) breaks on DOTENV-Werte, die + # ungequotete Leerzeichen enthalten — z.B. `MANA_AI_PUBLIC_KEY_PEM` + # mit `-----BEGIN PUBLIC KEY-----…`: bash interpretiert "PUBLIC" + # als nächstes Command und scheitert mit `PUBLIC: command not + # found`. Backup-Script hat dasselbe Problem (Commit 97e285bc6). + # Wir lesen daher gezielt nur die benötigten Variablen via grep + # statt die ganze Datei als Shell-Script zu sourcen. echo "=== Applying schema migrations ===" - set -a - # shellcheck source=/dev/null - . "$ENV_FILE" - set +a - PG_PASSWORD="${POSTGRES_PASSWORD:-mana123}" + PG_PASSWORD=$(grep -E '^POSTGRES_PASSWORD=' "$ENV_FILE" | head -1 | cut -d= -f2- | sed 's/^"\(.*\)"$/\1/; s/^'"'"'\(.*\)'"'"'$/\1/') + PG_PASSWORD="${PG_PASSWORD:-mana123}" # `drizzle-kit` reads `drizzle.config.ts`, which itself # `import {defineConfig} from 'drizzle-kit'`. Node's resolver diff --git a/.github/workflows/mirror-to-forgejo.yml b/.github/workflows/mirror-to-forgejo.yml index 9b0f20415..55f950024 100644 --- a/.github/workflows/mirror-to-forgejo.yml +++ b/.github/workflows/mirror-to-forgejo.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Mirror to Forgejo via SSH run: | - cd /Users/mana/projects/mana-monorepo + cd /Users/mana/projects/managarten # Stash any local changes so pull never fails git stash --quiet 2>/dev/null || true @@ -25,5 +25,5 @@ jobs: # Push to Forgejo via localhost SSH (runner is on the Mac Mini) GIT_SSH_COMMAND='ssh -p 2222 -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no' \ - git push ssh://git@localhost:2222/till/mana-monorepo.git main 2>&1 + git push ssh://git@localhost:2222/till/managarten.git main 2>&1 echo "Mirrored to Forgejo" diff --git a/CLAUDE.md b/CLAUDE.md index bf8e39cc2..9c699c686 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -160,7 +160,7 @@ Enforced by `pnpm run validate:turbo` (`scripts/validate-no-recursive-turbo.mjs` | `@mana/shared-branding` | App registry, tiers, branding | | `@mana/shared-types` | Common TS types | | `@mana/shared-utils` | Utility functions | -| `@mana/shared-ui` | React Native UI components | +| `@mana/shared-ui` | **Svelte-5-Komponenten-Bibliothek** (Pills, Modals, Toast, Quick-Input, Skeletons …). **Heimat seit 2026-05-09: `mana/packages/shared-ui` und `npm.mana.how`** — die Kopie hier ist eingefroren bis zum Rückbau dieses Repos. Bei Änderungen in mana/ zuerst, dann erst hierher. | | `@mana/shared-theme` | Theme config | | `@mana/shared-i18n` | i18n | | `@mana/shared-privacy` | Unified visibility/privacy system: `VisibilityLevel` enum + zod schema + `` + predicates (`canEmbedOnWebsite`, …). Plan: [`docs/plans/visibility-system.md`](docs/plans/visibility-system.md). Rollout per-module, not yet adopted anywhere. | diff --git a/README.md b/README.md index 2759e7b95..457739a04 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Mana Monorepo +# Managarten -Monorepo containing all Mana projects — a self-hosted multi-app ecosystem with shared packages and unified tooling. +Der Garten der mana-Apps — ein selbst-gehostetes Multi-App-Ökosystem unter `mana.how` mit geteilten Packages und einheitlichem Tooling. Teil der mana-e.V.-Plattform. ## Projects @@ -62,7 +62,7 @@ See [CLAUDE.md](./CLAUDE.md) for comprehensive development documentation. ## Architecture ``` -mana-monorepo/ +managarten/ ├── apps/ # Product applications ├── services/ # Microservices (auth, search, LLM, bots) ├── packages/ # Shared packages diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index ee26ba904..cf7b8f407 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -1,6 +1,6 @@ # Troubleshooting Guide -Common issues and solutions for the mana-monorepo. +Common issues and solutions for the managarten. ## Table of Contents @@ -409,7 +409,7 @@ docker run --rm --entrypoint cat test /app/dist/ai/ai.service.js ### Related Issues -- [Commit d69cc607](https://github.com/Memo-2023/mana-monorepo/commit/d69cc607) - Fixed type-only ConfigService import in AiService +- [Commit d69cc607](https://github.com/Memo-2023/managarten/commit/d69cc607) - Fixed type-only ConfigService import in AiService - TypeScript `import type` vs `import {}` - both erase at compile time - Docker layer caching can hide fixes if source wasn't properly copied @@ -425,7 +425,7 @@ docker run --rm --entrypoint cat test /app/dist/ai/ai.service.js If you encounter an issue not covered here: -1. Check the [GitHub Issues](https://github.com/Memo-2023/mana-monorepo/issues) +1. Check the [GitHub Issues](https://github.com/Memo-2023/managarten/issues) 2. Review recent commits that may have introduced the issue 3. Run `pnpm clean` and `pnpm install` to reset 4. Create a new issue with full error logs diff --git a/apps/cards/COMPETITORS_2026-05.md b/apps/cards/COMPETITORS_2026-05.md deleted file mode 100644 index 377f042ba..000000000 --- a/apps/cards/COMPETITORS_2026-05.md +++ /dev/null @@ -1,353 +0,0 @@ -# Cardecky — Konkurrenz-Analyse (Mai 2026) - -> Stand: 2026-05-07. Quellen primär aus offiziellen Pricing-Seiten, G2/Trustpilot/Reddit/HN sowie Wikipedia/Crunchbase. Wo Daten fehlen oder nicht öffentlich sind, ist das explizit vermerkt. Preise schwanken regional/saisonal — die hier genannten Zahlen sind Listenpreise USD, sofern nicht anders angegeben. - ---- - -## 1. Executive Summary - -- **Anki bleibt der unschlagbare technische Gold-Standard**, aber UX-Schwächen (FSRS-„Difficulty Hell", Plugin-Hölle, kein natives Cloud-Sync mit Bildern) und der $25 iOS-Preis sind reale Lücken, in die wir stoßen können. Die Übergabe an AnkiHub im Februar 2026 könnte mittelfristig die Open-Source-Dynamik verändern — Beobachten lohnt. -- **Quizlet hat seine eigene Userbase verärgert**: Trustpilot 1.4/5, massive Beschwerden über Paywalls für Funktionen, die früher gratis waren. Genau dieses Vertrauensvakuum füllen Knowt und potenziell wir. -- **AI-Karten-Generierung ist Tischeinsatz, kein Differenzierer mehr.** Quizlet, Quizgecko, Knowt, RemNote, Wisdolia, sogar Memrise haben es. PDF-Import + KI ist erwartete Baseline. -- **Die „beautiful Anki"-Lücke ist umkämpft**: Mochi (5$/mo), RemNote (8$/mo), Noji (vormals AnkiPro). Cardecky mit _kostenlosem_ Sync sticht heraus — niemand sonst bietet die Kombination Markdown + FSRS + Cloud-Sync gratis. Das ist unsere wichtigste objektive Differenzierung. -- **Brand-Sniping ist real und schädlich**: AnkiPro (jetzt Noji) und AnkiApp (jetzt AlgoApp) haben sich einen Ruf als „Anki-Klone, die täuschen" erarbeitet — inkl. eines 10-tägigen Sync-Outages bei AnkiPro im Mai 2025. Lehre für uns: nie Anki im Namen führen, Kompatibilität sauber kommunizieren. - ---- - -## 2. Vergleichstabelle - -| Konkurrent | USP-Kurz | Lizenz | Free-Tier | Pro-Preis | Bedrohung | -| ------------------------------ | -------------------------------------- | --------------------------- | -------------------------------- | ----------------------------------------------- | -------------------------------- | -| **Anki (Desktop/Web/Android)** | Tech-Gold-Standard, FSRS, Add-ons | AGPL-3.0 | Voll-Funktional gratis | $0 (iOS: $24.99-29.99 lifetime) | **Hoch** | -| **AnkiHub** | Kollaborative Anki-Decks (USMLE-Fokus) | proprietär (auf Anki-Basis) | Trial | $5/mo | Mittel (Power-User) | -| **Quizlet** | Marktführer Volumen + Schule | proprietär | Sehr eingeschränkt, viele Ads | $35.99/Jahr (Plus), ~$45/Jahr (Unlimited) | **Hoch** (Reichweite) | -| **RemNote** | Notes + SR Hybrid | proprietär | Großzügig (3 PDFs, 5 Image-Occ.) | $8/mo annual (Pro) | Mittel | -| **Mochi** | Markdown, Local-First, schickes UI | proprietär | Single-Device | $5/mo (Sync) | **Hoch** (direkter Wettbewerber) | -| **Brainscape** | Confidence-Based-Repetition | proprietär | Limited Decks | ~$19.99/mo, $79.99 lifetime | Gering-Mittel | -| **Memrise** | Sprachen + AI-Buddies | proprietär | Eingeschränkt | $130.99/Jahr, $199.99 lifetime | Gering (Nische Sprachen) | -| **SuperMemo** | Algorithmus-Urvater (SM-20) | proprietär | Monatstrial Mobile | ~9.90$/mo Mobile, ~$66 Desktop perp. | Gering (Nische, sperrige UX) | -| **AnkiPro / Noji** | „Anki-Look" mit modernem UI | proprietär | mit Ads/Limits | nicht öffentlich klar (~$5-10/mo) | Mittel (Brand-Verwirrung) | -| **AnkiApp / AlgoApp** | Cloud-First Closed-Source | proprietär | Limited | Subscription (Details schwammig) | Gering (Reputation kaputt) | -| **Quizgecko** | AI-First (Quizzes, Podcasts) | proprietär | 1 AI-Lesson/Monat | $16/mo (Pro), $29 (Ultra) | Mittel (AI-Side) | -| **Knowt** | „Free Quizlet-Alternative" + AI | proprietär | Sehr großzügig | $9.99/mo (Ultra) | **Hoch** (gleiches Spielfeld) | -| **Wisdolia** | Browser-Ext: Karten aus Webcontent | proprietär | 50 Sets/Monat | $2.50/mo, $25/Jahr | Gering | -| **Mnemosyne** | Open-Source, Forschungs-Datasammlung | GPL | Voll gratis | — | Sehr gering | -| **Traverse** | Mind-Maps + SR (Mandarin Blueprint) | proprietär | Free-Plan | $15/mo Member, $35/User Enterprise | Gering | -| **Cerego** | Enterprise B2B Adaptive Learning | proprietär | — | ab $8.33/mo Indiv., Enterprise on req. | Sehr gering (B2B) | -| **NeuraCache** | Notion/Obsidian-Sync für SR | proprietär | Limited | 14d Trial → Pro (Preis nicht klar dokumentiert) | Gering | - -> Threat-Ranking: nur **Anki, Quizlet, Mochi, Knowt** sind Top-Bedrohungen für Cardeckys Kernzielgruppe. RemNote, Quizgecko, AnkiPro/Noji sind Nebenfront. - ---- - -## 3. Detail-Sektion pro Konkurrent - -### 3.1 Anki (Desktop / AnkiWeb / AnkiDroid / AnkiMobile) - -- **URL:** https://apps.ankiweb.net/ -- **Plattformen:** Windows, macOS, Linux (Desktop), Web (AnkiWeb), Android (AnkiDroid), iOS (AnkiMobile) -- **USP:** Der etablierte technische Standard für Spaced Repetition; mächtig, erweiterbar (Add-ons), FSRS v6 nativ, riesiges Deck-Ökosystem (insbes. Medizin: AnKing). -- **Lizenz:** AGPL-3.0 (Desktop, AnkiDroid, Web). AnkiMobile iOS proprietär (finanziert die Open-Source-Arbeit). -- **Kosten:** Desktop / Web / Android **kostenlos**. AnkiMobile iOS: **$24.99-29.99 einmalig (Lifetime)**. AnkiHub-Cloud-Decks: $5/Monat (separat). -- **User loben:** Mächtig & flexibel; FSRS-Wirksamkeit; freie Decks (insbes. AnKing Step Deck mit 100k+ Studenten); Dauerhaftigkeit (seit 2006). -- **User kritisieren:** Steile UX-Lernkurve; FSRS-„Difficulty Hell" (Karten reifen langsam, Reviews explodieren); Plugin-Brüche zwischen Versionen; iOS-Preis abschreckend; Sync-Setup für Bilder/Audio umständlich. -- **Firma & Geschichte:** Damien Elmes (Australien), gestartet 5.10.2006 ursprünglich für Japanisch-Lernen. Im **Februar 2026** angekündigt, dass AnkiHub (Austin, TX) Business-Operations und Open-Source-Stewardship übernimmt — Anki bleibt Open Source, keine externen Investoren, Versprechen „no enshittification". -- **Bedrohungsgrad: Hoch.** Power-User-Standard, riesiges Decks-Ökosystem, kostenlos. Wir können sie nicht im technischen Spielfeld schlagen — wir müssen über UX, Onboarding und „Anki-Import-Bridge" gewinnen. - -Quellen: [Anki Wikipedia]() · [AnkiMobile App Store](https://apps.apple.com/us/app/ankimobile-flashcards/id373493387) · [Class Central: Anki founder steps back](https://www.classcentral.com/report/anki-founder-steps-back/) · [AnkiHub](https://www.ankihub.net/) · [Difficulty Hell in Anki](https://skerritt.blog/difficulty-hell-in-anki/) · [Anki FSRS-Forum](https://forums.ankiweb.net/c/fsrs/41) - ---- - -### 3.2 Quizlet - -- **URL:** https://quizlet.com/ -- **Plattformen:** Web, iOS, Android. -- **USP:** Massen-Marktführer mit der größten Bibliothek shared decks (Schul-/Hochschul-Vokabeln); inzwischen stark AI-fokussiert (Q-Chat, Magic Notes, Coconote-Akquisition Feb 2026, ChatGPT-Integration März 2026). -- **Lizenz:** Proprietär. -- **Kosten:** Free (sehr eingeschränkt + Werbung). **Plus: $35.99/Jahr (~$2.99/mo)**. **Plus Unlimited: ~$44.99/Jahr** (entfernt Limits wie 3 Practice Tests/Monat, 20 Learn-Runden/Monat). -- **User loben:** Riesige Library shared sets; einfacher Einstieg; Multi-Device gut etabliert; AI-generierte Practice-Tests sind brauchbar. -- **User kritisieren:** **Trustpilot 1.4/5** aus 500+ Reviews. Aggressives Paywalling von Features, die früher gratis waren (Learn-Mode, Test, Lernrunden-Limit); Werbeflut im Free-Tier; Export-Möglichkeiten eingeschränkt (Lock-in); Bugs. -- **Firma & Geschichte:** 2005 gegründet von Andrew Sutherland (damals 15, Albany High School CA) für eigene Französisch-Vokabeln. Bootstrap bis 2015, dann $12M USV/Costanoa. 2020 $30M Series C bei General Atlantic, **$1B Bewertung**. Sitz San Francisco. Insgesamt ~$62M raised. CEO seit 2022 Lex Bayer. **Februar 2026: Akquisition Coconote**. -- **Bedrohungsgrad: Hoch (Reichweite), aber verwundbar.** Das Trustpilot-Desaster ist eine Steilvorlage. Unsere Chance: Quizlet-Refugees mit „so gut wie früher Quizlet, dazu FSRS und ohne Paywall-Gärten" abholen. - -Quellen: [Quizlet Wikipedia](https://en.wikipedia.org/wiki/Quizlet) · [Trustpilot Quizlet (1.4/5)](https://www.trustpilot.com/review/www.quizlet.com) · [Quizlet $1B Bewertung TechCrunch](https://techcrunch.com/2020/05/13/quizlet-valued-at-1-billion-as-it-raises-millions-during-a-global-pandemic/) · [Crunchbase Quizlet](https://www.crunchbase.com/organization/quizlet) · [Navigating Quizlet's Controversial Changes](https://medium.com/@maxtan0626/navigating-quizlets-controversial-changes-afeb97aafd1e) - ---- - -### 3.3 RemNote - -- **URL:** https://www.remnote.com/ -- **Plattformen:** Web, macOS, Windows, iOS, Android. -- **USP:** Hybrid aus Outliner-Notetaking (Roam-/Logseq-ähnlich) und integrierten SR-Karten — Karten entstehen direkt im Notiz-Flow via `::`-Syntax. Plus PDF-Annotation und Image-Occlusion. -- **Lizenz:** Proprietär. -- **Kosten:** Free (3 PDF-Annotationen, 5 Image-Occlusion-Karten). **Pro: $8/Monat annual ($96/Jahr)** oder $10/mo monthly. -- **User loben:** Notes + Cards in _einem_ Workflow; flexible nested Outline-Struktur; PDF-Annotation; AI-Generierung aus Notizen/PDFs. -- **User kritisieren:** Steile Lernkurve („nichts versteht man in 10 Min"); UI als überladen empfunden; **Performance-Probleme** (langsam beim Laden großer Datenbanken, iPad-Stabilität); Bugs nach Beta-Updates; non-English-Support schwach. -- **Firma & Geschichte:** Gegründet 2019 von Martin Schneider (MIT) und Moritz Wallawitsch (Berlin, HTW). Sitz: USA. **$2.8M Seed (Sept 2021)** unter General Catalyst. Hat 2025 ~$2M Revenue mit ~18 Personen erreicht. -- **Bedrohungsgrad: Mittel.** Andere Zielgruppe (PKM-Power-User, Studenten, die Notes wollen). Cardecky ist fokussierter — wir müssen die „nur-Karten"-Nische gegen ihre Hybrid-Erweiterung verteidigen. - -Quellen: [RemNote Pricing](https://www.remnote.com/pricing) · [Crunchbase RemNote](https://www.crunchbase.com/organization/remnote) · [RemNote Reviews Product Hunt](https://www.producthunt.com/products/remnote/reviews) · [RemNote Performance-Forum](https://forum.remnote.io/t/remnote-is-my-dream-pkm-yet-its-too-slow-am-i-doing-something-wrong/10920) · [Latka RemNote $2M ARR](https://getlatka.com/companies/remnote.com) - ---- - -### 3.4 Mochi - -- **URL:** https://mochi.cards/ -- **Plattformen:** macOS (Intel/AS), Windows, Linux Desktop, iOS, Android, Web. -- **USP:** Markdown-First, sauberes minimalistisches UI, Local-First mit Offline-Support; Image-Occlusion und Anki-`.apkg`-Import als First-Class-Feature ohne Plugin-Frickelei. **FSRS seit Mid-2025 unterstützt.** -- **Lizenz:** Proprietär. -- **Kosten:** Free (Single-Device, alle Karten lokal). **Pro: $5/Monat** (Sync, mehrere Geräte, Translations). -- **User loben:** „schönes" UI; intuitiver als Anki; sofortiges Karten-Lernen ohne Onboarding-Friktion; Anki-Import als Plugin-Free-Feature; Markdown-Workflow. -- **User kritisieren:** Sync nur in Pro (Single-Device-Free fühlt sich begrenzt an); algorithm war bis Mid-2025 schwächer als Anki (FSRS-Beta hat das gefixt); kleinere Community → weniger shared decks; Solo-Developer (Bus-Faktor). -- **Firma & Geschichte:** Solo-Projekt von **Matthew Steedman**, eigenfinanziert, Forum auf forum.mochi.cards. Keine externen Investoren öffentlich bekannt. -- **Bedrohungsgrad: Hoch — direktester Wettbewerber.** Praktisch identische Positionierung (Markdown, schickes UI, modern, FSRS, Local-First). Unterschied: Mochi nimmt $5/mo für Sync — **wir bieten Sync gratis**, das ist unsere stärkste objektive Differenzierung gegen Mochi. - -Quellen: [Mochi Cards](https://mochi.cards/) · [Mochi App Store](https://apps.apple.com/us/app/mochi-flashcards-and-notes/id1507775056) · [First Impressions of Mochi (borretti.me)](https://borretti.me/article/first-impressions-mochi) · [Bunpro: If you don't like Anki, try Mochi](https://community.bunpro.jp/t/if-you-dont-like-anki-consider-giving-mochi-a-try/59955) · [Mochi Changelog](https://mochi.cards/changelog/) - ---- - -### 3.5 Brainscape - -- **URL:** https://www.brainscape.com/ -- **Plattformen:** Web, iOS, Android. -- **USP:** „Confidence-Based Repetition" — User raten Selbsteinschätzung auf 1-5-Skala (statt SM-2/FSRS), als wissenschaftlich vermarktetes Schedule-System. Große kuratierte Decks-Library und EDU/Enterprise-Vertrieb. -- **Lizenz:** Proprietär. -- **Kosten:** Free (limitierter Zugang zu Deck-Bibliothek). **Pro: ~$19.99/Monat** (Discounts bei Jahres-/Lifetime-Plan). **Lifetime: $79.99**. -- **User loben:** Kuratierte Content-Bibliothek; klares Lernkonzept; schickes UI; gute Statistiken; Collaboration-Features für Teams. -- **User kritisieren:** Algorithmus weniger anpassbar als Anki/FSRS; Pro-Preis wird als hoch empfunden; Free-Tier-Decks sehr begrenzt; weniger Power-User-Features. -- **Firma & Geschichte:** Gegründet von Andrew Cohen (Idee 2006 Panama-Spanisch-Excel-Macro, später Master's Columbia EdTech). Sitz: New York. Founding Team: Cohen, Andy Lutz, Jay Stramel, Jonathan Thomas, Ron Cadet (2018). >$3M raised bis 2015. -- **Bedrohungsgrad: Gering-Mittel.** Andere Zielgruppe (Pro-Decks-Käufer, EDU-Markt). Wir konkurrieren wenig direkt. - -Quellen: [Brainscape](https://www.brainscape.com/) · [G2 Brainscape Reviews](https://www.g2.com/products/brainscape/reviews) · [Brainscape Wikipedia](https://en.wikipedia.org/wiki/Brainscape) · [How Brainscape Was Born](https://www.brainscape.com/academy/how-brainscape-was-born/) - ---- - -### 3.6 Memrise - -- **URL:** https://www.memrise.com/ -- **Plattformen:** Web, iOS, Android. -- **USP:** Sprachen-Fokus mit Native-Speaker-Videos und seit 2024/25 stark ausgebaute „AI Buddies" (Grammar Buddy, Translator Buddy, Culture Buddy, MemBot Chatbot auf GPT-Basis). -- **Lizenz:** Proprietär. -- **Kosten:** Free (limitiert + Ads). **Monthly $27.99**, **Annual $130.99 (~$11/mo)**, **Lifetime $199.99** (oft Discounts bis 50%). -- **User loben:** Native-Speaker-Video-Clips als Alleinstellungsmerkmal vs Duolingo; AI-Buddies bringen Konversationspraxis; gut für Vokabel-Aufbau. -- **User kritisieren:** Schwach in Grammatik; nicht für Fortgeschrittene; AI-Buddies hinter Paywall; teure Subscription verglichen mit Konkurrenten; legendäre community-„mems"-Funktion wurde entfernt (alte Community vergrätzt). -- **Firma & Geschichte:** Gegründet 2010 von **Ed Cooke** (Grand Master of Memory), **Ben Whately** und **Greg Detre** (Princeton-Neurowissenschaftler). Oxford-Trio. Sitz London. **$25.3M raised** über 7 Runden / 10 Investoren. Profitabel seit Ende 2016. **72M registrierte User (2024)**. -- **Bedrohungsgrad: Gering.** Sprach-Lerner-Nische, kaum Überlappung mit unserer generischen SR-Zielgruppe. - -Quellen: [Memrise](https://www.memrise.com/) · [Memrise Wikipedia](https://en.wikipedia.org/wiki/Memrise) · [Crunchbase Memrise](https://www.crunchbase.com/organization/memrise) · [Business of Apps: Memrise Statistics 2026](https://www.businessofapps.com/data/memrise-statistics/) - ---- - -### 3.7 SuperMemo - -- **URL:** https://www.supermemo.com/ (Web/Mobile) · https://supermemo.store/ (Desktop) -- **Plattformen:** Windows Desktop (Premium-Version), Web, iOS, Android, Browser-API. -- **USP:** Originator des Spaced-Repetition-Konzepts (1985 ff.) — Algorithmen SM-2 bis aktuell **SM-20 (2026)**. Die Desktop-Version hat Funktionen, die andere SR-Tools nicht haben (Incremental Reading, Concept Maps). -- **Lizenz:** Proprietär. -- **Kosten:** Mobile/Web: 1 Monat free, danach **~9.90 USD/EUR pro Monat**. Desktop SuperMemo 19 (Windows): **~$66 perpetual** (Käufer März 2026 bekommen kostenloses Upgrade auf SuperMemo 20). API: Early Access, 100 Repetitions/Tag gratis. -- **User loben:** Algorithmus-Tiefe; Incremental Reading; SM-20 als state-of-the-art; Hardcore-Power-User-Tool. -- **User kritisieren:** UI „aus den 1990ern"; sperrige Bedienung; Desktop-only für viele Features; Mobile-App stark eingeschränkt; Preis vs Anki nicht zu rechtfertigen für 95% der User. -- **Firma & Geschichte:** SuperMemo World Sp. z o.o., gegründet **5. Juli 1991** in Poznań, Polen, von Krzysztof Biedalak und **Piotr Wozniak** (mit Tomasz Kuehn, Janusz Murakowski, Marczello Georgiew). Wozniak begann SuperMemo 1.0 schon 13.12.1987. -- **Bedrohungsgrad: Gering.** Nische für Algorithmus-Enthusiasten und Incremental-Reading-Fans. Keine reale UX-Bedrohung für uns. - -Quellen: [SuperMemo Wikipedia](https://en.wikipedia.org/wiki/SuperMemo) · [SuperMemo Store](https://supermemo.store/products/supermemo-19-for-windows) · [Algorithm SM-18](https://supermemo.guru/wiki/Algorithm_SM-18) · [Piotr Wozniak](https://supermemo.guru/wiki/Piotr_Wozniak) · [SuperMemo iOS App Store](https://apps.apple.com/us/app/supermemo-effective-learning/id982498980) - ---- - -### 3.8 AnkiPro / Noji - -- **URL:** https://noji.io/ (vormals ankipro.net) -- **Plattformen:** iOS, Android, Web. -- **USP:** „Anki-Look-and-Feel" mit modernem UI und Cloud-Sync — verkauft sich aktiv als „die einfachere Anki-Variante". Nicht kompatibel mit echtem Anki (auch nicht mit `.apkg`-Decks ohne Workarounds). -- **Lizenz:** Proprietär. -- **Kosten:** Free mit Werbung/Limits. Pro-Subscription, Preise nicht prominent — nach Reports im Bereich **$5-10/mo** oder Jahresplan. -- **User loben:** Schickes Mobile-UI; einfacher Onboarding-Flow; Cross-Device-Sync „out of the box"; community Decks. -- **User kritisieren:** **Brand-Verwirrung** (User dachten, sie laden „echtes" Anki herunter); **10-Tage-Sync-Outage Mai 2025** mit Datenverlust für viele User; Lock-in (Export-Tools wurden vom Anbieter blockiert, ein Migrations-Tool erhielt einen **Rickroll-Response** von AnkiPro); offizielles Anki-Team distanziert sich. -- **Firma & Geschichte:** Anki Pro UAB; Co-Founder **Maksim Abramchuk** (im Crunchbase) und **Andrew Bond** (LinkedIn). 2021 gestartet, 2024/25 Rebrand zu **Noji**. Sitz nicht eindeutig öffentlich (LinkedIn-Indikatoren UK/Osteuropa). -- **Bedrohungsgrad: Mittel.** Nicht weil sie technisch besser sind, sondern weil Anki-Suchende auf sie reinfallen. **Lehre für Cardecky: Brand-Hygiene**. Wir sind „Cardecky" — nie „Anki" im Marketing, klare Trennung kommunizieren, Anki-Import sauber als Bridge dokumentieren. - -Quellen: [Anki knockoffs (offizielle Anki FAQ)](https://faqs.ankiweb.net/anki-knockoffs.html) · [AnkiPro Ripoff Forum](https://forums.ankiweb.net/t/ankipro-another-ripoff-anki-app/11791) · [Anki Users Get Rickrolled](https://broderic.blog/post/anki-users-get-rickrolled/) · [Noji App Store](https://apps.apple.com/us/app/noji-flashcards-anki-method/id1573585542) · [Crunchbase Anki Pro](https://www.crunchbase.com/organization/anki-pro) · [Speakada: Official Anki vs Fake Apps](https://speakada.com/official-anki-vs-fake-apps-the-critical-mistake-costing-language-learners-hours/) - ---- - -### 3.9 AnkiApp / AlgoApp - -- **URL:** https://www.algoapp.ai/ (vormals ankiapp.com) -- **Plattformen:** iOS, Android, Web, Desktop. -- **USP:** Closed-Source Cloud-First Karten-App, die seit Jahren den Namen „Anki" ausnutzt. **In manchen Regionen (z. B. japanischer App Store) firmiert sie weiterhin als „AnkiApp"**. -- **Lizenz:** Proprietär. -- **Kosten:** Free + Subscription-Tiers (Details vage, oft als „Trial-Trap" kritisiert). -- **User loben:** Funktioniert auf allen Plattformen; Cloud-Sync inkludiert; einfaches UI. -- **User kritisieren:** **Komplette Brand-Täuschung**; kein Import/Export zu echtem Anki; aggressive Subscription-Walls; Reviews mit „nichts mit echtem Anki zu tun" als wiederkehrendes Muster; Reputation in der Community unter null. -- **Firma & Geschichte:** AlgoApp Inc., gegründet **2021**, Sitz **San Mateo, CA**. Vor Kurzem von AnkiApp zu AlgoApp umbenannt (Anki-Brand-Druck wurde zu groß), aber teils noch unter altem Namen aktiv. -- **Bedrohungsgrad: Gering.** Reputation kaputt; informierte User meiden sie aktiv. Hauptthema für uns ist nicht Wettbewerb, sondern Brand-Hygiene-Lehre (siehe AnkiPro). - -Quellen: [Anki knockoffs FAQ](https://faqs.ankiweb.net/anki-knockoffs.html) · [AlgoApp on Anki Forum](https://forums.ankiweb.net/t/algoapp-still-using-ankiapp-name-in-japanese-app-store/69103) · [Crunchbase AlgoApp](https://www.crunchbase.com/organization/algoapp) · [Pitchbook AlgoApp](https://pitchbook.com/profiles/company/495884-44) - ---- - -### 3.10 Quizgecko - -- **URL:** https://quizgecko.com/ -- **Plattformen:** Web, iOS, Android. -- **USP:** AI-First-Workflow: aus PDF / Text / URL → Quizzes + Karten + Notizen + **Audio-Podcasts** (Notebook-LM-ähnlich). SR ist sekundär. -- **Lizenz:** Proprietär. -- **Kosten:** **Basic Free (1 AI-Lesson/Monat)**. **Pro $16/mo** (annual). **Ultra $29/mo** (50 Podcasts/mo, Custom Prompts). Business $32/mo (API + Branding). -- **User loben:** Vielseitige Output-Formate (Quiz/Karten/Podcast); guter PDF-Parser; multi-Question-Types. -- **User kritisieren:** SR ist „mitgeliefert" aber nicht der Fokus; Free-Tier sehr eng (1 Lesson); Pro-Preis hoch verglichen mit dedicated AI-Card-Tools. -- **Firma & Geschichte:** Privates Startup, kleinere Bekanntheit, keine prominente Funding-Information öffentlich. -- **Bedrohungsgrad: Mittel (in der AI-Front).** Wir konkurrieren am AI-Generierungs-Feature. Für reines SR-Lernen ist Quizgecko keine Bedrohung; für „ich habe ein Skript und will lernen" schon. Unser Konter: AI-Generierung ist bei uns „free with sync" und dann _dauerhaft_ in einem echten SR-System. - -Quellen: [Quizgecko](https://quizgecko.com/) · [Quizgecko Pricing](https://quizgecko.com/pricing) · [Toosio Quizgecko Review 2026](https://toosio.com/tool/quizgecko-ai-quiz-flashcard-podcast-generator) - ---- - -### 3.11 Knowt - -- **URL:** https://knowt.com/ -- **Plattformen:** Web, iOS, Android. -- **USP:** Positioniert sich explizit als **„free Quizlet alternative"**. Importiert Quizlet-Sets direkt, hat ähnliche Study-Modes (Learn, matching, practice tests, „Knowt Play") plus AI-Generierung aus Notizen/PDFs. -- **Lizenz:** Proprietär. -- **Kosten:** **Sehr großzügiges Free-Tier** (unlimited Karten, alle Study-Modes, basic AI mit monatlichen Limits). **Ultra: $9.99/mo annual** (Snap & Solve, unlimited AI). Manche Listen nennen einen $12.50/mo Premium. -- **User loben:** „Endlich Quizlet ohne Paywall"; Quizlet-Import funktioniert; AI-Note-zu-Karten brauchbar; Free-Tier wirklich nutzbar. -- **User kritisieren:** Hauptsächlich Schüler-/US-Highschool-Zielgruppe (für Erwachsene weniger durchdacht); AI-Limits im Free-Tier; SR-Algorithmus weniger ausgereift als Anki/FSRS. -- **Firma & Geschichte:** US-Startup, primär Studenten-Zielgruppe, keine prominente Funding-Information öffentlich verfügbar. -- **Bedrohungsgrad: Hoch (gleiches Spielfeld).** Beide Apps positionieren „free + AI + bessere UX als Quizlet". Unsere Differenzierung: **FSRS v6, Markdown, echtes Local-First-PWA-Modell, Anki-Import inkl. Bilder/Audio**. Knowt ist webbasiert, wir sind installierbar offline-first. - -Quellen: [Knowt](https://knowt.com/) · [Knowt vs Quizlet (StudyGenie 2026)](https://studygenie.io/blog/knowt-vs-quizlet) · [Best Quizlet Alternatives 2026](https://kvistly.com/blog/best-quizlet-alternatives) - ---- - -### 3.12 Wisdolia - -- **URL:** https://www.wisdolia.com/ (vorrangig als Chrome-Extension) -- **Plattformen:** Chrome Extension; Karten-Export zu Anki möglich. -- **USP:** Generiert Karten aus _jeder Webseite, PDF oder YouTube-Video_ in Sekunden — sehr fokussiert auf den „Capture beim Browsen"-Use-Case. -- **Lizenz:** Proprietär. -- **Kosten:** **Free: 50 Sets/Monat** (Limit: 15 PDF-Seiten, 12 Min YouTube). **Pro: $2.50/mo oder $25/Jahr** (unlimited). -- **User loben:** Spielerisch billig; Browser-Extension-Workflow ist reibungsarm; Anki-Export als Bridge. -- **User kritisieren:** Kein eigenes SR-System mit eigener Tiefe (eher Generator als Lern-App); Browser-only beschränkt. -- **Firma & Geschichte:** Kleines indie-Projekt; keine prominente Funding-Information öffentlich. -- **Bedrohungsgrad: Gering.** Komplementäres Tool eher als Wettbewerber — wer Wisdolia nutzt, exportiert oft _zu Anki_ (oder zu uns, wenn wir Wisdolia-Export sauber importieren). - -Quellen: [Wisdolia (Findmyaitool)](https://findmyaitool.com/tool/wisdolia) · [Wisdolia Plain English Walkthrough](https://plainenglish.io/artificial-intelligence/wisdolia-ai-generate-flashcards-anywhere-on-the-web-with-google-chrome-extension) - ---- - -### 3.13 Mnemosyne - -- **URL:** https://mnemosyne-proj.org/ -- **Plattformen:** Windows, macOS, Linux Desktop; Android (eingeschränkt). -- **USP:** Open-Source-Alternative zu Anki mit explizitem **Forschungs-Fokus**: Nutzer können (opt-in) anonyme Lerndaten beitragen, die seit 2006 zur Untersuchung von Langzeitgedächtnis gesammelt werden. -- **Lizenz:** GPL. -- **Kosten:** Komplett gratis. Kein Sync. -- **User loben:** Sauber, leichtgewichtig, ehrlich akademisch; gut für Forschung; lange Geschichte (>20 Jahre). -- **User kritisieren:** UI veraltet; Mobile-Support schwach (Android-App OK, iOS quasi nichts); kleine Community; weniger Decks als Anki. -- **Firma & Geschichte:** Community-Projekt um Peter Bienstman (Belgien). Letzte Release März 2026 — aktiv aber langsam. -- **Bedrohungsgrad: Sehr gering.** Akademisches Nischen-Tool, andere Zielgruppe. - -Quellen: [Mnemosyne Wikipedia]() · [Mnemosyne Project](https://mnemosyne-proj.org/) · [GitHub Mnemosyne](https://github.com/mnemosyne-proj/mnemosyne) - ---- - -### 3.14 Traverse - -- **URL:** https://traverse.link/ -- **Plattformen:** Web, iOS, Android. -- **USP:** Kombiniert Mind-Mapping + Note-Taking + SR-Karten in einer App; offizielle Integration mit „Mandarin Blueprint" (Chinesisch-Lernkurs). -- **Lizenz:** Proprietär. -- **Kosten:** Free, **Member $15/mo**, **Enterprise $35/User/mo**. -- **User loben:** Mind-Map + Karten kombiniert ist konzeptionell stark für Sprachen/komplexe Domains; Mandarin-Community schätzt es. -- **User kritisieren:** Member-Preis hoch; relativ kleine Bekanntheit außerhalb Mandarin-Sub-Community; nicht so viel feature parity mit Anki. -- **Firma & Geschichte:** Indie-Startup, primär Bootstrap; keine prominente Funding-Information öffentlich. -- **Bedrohungsgrad: Gering.** Andere Zielgruppe (visuelles Lernen, Sprachen). Keine direkte Konkurrenz. - -Quellen: [Traverse.link](https://traverse.link/) · [Traverse.link Capterra 2026](https://www.capterra.com/p/234102/Traverse/) - ---- - -### 3.15 Cerego - -- **URL:** https://www.cerego.com/ -- **Plattformen:** Web (B2B-Plattform). -- **USP:** Enterprise-Adaptive-Learning mit Versprechen „4-5× schnelleres Lernen, 90% Retention"; AI/ML-basierte Personalisierung. **Verkauft sich an Unternehmen, nicht Endkunden**. -- **Lizenz:** Proprietär. -- **Kosten:** Indiv. ab **$8.33/mo**, Enterprise ab 500 Seats individuell verhandelt (nicht öffentlich). -- **User loben:** Solide Lerneffekte in Enterprise-Trainings; gutes Reporting; sauberes UI. -- **User kritisieren:** Nicht für Selbstlerner gemacht; teuer für Einzelne; deck-Erstellungs-Workflow umständlich für Privatuser. -- **Firma & Geschichte:** US-Firma, in der Vergangenheit mehrfach pivotiert (B2C → B2B). Keine aktuelle Funding-Info. -- **Bedrohungsgrad: Sehr gering.** B2B, andere Welt. - -Quellen: [Cerego](https://www.cerego.com/) · [Cerego G2](https://www.g2.com/products/cerego/reviews) · [Cerego Capterra 2026](https://www.capterra.com/p/169739/Cerego/) - ---- - -### 3.16 NeuraCache - -- **URL:** https://neuracache.com/ -- **Plattformen:** iOS, Android. -- **USP:** SR-Karten **synchronisiert mit Notion / Obsidian / Logseq / Roam / Evernote / OneNote**, automatisches Extrahieren markierter Notizen → Karten. „Bridge"-Tool für PKM-Nutzer. -- **Lizenz:** Proprietär. -- **Kosten:** 14-Tage-Trial Pro. Pro-Subscription oder One-Time Lifetime; konkrete 2026-Preise nicht klar dokumentiert auf der öffentlichen Seite. -- **User loben:** Notion-/Obsidian-Sync ist die Killer-Funktion; spart Doppelarbeit für PKM-Power-User. -- **User kritisieren:** Klein, indie; UI weniger poliert als Mochi; Pricing intransparent; eher Mobile-only. -- **Firma & Geschichte:** Indie-Developer, geringe öffentliche Sichtbarkeit. -- **Bedrohungsgrad: Gering.** PKM-Nische; keine Überlappung mit unserer Generalist-Zielgruppe. - -Quellen: [NeuraCache](https://neuracache.com/) · [NeuraCache App Store](https://apps.apple.com/us/app/neuracache-spaced-repetition/id1450923453) · [NeuraCache AlternativeTo](https://alternativeto.net/software/neuracache/about/) - ---- - -## 4. Schluss-Empfehlung: 3 Differenzierungs-Hebel für Cardecky - -### Hebel 1: **„Free Sync" konsequent ausspielen** - -Niemand sonst bietet die Kombination, die wir liefern — _Markdown + FSRS + Multi-Device-Cloud-Sync inkl. Bilder/Audio + PWA + AI-Generierung_, alles im Free-Tier. Konkurrenten wollen für Sync Geld: - -- Mochi: $5/mo -- AnkiMobile iOS: $25-30 einmalig -- Quizlet: Sync ja, aber Features paywallen -- RemNote: Pro-Limit (3 PDFs) -- Brainscape: $20/mo - -**Action:** Marketing-Hauptbotschaft auf Pricing-Seite und Landingpage explizit machen: _„Sync gratis, immer. Karten gehören dir, lokal und in der Cloud."_ Gegen Mochi besonders direkt vergleichen. Wenn wir später monetarisieren, sollte Sync NIE in den Pro-Tier wandern — unser Reputations-Anker. - -### Hebel 2: **Anki-Migration als First-Class-Feature, ohne Brand-Sniping** - -Anki bleibt Power-User-Standard, aber Anki-User klagen über UX, FSRS-Tweaking und iOS-Preis. Sie sind die wertvollste Migrations-Zielgruppe (lange Lern-Historie, 100k+ Karten). Wir importieren bereits inkl. Bilder/Audio — das ist Gold. - -**Action:** - -- Eine dezidierte Landingpage `cardecky.com/from-anki` mit ehrlichem Vergleich (was wir besser machen, was Anki noch besser kann), Migrationsanleitung, und expliziter Distanzierung von AnkiPro/AnkiApp/Noji. -- Eine ehrliche Story dazu („Wir sind nicht Anki. Wir sind Cardecky. Aber wir respektieren deine Anki-Karten."). Das positioniert uns als seriöse Alternative gegen die Brand-Sniper. -- Für Bonus-Punkte: Imports von Mochi-Decks und Quizlet-Sets ebenfalls anbieten — Knowt lebt davon, wir können das auch. - -### Hebel 3: **„Local-First PWA" als Tech-Identität, nicht nur Implementierungsdetail** - -Cardeckys Local-First + PWA-Architektur ist konzeptionell anders als Quizlet/Knowt (Web-First) und besser als Mochi auf iOS (App-Store-Friktion). Wir sind installierbar, offline-funktional, ohne App Store. Das schlägt mehrere Fliegen: - -- Kein iOS-30%-Tax (vs AnkiMobile-Modell, das deshalb $25 kostet) -- Kein Vendor-Lock-in (Daten bleiben im Browser/lokal nutzbar) -- Kein Werbe-Modell nötig (vs Quizlet) -- Schnelles Auto-Update (vs Anki-Plugin-Brüche) - -**Action:** Konsequent „Local-First PWA" in Tech-Marketing nutzen (HN, Reddit /r/Anki, /r/medicalschool, indie-hacker-Communities). Genau dort sitzen Quizlet-Wechsler und Anki-frustrierte Med-Studenten, die diesen technischen Pitch verstehen. - ---- - -## Bonus: Was wir _nicht_ tun sollten - -- **Nicht „Anki" im Namen führen** — siehe AnkiPro/AnkiApp Reputation. „Cardecky" ist neutral, freundlich, und distanziert sich klar. -- **Nicht die SR-Algorithmus-Race spielen** — FSRS v6 reicht. SuperMemo SM-20 ist kein Marketing-Argument für 99% der User. -- **Nicht in Sprach-Lernen pivotieren** — Memrise und Duolingo besitzen das Feld, andere Mechaniken nötig. -- **Nicht alle AI-Features paywallen** — Knowt zeigt: ein großzügiges Free-Tier mit AI ist der Hebel gegen Quizlet. -- **Nicht Sync paywallen** — siehe Hebel 1. Das ist unser Anker-Wert. - ---- - -## Methodische Hinweise - -- Recherche durchgeführt 2026-05-07 via WebSearch (offizielle Pricing-Seiten, G2, Trustpilot, Capterra, Crunchbase, Wikipedia, Reddit, Anki-Forums, Hacker News). -- Einige Konkurrenten (NeuraCache, Quizgecko, Traverse, kleinere Indie-Tools) haben begrenzt öffentlich verfügbare Daten zu Funding/Team — wo Daten fehlen, ist „nicht öffentlich bekannt" eingetragen statt Spekulation. -- AnkiPro/Noji ist besonders intransparent (eigene Pricing-Seite versteckt klare Tier-Liste, Zahlen aus Reviews); wir sollten das im Auge behalten, wenn wir gegen sie konkurrieren. -- Quizlet-Bewertung mit „verwundbar" basiert real auf dem **Trustpilot-1.4/5** und der breiten Reddit-Stimmung — das ist eine echte Marktchance, kein Wunschdenken. diff --git a/apps/cards/GUIDELINES.md b/apps/cards/GUIDELINES.md deleted file mode 100644 index 38f8096b0..000000000 --- a/apps/cards/GUIDELINES.md +++ /dev/null @@ -1,367 +0,0 @@ -# Cardecky — Projekt-Leitlinien - -Verbindliche Regeln für den Spinoff. Ziel: in wenigen Wochen ein -ausspielbares Web-MVP, das ausschließlich seinen *Core Gameloop* -beherrscht und alles andere von zentralen Mana-Bausteinen erbt. - -**Status:** Planungsphase, noch kein Code. -**Name:** Cardecky. -**App-Domain:** `cardecky.mana.how` (Subdomain unter `*.mana.how`, SSO über mana-auth). -**Marketing-Landing:** `cardecky.com` (eigene Domain, statisch, SEO/Akquise — keine Auth, leitet auf `cardecky.mana.how` für die App). -**Zugang:** offen für jeden eingeloggten Mana-User (`requiredTier: 'public'`, kein Beta-Gate). - -## 1. Mission in einem Satz - -Die schönste, einfachste Karteikarten-App mit Spaced Repetition — -zuerst nur Web, später Mobile, KI-Generierung als Phase 2. - -## 2. Game-Dev-Prinzip: zuerst nur der Core Gameloop - -Wie bei einem Spielprototyp gilt: alles, was nicht zum Loop gehört, -wird zurückgestellt. Erst wenn der Loop sich gut anfühlt und Nutzer ihn -freiwillig wiederholen, wird gebaut, was drumherum gehört. - -### Der Core Gameloop von Cardecky - -``` -Start - │ - ▼ -"Du hast N Karten heute fällig" ─────► (wenn 0: "Alles gelernt — komm später wieder") - │ - ▼ -[Lernen starten] - │ - ▼ -Vorderseite zeigen ──► User denkt ──► Tap/Space ──► Rückseite zeigen - │ - ▼ -Selbst-Bewertung: 1=nochmal · 2=schwer · 3=gut · 4=leicht - │ - ▼ -FSRS rechnet next-due ──► nächste Karte (oder Session-Ende) - │ - ▼ -Session-Ende: "X Karten gelernt, nächste in Y Stunden" - │ - └─► zurück zum Start -``` - -Sekundäre Loops (Karten erstellen, Decks verwalten) werden gebaut, sind -aber UI-arm. **Tertiäre Loops (KI-Generierung, Voice, Sharing) sind -Phase 2 und werden in Phase 1 nicht angefasst.** - -### Was Phase 1 enthält - -- Decks anlegen / löschen / umbenennen -- Karten manuell erstellen (Markdown-Inhalt) -- **Kartentypen:** Basic, Basic + Reverse, Cloze, Type-In (siehe §6) -- Lernsession mit FSRS v6, **inklusive per-User-Parameter-Tuning** -- "Heute fällig"-Übersicht + Streak-Zähler -- Tags auf Decks (das Modul hat sie ohnehin schon, raus wäre Mehrarbeit) -- PWA-installierbar, offline-fähig -- Auth via mana-auth, Sync via mana-sync - -### Was Phase 1 absichtlich NICHT enthält - -- KI-Generierung von Karten (kein PDF-Upload, keine Bild→Karte) -- Voice/TTS-Lernen -- Anki-Import / Export -- Statistik-Dashboards (nur Streak + Tagessumme) -- Public Decks / Marktplatz / Sharing -- Stripe / Bezahlung -- Mobile-App (PWA-tauglich aber kein Expo) -- Eigene Domain & Marketing-Landing -- Mehrsprachigkeit über Deutsch hinaus -- Bilder / Audio in Karten -- Image-Occlusion-Karten, Audio-Karten, Multiple-Choice -- Custom Card-Templates / WYSIWYG-Editor -- Erweiterte Suche - -Jede dieser Features ist legitim — aber nur, wenn der Loop steht. - -## 3. Goldene Regeln - -1. **Simpel schlägt vollständig.** Wenn ein Feature nicht zum Core Gameloop gehört, kommt es in einen Phase-2-Backlog, nicht in den Code. -2. **Open Source only.** Jede Library, jedes Tool, jeder Dienst muss eine OSI-konforme Lizenz haben (MIT, Apache 2.0, BSD, MPL, AGPL akzeptabel). Keine Closed-Source-SDKs, keine proprietären APIs als Pflichtabhängigkeit. -3. **Bevorzugt was im Verein schon läuft.** Neue Technologie nur einführen, wenn ein konkreter Engpass es verlangt und kein vorhandenes Tool es löst. -4. **Zentrale Mana-Dienste statt Eigenbau.** Auth, Sync, Analytics, Notifications, Media usw. werden NICHT neu gebaut — siehe §5. -5. **Local-First wie der Rest des Verein-Stacks.** IndexedDB als Quelle der Wahrheit, Sync nach Postgres im Hintergrund. -6. **`cardecky.mana.how` als Subdomain unter `*.mana.how`.** Kein eigenes Auth-System, kein eigenes Hosting-Setup — Eintrag in `PRODUCTION_TRUSTED_ORIGINS` + Cloudflare-Tunnel-Route reichen. -7. **Eine UI-Schicht, ein Theme.** Wir verwenden `@mana/shared-theme(-ui)` und `@mana/shared-ui` so weit es geht — kein paralleles Design-System. -8. **Erweiterbare Daten, simples UI.** Das Datenmodell denkt zukünftige Kartentypen mit (siehe §6), das UI zeigt in Phase 1 nur die vier definierten Typen. - -## 4. Tech-Stack (Phase 1) - -Alles bereits im Verein verwendet, alles OSI-Open-Source. - -### Frontend -| Schicht | Wahl | Lizenz | -|---|---|---| -| Framework | SvelteKit 2 | MIT | -| UI-Sprache | Svelte 5 (Runes) | MIT | -| Sprache | TypeScript 5 | Apache-2.0 | -| Styling | Tailwind CSS 4 | MIT | -| Build/Dev | Vite | MIT | -| PWA | `@vite-pwa/sveltekit` (über `@mana/shared-pwa`) | MIT | -| Icons | über `@mana/shared-icons` | MIT | -| Markdown-Render | `marked` + `DOMPurify` | MIT | - -### Datenhaltung (Client) -| Schicht | Wahl | Lizenz | -|---|---|---| -| Local Store | IndexedDB via Dexie | Apache-2.0 | -| Local-Store-Wrapper | `@mana/local-store` (intern) | — | -| Verschlüsselung | AES-GCM-256 via `@mana/shared-crypto` (Phase 2 — Hooks bereits an allen Schreib-/Lese-Pfaden, Wirkung deferred bis Vault-Server-Roundtrip steht; siehe `src/lib/data/crypto.ts`) | — | - -### Spaced Repetition -| Schicht | Wahl | Lizenz | -|---|---|---| -| Algorithmus | FSRS v6 (Free Spaced Repetition Scheduler) | BSD-3 | -| TS-Implementation | `ts-fsrs` (offizielle Portierung, mit Optimizer) | MIT | -| Per-User-Tuning | `ts-fsrs`-Optimizer, läuft client-seitig nach ≥ 50 Reviews | MIT | - -### Deployment -| Schicht | Wahl | Lizenz | -|---|---|---| -| Adapter | `@sveltejs/adapter-node` | MIT | -| Container | Docker, hinter Cloudflare Tunnel | Apache-2.0 | -| Host | Mac mini (siehe `docker-compose.macmini.yml`) | — | - -### Tooling -| Schicht | Wahl | Lizenz | -|---|---|---| -| Paket-Manager | pnpm 9 | MIT | -| Monorepo-Orchestrierung | Turborepo (vorhanden) | MPL-2.0 | -| Linting | ESLint (`@mana/eslint-config`) | MIT | -| Formatierung | Prettier | MIT | -| Tests (Unit) | Vitest | MIT | -| Tests (E2E) | Playwright | Apache-2.0 | -| TS-Config | `@mana/test-config`, `@mana/shared-vite-config` | — | - -### Backend in Phase 1: keiner - -Phase 1 braucht **keinen eigenen Service**. Lese-/Schreibpfad geht -ausschließlich über IndexedDB → `mana-sync` (existiert) → Postgres. - -Erst wenn KI-Generierung (Phase 2) dazukommt, entsteht -`services/cards-server` (Hono + Bun, analog zu allen anderen -Verein-Services). - -## 5. Zentrale Mana-Bausteine (Pflicht in Phase 1) - -### Services (laufen bereits, nur konsumieren) -| Service | Port | Wofür in Cardecky | -|---|---|---| -| `mana-auth` | 3001 | SSO, JWT, Sessions, Tier-Claims. Cardecky-Origin in `PRODUCTION_TRUSTED_ORIGINS` eintragen. | -| `mana-sync` | 3050 | Sync der `cards`-AppId-Daten (Decks, Karten, Reviews, StudyBlocks). | -| `mana-user` | 3062 | Profilinfos / Settings. | -| `mana-analytics` | 3064 | Page-Views, Loop-Events (siehe §11). | -| `mana-events` | 3115 | Domain-Events für Streak-Logik. | -| `mana-notify` | 3040 | "Du hast X Karten fällig"-Push (Phase 1.5). | -| `mana-credits` | 3061 | **Erst Phase 2** (KI-Generierung). | -| `mana-subscriptions` | 3063 | **Erst Phase 2** (Pro-Tier). | -| `mana-llm`, `mana-stt`, `mana-tts` | – | **Erst Phase 2.** | -| `mana-media` | 3015 | **Erst wenn Bilder in Karten erlaubt sind.** | - -### Workspace-Pakete (`@mana/*`) -| Paket | Wofür in Cardecky | -|---|---| -| `@mana/shared-auth` | Client-seitiger Auth-Hook (SSO-Flow, JWT-Handling). | -| `@mana/shared-auth-ui` | Login/Logout-Komponenten. | -| `@mana/shared-hono` | (sobald cards-server existiert) Auth-/Health-/Error-Middleware. | -| `@mana/shared-branding` | App-Registry-Eintrag (Tier=`public`, Branding, Subdomain). | -| `@mana/shared-types` | Geteilte TS-Typen. | -| `@mana/shared-utils` | Utility-Funktionen. | -| `@mana/shared-ui` | UI-Komponenten. | -| `@mana/shared-theme`, `@mana/shared-theme-ui` | Theme-Tokens, Dark/Light. | -| `@mana/shared-tailwind` | Tailwind-Preset. | -| `@mana/shared-i18n` | Übersetzungsfundament (Phase 1: nur DE registriert). | -| `@mana/shared-icons` | Icon-Set. | -| `@mana/shared-privacy` | Visibility-Enum für Decks (Sharing erst Phase 2, aber Feld vorbereitet). | -| `@mana/shared-crypto` | AES-GCM-256 für sensible Felder. | -| `@mana/shared-pwa` | Manifest, Service-Worker, Install-Prompt. | -| `@mana/shared-vite-config` | Vite-Defaults. | -| `@mana/shared-error-tracking` | Error-Reporting. | -| `@mana/shared-logger` | Strukturiertes Logging (Server-Seite, sobald relevant). | -| `@mana/shared-stores` | Geteilte Local-Store-Helpers. | -| `@mana/shared-tags` | Tags auf Decks. | -| `@mana/local-store` | Dexie-Setup, Sync-Hooks. | -| `@mana/eslint-config` | Lint-Regeln. | -| `@mana/test-config` | Vitest-Defaults. | -| `@mana/feedback` | In-App-Feedback-Widget. | -| `@mana/help` | Hilfe-Overlay. | - -**Erst Phase 2 oder später:** `@mana/shared-llm`, `@mana/shared-ai`, -`@mana/local-llm`, `@mana/local-stt`, `@mana/credits`, `@mana/qr-export`, -`@mana/wallpaper-generator`, `@mana/website-blocks`, -`@mana/shared-research`, `@mana/shared-uload`, `@mana/shared-storage`. - -### Datenpfad - -Cardecky übernimmt 1:1 das Mana-Datenpfad-Pattern: - -``` -User-Aktion → Store → encryptRecord → Dexie → Hooks (_pendingChanges) - → mana-sync → Postgres (mana_platform.cards.*) → andere Clients -``` - -appId = `cards`. Tabellen: `cardDecks`, `cards`, `cardReviews`, -`cardStudyBlocks`, `deckTags`. - -## 6. Datenmodell — erweiterbar gedacht - -Heutiges Modul kennt nur `front`/`back`. Damit weitere Kartentypen -ohne Schema-Bruch dazukommen, wechseln wir auf ein **Felder-Map + -Typ-Diskriminator**: - -```ts -type CardType = - | 'basic' // Phase 1: front/back - | 'basic-reverse' // Phase 1: erzeugt zwei Lernrichtungen aus einer Karte - | 'cloze' // Phase 1: Lückentext, eine Subkarte pro Cluster - | 'type-in' // Phase 1: User tippt Antwort, exact-match-Vergleich - | 'image-occlusion' // Phase 2 - | 'audio' // Phase 2 - | 'multiple-choice' // ggf. Phase 2 - -interface LocalCard extends BaseRecord { - deckId: string - type: CardType - fields: Record // basic: { front, back } · cloze: { text, extra? } - // FSRS-State liegt nicht hier, sondern in cardReviews (1:N pro Subkarte) - order: number -} - -interface LocalCardReview extends BaseRecord { - cardId: string - subIndex: number // basic-reverse → 0|1, cloze → c1, c2, … - stability: number // FSRS - difficulty: number // FSRS - due: string // ISO - reps: number - lapses: number - state: 'new' | 'learning' | 'review' | 'relearning' - lastReview?: string -} - -interface LocalCardStudyBlock extends BaseRecord { - date: string // YYYY-MM-DD - cardsReviewed: number - durationMs: number -} -``` - -**Cloze-Syntax:** Anki-kompatibel: `{{c1::Wort}}`, `{{c1::Wort::Hinweis}}`. -Eine Cloze-Karte mit Cluster `c1`+`c2` erzeugt 2 Reviews -(`subIndex 1`, `subIndex 2`). - -**Markdown:** `marked` + `DOMPurify` rendern Front/Back. Cloze-Tags -werden vor dem Markdown-Parser zu HTML-Spans umgewandelt, damit sie im -Render erhalten bleiben. - -**Migration aus dem Bestand:** existierende `front`/`back`-Karten werden -beim ersten Schema-Upgrade auf `type='basic'` mit -`fields={front, back}` migriert. Alte Spalten bleiben für eine -Übergangsversion lesbar (siehe `docs/DATABASE_MIGRATIONS.md`). - -## 7. Daten-Contract mit dem mana-Modul - -Wichtig: das **bestehende `cards`-Modul in der Mana-Web-App bleibt -erhalten**. Cardecky und das mana-Modul schreiben in dieselben -Postgres-Tabellen. - -Daher gilt: -- Schema-Änderungen werden **gemeinsam** im mana-Modul und im - Cardecky-Code rolled out (nie nur auf einer Seite). -- Encryption-Registry-Einträge müssen in beiden Frontends identisch - sein (Field-Allowlist). -- Migrationen über `docs/DATABASE_MIGRATIONS.md`. - -**Reihenfolge:** Phase 0 (mana-Modul um neue Tabellen + Kartentyp-Felder -+ FSRS erweitern) wird **vor** dem Standalone-Build durchgezogen. So -gibt es nie zwei Wahrheiten zur Datenstruktur. - -## 8. Definition of Done für Phase 1 - -Phase 1 ist fertig, wenn: - -1. Ein eingeloggter Mana-User kann auf `cardecky.mana.how` - - mindestens ein Deck anlegen, - - Karten manuell hinzufügen (Basic, Basic+Reverse, Cloze, Type-In), - - Markdown im Front/Back nutzen (Bold, Listen, Code, Links), - - eine Lernsession starten und mit FSRS-Bewertung durchspielen, - - die App schließen und am nächsten Tag die richtigen fälligen Karten wiederfinden. -2. FSRS-Per-User-Tuning läuft automatisch nach ≥ 50 Reviews und überschreibt die Default-Parameter. -3. Die App ist als PWA installierbar und offline-bedienbar (Karten lernen ohne Netz). -4. Auth läuft komplett über mana-auth (kein Eigen-Login). -5. Daten landen in Postgres und sind im bestehenden mana-Modul sichtbar (gleiche Datenquelle, kein Drift). -6. `pnpm validate:all` grün. -7. Mindestens drei Smoke-E2E-Tests (Playwright): - - „Login → Deck anlegen → Basic-Karte → Lernsession → bewerten" - - „Cloze-Karte mit zwei Clustern → erzeugt zwei Subkarten" - - „Type-In: korrekte Antwort = grün, falsche = rot" -8. Container baut & läuft auf dem Mac mini hinter Cloudflare Tunnel (`cardecky.mana.how`). - -Alles andere ist Phase 2. - -## 9. Repo-Struktur (Phase 1) - -``` -apps/cards/ -├── apps/ -│ └── web/ # SvelteKit-App, einziges Surface in Phase 1 -│ ├── src/ -│ │ ├── lib/ -│ │ │ ├── data/ # Dexie + Sync-Anbindung -│ │ │ ├── fsrs/ # ts-fsrs-Wrapper + Optimizer-Hook -│ │ │ ├── cards/ # Kartentyp-Renderer (basic, cloze, type-in) -│ │ │ ├── stores/ # Decks, Cards, Reviews, StudyBlocks -│ │ │ └── ui/ # Komponenten (DeckList, CardEditor, Session) -│ │ └── routes/ -│ │ ├── +layout.svelte -│ │ ├── +page.svelte # Heute fällig + Decks -│ │ ├── decks/[id]/+page.svelte # Deck-Detail + Karten -│ │ └── learn/[deckId]/+page.svelte # Lernsession -│ ├── package.json -│ ├── svelte.config.js -│ └── vite.config.ts -├── GUIDELINES.md # ← dieses Dokument -└── README.md -``` - -`apps/cards/apps/mobile/` und `apps/cards/apps/landing/` sind erst -Phase 2/3. - -## 10. PR-Checkliste - -Bei jedem Pull-Request gefragt: - -- Gehört die Änderung zum Core Gameloop? -- Wenn nein: rechtfertigt sie sich aus einer Pflicht (Auth, Sync, Build)? -- Wird ein bestehendes `@mana/*` Paket genutzt statt neu zu bauen? -- Ist jede neue Dependency Open-Source und im Verein bereits in Verwendung? -- Sind Datenmodell-Änderungen mit dem mana-Modul konsistent? -- Bricht die Änderung das Versprechen "Erweiterbare Daten, simples UI"? - -## 11. Analytics-Events (Mindestumfang Phase 1) - -Über `mana-analytics`: - -- `cards_session_started` — `{ deckId, dueCount }` -- `cards_card_rated` — `{ cardId, type, grade (1–4), elapsedMs }` -- `cards_session_completed` — `{ deckId, cardCount, durationMs }` -- `cards_deck_created` — `{ deckId }` -- `cards_card_created` — `{ deckId, type }` -- `cards_fsrs_optimized` — `{ reviewCount, paramsHash }` -- `cards_pwa_installed` — Standard-PWA-Event - -Reicht für die Core-Loop-Validierung. Mehr Events erst, wenn eine -konkrete Frage entsteht, die Daten beantworten sollen. - -## 12. Hinweis im mana-Modul - -Sobald `cardecky.mana.how` live ist, bekommt das mana-Modul einen -**dezenten** Hinweis (z.B. ein Banner oder Badge über der ListView): -"Cardecky gibt es jetzt auch als eigenständige App". Kein Pop-up, kein -forcierter Redirect — User entscheiden selbst. diff --git a/apps/cards/README.md b/apps/cards/README.md deleted file mode 100644 index 75891b4a9..000000000 --- a/apps/cards/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Cardecky - -Spaced-repetition flashcards on **cardecky.mana.how**. - -Phase-1 standalone web app. The frontend lives here; data, auth, and -sync are shared with the rest of the Mana stack: - -- **Auth:** mana-auth (SSO), `*.mana.how` -- **Sync:** mana-sync, app-id `cards` -- **Storage:** `mana_platform.cards.*` (Postgres, RLS) - -The same `cards` data backs the **mana** built-in Cardecky module at -`mana.how/cards`. Schema changes ship to both frontends together — see -`apps/cards/GUIDELINES.md`. - -## Layout - -``` -apps/cards/ -├── apps/ -│ └── web/ # SvelteKit 2 + Svelte 5 — the Phase-1 surface -├── GUIDELINES.md # Project rules (read first) -└── README.md -``` - -`apps/cards/apps/mobile/` and any production `apps/cards/apps/landing/` -will land in Phase 2/3. - -## Quick start - -```bash -pnpm install -pnpm --filter @cards/web dev # cardecky.mana.how on http://localhost:5180 -``` diff --git a/apps/cards/apps/web/.gitignore b/apps/cards/apps/web/.gitignore deleted file mode 100644 index 29d6acad1..000000000 --- a/apps/cards/apps/web/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -node_modules -.DS_Store -.svelte-kit -build -.env -.env.* -!.env.example diff --git a/apps/cards/apps/web/Dockerfile b/apps/cards/apps/web/Dockerfile deleted file mode 100644 index 4f4174f67..000000000 --- a/apps/cards/apps/web/Dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -# syntax=docker/dockerfile:1 -# Cardecky Standalone — cardecky.mana.how. Mirrors apps/manavoxel/apps/web/Dockerfile. - -# ─── Stage 1: Build ────────────────────────────────────────── -FROM sveltekit-base:local AS builder - -ARG PUBLIC_MANA_AUTH_URL=http://mana-auth:3001 -ARG PUBLIC_SYNC_SERVER_URL=http://mana-sync:3050 -ENV PUBLIC_MANA_AUTH_URL=$PUBLIC_MANA_AUTH_URL -ENV PUBLIC_SYNC_SERVER_URL=$PUBLIC_SYNC_SERVER_URL - -# Cards-specific app sources. The shared @mana/* packages already live in -# the sveltekit-base image; we only copy what's unique to this app. -COPY apps/cards/apps/web ./apps/cards/apps/web -COPY packages/cards-core ./packages/cards-core - -RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ - pnpm install --no-frozen-lockfile --ignore-scripts - -WORKDIR /app/apps/cards/apps/web -RUN pnpm exec svelte-kit sync -RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm build - -# ─── Stage 2: Production ───────────────────────────────────── -FROM node:20-alpine AS production - -WORKDIR /app/apps/cards/apps/web - -COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm -COPY --from=builder /app/apps/cards/apps/web/node_modules ./node_modules -COPY --from=builder /app/apps/cards/apps/web/build ./build -COPY --from=builder /app/apps/cards/apps/web/package.json ./ - -EXPOSE 5180 - -ENV NODE_ENV=production -ENV PORT=5180 -ENV HOST=0.0.0.0 - -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:5180/ || exit 1 - -CMD ["node", "build"] diff --git a/apps/cards/apps/web/package.json b/apps/cards/apps/web/package.json deleted file mode 100644 index 5869ae34a..000000000 --- a/apps/cards/apps/web/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@cards/web", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "dev": "vite dev --port 5180", - "build": "vite build", - "preview": "vite preview --port 5180", - "prepare": "svelte-kit sync || echo ''", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --fail-on-warnings" - }, - "devDependencies": { - "@mana/shared-vite-config": "workspace:*", - "@sveltejs/adapter-node": "^5.0.0", - "@sveltejs/kit": "^2.47.1", - "@sveltejs/vite-plugin-svelte": "^5.0.4", - "@tailwindcss/vite": "^4.1.7", - "@types/node": "^22.10.5", - "@types/sql.js": "^1.4.11", - "@vite-pwa/sveltekit": "^1.1.0", - "svelte": "^5.41.0", - "svelte-check": "^4.3.3", - "tailwindcss": "^4.1.17", - "typescript": "^5.7.2", - "vite": "^6.0.7" - }, - "dependencies": { - "@mana/cards-core": "workspace:*", - "@mana/local-store": "workspace:*", - "@mana/shared-auth": "workspace:*", - "@mana/shared-auth-ui": "workspace:*", - "@mana/shared-branding": "workspace:*", - "@mana/shared-icons": "workspace:*", - "@mana/shared-privacy": "workspace:*", - "@mana/shared-pwa": "workspace:*", - "@mana/shared-stores": "workspace:*", - "@mana/shared-tailwind": "workspace:*", - "@mana/shared-theme": "workspace:*", - "@mana/shared-theme-ui": "workspace:*", - "@mana/shared-types": "workspace:*", - "@mana/shared-utils": "workspace:*", - "dexie": "^4.4.1", - "jszip": "^3.10.1", - "pdfjs-dist": "^5.7.284", - "sql.js": "^1.14.1" - } -} diff --git a/apps/cards/apps/web/src/app.css b/apps/cards/apps/web/src/app.css deleted file mode 100644 index 94506ec9d..000000000 --- a/apps/cards/apps/web/src/app.css +++ /dev/null @@ -1,63 +0,0 @@ -@import 'tailwindcss'; -@import '@mana/shared-tailwind/themes.css'; -@import '@mana/shared-tailwind/sources.css'; - -/* Phase A — Cards now lives on the unified @mana/shared-theme tokens. - The placeholder --color-cards-* palette is gone; everything goes - through `--color-{background,foreground,surface,muted,…}` from - shared-tailwind. The runtime `createThemeStore({ appId: 'cards' })` - in +layout.svelte writes the live variant + mode onto the - document. */ - -/* Cloze rendering — produced by @mana/cards-core/render. Uses the - active app accent so the highlight follows the Cards brand. */ -.cloze-blank { - background: hsl(var(--color-app-accent) / 0.18); - border-radius: 0.25rem; - padding: 0.05rem 0.4rem; - color: hsl(var(--color-app-accent)); - font-style: italic; -} - -mark.cloze-active { - background: hsl(var(--color-success) / 0.2); - color: hsl(var(--color-success)); - padding: 0.05rem 0.25rem; - border-radius: 0.25rem; -} - -/* Minimal styling for HTML produced by marked() — Tailwind v4 ships - without typography plugin so we set the basics by hand. */ -.card-content :where(p, ul, ol) { - margin-block: 0.5rem; -} -.card-content :where(ul) { - list-style: disc; - padding-inline-start: 1.25rem; -} -.card-content :where(ol) { - list-style: decimal; - padding-inline-start: 1.25rem; -} -.card-content :where(code) { - background: hsl(var(--color-muted) / 0.6); - padding: 0.1rem 0.3rem; - border-radius: 0.25rem; - font-size: 0.95em; -} -.card-content :where(pre) { - background: hsl(var(--color-muted) / 0.4); - padding: 0.75rem; - border-radius: 0.5rem; - overflow-x: auto; -} -.card-content :where(a) { - color: hsl(var(--color-app-accent)); - text-decoration: underline; -} -.card-content :where(strong) { - font-weight: 600; -} -.card-content :where(em) { - font-style: italic; -} diff --git a/apps/cards/apps/web/src/app.d.ts b/apps/cards/apps/web/src/app.d.ts deleted file mode 100644 index 3b4b2bb75..000000000 --- a/apps/cards/apps/web/src/app.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Virtual modules provided by vite-plugin-pwa (wrapped by @vite-pwa/sveltekit): -// - virtual:pwa-info → pwaInfo.webManifest.linkTag for -// - virtual:pwa-register/svelte → useRegisterSW() Svelte-store hook -/// -/// - -declare global { - namespace App { - // eslint-disable-next-line @typescript-eslint/no-empty-object-type - interface Locals {} - // eslint-disable-next-line @typescript-eslint/no-empty-object-type - interface PageData {} - } -} - -export {}; diff --git a/apps/cards/apps/web/src/app.html b/apps/cards/apps/web/src/app.html deleted file mode 100644 index 470d4ca25..000000000 --- a/apps/cards/apps/web/src/app.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - %sveltekit.head% - - -
%sveltekit.body%
- - diff --git a/apps/cards/apps/web/src/hooks.server.ts b/apps/cards/apps/web/src/hooks.server.ts deleted file mode 100644 index 846ce98e2..000000000 --- a/apps/cards/apps/web/src/hooks.server.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { Handle } from '@sveltejs/kit'; - -/** - * Inject the runtime client URLs into on every SSR'd page. - * - * `@mana/shared-auth-ui` reads `window.__PUBLIC_MANA_AUTH_URL__` to know - * where to POST /api/v1/auth/login (and friends). Without this hook the - * client falls back to a relative URL → 404 on cardecky.mana.how. - * - * `process.env.PUBLIC_MANA_*_URL_CLIENT` come from the container - * environment (docker-compose.macmini.yml). $env/static/public would - * bake the URLs at build time; we want runtime so the same image can - * serve dev and prod. - */ - -const PUBLIC_MANA_AUTH_URL_CLIENT = - process.env.PUBLIC_MANA_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_AUTH_URL || ''; -const PUBLIC_MANA_SYNC_URL_CLIENT = - process.env.PUBLIC_MANA_SYNC_URL_CLIENT || process.env.PUBLIC_MANA_SYNC_URL || ''; -const PUBLIC_MANA_LLM_URL_CLIENT = - process.env.PUBLIC_MANA_LLM_URL_CLIENT || process.env.PUBLIC_MANA_LLM_URL || ''; -const PUBLIC_MANA_MEDIA_URL_CLIENT = - process.env.PUBLIC_MANA_MEDIA_URL_CLIENT || process.env.PUBLIC_MANA_MEDIA_URL || ''; -const PUBLIC_CARDS_API_URL_CLIENT = - process.env.PUBLIC_CARDS_API_URL_CLIENT || process.env.PUBLIC_CARDS_API_URL || ''; - -export const handle: Handle = async ({ event, resolve }) => { - return resolve(event, { - transformPageChunk: ({ html }) => { - const envScript = - ``; - return html.replace('', `${envScript}`); - }, - }); -}; diff --git a/apps/cards/apps/web/src/lib/ai/generate.ts b/apps/cards/apps/web/src/lib/ai/generate.ts deleted file mode 100644 index 265741222..000000000 --- a/apps/cards/apps/web/src/lib/ai/generate.ts +++ /dev/null @@ -1,118 +0,0 @@ -/** - * AI card generation — text → list of basic cards via mana-llm. - * - * Uses mana-llm's OpenAI-compatible /v1/chat/completions endpoint with - * a system prompt that constrains the output to a JSON array. We strip - * Markdown code fences before parsing because most chat models wrap - * JSON output in ```json blocks even when explicitly told not to. - * - * No streaming — we need the full JSON before we can show anything. - * Phase-2 ideas: chunk long inputs, PDF parsing, image OCR. - */ - -const SYSTEM_PROMPT = `Du bist ein Karteikarten-Generator. Aus dem vom Nutzer gegebenen Text erstellst du Lernkarten zum Auswendiglernen. - -Regeln: -- Antworte AUSSCHLIESSLICH mit einem JSON-Array, ohne Erklärung, ohne Markdown-Code-Fences. -- Schema: [{"front": "Frage oder Begriff", "back": "Antwort"}, ...] -- 5–15 Karten je nach Textlänge. -- Front: kurze, präzise Frage oder ein Begriff. Back: prägnante Antwort, max. 2 Sätze. -- Eine Karte pro klar abgegrenzter Faktenerinnerung — nicht ganze Absätze umkopieren. -- Sprache: dieselbe wie der Quelltext.`; - -export interface GeneratedCard { - front: string; - back: string; -} - -function llmUrl(): string { - if (typeof window !== 'undefined') { - const fromWindow = (window as unknown as { __PUBLIC_MANA_LLM_URL__?: string }) - .__PUBLIC_MANA_LLM_URL__; - if (fromWindow) return fromWindow.replace(/\/$/, ''); - } - return 'http://localhost:3025'; -} - -function stripCodeFences(s: string): string { - return s - .replace(/^\s*```(?:json|javascript|js)?\s*/i, '') - .replace(/\s*```\s*$/i, '') - .trim(); -} - -function defaultModel(): string { - if (typeof window !== 'undefined') { - const fromWindow = (window as unknown as { __PUBLIC_CARDS_AI_MODEL__?: string }) - .__PUBLIC_CARDS_AI_MODEL__; - if (fromWindow) return fromWindow; - } - // mana-llm proxies many providers — this id matches what the - // playground module uses as a sensible default. Adjust per env via - // __PUBLIC_CARDS_AI_MODEL__ injection. - return 'gpt-4o-mini'; -} - -export async function generateCardsFromText( - source: string, - opts: { model?: string; signal?: AbortSignal } = {} -): Promise { - const trimmed = source.trim(); - if (!trimmed) return []; - - const res = await fetch(`${llmUrl()}/v1/chat/completions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - signal: opts.signal, - body: JSON.stringify({ - model: opts.model ?? defaultModel(), - temperature: 0.3, - messages: [ - { role: 'system', content: SYSTEM_PROMPT }, - { role: 'user', content: trimmed }, - ], - }), - }); - - if (!res.ok) { - const detail = await res.text().catch(() => ''); - throw new Error(`mana-llm: ${res.status} ${res.statusText}${detail ? ` — ${detail}` : ''}`); - } - - const json = (await res.json()) as { - choices?: { message?: { content?: string } }[]; - }; - const raw = json.choices?.[0]?.message?.content?.trim(); - if (!raw) throw new Error('Leere Antwort vom LLM erhalten.'); - - let parsed: unknown; - try { - parsed = JSON.parse(stripCodeFences(raw)); - } catch (e) { - throw new Error(`LLM-Antwort war kein gültiges JSON:\n${raw.slice(0, 200)}`); - } - - if (!Array.isArray(parsed)) { - throw new Error('LLM-Antwort ist kein Array.'); - } - - const cards: GeneratedCard[] = []; - for (const item of parsed) { - if ( - typeof item === 'object' && - item !== null && - typeof (item as GeneratedCard).front === 'string' && - typeof (item as GeneratedCard).back === 'string' - ) { - const c = item as GeneratedCard; - if (c.front.trim() && c.back.trim()) { - cards.push({ front: c.front.trim(), back: c.back.trim() }); - } - } - } - - if (cards.length === 0) { - throw new Error('Keine gültigen Karten in der LLM-Antwort gefunden.'); - } - return cards; -} diff --git a/apps/cards/apps/web/src/lib/ai/pdf.ts b/apps/cards/apps/web/src/lib/ai/pdf.ts deleted file mode 100644 index 9cb5655a8..000000000 --- a/apps/cards/apps/web/src/lib/ai/pdf.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * PDF text extraction using pdfjs-dist. - * - * Loads each page, walks the text layer, joins items with spaces and - * pages with double newlines so the LLM gets a structured input. We - * don't try to preserve columns / tables — the use case is "feed me - * the prose so I can make cards", not document fidelity. - * - * Worker is wired via Vite's `?worker` suffix so the heavy parsing - * happens off the main thread (PDF extraction is CPU-heavy). - */ - -import * as pdfjs from 'pdfjs-dist'; -import PdfjsWorker from 'pdfjs-dist/build/pdf.worker.mjs?worker'; - -let workerWired = false; -function ensureWorker() { - if (workerWired) return; - pdfjs.GlobalWorkerOptions.workerPort = new PdfjsWorker(); - workerWired = true; -} - -export interface PdfExtractResult { - text: string; - pageCount: number; -} - -export async function extractTextFromPdf(file: File | Blob): Promise { - ensureWorker(); - const buffer = await file.arrayBuffer(); - const doc = await pdfjs.getDocument({ data: new Uint8Array(buffer) }).promise; - - const pages: string[] = []; - for (let i = 1; i <= doc.numPages; i++) { - const page = await doc.getPage(i); - const content = await page.getTextContent(); - const pieces: string[] = []; - for (const item of content.items) { - if (typeof (item as { str?: string }).str === 'string') { - pieces.push((item as { str: string }).str); - } - } - pages.push( - pieces - .join(' ') - .replace(/[ \t]+/g, ' ') - .trim() - ); - } - - await doc.destroy(); - return { - text: pages.filter(Boolean).join('\n\n'), - pageCount: doc.numPages, - }; -} diff --git a/apps/cards/apps/web/src/lib/anki/import.ts b/apps/cards/apps/web/src/lib/anki/import.ts deleted file mode 100644 index b01176c17..000000000 --- a/apps/cards/apps/web/src/lib/anki/import.ts +++ /dev/null @@ -1,212 +0,0 @@ -/** - * Apply a `ParsedAnki` to the local DB. - * - * Strategy: every Anki deck becomes one of our decks (1:1, name-mapped). - * Card content is HTML-sanitized to plain Markdown / inline media tags - * before save. Reviews are auto-generated by reviewStore.ensureReviewsForCard - * — the imported cards become "new" in the FSRS sense, no inherited schedule. - * - * Media: every referenced file is uploaded to mana-media first; the - * resulting URL replaces the original Anki filename in the field text. - * Files referenced from no card are skipped — many Anki decks bundle - * orphaned media that bloats the upload time. - * - * No de-dupe: re-importing the same .apkg adds duplicate decks. The UI - * warns about this once we decide it matters. - */ - -import { deckStore } from '../stores/decks.svelte'; -import { cardStore } from '../stores/cards.svelte'; -import { uploadCardMedia, MediaUploadError } from '../media/upload'; -import { sanitizeAnkiHtml, type ParsedAnki } from './parse'; - -export interface ImportResult { - decksCreated: number; - cardsCreated: number; - mediaUploaded: number; - mediaFailed: number; - failed: number; -} - -export interface MediaProgress { - uploaded: number; - total: number; -} - -const MEDIA_CONCURRENCY = 4; -// Anki's always quotes; we also catch [sound:foo.mp3]. -const IMG_RE = /]*\bsrc=["']([^"']+)["']/gi; -const SOUND_RE = /\[sound:([^\]]+)\]/g; - -function collectMediaRefs(parsed: ParsedAnki): Set { - const refs = new Set(); - for (const card of parsed.cards) { - for (const value of Object.values(card.fields)) { - let m: RegExpExecArray | null; - IMG_RE.lastIndex = 0; - while ((m = IMG_RE.exec(value))) refs.add(m[1]); - SOUND_RE.lastIndex = 0; - while ((m = SOUND_RE.exec(value))) refs.add(m[1]); - } - } - return refs; -} - -async function uploadOne( - filename: string, - parsed: ParsedAnki -): Promise<{ filename: string; url: string | null }> { - const entry = parsed.mediaByFilename.get(filename); - if (!entry) return { filename, url: null }; - try { - const blob = await entry.async('blob'); - const file = new File([blob], filename, { type: guessMime(filename) }); - const media = await uploadCardMedia(file); - return { filename, url: media.url }; - } catch (e) { - if (e instanceof MediaUploadError) { - console.warn(`[anki] media upload failed: ${filename}`, e.message); - } else { - console.warn(`[anki] media upload failed: ${filename}`, e); - } - return { filename, url: null }; - } -} - -function guessMime(filename: string): string { - const ext = filename.split('.').pop()?.toLowerCase() ?? ''; - const map: Record = { - jpg: 'image/jpeg', - jpeg: 'image/jpeg', - png: 'image/png', - gif: 'image/gif', - webp: 'image/webp', - svg: 'image/svg+xml', - mp3: 'audio/mpeg', - ogg: 'audio/ogg', - oga: 'audio/ogg', - wav: 'audio/wav', - m4a: 'audio/mp4', - mp4: 'video/mp4', - webm: 'video/webm', - }; - return map[ext] ?? 'application/octet-stream'; -} - -async function uploadAllMedia( - parsed: ParsedAnki, - onProgress?: (p: MediaProgress) => void -): Promise<{ urlByFilename: Map; uploaded: number; failed: number }> { - const referenced = [...collectMediaRefs(parsed)].filter((f) => parsed.mediaByFilename.has(f)); - const urlByFilename = new Map(); - let uploaded = 0; - let failed = 0; - - if (referenced.length === 0) { - onProgress?.({ uploaded: 0, total: 0 }); - return { urlByFilename, uploaded, failed }; - } - - let nextIdx = 0; - async function worker() { - while (true) { - const idx = nextIdx++; - if (idx >= referenced.length) return; - const result = await uploadOne(referenced[idx], parsed); - if (result.url) { - urlByFilename.set(result.filename, result.url); - uploaded++; - } else { - failed++; - } - onProgress?.({ uploaded: uploaded + failed, total: referenced.length }); - } - } - - await Promise.all(Array.from({ length: MEDIA_CONCURRENCY }, () => worker())); - return { urlByFilename, uploaded, failed }; -} - -export async function importParsedAnki( - parsed: ParsedAnki, - opts: { onMediaProgress?: (p: MediaProgress) => void } = {} -): Promise { - const result: ImportResult = { - decksCreated: 0, - cardsCreated: 0, - mediaUploaded: 0, - mediaFailed: 0, - failed: 0, - }; - - // 1) Media — upload before any cards so the field-text rewrite has - // real URLs to point at. Empty in the no-media case. - const { urlByFilename, uploaded, failed } = await uploadAllMedia(parsed, opts.onMediaProgress); - result.mediaUploaded = uploaded; - result.mediaFailed = failed; - - // 2) Decks — Anki "::" hierarchy flattened to " / ". - const ankiIdToDeckId = new Map(); - for (const ankiDeck of parsed.decks) { - const title = ankiDeck.name.replace(/::/g, ' / '); - const created = await deckStore.createDeck({ title, description: 'Aus Anki importiert' }); - if (!created) { - result.failed++; - continue; - } - ankiIdToDeckId.set(ankiDeck.ankiId, created.id); - result.decksCreated++; - } - - // Fallback deck for cards whose Anki deck wasn't in the parsed list - // (the "Default" deck Anki uses for orphans, mostly). - const ensureFallbackDeck = (() => { - let id: string | null = null; - return async () => { - if (id) return id; - const created = await deckStore.createDeck({ - title: 'Anki-Import', - description: 'Karten ohne explizites Quell-Deck', - }); - if (created) { - id = created.id; - result.decksCreated++; - } - return id; - }; - })(); - - // 3) Cards — sanitize each field with the media URL map. - const orderByDeck = new Map(); - for (const card of parsed.cards) { - let targetDeckId = ankiIdToDeckId.get(card.ankiDeckId); - if (!targetDeckId) { - const fallback = await ensureFallbackDeck(); - if (!fallback) { - result.failed++; - continue; - } - targetDeckId = fallback; - } - - const cleanFields: Record = {}; - for (const [key, value] of Object.entries(card.fields)) { - cleanFields[key] = sanitizeAnkiHtml(value, urlByFilename); - } - - const order = orderByDeck.get(targetDeckId) ?? 0; - orderByDeck.set(targetDeckId, order + 1); - - const created = await cardStore.createCard( - { deckId: targetDeckId, type: card.type, fields: cleanFields }, - order - ); - if (created) { - result.cardsCreated++; - } else { - result.failed++; - } - } - - return result; -} diff --git a/apps/cards/apps/web/src/lib/anki/parse.ts b/apps/cards/apps/web/src/lib/anki/parse.ts deleted file mode 100644 index afccaadc7..000000000 --- a/apps/cards/apps/web/src/lib/anki/parse.ts +++ /dev/null @@ -1,247 +0,0 @@ -/** - * Parse an Anki .apkg / .colpkg file in the browser. - * - * .apkg = ZIP archive containing a SQLite collection (`collection.anki2` - * or `collection.anki21`) plus media files. We open the SQLite blob with - * sql.js (WASM-backed in-browser SQLite) and walk Anki's three core - * tables: `col` (collection meta with JSON-encoded models + decks), - * `notes` (the user-typed content), and `cards` (one row per learnable - * unit — basic = 1, basic-reverse = 2, cloze = N). - * - * MVP scope: basic + basic-reverse + cloze. Image/audio media is - * skipped (Phase 2). Review history is skipped — FSRS state will be - * regenerated on first sight. - */ - -import JSZip, { type JSZipObject } from 'jszip'; -import initSqlJs, { type Database } from 'sql.js'; -import type { CardType } from '@mana/cards-core'; - -export interface ParsedDeck { - ankiId: string; // Anki's numeric deck id, stringified - name: string; // "Studies::Spanish" — Anki uses :: as separator -} - -export interface ParsedCard { - ankiDeckId: string; - type: CardType; - fields: Record; -} - -export interface ParsedAnki { - decks: ParsedDeck[]; - cards: ParsedCard[]; - skipped: number; - warnings: string[]; - /** - * Mapping from the original media filename (as referenced in card - * fields, e.g. `paris.jpg` or `audio_001.mp3`) to its ZIP entry. Anki - * stores files numerically (`0`, `1`, …) and the JSON manifest - * (`media`) maps numbers → original names; we flip that here so the - * importer can look up by the name it sees in the field text. - */ - mediaByFilename: Map; -} - -interface AnkiModel { - id: number; - name: string; - type: number; // 0 = standard, 1 = cloze - flds: { name: string }[]; - tmpls: { name: string }[]; -} - -interface AnkiDeckJson { - id: number; - name: string; -} - -let SQL: Awaited> | null = null; -async function getSql() { - if (SQL) return SQL; - SQL = await initSqlJs({ locateFile: (file) => `/${file}` }); - return SQL; -} - -export async function parseApkg(file: File | Blob): Promise { - const zip = await JSZip.loadAsync(await file.arrayBuffer()); - - const collectionEntry = zip.file('collection.anki21') ?? zip.file('collection.anki2'); - if (!collectionEntry) { - throw new Error( - 'Keine Anki-Collection-Datei in der .apkg gefunden (erwartet: collection.anki21 oder collection.anki2).' - ); - } - - const sqliteBytes = await collectionEntry.async('uint8array'); - const sql = await getSql(); - const db: Database = new sql.Database(sqliteBytes); - - const mediaByFilename = await extractMediaManifest(zip); - - try { - const result = extract(db); - return { ...result, mediaByFilename }; - } finally { - db.close(); - } -} - -async function extractMediaManifest(zip: JSZip): Promise> { - const out = new Map(); - const manifestEntry = zip.file('media'); - if (!manifestEntry) return out; - let manifest: Record; - try { - manifest = JSON.parse(await manifestEntry.async('string')); - } catch { - return out; - } - for (const [numericKey, originalName] of Object.entries(manifest)) { - const entry = zip.file(numericKey); - if (entry) out.set(originalName, entry); - } - return out; -} - -// Internal extract returns everything except media — that's plumbed in -// at the parseApkg layer so the SQLite-only path stays focused. -type ExtractResult = Omit; -function extract(db: Database): ExtractResult { - const colRow = db.exec('SELECT models, decks FROM col LIMIT 1'); - if (colRow.length === 0 || colRow[0].values.length === 0) { - throw new Error('Anki-Collection ist leer.'); - } - const [modelsJson, decksJson] = colRow[0].values[0] as [string, string]; - const models: Record = JSON.parse(modelsJson); - const decksMap: Record = JSON.parse(decksJson); - - const decks: ParsedDeck[] = Object.values(decksMap) - .filter((d) => d.id !== 1) // Anki's "Default" deck has id 1; skip if empty later - .map((d) => ({ ankiId: String(d.id), name: d.name })); - - // Pre-load notes into a Map so we don't hit SQLite per card. - type NoteRow = { id: string; mid: string; flds: string }; - const notesById = new Map(); - const notesRes = db.exec('SELECT id, mid, flds FROM notes'); - if (notesRes.length > 0) { - for (const row of notesRes[0].values) { - const [id, mid, flds] = row as [number, number, string]; - notesById.set(String(id), { id: String(id), mid: String(mid), flds }); - } - } - - const warnings: string[] = []; - const cards: ParsedCard[] = []; - let skipped = 0; - - const cardsRes = db.exec('SELECT nid, did, ord FROM cards'); - if (cardsRes.length === 0) - return { decks, cards: [], skipped: 0, warnings: ['Keine Karten gefunden.'] }; - - // We dedupe at the note level — Anki stores one DB-row per generated - // card (basic-reverse = 2 rows, cloze cluster c1+c2 = 2 rows). Our - // model regenerates these from `type` + `fields` automatically, so - // pulling each note once is enough. - const seenNotes = new Set(); - for (const row of cardsRes[0].values) { - const [nid, did] = row as [number, number, number]; - const noteKey = String(nid); - if (seenNotes.has(noteKey)) continue; - seenNotes.add(noteKey); - - const note = notesById.get(noteKey); - if (!note) { - skipped++; - continue; - } - const model = models[note.mid]; - if (!model) { - skipped++; - warnings.push(`Note ${nid}: unknown model ${note.mid}`); - continue; - } - - const fieldValues = note.flds.split('\x1f'); - const result = mapNoteToCard(model, fieldValues); - if (!result) { - skipped++; - continue; - } - cards.push({ ankiDeckId: String(did), ...result }); - } - - if (skipped > 0) warnings.unshift(`${skipped} Karten übersprungen (unbekannter Typ).`); - return { decks, cards, skipped, warnings }; -} - -function mapNoteToCard( - model: AnkiModel, - fields: string[] -): { type: CardType; fields: Record } | null { - // Cloze: exactly one input field with {{cN::...}} markup. - if (model.type === 1) { - const text = fields[0] ?? ''; - return { type: 'cloze', fields: { text, ...(fields[1] ? { extra: fields[1] } : {}) } }; - } - - // Standard: one or two templates → basic / basic-reverse. - if (model.type === 0) { - const front = fields[0] ?? ''; - const back = fields[1] ?? ''; - if (model.tmpls.length === 2) { - return { type: 'basic-reverse', fields: { front, back } }; - } - // 1 (or unusual N) → treat as basic. Custom multi-card templates - // lose their extra surfaces; the user-typed content survives. - return { type: 'basic', fields: { front, back } }; - } - - return null; -} - -/** - * Convert Anki's HTML / image / sound markup to plain text + Markdown. - * - * `mediaUrlByFilename` maps the filename Anki references in the field - * (e.g. `paris.jpg` for `` or `audio.mp3` for - * `[sound:audio.mp3]`) to its post-upload URL on mana-media. Anything - * not in the map is dropped silently — same as the no-media path. - */ -export function sanitizeAnkiHtml( - html: string, - mediaUrlByFilename: Map = new Map() -): string { - const imgReplaced = html.replace( - /]*\bsrc=["']([^"']+)["'][^>]*>/gi, - (_, src: string) => { - const url = mediaUrlByFilename.get(src); - return url ? `` : ''; - } - ); - const soundReplaced = imgReplaced.replace(/\[sound:([^\]]+)\]/g, (_, name: string) => { - const url = mediaUrlByFilename.get(name); - return url ? `` : ''; - }); - - return ( - soundReplaced - .replace(//gi, '\n') - .replace(/<\/?(?:b|strong)>/gi, '**') - .replace(/<\/?(?:i|em)>/gi, '*') - .replace(/<\/?p>/gi, '\n') - .replace(/<\/?div>/gi, '\n') - // Drop remaining HTML tags except the ones we just emitted - // (img/audio/video/source) — those need to survive into the - // rendered card. Negative lookahead does that in one pass. - .replace(/<(?!\/?(?:img|audio|video|source)\b)[^>]+>/gi, '') - .replace(/ /g, ' ') - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/\n{3,}/g, '\n\n') - .trim() - ); -} diff --git a/apps/cards/apps/web/src/lib/api/cards-api.ts b/apps/cards/apps/web/src/lib/api/cards-api.ts deleted file mode 100644 index 6ed102197..000000000 --- a/apps/cards/apps/web/src/lib/api/cards-api.ts +++ /dev/null @@ -1,505 +0,0 @@ -/** - * Thin client for cards-server (https://cardecky-api.mana.how / dev :3072). - * - * The auth-store provides the JWT; we never read tokens from storage - * here directly so there's only one place that knows about token - * lifecycle (refresh, expiry, vault). - * - * All endpoints under /v1 require auth; the wrapper just always - * sends `Authorization: Bearer …`. Errors come back as Hono's - * `{ statusCode, message, details? }` shape — we surface that to - * callers via the typed `CardsApiError` so UIs can branch on it. - */ - -import { authStore } from '$lib/stores/auth.svelte'; - -function baseUrl(): string { - if (typeof window !== 'undefined') { - const fromWindow = (window as unknown as { __PUBLIC_CARDS_API_URL__?: string }) - .__PUBLIC_CARDS_API_URL__; - if (fromWindow) return fromWindow.replace(/\/$/, ''); - } - return 'http://localhost:3072'; -} - -export class CardsApiError extends Error { - constructor( - public status: number, - message: string, - public details?: unknown - ) { - super(message); - this.name = 'CardsApiError'; - } -} - -interface RequestOptions { - method?: 'GET' | 'POST' | 'PATCH' | 'DELETE'; - body?: unknown; - signal?: AbortSignal; - /** - * - `true` (default): require an Authorization header — throws 401 if no token. - * - `'optional'`: include token if available, otherwise send anonymously. - * - `false`: never send a token. - */ - auth?: boolean | 'optional'; -} - -async function request(path: string, opts: RequestOptions = {}): Promise { - const headers: Record = {}; - if (opts.body !== undefined) headers['Content-Type'] = 'application/json'; - if (opts.auth === 'optional') { - // Best-effort: include token if present, otherwise anonymous. - const token = await authStore.getValidToken?.(); - if (token) headers['Authorization'] = `Bearer ${token}`; - } else if (opts.auth !== false) { - const token = await authStore.getValidToken?.(); - if (!token) throw new CardsApiError(401, 'Not signed in'); - headers['Authorization'] = `Bearer ${token}`; - } - - const res = await fetch(`${baseUrl()}${path}`, { - method: opts.method ?? 'GET', - headers, - body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined, - signal: opts.signal, - }); - - if (res.status === 204) return undefined as T; - - const text = await res.text(); - const json: unknown = text ? safeJsonParse(text) : null; - - if (!res.ok) { - const payload = (json ?? {}) as { message?: string; details?: unknown }; - throw new CardsApiError(res.status, payload.message ?? `HTTP ${res.status}`, payload.details); - } - return json as T; -} - -function safeJsonParse(s: string): unknown { - try { - return JSON.parse(s); - } catch { - return s; - } -} - -// ─── Authors ──────────────────────────────────────────────── - -export interface Author { - userId: string; - slug: string; - displayName: string; - bio: string | null; - avatarUrl: string | null; - pseudonym: boolean; - verifiedMana: boolean; - verifiedCommunity: boolean; - bannedAt: string | null; -} - -export interface PublicAuthor { - slug: string; - displayName: string; - bio: string | null; - avatarUrl: string | null; - joinedAt: string; - pseudonym: boolean; - verifiedMana: boolean; - verifiedCommunity: boolean; - banned: boolean; -} - -export const cardsApi = { - authors: { - me: () => request('/v1/authors/me'), - upsertMe: (input: { - slug: string; - displayName: string; - bio?: string; - avatarUrl?: string; - pseudonym?: boolean; - }) => request('/v1/authors/me', { method: 'POST', body: input }), - bySlug: (slug: string) => request(`/v1/authors/${encodeURIComponent(slug)}`), - }, - decks: { - init: (input: { - slug: string; - title: string; - description?: string; - language?: string; - license?: string; - priceCredits?: number; - }) => request('/v1/decks', { method: 'POST', body: input }), - bySlug: (slug: string) => - request<{ - deck: PublicDeck; - latestVersion: PublicDeckVersion | null; - hasPurchased: boolean | null; - }>(`/v1/decks/${encodeURIComponent(slug)}`, { auth: 'optional' }), - publish: ( - slug: string, - input: { - semver: string; - changelog?: string; - cards: { type: string; fields: Record }[]; - } - ) => - request(`/v1/decks/${encodeURIComponent(slug)}/publish`, { - method: 'POST', - body: input, - }), - star: (slug: string) => - request<{ ok: true }>(`/v1/decks/${encodeURIComponent(slug)}/star`, { method: 'POST' }), - unstar: (slug: string) => - request<{ ok: true }>(`/v1/decks/${encodeURIComponent(slug)}/star`, { method: 'DELETE' }), - }, - explore: { - landing: () => - request<{ featured: DeckSummary[]; trending: DeckSummary[] }>('/v1/explore', { - auth: 'optional', - }), - browse: (params: { - q?: string; - tag?: string; - lang?: string; - author?: string; - sort?: 'recent' | 'popular' | 'trending'; - limit?: number; - offset?: number; - }) => { - const qs = new URLSearchParams(); - for (const [k, v] of Object.entries(params)) { - if (v !== undefined && v !== null && v !== '') qs.set(k, String(v)); - } - const path = `/v1/decks${qs.toString() ? '?' + qs.toString() : ''}`; - return request<{ items: DeckSummary[]; total: number }>(path, { auth: 'optional' }); - }, - tags: () => request('/v1/tags', { auth: 'optional' }), - }, - follows: { - follow: (authorSlug: string) => - request<{ ok: true }>(`/v1/authors/${encodeURIComponent(authorSlug)}/follow`, { - method: 'POST', - }), - unfollow: (authorSlug: string) => - request<{ ok: true }>(`/v1/authors/${encodeURIComponent(authorSlug)}/follow`, { - method: 'DELETE', - }), - }, - subscriptions: { - list: () => request('/v1/me/subscriptions'), - subscribe: (deckSlug: string) => - request<{ deckSlug: string; latestVersionId: string }>( - `/v1/decks/${encodeURIComponent(deckSlug)}/subscribe`, - { method: 'POST' } - ), - unsubscribe: (deckSlug: string) => - request<{ ok: true }>(`/v1/decks/${encodeURIComponent(deckSlug)}/subscribe`, { - method: 'DELETE', - }), - version: (deckSlug: string, semver: string) => - request( - `/v1/decks/${encodeURIComponent(deckSlug)}/versions/${encodeURIComponent(semver)}`, - { auth: 'optional' } - ), - diff: (deckSlug: string, fromSemver: string) => - request( - `/v1/decks/${encodeURIComponent(deckSlug)}/diff?from=${encodeURIComponent(fromSemver)}`, - { auth: 'optional' } - ), - }, - pullRequests: { - create: ( - deckSlug: string, - input: { - title: string; - body?: string; - diff: PullRequestDiffInput; - } - ) => - request(`/v1/decks/${encodeURIComponent(deckSlug)}/pull-requests`, { - method: 'POST', - body: input, - }), - list: (deckSlug: string, status?: 'open' | 'merged' | 'closed' | 'rejected') => { - const qs = status ? `?status=${status}` : ''; - return request( - `/v1/decks/${encodeURIComponent(deckSlug)}/pull-requests${qs}`, - { auth: 'optional' } - ); - }, - get: (id: string) => request(`/v1/pull-requests/${id}`, { auth: 'optional' }), - merge: (id: string, opts: { newSemver?: string; mergeNote?: string } = {}) => - request<{ pullRequest: PullRequest; version: PublicDeckVersion }>( - `/v1/pull-requests/${id}/merge`, - { method: 'POST', body: opts } - ), - close: (id: string) => - request<{ ok: true }>(`/v1/pull-requests/${id}/close`, { method: 'POST' }), - reject: (id: string) => - request<{ ok: true }>(`/v1/pull-requests/${id}/reject`, { method: 'POST' }), - }, - moderation: { - report: (input: { - deckSlug: string; - cardContentHash?: string; - category: ReportCategory; - body?: string; - }) => request('/v1/reports', { method: 'POST', body: input }), - }, - admin: { - listReports: () => request('/v1/admin/reports'), - resolveReport: (id: string, input: { action: ResolveAction; notes?: string }) => - request<{ action: ResolveAction }>(`/v1/admin/reports/${id}/resolve`, { - method: 'POST', - body: input, - }), - takedownDeck: (slug: string, reason?: string) => - request<{ alreadyDown: boolean }>(`/v1/admin/decks/${encodeURIComponent(slug)}/takedown`, { - method: 'POST', - body: { reason }, - }), - restoreDeck: (slug: string) => - request<{ restored: boolean }>(`/v1/admin/decks/${encodeURIComponent(slug)}/restore`, { - method: 'POST', - body: {}, - }), - verifyAuthor: (slug: string, verifiedMana: boolean) => - request<{ authorSlug: string; verifiedMana: boolean }>( - `/v1/admin/authors/${encodeURIComponent(slug)}/verify`, - { method: 'POST', body: { verifiedMana } } - ), - }, - purchases: { - buy: (deckSlug: string) => - request(`/v1/decks/${encodeURIComponent(deckSlug)}/purchase`, { - method: 'POST', - body: {}, - }), - listMine: () => request('/v1/me/purchases'), - }, - payouts: { - listMine: () => request('/v1/authors/me/payouts'), - }, - discussions: { - countsForDeck: (deckSlug: string) => - request>( - `/v1/decks/${encodeURIComponent(deckSlug)}/discussion-counts`, - { auth: 'optional' } - ), - listForCard: (contentHash: string) => - request(`/v1/cards/${encodeURIComponent(contentHash)}/discussions`, { - auth: 'optional', - }), - post: (contentHash: string, input: { deckSlug: string; body: string; parentId?: string }) => - request(`/v1/cards/${encodeURIComponent(contentHash)}/discussions`, { - method: 'POST', - body: input, - }), - hide: (id: string) => request<{ ok: true }>(`/v1/discussions/${id}/hide`, { method: 'POST' }), - }, -}; - -// Override author lookup to send token opportunistically — public reads. -cardsApi.authors.bySlug = (slug: string) => - request(`/v1/authors/${encodeURIComponent(slug)}`, { auth: 'optional' }); - -export interface DeckSummary { - slug: string; - title: string; - description: string | null; - language: string | null; - license: string; - priceCredits: number; - cardCount: number; - starCount: number; - subscriberCount: number; - isFeatured: boolean; - createdAt: string; - owner: { - slug: string; - displayName: string; - verifiedMana: boolean; - verifiedCommunity: boolean; - }; -} - -export interface TagDefinition { - id: string; - slug: string; - name: string; - parentId: string | null; - description: string | null; - curated: boolean; - createdAt: string; -} - -export interface PublicDeck { - id: string; - slug: string; - title: string; - description: string | null; - language: string | null; - license: string; - priceCredits: number; - ownerUserId: string; - latestVersionId: string | null; - isFeatured: boolean; - isTakedown: boolean; - createdAt: string; -} - -export interface PublicDeckVersion { - id: string; - deckId: string; - semver: string; - changelog: string | null; - contentHash: string; - cardCount: number; - publishedAt: string; - deprecatedAt: string | null; -} - -export interface PublishResult { - deck: PublicDeck; - version: PublicDeckVersion; - moderation: { verdict: 'pass' | 'flag' | 'block'; categories: string[] }; -} - -export interface SubscriptionInfo { - deckSlug: string; - deckTitle: string; - deckDescription: string | null; - subscribedAt: string; - notifyUpdates: boolean; - currentVersionId: string | null; - latestVersionId: string | null; - updateAvailable: boolean; -} - -export interface ServerCard { - contentHash: string; - type: string; - fields: Record; - ord: number; -} - -export interface DeckVersionPayload { - id: string; - semver: string; - contentHash: string; - publishedAt: string; - changelog: string | null; - cards: ServerCard[]; -} - -export interface DiffPayload { - from: string; - to: string; - added: ServerCard[]; - changed: { previous: { contentHash: string }; next: ServerCard }[]; - unchanged: { contentHash: string; ord: number }[]; - removed: { contentHash: string }[]; -} - -export interface PullRequestDiffInput { - add: { type: string; fields: Record }[]; - modify: { previousContentHash: string; type: string; fields: Record }[]; - remove: { contentHash: string }[]; -} - -export type PullRequestStatus = 'open' | 'merged' | 'closed' | 'rejected'; - -export interface PullRequest { - id: string; - deckId: string; - authorUserId: string; - status: PullRequestStatus; - title: string; - body: string | null; - diff: { - add: { type: string; fields: Record }[]; - modify: { contentHash: string; fields: Record }[]; - remove: { contentHash: string }[]; - }; - mergedIntoVersionId: string | null; - createdAt: string; - resolvedAt: string | null; -} - -export type ReportCategory = 'spam' | 'copyright' | 'nsfw' | 'misinformation' | 'hate' | 'other'; - -export type ResolveAction = 'dismiss' | 'takedown' | 'ban-author'; - -export interface DeckReport { - id: string; - deckId: string; - versionId: string | null; - cardContentHash: string | null; - reporterUserId: string; - category: ReportCategory; - body: string | null; - status: 'open' | 'dismissed' | 'actioned'; - createdAt: string; -} - -export interface DeckReportItem extends DeckReport { - deckSlug: string; - deckTitle: string; -} - -export interface PurchaseResult { - purchase: { - id: string; - buyerUserId: string; - deckId: string; - versionId: string; - priceCredits: number; - authorShare: number; - manaShare: number; - purchasedAt: string; - refundedAt: string | null; - }; - payout: { - id: string; - authorUserId: string; - creditsGranted: number; - grantedAt: string; - } | null; - alreadyOwned: boolean; -} - -export interface BuyerPurchase { - id: string; - deckId: string; - deckSlug: string; - deckTitle: string; - priceCredits: number; - purchasedAt: string; - refundedAt: string | null; - versionId: string; - versionSemver: string; -} - -export interface AuthorPayout { - id: string; - purchaseId: string; - creditsGranted: number; - grantedAt: string; - deckSlug: string; - deckTitle: string; - priceCredits: number; -} - -export interface CardDiscussion { - id: string; - cardContentHash: string; - deckId: string; - authorUserId: string; - parentId: string | null; - body: string; - hidden: boolean; - createdAt: string; -} diff --git a/apps/cards/apps/web/src/lib/components/AiCardGen.svelte b/apps/cards/apps/web/src/lib/components/AiCardGen.svelte deleted file mode 100644 index dedcee6e7..000000000 --- a/apps/cards/apps/web/src/lib/components/AiCardGen.svelte +++ /dev/null @@ -1,209 +0,0 @@ - - -
-
- ✨ Karten aus Text generieren - {#if stage !== 'idle'} - - {/if} -
- - {#if stage === 'idle' || stage === 'error'} - - {#if stage === 'error' && error} -

{error}

- {/if} -
-
- {source.length} Zeichen - {#if pdfStatus}📄 {pdfStatus}{/if} -
-
- - -
-
- - {:else if stage === 'reading-pdf'} -
{pdfStatus ?? 'Lese PDF…'}
- {:else if stage === 'generating'} -
Modell denkt nach…
- {:else if stage === 'preview'} -
-
- {generated.length} Karten generiert. Wähle aus, was übernommen werden soll: -
-
    - {#each generated as card, i (i)} -
  • - - -
  • - {/each} -
-
- - - -
-
- {:else if stage === 'creating'} -
Lege Karten an…
- {:else if stage === 'done'} -
✓ {createdCount} Karten angelegt.
- - {/if} -
diff --git a/apps/cards/apps/web/src/lib/components/AnkiImport.svelte b/apps/cards/apps/web/src/lib/components/AnkiImport.svelte deleted file mode 100644 index 981074a5f..000000000 --- a/apps/cards/apps/web/src/lib/components/AnkiImport.svelte +++ /dev/null @@ -1,187 +0,0 @@ - - -
-
Aus Anki importieren
- - {#if stage === 'idle'} - - -
e.preventDefault()} - ondrop={onDrop} - onclick={() => fileInput?.click()} - > -
📦 .apkg-Datei hier ablegen oder klicken
-
- Basic, Basic + Reverse, Cloze · Bilder + Audio werden mit übernommen. -
-
- - {:else if stage === 'parsing'} -
Lese {fileName}…
- {:else if stage === 'preview' && parsed} -
-
- Gefunden in - {fileName}: -
-
    -
  • {parsed.decks.length} {parsed.decks.length === 1 ? 'Deck' : 'Decks'}
  • -
  • {parsed.cards.length} {parsed.cards.length === 1 ? 'Karte' : 'Karten'}
  • - {#if mediaCount > 0} -
  • {mediaCount} Medien (Bilder/Audio)
  • - {/if} - {#if parsed.skipped > 0} -
  • {parsed.skipped} übersprungen (unbekannter Typ)
  • - {/if} -
- {#if parsed.warnings.length > 0} -
- Hinweise ({parsed.warnings.length}) -
    - {#each parsed.warnings.slice(0, 10) as w (w)}
  • {w}
  • {/each} -
-
- {/if} -
- - -
-
- {:else if stage === 'uploading-media'} -
-
Lade Medien hoch · {mediaProgress.uploaded} / {mediaProgress.total}
-
-
-
-
- {:else if stage === 'importing'} -
- Importiere {parsed?.cards.length ?? 0} Karten… -
- {:else if stage === 'done' && result} -
-
- ✓ {result.cardsCreated} Karten in {result.decksCreated} - {result.decksCreated === 1 ? 'Deck' : 'Decks'} angelegt. -
- {#if result.mediaUploaded > 0 || result.mediaFailed > 0} -
- {result.mediaUploaded} Medien übernommen{#if result.mediaFailed > 0} - · {result.mediaFailed} fehlgeschlagen - {/if} -
- {/if} - {#if result.failed > 0} -
{result.failed} Karten konnten nicht angelegt werden.
- {/if} - -
- {:else if stage === 'error'} -
-
Fehler: {error}
- -
- {/if} -
diff --git a/apps/cards/apps/web/src/lib/components/CardDiscussions.svelte b/apps/cards/apps/web/src/lib/components/CardDiscussions.svelte deleted file mode 100644 index 38723a5d2..000000000 --- a/apps/cards/apps/web/src/lib/components/CardDiscussions.svelte +++ /dev/null @@ -1,134 +0,0 @@ - - - diff --git a/apps/cards/apps/web/src/lib/components/CardFace.svelte b/apps/cards/apps/web/src/lib/components/CardFace.svelte deleted file mode 100644 index 19d09cf1f..000000000 --- a/apps/cards/apps/web/src/lib/components/CardFace.svelte +++ /dev/null @@ -1,194 +0,0 @@ - - -{#if isTypeIn} - -
-
- {@html view.prompt} -
- - onTypedAnswer?.((e.currentTarget as HTMLInputElement).value)} - disabled={showBack} - /> - - {#if showBack} -
- {@html view.answer} -
- {/if} -
-{:else} -
- -
-{/if} - - diff --git a/apps/cards/apps/web/src/lib/components/CardsLogo.svelte b/apps/cards/apps/web/src/lib/components/CardsLogo.svelte deleted file mode 100644 index 556e51700..000000000 --- a/apps/cards/apps/web/src/lib/components/CardsLogo.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - diff --git a/apps/cards/apps/web/src/lib/components/DeckCardList.svelte b/apps/cards/apps/web/src/lib/components/DeckCardList.svelte deleted file mode 100644 index 351216754..000000000 --- a/apps/cards/apps/web/src/lib/components/DeckCardList.svelte +++ /dev/null @@ -1,104 +0,0 @@ - - -
-
-

- Karten {cards.length > 0 ? `(${cards.length})` : ''} -

- {#if loading} - Lädt… - {/if} -
- - {#if error} -

- {error} -

- {:else if cards.length === 0 && !loading} -

- Diese Version enthält keine Karten. -

- {:else} -
    - {#each cards as c (c.contentHash)} - {@const n = counts[c.contentHash] ?? 0} - {@const isOpen = openHash === c.contentHash} -
  • - - - {#if isOpen} - - {/if} -
  • - {/each} -
- {/if} -
diff --git a/apps/cards/apps/web/src/lib/components/DeckGrid.svelte b/apps/cards/apps/web/src/lib/components/DeckGrid.svelte deleted file mode 100644 index def2e7c11..000000000 --- a/apps/cards/apps/web/src/lib/components/DeckGrid.svelte +++ /dev/null @@ -1,62 +0,0 @@ - - -{#if decks.length === 0} -

- {emptyText} -

-{:else} - -{/if} diff --git a/apps/cards/apps/web/src/lib/components/PublishDeckModal.svelte b/apps/cards/apps/web/src/lib/components/PublishDeckModal.svelte deleted file mode 100644 index 97145ffed..000000000 --- a/apps/cards/apps/web/src/lib/components/PublishDeckModal.svelte +++ /dev/null @@ -1,353 +0,0 @@ - - -
e.key === 'Escape' && onClose()} - role="presentation" -> - - -
e.stopPropagation()} - > -
-

Deck veröffentlichen

- -
- - {#if stage === 'loading'} -
Lade Author-Profil…
- {:else if stage === 'become-author'} -
-

- Erstelle ein Author-Profil — andere User finden deine Decks unter - cardecky.mana.how/u/dein-slug. -

-
- - -
-
- - -
- - {#if authorStore.error} -

{authorStore.error}

- {/if} -
- - -
-
- {:else if stage === 'meta'} -
-

- Veröffentlicht als cardecky.mana.how/d/{deckSlug || '...'} -

-
- - -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-

- {cards.length} - {cards.length === 1 ? 'Karte' : 'Karten'} werden veröffentlicht. Das Deck durchläuft eine KI-Inhaltsprüfung - — offensichtlich harmloses Material geht direkt durch. -

-
- - -
-
- {:else if stage === 'publishing'} -
- Lade {cards.length} Karten hoch und prüfe Inhalt… -
- {:else if stage === 'done' && result} -
-
- ✓ Veröffentlicht als Version {result.version.semver} -
-
- {result.version.cardCount} Karten · Lizenz: {result.deck.license} -
- {#if result.moderation.verdict === 'flag'} -
- Inhalt wurde zur Moderations-Prüfung markiert ({result.moderation.categories.join( - ', ' - )}). Das Deck ist veröffentlicht; ein Mensch schaut bei Gelegenheit drüber. -
- {/if} - -
- {:else if stage === 'error'} -
-
Fehler: {error}
- -
- {/if} -
-
diff --git a/apps/cards/apps/web/src/lib/components/PullRequestsSection.svelte b/apps/cards/apps/web/src/lib/components/PullRequestsSection.svelte deleted file mode 100644 index f52564aff..000000000 --- a/apps/cards/apps/web/src/lib/components/PullRequestsSection.svelte +++ /dev/null @@ -1,233 +0,0 @@ - - -
-
-

- Pull Requests {prs.length > 0 ? `(${prs.length})` : ''} -

- -
- - {#if error} -

- {error} -

- {/if} - - {#if loading && prs.length === 0} -

- Lädt… -

- {:else if prs.length === 0} -

- Noch keine Pull Requests. Abonnenten können Verbesserungen vorschlagen. -

- {:else} -
    - {#each prs as pr (pr.id)} -
  • -
    -
    -
    - - {pr.status} - -

    {pr.title}

    -
    -

    - {diffSummary(pr)} · {new Date(pr.createdAt).toLocaleDateString('de-DE')} -

    -
    - -
    - - {#if expanded[pr.id]} - {#if pr.body} -

    {pr.body}

    - {/if} - - {#if pr.diff.modify.length > 0} -
    -
    Geändert
    -
      - {#each pr.diff.modify as m (m.contentHash)} -
    • -
      - ← {m.contentHash.slice(0, 12)}… -
      - {#each Object.entries(m.fields) as [k, v]} -
      - {k}: - {v} -
      - {/each} -
    • - {/each} -
    -
    - {/if} - - {#if pr.diff.add.length > 0} -
    -
    - Neu (+{pr.diff.add.length}) -
    -
      - {#each pr.diff.add as a, i (i)} -
    • -
      {a.type}
      - {#each Object.entries(a.fields) as [k, v]} -
      - {k}: - {v} -
      - {/each} -
    • - {/each} -
    -
    - {/if} - - {#if pr.diff.remove.length > 0} -
    -
    - Entfernt (−{pr.diff.remove.length}) -
    -
      - {#each pr.diff.remove as r (r.contentHash)} -
    • · {r.contentHash.slice(0, 12)}…
    • - {/each} -
    -
    - {/if} - - {#if pr.status === 'open' && viewerIsOwner} -
    - - - -
    - {/if} - {/if} -
  • - {/each} -
- {/if} -
diff --git a/apps/cards/apps/web/src/lib/components/ReportButton.svelte b/apps/cards/apps/web/src/lib/components/ReportButton.svelte deleted file mode 100644 index 1a7dd0b86..000000000 --- a/apps/cards/apps/web/src/lib/components/ReportButton.svelte +++ /dev/null @@ -1,142 +0,0 @@ - - -{#if authStore.isAuthenticated} - {#if variant === 'icon'} - - {:else} - - {/if} -{/if} - -{#if open} - -{/if} diff --git a/apps/cards/apps/web/src/lib/components/StudyHeatmap.svelte b/apps/cards/apps/web/src/lib/components/StudyHeatmap.svelte deleted file mode 100644 index d2c8b6795..000000000 --- a/apps/cards/apps/web/src/lib/components/StudyHeatmap.svelte +++ /dev/null @@ -1,93 +0,0 @@ - - -
-
- Lernaktivität - - {total} Karten · {activeDays} aktive {activeDays === 1 ? 'Tag' : 'Tage'} · letzte {weeks} Wochen - -
-
- {#each columns as col, ci (ci)} -
- {#each col as cell, ri (ri)} - {#if cell.date === null} -
- {:else} -
- {/if} - {/each} -
- {/each} -
-
- weniger - - - - - - mehr -
-
diff --git a/apps/cards/apps/web/src/lib/components/SuggestEditModal.svelte b/apps/cards/apps/web/src/lib/components/SuggestEditModal.svelte deleted file mode 100644 index f8077a514..000000000 --- a/apps/cards/apps/web/src/lib/components/SuggestEditModal.svelte +++ /dev/null @@ -1,188 +0,0 @@ - - -{#if open} - -{/if} diff --git a/apps/cards/apps/web/src/lib/data/crypto.ts b/apps/cards/apps/web/src/lib/data/crypto.ts deleted file mode 100644 index 07df39beb..000000000 --- a/apps/cards/apps/web/src/lib/data/crypto.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Encryption wrapper — Phase-1 stub. - * - * The full Mana crypto stack (vault server roundtrip, KEK-wrapped - * master key, recovery codes, zero-knowledge mode) lives in the mana - * web app under `apps/mana/.../data/crypto/`. Lifting it intact into - * the standalone Cards app is a Phase-2 task — it requires a vault - * client, key provider, and boot-race handling that aren't worth - * dragging in until we have the deployment story for them. - * - * For Phase 1 these helpers are intentionally identity functions: - * data lands in IndexedDB and on `mana-sync` as plaintext. Everything - * is wired up at the right call sites (stores → write, queries → read, - * sync.applyServerChanges → apply) so flipping to real encryption is a - * single-file change here, not a sweep through every store. - * - * Allowlist is the contract with the future vault. It mirrors the - * mana-modul registry exactly so when sync converges, the same fields - * are protected on both ends. - */ - -const ENCRYPTED_FIELDS: Record = { - cards: ['front', 'back', 'fields'], - cardDecks: ['name', 'description'], -}; - -/** - * Phase-1 identity. Phase-2 swap-in: import `wrapValue` from - * `@mana/shared-crypto`, fetch master key from the vault, encrypt - * each allowlisted field in place. - */ -export async function encryptRecord(tableName: string, record: T): Promise { - void ENCRYPTED_FIELDS[tableName]; - return record; -} - -export async function decryptRecord(_tableName: string, record: T): Promise { - return record; -} - -export async function decryptRecords( - tableName: string, - records: T[] -): Promise { - if (records.length === 0) return records; - return Promise.all(records.map((r) => decryptRecord(tableName, r))); -} - -/** - * Reports the fields that *will* be encrypted once the vault is on. - * Stays exported so the GUIDELINES audit script can prove parity with - * the mana-modul registry. - */ -export function encryptedFieldsFor(tableName: string): readonly string[] { - return ENCRYPTED_FIELDS[tableName] ?? []; -} diff --git a/apps/cards/apps/web/src/lib/data/database.ts b/apps/cards/apps/web/src/lib/data/database.ts deleted file mode 100644 index 7088f15e1..000000000 --- a/apps/cards/apps/web/src/lib/data/database.ts +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Standalone Cards Dexie database. - * - * Phase-1 sync: every write to a sync-relevant table fires a Dexie hook - * that records a row into `_pendingChanges`. The sync engine drains - * that queue against `mana-sync` (POST /sync/cards). When server changes - * come back, they're applied with `beginApplying(table)` set so the - * hooks suppress queueing for those rows — otherwise client and server - * would ping-pong forever. - * - * Encryption is intentionally NOT wired here. Phase-1 ships plaintext; - * Etappe 3c.3 turns it on once the vault client is in place. - */ - -import Dexie, { type Table } from 'dexie'; -import type { LocalDeck, LocalCard, LocalCardReview, LocalCardStudyBlock } from '@mana/cards-core'; - -interface DeckTag { - id: string; - deckId: string; - tagId: string; - createdAt?: string; - updatedAt?: string; - deletedAt?: string | null; -} - -/** Server protocol expects this shape on push. */ -export interface FieldChange { - value: unknown; - at: string; -} - -export type ChangeOp = 'insert' | 'update' | 'delete'; - -export interface PendingChange { - /** Auto-increment PK (Dexie ++id). */ - pk?: number; - table: string; - id: string; - op: ChangeOp; - fields?: Record; - data?: Record; - deletedAt?: string; - queuedAt: string; -} - -/** Tables whose writes are mirrored to mana-sync. */ -const SYNC_TABLES = ['cardDecks', 'cards', 'cardReviews', 'cardStudyBlocks', 'deckTags'] as const; - -class CardsDatabase extends Dexie { - cardDecks!: Table; - cards!: Table; - cardReviews!: Table; - cardStudyBlocks!: Table; - deckTags!: Table; - _pendingChanges!: Table; - - constructor() { - super('cards'); - this.version(1).stores({ - cardDecks: 'id, lastStudied', - cards: 'id, deckId, order, [deckId+order]', - cardReviews: 'id, cardId, due, [cardId+subIndex], state', - cardStudyBlocks: 'id, date', - deckTags: 'id, deckId, tagId', - _pendingChanges: '++pk, table, queuedAt', - }); - // v2 — Phase δ.2: index `subscribedFromSlug` on cardDecks so the - // subscribe service can lookup-by-slug to avoid duplicating - // subscriptions on re-pull. - this.version(2).stores({ - cardDecks: 'id, lastStudied, subscribedFromSlug', - }); - // v3 — Phase δ.3: compound index on (deckId, serverContentHash) - // for the smart-merge lookup. Diff payloads reference cards by - // their content hash; we need O(1) lookups per (deck, hash) to - // classify each diff entry against local rows. - this.version(3).stores({ - cards: 'id, deckId, order, [deckId+order], [deckId+serverContentHash]', - }); - } -} - -export const db = new CardsDatabase(); - -export const cardDeckTable = db.cardDecks; -export const cardTable = db.cards; -export const cardReviewTable = db.cardReviews; -export const cardStudyBlockTable = db.cardStudyBlocks; -export const pendingChangesTable = db._pendingChanges; - -// ─── Server-apply suppression ────────────────────────────── - -const applying = new Set(); - -/** Mark a table as "currently applying server changes" — hooks skip - * queueing for the duration. Caller must always pair with `endApplying`. */ -export function beginApplying(tableName: string) { - applying.add(tableName); -} -export function endApplying(tableName: string) { - applying.delete(tableName); -} - -// ─── Field-meta diff ─────────────────────────────────────── - -function diffToFields( - previous: Record, - next: Record -): Record { - const at = new Date().toISOString(); - const out: Record = {}; - for (const key of Object.keys(next)) { - if (key.startsWith('_') || key === 'updatedAt') continue; - if (previous[key] === next[key]) continue; - out[key] = { value: next[key], at }; - } - return out; -} - -function snapshotForInsert(row: Record): Record { - const out: Record = {}; - for (const key of Object.keys(row)) { - if (key.startsWith('_')) continue; - out[key] = row[key]; - } - return out; -} - -// ─── Hook installation ───────────────────────────────────── - -function installSyncHooks(table: Table, name: string) { - table.hook('creating', (_pk, row) => { - if (applying.has(name)) return; - void db._pendingChanges.add({ - table: name, - id: row.id, - op: 'insert', - data: snapshotForInsert(row), - queuedAt: new Date().toISOString(), - }); - }); - - table.hook('updating', (mods, _pk, prev) => { - if (applying.has(name)) return; - const next = { ...prev, ...mods }; - const fields = diffToFields(prev, next); - if (Object.keys(fields).length === 0 && !('deletedAt' in mods)) return; - const isDelete = (mods as { deletedAt?: string }).deletedAt; - void db._pendingChanges.add({ - table: name, - id: prev.id, - op: isDelete ? 'delete' : 'update', - fields: Object.keys(fields).length > 0 ? fields : undefined, - deletedAt: isDelete ?? undefined, - queuedAt: new Date().toISOString(), - }); - }); -} - -for (const name of SYNC_TABLES) { - installSyncHooks(db.table(name), name); -} diff --git a/apps/cards/apps/web/src/lib/data/sync.ts b/apps/cards/apps/web/src/lib/data/sync.ts deleted file mode 100644 index 8889d1f54..000000000 --- a/apps/cards/apps/web/src/lib/data/sync.ts +++ /dev/null @@ -1,290 +0,0 @@ -/** - * Cards sync engine — talks to mana-sync (POST /sync/cards, GET /sync/cards/pull). - * - * Two loops, both polling-based for the Phase-1 MVP. WebSocket - * notifications can replace the pull poll later without changing - * anything outside this file. - * - * Push: drain `_pendingChanges` every 1s when there's anything queued. - * On success, delete drained rows and apply any server-changes - * the response carried back. Failures keep the rows queued — - * the next tick retries. - * - * Pull: every 5s, ask each sync table for changes since its cursor. - * Apply with suppression so the apply doesn't re-enqueue a push. - * Cursor lives in localStorage per table. - * - * Cursor format: ISO timestamp string. The server returns - * `syncedUntil` on push and we store that as a global push cursor; pull - * uses one cursor per collection. - */ - -import { browser } from '$app/environment'; -import { - beginApplying, - endApplying, - db, - pendingChangesTable, - type PendingChange, -} from './database'; -import { encryptRecord } from './crypto'; - -const APP_ID = 'cards'; -const PUSH_INTERVAL_MS = 1_000; -const PULL_INTERVAL_MS = 5_000; -const SYNC_TABLES = ['cardDecks', 'cards', 'cardReviews', 'cardStudyBlocks', 'deckTags']; - -// ─── URL + Auth wiring ───────────────────────────────────── - -function getSyncUrl(): string { - if (browser && typeof window !== 'undefined') { - const injected = (window as unknown as { __PUBLIC_MANA_SYNC_URL__?: string }) - .__PUBLIC_MANA_SYNC_URL__; - if (injected) return injected; - } - return import.meta.env.DEV ? 'http://localhost:3050' : ''; -} - -interface AuthLike { - getValidToken?: () => Promise; - readonly isAuthenticated: boolean; -} - -let authProvider: AuthLike | null = null; - -// ─── Client ID ───────────────────────────────────────────── - -const CLIENT_ID_KEY = 'mana.cards.clientId'; - -function getClientId(): string { - if (!browser) return 'ssr'; - let id = localStorage.getItem(CLIENT_ID_KEY); - if (!id) { - id = crypto.randomUUID(); - localStorage.setItem(CLIENT_ID_KEY, id); - } - return id; -} - -// ─── Cursors ─────────────────────────────────────────────── - -const PUSH_CURSOR_KEY = 'mana.cards.pushCursor'; -const PULL_CURSOR_KEY = (table: string) => `mana.cards.pullCursor.${table}`; - -function getPushCursor(): string { - if (!browser) return ''; - return localStorage.getItem(PUSH_CURSOR_KEY) || '1970-01-01T00:00:00.000Z'; -} -function setPushCursor(at: string) { - if (browser) localStorage.setItem(PUSH_CURSOR_KEY, at); -} -function getPullCursor(table: string): string { - if (!browser) return ''; - return localStorage.getItem(PULL_CURSOR_KEY(table)) || '1970-01-01T00:00:00.000Z'; -} -function setPullCursor(table: string, at: string) { - if (browser) localStorage.setItem(PULL_CURSOR_KEY(table), at); -} - -// ─── Server-Change shape ─────────────────────────────────── - -interface ServerChange { - eventId?: string; - schemaVersion?: number; - table: string; - id: string; - op: 'insert' | 'update' | 'delete'; - fields?: Record; - data?: Record; - deletedAt?: string; -} - -interface SyncResponse { - serverChanges: ServerChange[]; - conflicts: unknown[]; - syncedUntil: string; - hasMore?: boolean; -} - -// ─── Apply server changes ────────────────────────────────── - -async function applyServerChanges(changes: ServerChange[]) { - if (changes.length === 0) return; - const byTable = new Map(); - for (const c of changes) { - const arr = byTable.get(c.table) ?? []; - arr.push(c); - byTable.set(c.table, arr); - } - - for (const [table, list] of byTable) { - if (!SYNC_TABLES.includes(table)) continue; - const t = db.table(table); - beginApplying(table); - try { - for (const c of list) { - try { - if (c.op === 'delete') { - await t.update(c.id, { deletedAt: c.deletedAt ?? new Date().toISOString() }); - continue; - } - if (c.op === 'insert' && c.data) { - const row = { ...c.data, id: c.id }; - // Server data may already be ciphertext-on-the-wire when - // encryption flips on. Re-running encryptRecord on it is a - // safe no-op today (Phase-1 stub) and the right hook in - // Phase-2 because existing-ciphertext values are detected - // upstream via `isEncrypted(...)`. - await encryptRecord(table, row); - await t.put(row); - continue; - } - // update — merge fields - if (c.fields) { - const existing = (await t.get(c.id)) ?? { id: c.id }; - const merged: Record = { ...existing }; - for (const [k, v] of Object.entries(c.fields)) { - merged[k] = v.value; - } - await encryptRecord(table, merged); - await t.put(merged); - } - } catch (err) { - console.error('[cards-sync] apply failed', { table, id: c.id, op: c.op, err }); - } - } - } finally { - endApplying(table); - } - } -} - -// ─── Push ────────────────────────────────────────────────── - -async function flushPush(): Promise { - if (!authProvider?.isAuthenticated) return; - - const queued = await pendingChangesTable.orderBy('queuedAt').limit(500).toArray(); - if (queued.length === 0) return; - - const token = (await authProvider.getValidToken?.()) ?? null; - if (!token) return; - - const since = getPushCursor(); - const body = { - clientId: getClientId(), - appId: APP_ID, - since, - schemaVersion: 1, - changes: queued.map(toWireChange), - }; - - let res: Response; - try { - res = await fetch(`${getSyncUrl()}/sync/${APP_ID}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Client-Id': getClientId(), - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify(body), - }); - } catch (err) { - console.warn('[cards-sync] push network error', err); - return; - } - - if (!res.ok) { - console.warn('[cards-sync] push HTTP', res.status, await res.text().catch(() => '')); - return; - } - - const json = (await res.json()) as SyncResponse; - await pendingChangesTable.bulkDelete(queued.map((q) => q.pk!).filter((pk) => pk !== undefined)); - setPushCursor(json.syncedUntil); - await applyServerChanges(json.serverChanges ?? []); -} - -function toWireChange(p: PendingChange): ServerChange { - const out: ServerChange = { table: p.table, id: p.id, op: p.op }; - if (p.fields) out.fields = p.fields; - if (p.data) out.data = p.data; - if (p.deletedAt) out.deletedAt = p.deletedAt; - return out; -} - -// ─── Pull ────────────────────────────────────────────────── - -async function pollPull(): Promise { - if (!authProvider?.isAuthenticated) return; - const token = (await authProvider.getValidToken?.()) ?? null; - if (!token) return; - - for (const table of SYNC_TABLES) { - const since = getPullCursor(table); - const url = - `${getSyncUrl()}/sync/${APP_ID}/pull?collection=${encodeURIComponent(table)}` + - `&since=${encodeURIComponent(since)}`; - - let res: Response; - try { - res = await fetch(url, { - headers: { - 'X-Client-Id': getClientId(), - Authorization: `Bearer ${token}`, - }, - }); - } catch (err) { - console.warn('[cards-sync] pull network error', err); - continue; - } - - if (!res.ok) { - console.warn('[cards-sync] pull HTTP', res.status, table); - continue; - } - - const json = (await res.json()) as SyncResponse; - await applyServerChanges(json.serverChanges ?? []); - if (json.syncedUntil) setPullCursor(table, json.syncedUntil); - } -} - -// ─── Lifecycle ───────────────────────────────────────────── - -let pushTimer: ReturnType | null = null; -let pullTimer: ReturnType | null = null; -let pushBusy = false; -let pullBusy = false; - -export function startSync(authStore: AuthLike) { - authProvider = authStore; - if (!browser) return; - stopSync(); - pushTimer = setInterval(async () => { - if (pushBusy) return; - pushBusy = true; - try { - await flushPush(); - } finally { - pushBusy = false; - } - }, PUSH_INTERVAL_MS); - pullTimer = setInterval(async () => { - if (pullBusy) return; - pullBusy = true; - try { - await pollPull(); - } finally { - pullBusy = false; - } - }, PULL_INTERVAL_MS); -} - -export function stopSync() { - if (pushTimer) clearInterval(pushTimer); - if (pullTimer) clearInterval(pullTimer); - pushTimer = null; - pullTimer = null; -} diff --git a/apps/cards/apps/web/src/lib/index.ts b/apps/cards/apps/web/src/lib/index.ts deleted file mode 100644 index 648b5d03a..000000000 --- a/apps/cards/apps/web/src/lib/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// place files you want to import through the `$lib` alias in this folder. -export {}; diff --git a/apps/cards/apps/web/src/lib/media/upload.ts b/apps/cards/apps/web/src/lib/media/upload.ts deleted file mode 100644 index 2a28d01e1..000000000 --- a/apps/cards/apps/web/src/lib/media/upload.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Upload an image or audio file to mana-media and get back a media id - * + a public URL ready to drop into a card field. - * - * Resolves the media base URL from window.__PUBLIC_MANA_MEDIA_URL__ - * (injected by hooks.server.ts) so the same code works in dev (when - * mana-media runs on localhost) and prod (https://media.mana.how). - * - * 25 MB hard-cap mirrors the website-upload pattern in mana-web. - */ - -const MAX_BYTES = 25 * 1024 * 1024; - -export class MediaUploadError extends Error { - constructor( - message: string, - public status?: number - ) { - super(message); - this.name = 'MediaUploadError'; - } -} - -function mediaBaseUrl(): string { - if (typeof window !== 'undefined') { - const fromWindow = (window as unknown as { __PUBLIC_MANA_MEDIA_URL__?: string }) - .__PUBLIC_MANA_MEDIA_URL__; - if (fromWindow) return fromWindow.replace(/\/$/, ''); - } - return 'http://localhost:3015'; -} - -export interface UploadedMedia { - id: string; - url: string; - kind: 'image' | 'audio' | 'video' | 'other'; -} - -function classify(mime: string): UploadedMedia['kind'] { - if (mime.startsWith('image/')) return 'image'; - if (mime.startsWith('audio/')) return 'audio'; - if (mime.startsWith('video/')) return 'video'; - return 'other'; -} - -export async function uploadCardMedia(file: File): Promise { - if (file.size > MAX_BYTES) { - throw new MediaUploadError(`Datei zu groß (max ${MAX_BYTES / 1024 / 1024} MB).`, 400); - } - const kind = classify(file.type); - if (kind === 'other') { - throw new MediaUploadError('Nur Bilder, Audio oder Video werden unterstützt.', 400); - } - - const formData = new FormData(); - formData.append('file', file); - formData.append('app', 'cards'); - - const res = await fetch(`${mediaBaseUrl()}/api/v1/media/upload`, { - method: 'POST', - body: formData, - }); - if (!res.ok) { - throw new MediaUploadError(`Upload fehlgeschlagen (${res.status})`, res.status); - } - const data = (await res.json()) as { id?: string }; - if (!data.id) throw new MediaUploadError('Upload-Antwort ohne Media-ID.', 500); - - const variant = kind === 'image' ? '/file/medium' : '/file'; - return { - id: data.id, - url: `${mediaBaseUrl()}/api/v1/media/${data.id}${variant}`, - kind, - }; -} - -/** Snippet to drop into a card field. Markdown for images, raw HTML for - * audio/video so the user can also tweak attributes by hand later. */ -export function mediaToFieldSnippet(media: UploadedMedia, label: string): string { - switch (media.kind) { - case 'image': - return `![${label}](${media.url})`; - case 'audio': - return ``; - case 'video': - return ``; - default: - return media.url; - } -} diff --git a/apps/cards/apps/web/src/lib/queries.ts b/apps/cards/apps/web/src/lib/queries.ts deleted file mode 100644 index 869f7ae45..000000000 --- a/apps/cards/apps/web/src/lib/queries.ts +++ /dev/null @@ -1,251 +0,0 @@ -/** - * Reactive queries — standalone. - * - * Wraps Dexie's liveQuery so Svelte components get auto-updates whenever - * the underlying tables change. Type converters mirror the mana-modul - * shape so component code stays portable. - */ - -import { liveQuery } from 'dexie'; -import { - db, - cardDeckTable, - cardTable, - cardReviewTable, - cardStudyBlockTable, -} from './data/database'; -import { decryptRecord, decryptRecords } from './data/crypto'; -import type { - CardFields, - CardType, - Card, - CardReview, - Deck, - LocalCard, - LocalCardReview, - LocalDeck, -} from '@mana/cards-core'; - -// ─── Type Converters ─────────────────────────────────────── - -export function toDeck(local: LocalDeck): Deck { - return { - id: local.id, - title: local.name, - description: local.description ?? undefined, - color: local.color, - visibility: local.visibility ?? 'private', - tags: [], - cardCount: local.cardCount, - createdAt: local.createdAt ?? new Date().toISOString(), - updatedAt: local.updatedAt ?? local.createdAt ?? new Date().toISOString(), - subscribedFromSlug: local.subscribedFromSlug, - subscribedAtVersion: local.subscribedAtVersion, - }; -} - -export function toLogicalCard(local: LocalCard): { - type: CardType; - fields: CardFields; - front: string; - back: string; -} { - const type: CardType = local.type ?? 'basic'; - const fields: CardFields = local.fields ?? { - front: local.front ?? '', - back: local.back ?? '', - }; - const front = fields.front ?? local.front ?? ''; - const back = fields.back ?? local.back ?? ''; - return { type, fields, front, back }; -} - -export function toCard(local: LocalCard): Card { - const { type, fields, front, back } = toLogicalCard(local); - return { - id: local.id, - deckId: local.deckId, - type, - fields, - front, - back, - order: local.order, - createdAt: local.createdAt ?? new Date().toISOString(), - updatedAt: local.updatedAt ?? local.createdAt ?? new Date().toISOString(), - serverContentHash: local.serverContentHash, - }; -} - -function toCardReview(r: LocalCardReview): CardReview { - return { - id: r.id, - cardId: r.cardId, - subIndex: r.subIndex, - state: r.state, - stability: r.stability, - difficulty: r.difficulty, - due: r.due, - reps: r.reps, - lapses: r.lapses, - lastReview: r.lastReview, - elapsedDays: r.elapsedDays, - scheduledDays: r.scheduledDays, - }; -} - -// ─── Live Queries ────────────────────────────────────────── - -export function useAllDecks() { - return liveQuery(async () => { - const all = await cardDeckTable.toArray(); - const visible = all.filter((d) => !d.deletedAt); - const decrypted = await decryptRecords('cardDecks', visible); - return decrypted.map(toDeck); - }); -} - -export function useDeck(deckId: string) { - return liveQuery(async () => { - const local = await cardDeckTable.get(deckId); - if (!local || local.deletedAt) return null; - const decrypted = await decryptRecord('cardDecks', { ...local }); - return toDeck(decrypted); - }); -} - -export function useCardsByDeck(deckId: string) { - return liveQuery(async () => { - const visible = (await cardTable.where('deckId').equals(deckId).sortBy('order')).filter( - (c) => !c.deletedAt - ); - const decrypted = await decryptRecords('cards', visible); - return decrypted.map(toCard); - }); -} - -/** - * All reviews due now (or overdue) optionally filtered by deck. Joined - * with the parent card so the learn session can render immediately. - */ -export function useDueReviews(deckId?: string) { - return liveQuery(async () => { - const nowIso = new Date().toISOString(); - const due = await cardReviewTable.where('due').belowOrEqual(nowIso).toArray(); - const live = due.filter((r) => !r.deletedAt); - if (live.length === 0) return [] as { review: CardReview; card: Card }[]; - - const cardIds = [...new Set(live.map((r) => r.cardId))]; - const cardRows = await db.cards.where('id').anyOf(cardIds).toArray(); - const decryptedCards = await decryptRecords( - 'cards', - cardRows.filter((c) => !c.deletedAt) - ); - const cardById = new Map(decryptedCards.map((c) => [c.id, toCard(c)] as const)); - - return live - .filter((r) => { - const c = cardById.get(r.cardId); - if (!c) return false; - if (deckId && c.deckId !== deckId) return false; - return true; - }) - .sort((a, b) => (a.due < b.due ? -1 : a.due > b.due ? 1 : 0)) - .map((r) => ({ review: toCardReview(r), card: cardById.get(r.cardId)! })); - }); -} - -export function useReview(reviewId: string) { - return liveQuery(async () => { - const r = await cardReviewTable.get(reviewId); - if (!r || r.deletedAt) return null; - return toCardReview(r); - }); -} - -/** - * Map of deckId → count of currently-due reviews. Used by the deck list - * so the user can see at a glance which deck wants attention without - * opening it. - */ -export function useDueCountByDeck() { - return liveQuery(async () => { - const nowIso = new Date().toISOString(); - const due = await cardReviewTable.where('due').belowOrEqual(nowIso).toArray(); - const live = due.filter((r) => !r.deletedAt); - if (live.length === 0) return new Map(); - - const cardIds = [...new Set(live.map((r) => r.cardId))]; - const cards = await cardTable.where('id').anyOf(cardIds).toArray(); - const cardToDeck = new Map(cards.filter((c) => !c.deletedAt).map((c) => [c.id, c.deckId])); - - const counts = new Map(); - for (const r of live) { - const deckId = cardToDeck.get(r.cardId); - if (!deckId) continue; - counts.set(deckId, (counts.get(deckId) ?? 0) + 1); - } - return counts; - }); -} - -/** - * Per-day review counts for the last `weeks * 7` days (default 12 weeks - * = 84 days). Used by the GitHub-style heatmap on the dashboard. Days - * with no row in cardStudyBlocks come back as count=0 so the renderer - * doesn't have to fill gaps itself. - */ -export function useStudyHeatmap(weeks: number = 12) { - return liveQuery(async () => { - const today = new Date(); - const localKey = (d: Date) => { - const y = d.getFullYear(); - const m = `${d.getMonth() + 1}`.padStart(2, '0'); - const day = `${d.getDate()}`.padStart(2, '0'); - return `${y}-${m}-${day}`; - }; - - const days = weeks * 7; - const rows = await cardStudyBlockTable.toArray(); - const byDate = new Map(); - for (const r of rows) { - if (r.deletedAt) continue; - byDate.set(r.date, (byDate.get(r.date) ?? 0) + r.cardsReviewed); - } - - const out: { date: string; count: number }[] = []; - for (let i = days - 1; i >= 0; i--) { - const d = new Date(today); - d.setDate(d.getDate() - i); - const key = localKey(d); - out.push({ date: key, count: byDate.get(key) ?? 0 }); - } - return out; - }); -} - -/** - * Days-in-a-row with at least one review. Walks back from today; the - * first day with no row (or a soft-deleted/empty one) ends the count. - * Capped at 365 to bound the worst-case scan. - */ -export function useStreak() { - return liveQuery(async () => { - const today = new Date(); - const localKey = (d: Date) => { - const y = d.getFullYear(); - const m = `${d.getMonth() + 1}`.padStart(2, '0'); - const day = `${d.getDate()}`.padStart(2, '0'); - return `${y}-${m}-${day}`; - }; - - let streak = 0; - for (let i = 0; i < 365; i++) { - const d = new Date(today); - d.setDate(d.getDate() - i); - const row = await cardStudyBlockTable.where('date').equals(localKey(d)).first(); - if (!row || row.deletedAt || row.cardsReviewed <= 0) break; - streak++; - } - return streak; - }); -} diff --git a/apps/cards/apps/web/src/lib/services/subscribe.ts b/apps/cards/apps/web/src/lib/services/subscribe.ts deleted file mode 100644 index 24c179fb8..000000000 --- a/apps/cards/apps/web/src/lib/services/subscribe.ts +++ /dev/null @@ -1,314 +0,0 @@ -/** - * Subscribe to a marketplace deck and pull its latest version into - * the local Dexie. Phase δ.2 — initial pull only; smart-merge of - * subsequent updates lands in δ.3 via `applySubscriptionUpdate` - * (placeholder export below). - * - * The subscribed deck shows up alongside own decks but is marked - * `subscribedFromSlug` + `subscribedAtVersion` so the UI can hide - * mutate controls and show an "Update available" indicator when - * cards-server reports a newer version. - */ - -import { cardsApi, CardsApiError } from '$lib/api/cards-api'; -import type { ServerCard } from '$lib/api/cards-api'; -import { cardDeckTable, cardTable } from '$lib/data/database'; -import { reviewStore } from '$lib/stores/reviews.svelte'; -import type { CardType, LocalCard, LocalDeck } from '@mana/cards-core'; - -const ALLOWED_TYPES: CardType[] = [ - 'basic', - 'basic-reverse', - 'cloze', - 'type-in', - 'image-occlusion', - 'audio', - 'multiple-choice', -]; - -function asCardType(t: string): CardType { - return (ALLOWED_TYPES as string[]).includes(t) ? (t as CardType) : 'basic'; -} - -export interface SubscribeResult { - deckId: string; - cardCount: number; -} - -export async function subscribeAndPull(deckSlug: string): Promise { - // 1. Tell the server we're subscribed (idempotent, returns the - // version we should pull). - const sub = await cardsApi.subscriptions.subscribe(deckSlug); - - // 2. Fetch the deck metadata so we know title/description/etc. - const { deck, latestVersion } = await cardsApi.decks.bySlug(deckSlug); - if (!latestVersion) { - throw new Error('Subscribed but the deck has no published version yet'); - } - - // 3. Fetch the version's cards (full payload). - const version = await cardsApi.subscriptions.version(deckSlug, latestVersion.semver); - - // 4. Already subscribed locally? Don't duplicate — refresh in - // place. Phase δ.3 will swap this for a real diff-apply. - const existingDeck = await cardDeckTable - .where('subscribedFromSlug') - .equals(deckSlug) - .first() - .catch(() => undefined); - - const now = new Date().toISOString(); - const localDeck: LocalDeck = existingDeck ?? { - id: crypto.randomUUID(), - name: deck.title, - description: deck.description, - color: '#6366f1', - cardCount: version.cards.length, - visibility: 'private', - createdAt: now, - updatedAt: now, - subscribedFromSlug: deckSlug, - subscribedAtVersion: latestVersion.semver, - }; - - if (existingDeck) { - await cardDeckTable.update(existingDeck.id, { - name: deck.title, - description: deck.description, - cardCount: version.cards.length, - subscribedAtVersion: latestVersion.semver, - updatedAt: now, - }); - } else { - await cardDeckTable.add(localDeck); - } - - // 5. Replace cards (initial-pull strategy; δ.3 keeps FSRS state). - if (existingDeck) { - const oldCards = await cardTable.where('deckId').equals(existingDeck.id).toArray(); - for (const c of oldCards) { - if (!c.deletedAt) await cardTable.update(c.id, { deletedAt: now }); - } - } - - for (const sc of version.cards) { - const card: LocalCard = { - id: crypto.randomUUID(), - deckId: localDeck.id, - type: asCardType(sc.type), - fields: sc.fields, - order: sc.ord, - serverContentHash: sc.contentHash, - createdAt: now, - updatedAt: now, - }; - await cardTable.add(card); - await reviewStore.ensureReviewsForCard({ - id: card.id, - type: card.type as CardType, - fields: card.fields ?? {}, - }); - } - - return { deckId: localDeck.id, cardCount: version.cards.length }; -} - -export async function unsubscribe(deckSlug: string): Promise { - await cardsApi.subscriptions.unsubscribe(deckSlug); - const local = await cardDeckTable - .where('subscribedFromSlug') - .equals(deckSlug) - .first() - .catch(() => undefined); - if (!local) return; - const now = new Date().toISOString(); - const cards = await cardTable.where('deckId').equals(local.id).toArray(); - for (const c of cards) { - if (!c.deletedAt) await cardTable.update(c.id, { deletedAt: now }); - } - await cardDeckTable.update(local.id, { deletedAt: now }); -} - -/** Helper: am I already subscribed locally to this slug? */ -export async function isSubscribedLocally(slug: string): Promise { - try { - const row = await cardDeckTable.where('subscribedFromSlug').equals(slug).first(); - return Boolean(row && !row.deletedAt); - } catch { - return false; - } -} - -export interface UpdatePreview { - from: string; - to: string; - added: number; - changed: number; - removed: number; - unchanged: number; -} - -/** - * Compute what would change if we pulled the latest version. Returns - * `null` if already on latest. Used by the deck-detail banner so the - * user sees "X neue, Y geänderte, Z entfernte" before committing. - */ -export async function previewUpdate(deckSlug: string): Promise { - const local = await cardDeckTable - .where('subscribedFromSlug') - .equals(deckSlug) - .first() - .catch(() => undefined); - if (!local || local.deletedAt || !local.subscribedAtVersion) return null; - const diff = await cardsApi.subscriptions.diff(deckSlug, local.subscribedAtVersion); - if (diff.from === diff.to) return null; - return { - from: diff.from, - to: diff.to, - added: diff.added.length, - changed: diff.changed.length, - removed: diff.removed.length, - unchanged: diff.unchanged.length, - }; -} - -/** - * Smart-merge the latest server version into the local Dexie copy - * without losing FSRS state. - * - * - **unchanged**: leave the local card alone — its FSRS reviews - * stay attached and the learning schedule continues unbroken. - * - **changed**: lookup local card by previous-hash, update fields/ - * type/order/serverContentHash to the new values. FSRS reviews - * stay attached because we don't touch the card id. Re-runs - * ensureReviewsForCard so cloze-cluster fan-out matches the new - * content. - * - **added**: insert a new card with fresh FSRS reviews. - * - **removed**: soft-delete by content-hash + cascade reviews. - * - * Final step: bump local subscribedAtVersion + re-stamp server-side - * (POST /subscribe is idempotent and re-anchors the user's row). - */ -export async function applyUpdate(deckSlug: string): Promise { - const local = await cardDeckTable - .where('subscribedFromSlug') - .equals(deckSlug) - .first() - .catch(() => undefined); - if (!local || local.deletedAt || !local.subscribedAtVersion) return null; - - const diff = await cardsApi.subscriptions.diff(deckSlug, local.subscribedAtVersion); - if (diff.from === diff.to) return null; - - const now = new Date().toISOString(); - - for (const r of diff.removed) { - const localCard = await cardTable - .where('[deckId+serverContentHash]') - .equals([local.id, r.contentHash]) - .first(); - if (localCard && !localCard.deletedAt) { - await cardTable.update(localCard.id, { deletedAt: now }); - await reviewStore.softDeleteForCard(localCard.id); - } - } - - for (const c of diff.changed) { - const localCard = await cardTable - .where('[deckId+serverContentHash]') - .equals([local.id, c.previous.contentHash]) - .first(); - if (!localCard) { - // Heuristic mismatch — treat as added. - await insertSubscribedCard(local.id, c.next, now); - continue; - } - const nextType = asCardType(c.next.type); - await cardTable.update(localCard.id, { - type: nextType, - fields: c.next.fields, - order: c.next.ord, - serverContentHash: c.next.contentHash, - updatedAt: now, - }); - await reviewStore.ensureReviewsForCard({ - id: localCard.id, - type: nextType, - fields: c.next.fields, - }); - } - - for (const a of diff.added) { - await insertSubscribedCard(local.id, a, now); - } - - for (const u of diff.unchanged) { - const localCard = await cardTable - .where('[deckId+serverContentHash]') - .equals([local.id, u.contentHash]) - .first(); - if (localCard && localCard.order !== u.ord) { - await cardTable.update(localCard.id, { order: u.ord, updatedAt: now }); - } - } - - const liveCards = await cardTable.where('deckId').equals(local.id).toArray(); - const liveCount = liveCards.filter((c) => !c.deletedAt).length; - await cardDeckTable.update(local.id, { - subscribedAtVersion: diff.to, - cardCount: liveCount, - updatedAt: now, - }); - - try { - await cardsApi.subscriptions.subscribe(deckSlug); - } catch { - // Idempotent server-side; if this fails the local pointer - // already advanced and the next sync will reconcile. - } - - return { - from: diff.from, - to: diff.to, - added: diff.added.length, - changed: diff.changed.length, - removed: diff.removed.length, - unchanged: diff.unchanged.length, - }; -} - -async function insertSubscribedCard(deckId: string, sc: ServerCard, now: string): Promise { - const card: LocalCard = { - id: crypto.randomUUID(), - deckId, - type: asCardType(sc.type), - fields: sc.fields, - order: sc.ord, - serverContentHash: sc.contentHash, - createdAt: now, - updatedAt: now, - }; - await cardTable.add(card); - await reviewStore.ensureReviewsForCard({ - id: card.id, - type: card.type as CardType, - fields: card.fields ?? {}, - }); -} - -/** - * One-shot poll of the user's subscriptions to see which decks have - * a newer version waiting. Powers the dashboard "Updates"-banner. - */ -export async function listSubscriptionUpdates(): Promise<{ slug: string; title: string }[]> { - let subs; - try { - subs = await cardsApi.subscriptions.list(); - } catch (e) { - if (e instanceof CardsApiError && e.status === 401) return []; - throw e; - } - return subs - .filter((s) => s.updateAvailable) - .map((s) => ({ slug: s.deckSlug, title: s.deckTitle })); -} diff --git a/apps/cards/apps/web/src/lib/stores/auth.svelte.ts b/apps/cards/apps/web/src/lib/stores/auth.svelte.ts deleted file mode 100644 index ce4e9f88c..000000000 --- a/apps/cards/apps/web/src/lib/stores/auth.svelte.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Auth Store — uses the shared Mana auth factory. - * - * SSO: tokens land in the shared `*.mana.how` storage so a user already - * signed into mana.how / cardecky.mana.how lands directly in the app - * without re-typing credentials. The factory wires up the token - * manager + refresh + storage adapter for us. - */ - -import { createManaAuthStore } from '@mana/shared-auth-ui'; - -export const authStore = createManaAuthStore(); diff --git a/apps/cards/apps/web/src/lib/stores/author.svelte.ts b/apps/cards/apps/web/src/lib/stores/author.svelte.ts deleted file mode 100644 index 84b94f289..000000000 --- a/apps/cards/apps/web/src/lib/stores/author.svelte.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Author-state store. - * - * Lazily fetches the user's author row on first access. Runtime - * components never read the API directly — they go through this - * store, so refresh-on-mutation is centralised. - */ - -import { cardsApi, CardsApiError, type Author } from '$lib/api/cards-api'; - -let _author = $state(null); -let _loaded = $state(false); -let _loading = $state(false); -let _error = $state(null); - -export const authorStore = { - get author() { - return _author; - }, - get loaded() { - return _loaded; - }, - get loading() { - return _loading; - }, - get error() { - return _error; - }, - get isAuthor() { - return _loaded && _author !== null; - }, - - async load(force = false): Promise { - if (_loaded && !force) return _author; - _loading = true; - _error = null; - try { - _author = await cardsApi.authors.me(); - } catch (e) { - if (e instanceof CardsApiError && e.status === 401) { - // Not authed — caller's problem, don't poison the store. - _author = null; - } else { - _error = (e as Error).message ?? 'Konnte Author-Profil nicht laden'; - } - } finally { - _loaded = true; - _loading = false; - } - return _author; - }, - - async upsert(input: Parameters[0]): Promise { - _loading = true; - _error = null; - try { - _author = await cardsApi.authors.upsertMe(input); - return _author; - } catch (e) { - _error = (e as Error).message ?? 'Speichern fehlgeschlagen'; - return null; - } finally { - _loading = false; - } - }, - - reset() { - _author = null; - _loaded = false; - _error = null; - }, -}; diff --git a/apps/cards/apps/web/src/lib/stores/cards.svelte.ts b/apps/cards/apps/web/src/lib/stores/cards.svelte.ts deleted file mode 100644 index 079ae5c62..000000000 --- a/apps/cards/apps/web/src/lib/stores/cards.svelte.ts +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Card Store — standalone. - * - * Writes the {type, fields} shape directly. Legacy mirror (front/back - * columns) kept on for cross-compat with the mana-modul data model - * once sync flips on. No encryption, no domain events — that's the - * deliberate Phase-1 simplification. - */ - -import { cardTable, cardDeckTable } from '../data/database'; -import { encryptRecord, decryptRecord } from '../data/crypto'; -import { reviewStore } from './reviews.svelte'; -import { - type CardFields, - type CardType, - type LocalCard, - type CreateCardInput, - type UpdateCardInput, -} from '@mana/cards-core'; - -let error = $state(null); - -function resolveTypeAndFields(input: CreateCardInput): { - type: CardType; - fields: CardFields; -} { - const type = input.type ?? 'basic'; - if (input.fields) return { type, fields: input.fields }; - if (type === 'cloze') return { type, fields: { text: input.front ?? '' } }; - return { type, fields: { front: input.front ?? '', back: input.back ?? '' } }; -} - -function legacyMirror(type: CardType, fields: CardFields): { front?: string; back?: string } { - if (type === 'basic' || type === 'basic-reverse' || type === 'type-in') { - return { front: fields.front ?? '', back: fields.back ?? '' }; - } - if (type === 'cloze') { - return { front: fields.text ?? '', back: '' }; - } - return {}; -} - -export const cardStore = { - get error() { - return error; - }, - - async createCard( - input: CreateCardInput, - currentCardCount: number = 0 - ): Promise { - error = null; - try { - const { type, fields } = resolveTypeAndFields(input); - const legacy = legacyMirror(type, fields); - const now = new Date().toISOString(); - - const newLocal: LocalCard = { - id: crypto.randomUUID(), - deckId: input.deckId, - type, - fields, - order: currentCardCount, - createdAt: now, - updatedAt: now, - ...legacy, - }; - - await encryptRecord('cards', newLocal); - await cardTable.add(newLocal); - - const deck = await cardDeckTable.get(input.deckId); - if (deck) { - await cardDeckTable.update(input.deckId, { - cardCount: (deck.cardCount || 0) + 1, - updatedAt: now, - }); - } - - await reviewStore.ensureReviewsForCard({ id: newLocal.id, type, fields }); - return newLocal; - } catch (err: any) { - error = err.message || 'Failed to create card'; - console.error('Create card error:', err); - return null; - } - }, - - async updateCard(id: string, updates: UpdateCardInput) { - error = null; - try { - const existingRaw = await cardTable.get(id); - if (!existingRaw) return; - const existing = await decryptRecord('cards', { ...existingRaw }); - - const currentType: CardType = existing.type ?? 'basic'; - const currentFields: CardFields = existing.fields ?? { - front: existing.front ?? '', - back: existing.back ?? '', - }; - - const nextType: CardType = updates.type ?? currentType; - const nextFields: CardFields = updates.fields - ? updates.fields - : updates.front !== undefined || updates.back !== undefined - ? nextType === 'cloze' - ? { ...currentFields, text: updates.front ?? currentFields.text ?? '' } - : { - ...currentFields, - front: updates.front ?? currentFields.front ?? '', - back: updates.back ?? currentFields.back ?? '', - } - : currentFields; - - const legacy = legacyMirror(nextType, nextFields); - const diff: Partial = { - type: nextType, - fields: nextFields, - updatedAt: new Date().toISOString(), - ...legacy, - }; - if (updates.order !== undefined) diff.order = updates.order; - - await encryptRecord('cards', diff as Record); - await cardTable.update(id, diff); - - const structuralChange = - updates.type !== undefined || - updates.fields !== undefined || - (nextType === 'cloze' && updates.front !== undefined); - if (structuralChange) { - await reviewStore.ensureReviewsForCard({ id, type: nextType, fields: nextFields }); - } - } catch (err: any) { - error = err.message || 'Failed to update card'; - console.error('Update card error:', err); - } - }, - - async deleteCard(id: string, deckId?: string) { - error = null; - try { - const now = new Date().toISOString(); - await cardTable.update(id, { deletedAt: now }); - await reviewStore.softDeleteForCard(id); - - if (deckId) { - const deck = await cardDeckTable.get(deckId); - if (deck) { - await cardDeckTable.update(deckId, { - cardCount: Math.max(0, (deck.cardCount || 0) - 1), - updatedAt: now, - }); - } - } - } catch (err: any) { - error = err.message || 'Failed to delete card'; - console.error('Delete card error:', err); - } - }, - - clearError() { - error = null; - }, -}; diff --git a/apps/cards/apps/web/src/lib/stores/decks.svelte.ts b/apps/cards/apps/web/src/lib/stores/decks.svelte.ts deleted file mode 100644 index 79fcd0f01..000000000 --- a/apps/cards/apps/web/src/lib/stores/decks.svelte.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Deck Store — standalone. - * - * Slim version of the mana-modul decks store: no time-blocks, no - * domain-events, no Mana-wide visibility hooks. Just CRUD against the - * standalone Dexie DB. - */ - -import { cardDeckTable, cardTable, db } from '../data/database'; -import { encryptRecord } from '../data/crypto'; -import type { CreateDeckInput, UpdateDeckInput, LocalDeck } from '@mana/cards-core'; - -let error = $state(null); - -export const deckStore = { - get error() { - return error; - }, - - async createDeck(input: CreateDeckInput): Promise { - error = null; - try { - const now = new Date().toISOString(); - const newLocal: LocalDeck = { - id: crypto.randomUUID(), - name: input.title, - description: input.description ?? null, - color: '#6366f1', - cardCount: 0, - visibility: 'private', - createdAt: now, - updatedAt: now, - }; - await encryptRecord('cardDecks', newLocal); - await cardDeckTable.add(newLocal); - return newLocal; - } catch (err: any) { - error = err.message || 'Failed to create deck'; - console.error('Create deck error:', err); - return null; - } - }, - - async updateDeck(id: string, updates: UpdateDeckInput) { - error = null; - try { - const diff: Partial = { updatedAt: new Date().toISOString() }; - if (updates.title !== undefined) diff.name = updates.title; - if (updates.description !== undefined) diff.description = updates.description; - await encryptRecord('cardDecks', diff as Record); - await cardDeckTable.update(id, diff); - } catch (err: any) { - error = err.message || 'Failed to update deck'; - console.error('Update deck error:', err); - } - }, - - async deleteDeck(id: string) { - error = null; - try { - const now = new Date().toISOString(); - await db.transaction('rw', cardDeckTable, cardTable, async () => { - const cards = await cardTable.where('deckId').equals(id).toArray(); - for (const card of cards) { - await cardTable.update(card.id, { deletedAt: now }); - } - await cardDeckTable.update(id, { deletedAt: now }); - }); - } catch (err: any) { - error = err.message || 'Failed to delete deck'; - console.error('Delete deck error:', err); - } - }, - - clearError() { - error = null; - }, -}; diff --git a/apps/cards/apps/web/src/lib/stores/reviews.svelte.ts b/apps/cards/apps/web/src/lib/stores/reviews.svelte.ts deleted file mode 100644 index 7caa295d1..000000000 --- a/apps/cards/apps/web/src/lib/stores/reviews.svelte.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Card-Review Store — standalone. - * - * Plaintext, no encryption hook (Phase 1). Fan-out logic comes from - * @mana/cards-core; the only standalone bit is which Dexie table to write to. - */ - -import { cardReviewTable } from '../data/database'; -import { - newReview, - gradeReview as fsrsGrade, - subIndexesFor, - type CardFields, - type CardType, - type LocalCardReview, - type ReviewGrade, -} from '@mana/cards-core'; - -let error = $state(null); - -export const reviewStore = { - get error() { - return error; - }, - - async ensureReviewsForCard(card: { - id: string; - type: CardType; - fields: CardFields; - }): Promise { - error = null; - try { - const existing = await cardReviewTable.where('cardId').equals(card.id).toArray(); - const live = existing.filter((r) => !r.deletedAt); - const liveByIdx = new Map(live.map((r) => [r.subIndex, r])); - - const wanted = subIndexesFor(card); - const wantedSet = new Set(wanted); - const nowIso = new Date().toISOString(); - - for (const subIndex of wanted) { - if (!liveByIdx.has(subIndex)) { - const r = newReview({ cardId: card.id, subIndex }); - await cardReviewTable.add(r); - liveByIdx.set(subIndex, r); - } - } - - for (const r of live) { - if (!wantedSet.has(r.subIndex)) { - await cardReviewTable.update(r.id, { deletedAt: nowIso }); - liveByIdx.delete(r.subIndex); - } - } - - return [...liveByIdx.values()].sort((a, b) => a.subIndex - b.subIndex); - } catch (err: any) { - error = err.message || 'Failed to ensure reviews'; - console.error('Ensure reviews error:', err); - return []; - } - }, - - async grade(reviewId: string, grade: ReviewGrade): Promise { - error = null; - try { - const existing = await cardReviewTable.get(reviewId); - if (!existing) return null; - const next = fsrsGrade(existing, grade); - await cardReviewTable.put(next); - return next; - } catch (err: any) { - error = err.message || 'Failed to grade review'; - console.error('Grade review error:', err); - return null; - } - }, - - async softDeleteForCard(cardId: string): Promise { - const reviews = await cardReviewTable.where('cardId').equals(cardId).toArray(); - const now = new Date().toISOString(); - for (const r of reviews) { - if (!r.deletedAt) await cardReviewTable.update(r.id, { deletedAt: now }); - } - }, - - clearError() { - error = null; - }, -}; diff --git a/apps/cards/apps/web/src/lib/stores/study-blocks.svelte.ts b/apps/cards/apps/web/src/lib/stores/study-blocks.svelte.ts deleted file mode 100644 index 91fb0b517..000000000 --- a/apps/cards/apps/web/src/lib/stores/study-blocks.svelte.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Study-Block Store — standalone. - * - * Local daily-aggregate row for streak + per-day-stats. - */ - -import { cardStudyBlockTable } from '../data/database'; -import type { LocalCardStudyBlock } from '@mana/cards-core'; - -let error = $state(null); - -function localDateKey(d: Date = new Date()): string { - const y = d.getFullYear(); - const m = `${d.getMonth() + 1}`.padStart(2, '0'); - const day = `${d.getDate()}`.padStart(2, '0'); - return `${y}-${m}-${day}`; -} - -export const studyBlockStore = { - get error() { - return error; - }, - - async recordReview(durationMs: number, count: number = 1): Promise { - error = null; - try { - const date = localDateKey(); - const existing = await cardStudyBlockTable.where('date').equals(date).first(); - if (existing && !existing.deletedAt) { - await cardStudyBlockTable.update(existing.id, { - cardsReviewed: existing.cardsReviewed + count, - durationMs: existing.durationMs + durationMs, - }); - } else { - const row: LocalCardStudyBlock = { - id: crypto.randomUUID(), - date, - cardsReviewed: count, - durationMs, - }; - await cardStudyBlockTable.add(row); - } - } catch (err: any) { - error = err.message || 'Failed to record review'; - console.error('Record review error:', err); - } - }, - - async getRecentStreak(): Promise { - const today = new Date(); - let streak = 0; - for (let i = 0; i < 365; i++) { - const d = new Date(today); - d.setDate(d.getDate() - i); - const row = await cardStudyBlockTable.where('date').equals(localDateKey(d)).first(); - if (!row || row.deletedAt || row.cardsReviewed <= 0) break; - streak++; - } - return streak; - }, - - clearError() { - error = null; - }, -}; diff --git a/apps/cards/apps/web/src/lib/stores/theme.ts b/apps/cards/apps/web/src/lib/stores/theme.ts deleted file mode 100644 index 1260137f3..000000000 --- a/apps/cards/apps/web/src/lib/stores/theme.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Cards Theme Store - * - * Uses the shared theme system. The Cards brand accent (#8b5cf6 from - * MANA_APPS) becomes `--color-app-accent` on document.documentElement - * so the existing `bg-app-accent` / `text-app-accent` utilities work - * everywhere — Lernen-CTA, cloze highlight, link colours, etc. - * - * The accent is theme-agnostic by design: it stays the same whether - * the user picks Lume / Nature / Stone / Ocean × Light / Dark, so the - * Cards identity reads consistently across variants. - */ -import { createThemeStore } from '@mana/shared-theme'; - -export type { ThemeMode, ThemeVariant, EffectiveMode } from '@mana/shared-theme'; - -// Cards brand: #8b5cf6 (violet-500) → HSL channels. -const CARDS_ACCENT_HSL = '258 90% 66%'; - -export const theme = createThemeStore({ - appId: 'cards', -}); - -/** - * Write the Cards app accent onto documentElement once at boot. The - * shared theme store doesn't know about per-app accents — it only - * touches the variant tokens — so we set this independently and it - * survives every variant switch. - */ -export function applyCardsAccent(): void { - if (typeof document === 'undefined') return; - document.documentElement.style.setProperty('--color-app-accent', CARDS_ACCENT_HSL); -} diff --git a/apps/cards/apps/web/src/lib/util/slug.ts b/apps/cards/apps/web/src/lib/util/slug.ts deleted file mode 100644 index 677d94af7..000000000 --- a/apps/cards/apps/web/src/lib/util/slug.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Best-effort slug suggestion. Server-side validateSlug is the - * authoritative gate; this just gives the user a sensible default - * to edit. - */ -export function slugify(input: string): string { - return input - .normalize('NFKD') - .replace(/[̀-ͯ]/g, '') - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') - .slice(0, 60); -} diff --git a/apps/cards/apps/web/src/routes/+layout.svelte b/apps/cards/apps/web/src/routes/+layout.svelte deleted file mode 100644 index 0c6207d26..000000000 --- a/apps/cards/apps/web/src/routes/+layout.svelte +++ /dev/null @@ -1,98 +0,0 @@ - - - - {@html webManifestLink} - - -{#if isPublic} - {@render children()} -{:else} - -
-
- - 🃏 Cards - - -
- {#if streak > 0} - - 🔥 {streak} - - {/if} - - {#if authStore.user?.email} - - {/if} - -
-
-
- - {@render children()} -
-{/if} diff --git a/apps/cards/apps/web/src/routes/+page.svelte b/apps/cards/apps/web/src/routes/+page.svelte deleted file mode 100644 index f59253390..000000000 --- a/apps/cards/apps/web/src/routes/+page.svelte +++ /dev/null @@ -1,156 +0,0 @@ - - - - Cards - - -
-
-
-

Cards

-

- {decks.length} - {decks.length === 1 ? 'Deck' : 'Decks'}{#if totalDue > 0} - · {totalDue} fällig - {/if} -

-
- -
- - {#if showNew} -
{ - e.preventDefault(); - handleCreate(); - }} - > - - - -
- - -
-
- {/if} - - {#if decks.length === 0 && !showNew} -
-
🃏
-

Noch keine Decks. Leg dein erstes an.

- -
- {:else} - - {/if} - -
- -
- -
- -
- -

- Phase 1 · synct mit mana.how/cards -

-
diff --git a/apps/cards/apps/web/src/routes/admin/reports/+page.svelte b/apps/cards/apps/web/src/routes/admin/reports/+page.svelte deleted file mode 100644 index 70d766f0c..000000000 --- a/apps/cards/apps/web/src/routes/admin/reports/+page.svelte +++ /dev/null @@ -1,170 +0,0 @@ - - - - Moderation — Cards - - -
-
-

Moderation-Inbox

- {#if stage === 'ok'} - - {/if} -
- - {#if stage === 'loading'} -

Lädt…

- {:else if stage === 'forbidden' || !isAdmin} -

- Nur Admins haben Zugang zur Moderation-Inbox. -

- {:else if stage === 'error'} -

- {error} -

- {:else if reports.length === 0} -

- Keine offenen Reports. -

- {:else} -
    - {#each reports as r (r.id)} -
  • -
    -
    -
    - - {r.category} - - - {r.deckTitle} - - {#if r.cardContentHash} - · Karte {r.cardContentHash.slice(0, 8)}… - {/if} -
    -

    - {new Date(r.createdAt).toLocaleString('de-DE')} -

    -
    -
    - - {#if r.body} -

    - {r.body} -

    - {/if} - - {#if error} -

    {error}

    - {/if} - -
    - - - -
    -
  • - {/each} -
- {/if} -
diff --git a/apps/cards/apps/web/src/routes/d/[slug]/+page.svelte b/apps/cards/apps/web/src/routes/d/[slug]/+page.svelte deleted file mode 100644 index 692635ddd..000000000 --- a/apps/cards/apps/web/src/routes/d/[slug]/+page.svelte +++ /dev/null @@ -1,267 +0,0 @@ - - - - {deck?.title ?? slug} — Cards - - -
- {#if stage === 'loading'} -

Lade Deck…

- {:else if stage === 'not-found'} -

- Deck {slug} existiert nicht. -

- {:else if stage === 'error'} -

- {error} -

- {:else if deck} -
-
-

{deck.title}

- {#if deck.description} -

{deck.description}

- {/if} -
- -
- {#if version} - - v{version.semver} - - {version.cardCount} Karten - {/if} - {deck.license} - {#if deck.language} - {deck.language.toUpperCase()} - {/if} - {#if deck.priceCredits > 0} - - {deck.priceCredits} 💎 - - {/if} -
- - {#if version?.changelog} -
-

- Changelog v{version.semver} -

-

{version.changelog}

-
- {/if} - -
- {#if authStore.isAuthenticated} - - - {#if subscribed} - - {#if subscribedDeckId} - - {/if} - {:else if isPaid && !canSubscribeNow && !isOwner} - - {:else} - - {#if isPaid && hasPurchased} - - ✓ Gekauft - - {/if} - {/if} - {:else} - - Anmelden um zu abonnieren - - {/if} -
- - {#if error} -

{error}

- {/if} - -
- Veröffentlicht: {new Date(deck.createdAt).toLocaleDateString('de-DE')} - {#if !isOwner} - - {/if} -
- - {#if deck.isTakedown} -

- Dieses Deck wurde von der Moderation entfernt. -

- {/if} - - {#if version} - - {/if} - - -
- {/if} - -

- ← Marktplatz -

-
diff --git a/apps/cards/apps/web/src/routes/decks/[id]/+page.svelte b/apps/cards/apps/web/src/routes/decks/[id]/+page.svelte deleted file mode 100644 index 79387ec37..000000000 --- a/apps/cards/apps/web/src/routes/decks/[id]/+page.svelte +++ /dev/null @@ -1,547 +0,0 @@ - - - - {deck?.title ?? 'Deck'} — Cards - - -
- ← Decks - - {#if deck} -
-
-
- -

{deck.title}

-
- {#if deck.description} -

{deck.description}

- {/if} -
- -
- - {#if isSubscribed} -
-
-
-
- 📥 Abonniert · v{subscribedAtVersion} -
-

- Aus dem Marktplatz von {subscribedFromSlug}. Karten sind read-only — Author entscheidet über Inhalte. Forken um eigene Variante - zu machen (Phase ε). -

-
-
- {#if updatePreview} -
- - Update auf v{updatePreview.to} verfügbar - - - +{updatePreview.added} neu · ~{updatePreview.changed} geändert · −{updatePreview.removed} - entfernt - - -
- {/if} - {#if updateError} -

{updateError}

- {/if} -
- {/if} - -
- - {#if !isSubscribed} - - {/if} - {#if dueCount === 0 && cards.length > 0} - Heute alles gelernt — schau später wieder rein. - {/if} -
- -
-
-
{cards.length}
-
Karten
-
-
-
{dueCount}
-
Fällig
-
-
- - {#if !isSubscribed} -
- - -
- {/if} - - {#if showAi} -
- (showAi = false)} /> -
- {/if} - - {#if showNew} -
-

Neue Karte

- -
- {#each cardTypeOptions as opt (opt.value)} - - {/each} -
- -
- {#if newType === 'cloze'} -
-
- - - -
- - -

- Markiere mit - {{c1::Wort}} - — optional Hinweis: ::Hinweis. -

-
- {:else} -
-
- - - -
- - -
-
-
- - - -
- -
- {/if} - {#if attachError} -

{attachError}

- {/if} -
- - -
-
-
- {/if} - -
-

- Karten ({cards.length}) -

- {#if cards.length === 0} -
- Noch keine Karten. Erstelle deine erste! -
- {:else} -
    - {#each cards as card, i (card.id)} - {@const p = preview(card)} -
  • - {i + 1}. -
    -
    - {@html renderMarkdown(p.primary)} -
    - {#if p.secondary} -
    - {@html renderMarkdown(p.secondary)} -
    - {/if} -
    -
    - - {typeBadge(card.type)} - - {#if !isSubscribed} - - {/if} -
    -
  • - {/each} -
- {/if} -
- - {#if confirmDelete} -
(confirmDelete = false)} - onkeydown={(e) => e.key === 'Escape' && (confirmDelete = false)} - role="presentation" - > - - -
e.stopPropagation()} - > -

Deck löschen?

-

- "{deck.title}" wird mit allen Karten gelöscht. -

-
- - -
-
-
- {/if} - {:else} -
- Deck nicht gefunden. - zurück -
- {/if} -
- -{#if showPublish && deck} - (showPublish = false)} /> -{/if} diff --git a/apps/cards/apps/web/src/routes/explore/+page.svelte b/apps/cards/apps/web/src/routes/explore/+page.svelte deleted file mode 100644 index a0f60ce9a..000000000 --- a/apps/cards/apps/web/src/routes/explore/+page.svelte +++ /dev/null @@ -1,130 +0,0 @@ - - - - Entdecken — Cards - - -
-
-

Entdecken

-

- Decks aus dem Cards-Marktplatz — kostenlos lernen oder eigene veröffentlichen. -

-
- -
{ - e.preventDefault(); - runSearch(); - }} - > - - -
- - {#if stage === 'loading'} -

Lade Marktplatz…

- {:else if stage === 'error'} -

- {error} - -

- {:else if stage === 'search'} -
-
-

- {searchTotal} Treffer für „{searchQuery}" -

- -
- -
- {:else if stage === 'landing'} - {#if featured.length > 0} -
-

- 🛡️ Featured · vom Mana-Verein empfohlen -

- -
- {/if} - -
-

📈 Trending · letzte 7 Tage

- -
- {/if} - -

- ← Eigene Decks -

-
diff --git a/apps/cards/apps/web/src/routes/learn/[deckId]/+page.svelte b/apps/cards/apps/web/src/routes/learn/[deckId]/+page.svelte deleted file mode 100644 index 6588788b0..000000000 --- a/apps/cards/apps/web/src/routes/learn/[deckId]/+page.svelte +++ /dev/null @@ -1,226 +0,0 @@ - - - - Lernen — {deckTitle} — Cards - - -
-
-
- -

Lernen

-
- {#if queue.length > 0 && !finished} -
- {Math.min(currentIndex + 1, queue.length)} / {queue.length} -
- {/if} -
- - {#if empty} -
-
Alles gelernt
-

- Komm später wieder — fällige Karten erscheinen automatisch. -

- -
- {:else if finished} -
-
Session abgeschlossen
-

- {sessionCount} Karten in {Math.round((Date.now() - sessionStartedAt) / 1000)} s. -

- -
- {:else if current} - (typedAnswer = v)} - onReveal={reveal} - /> - - {#if canSuggest} -
- - -
- - {#if discussionsOpen && subscribedSlug && current?.card.serverContentHash} - - {/if} - {/if} - - {#if !showBack && current.card.type === 'type-in'} - - {:else if showBack} -
- - - - -
- {/if} - {:else} -
Lade…
- {/if} -
- -{#if subscribedSlug && current} - (suggestOpen = false)} - /> -{/if} diff --git a/apps/cards/apps/web/src/routes/login/+page.svelte b/apps/cards/apps/web/src/routes/login/+page.svelte deleted file mode 100644 index b09129a41..000000000 --- a/apps/cards/apps/web/src/routes/login/+page.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - - diff --git a/apps/cards/apps/web/src/routes/me/purchases/+page.svelte b/apps/cards/apps/web/src/routes/me/purchases/+page.svelte deleted file mode 100644 index 5f0d29f0f..000000000 --- a/apps/cards/apps/web/src/routes/me/purchases/+page.svelte +++ /dev/null @@ -1,130 +0,0 @@ - - - - Meine Käufe — Cards - - -
-

Käufe & Auszahlungen

- - {#if error} -

- {error} -

- {/if} - -
-
-

Käufe

- Ausgegeben: {totalSpent} 💎 -
- - {#if loading} -

- Lädt… -

- {:else if purchases.length === 0} -

- Du hast noch keine Decks gekauft. -

- {:else} -
    - {#each purchases as p (p.id)} -
  • -
    - - {p.deckTitle} - -

    - v{p.versionSemver} · {new Date(p.purchasedAt).toLocaleDateString('de-DE')} - {#if p.refundedAt} - Erstattet - {/if} -

    -
    - {p.priceCredits} 💎 -
  • - {/each} -
- {/if} -
- - {#if payouts.length > 0 || (!loading && payouts.length === 0)} -
-
-

- Author-Auszahlungen -

- Erhalten: {totalEarned} 💎 -
- - {#if payouts.length === 0} -

- Noch keine Auszahlungen — sobald jemand eines deiner kostenpflichtigen Decks kauft, landet - die Author-Beteiligung hier. -

- {:else} -
    - {#each payouts as p (p.id)} -
  • -
    - - {p.deckTitle} - -

    - Verkauf {p.priceCredits} 💎 · gutgeschrieben {new Date( - p.grantedAt - ).toLocaleDateString('de-DE')} -

    -
    - +{p.creditsGranted} 💎 -
  • - {/each} -
- {/if} -
- {/if} -
diff --git a/apps/cards/apps/web/src/routes/register/+page.svelte b/apps/cards/apps/web/src/routes/register/+page.svelte deleted file mode 100644 index 098ca4162..000000000 --- a/apps/cards/apps/web/src/routes/register/+page.svelte +++ /dev/null @@ -1,27 +0,0 @@ - - - diff --git a/apps/cards/apps/web/src/routes/u/[slug]/+page.svelte b/apps/cards/apps/web/src/routes/u/[slug]/+page.svelte deleted file mode 100644 index da381fdc1..000000000 --- a/apps/cards/apps/web/src/routes/u/[slug]/+page.svelte +++ /dev/null @@ -1,138 +0,0 @@ - - - - {author?.displayName ?? '@' + slug} — Cards - - -
- {#if stage === 'loading'} -

Lade Profil…

- {:else if stage === 'not-found'} -

- Profil @{slug} existiert nicht. -

- {:else if stage === 'error'} -

- {error} -

- {:else if author} -
- {#if author.avatarUrl} - - {:else} -
- {author.displayName.slice(0, 1).toUpperCase()} -
- {/if} -
-
-

{author.displayName}

- {#if author.verifiedMana} - - 🛡️ Mana - - {/if} - {#if author.verifiedCommunity} - - ⭐ Community - - {/if} -
-

- @{author.slug} · seit {new Date(author.joinedAt).toLocaleDateString('de-DE', { - year: 'numeric', - month: 'short', - })} -

- {#if author.bio} -

{author.bio}

- {/if} -
- {#if authStore.isAuthenticated} - - {/if} -
- -

- {decks.length} - {decks.length === 1 ? 'Deck' : 'Decks'} -

- - {/if} - -

- ← Marktplatz -

-
diff --git a/apps/cards/apps/web/static/apple-touch-icon.png b/apps/cards/apps/web/static/apple-touch-icon.png deleted file mode 100644 index d09ef49e5..000000000 Binary files a/apps/cards/apps/web/static/apple-touch-icon.png and /dev/null differ diff --git a/apps/cards/apps/web/static/favicon.svg b/apps/cards/apps/web/static/favicon.svg deleted file mode 100644 index 1f160f709..000000000 --- a/apps/cards/apps/web/static/favicon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/apps/cards/apps/web/static/pwa-192x192.png b/apps/cards/apps/web/static/pwa-192x192.png deleted file mode 100644 index 7cd2f8d33..000000000 Binary files a/apps/cards/apps/web/static/pwa-192x192.png and /dev/null differ diff --git a/apps/cards/apps/web/static/pwa-512x512.png b/apps/cards/apps/web/static/pwa-512x512.png deleted file mode 100644 index 2ab569c8b..000000000 Binary files a/apps/cards/apps/web/static/pwa-512x512.png and /dev/null differ diff --git a/apps/cards/apps/web/static/sql-wasm-browser.wasm b/apps/cards/apps/web/static/sql-wasm-browser.wasm deleted file mode 100755 index b32b66473..000000000 Binary files a/apps/cards/apps/web/static/sql-wasm-browser.wasm and /dev/null differ diff --git a/apps/cards/apps/web/static/sql-wasm.wasm b/apps/cards/apps/web/static/sql-wasm.wasm deleted file mode 100755 index b32b66473..000000000 Binary files a/apps/cards/apps/web/static/sql-wasm.wasm and /dev/null differ diff --git a/apps/cards/apps/web/svelte.config.js b/apps/cards/apps/web/svelte.config.js deleted file mode 100644 index fc92816a8..000000000 --- a/apps/cards/apps/web/svelte.config.js +++ /dev/null @@ -1,12 +0,0 @@ -import adapter from '@sveltejs/adapter-node'; -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - preprocess: vitePreprocess(), - kit: { - adapter: adapter(), - }, -}; - -export default config; diff --git a/apps/cards/apps/web/tsconfig.json b/apps/cards/apps/web/tsconfig.json deleted file mode 100644 index 9637d322e..000000000 --- a/apps/cards/apps/web/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true - } -} diff --git a/apps/cards/apps/web/vite.config.ts b/apps/cards/apps/web/vite.config.ts deleted file mode 100644 index 89272593f..000000000 --- a/apps/cards/apps/web/vite.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { sveltekit } from '@sveltejs/kit/vite'; -import { SvelteKitPWA } from '@vite-pwa/sveltekit'; -import tailwindcss from '@tailwindcss/vite'; -import { createPWAConfig } from '@mana/shared-pwa'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - plugins: [ - tailwindcss(), - sveltekit(), - SvelteKitPWA( - createPWAConfig({ - name: 'Cards', - shortName: 'Cards', - description: 'Karteikarten mit Spaced Repetition', - themeColor: '#0a0a0a', - }) - ), - ], -}); diff --git a/apps/cards/docs/MARKETPLACE_PLAN.md b/apps/cards/docs/MARKETPLACE_PLAN.md deleted file mode 100644 index d753bc7a4..000000000 --- a/apps/cards/docs/MARKETPLACE_PLAN.md +++ /dev/null @@ -1,654 +0,0 @@ -# Cardecky-Marktplatz — Plan - -> **Status**: Plan, kein Code. Stand 2026-05-07. -> **Goal-Setting**: Vollvision, kein MVP-Druck. Wir bauen die optimale Lösung. -> **Alignment**: User hat folgende Eckpunkte gesetzt: -> - Versionierte Decks + Live-Updates + Pull-Requests = ja, volle Vision -> - mana-credits zentral, sowohl für User-Käufe als auch Author-Verdienst -> - „Verified" zweigleisig: Mana-Verein-Kuration UND Community-Schwellen, mit unterschiedlichen Badges -> - Co-Learn-Sessions explizit **nicht** für Phase 1 — auf Phase 2 verschoben -> - Mobile-App auch später - ---- - -## 1. Mission - -**Die Karteikarten-Plattform mit der besten Lern-Community im Netz.** Wo qualitativ hochwertige Decks entstehen, gepflegt, geteilt und gelernt werden — und wo Lernende einander helfen. - -## 2. Was wir gegen die Konkurrenz aufbieten - -(verdichtet aus `apps/cards/COMPETITORS_2026-05.md`) - -| Differenzierer | Wir | Wer noch | -|---|---|---| -| Free Cloud-Sync | ✓ | niemand | -| Versionierte Decks mit Live-Updates | ✓ | nur AnkiHub (paywalled, Medizin-only) | -| Pull-Requests auf Decks | ✓ | niemand | -| Card-Discussions (inline pro Karte) | ✓ | niemand | -| AI-Karten + AI-Moderation + AI-Tags | ✓ | fragmentiert bei anderen | -| Open Source PWA | ✓ | nur Anki/Mnemosyne (Desktop) | -| Anki-Migration mit Bildern/Audio | ✓ (vorhanden) | niemand vollständig | -| Author-Followings + Activity-Feed | ✓ | niemand | -| Bezahlte Decks mit Author-Erlös via mana-credits | ✓ | nur Brainscape (eigenes Closed-Pricing) | -| Pseudonym + verifiziert kombinierbar | ✓ | niemand klar | - -## 3. Architektur-Prinzipien - -1. **API ist `/v1` ab Tag 1** — OpenAPI-Spec als Quelle der Wahrheit, Versionierungs-Bewusstsein eingebaut. -2. **Public-Decks leben separat** vom Local-First-Sync-Pfad (eigene Postgres-Tabellen, eigene Service, eigene RLS-Policies). Kein Vermischen mit `mana_sync.sync_changes`. -3. **Subscribed Decks sind unidirektional**: Author → Subscribers. Updates fließen einseitig. Wer ändern will, forkt. -4. **Content-Hash überall.** Jede Karte und jede Version bekommt einen deterministischen SHA-256 → Trust + Cache + Diff kostenlos. -5. **Lizenzen sind explizit + maschinen-lesbar** (SPDX-IDs: `CC0-1.0`, `CC-BY-4.0`, `CC-BY-SA-4.0`, plus eigener `Cardecky-Personal-Use-1.0` für Default-Käufe und `Cardecky-Pro-Only-1.0` für paid Decks). -6. **AI ist Moderator, nicht Gatekeeper** — KI-First-Pass + Human-Review-Eskalation. Niemals KI-allein-Take-down. -7. **Search ist von der DB entkoppelt** — Read-Only-Index, asynchron befüllt. Bricht der Search-Service, läuft der Marktplatz weiter. -8. **mana-credits ist die einzige Geld-Schnittstelle** — niemals Stripe direkt im cards-server. Alles geht über `/api/v1/credits/use`, `/credits/grant`, `/credits/reservations/*`. -9. **Anonymisiertes Lern-Verhalten**: aggregierte Stats sichtbar (z.B. „1.200 Lernende"), individuelles Lernverhalten nie öffentlich ohne explizites Opt-in. -10. **Keine Drittanbieter-Tracker.** Telemetrie ausschließlich über mana-analytics, opt-out möglich. - -## 4. Datenmodell - -Neues Schema `cards` in `mana_platform`. Alle Tabellen über `pgSchema('cards').table(...)` (Mana-Konvention). - -### 4.1 Authoren - -```sql -public_authors ( - user_id uuid PRIMARY KEY REFERENCES auth.users(id), - slug text UNIQUE NOT NULL, -- @anna-lang - display_name text NOT NULL, - bio text, - avatar_url text, - joined_at timestamptz DEFAULT now(), - pseudonym boolean DEFAULT false, -- true = klarname versteckt - verified_mana boolean DEFAULT false, -- vom Verein verliehen - verified_community boolean DEFAULT false, -- automatisch ab Schwelle - banned_at timestamptz, -- soft-ban - banned_reason text -) -``` - -Drei Verifizierungs-Stufen mit unterschiedlichen Badges in der UI: - -| Status | Badge | Wer / wie | -|---|---|---| -| `verified_mana = true` | 🛡️ **Mana Verifiziert** | Manuell vom Mana-Verein vergeben (Lehrer, Profis, Sprachschulen, Ärzte). Nicht erkaufbar. | -| `verified_community = true` | ⭐ **Community Verifiziert** | Automatisch bei: ≥ 500 Stars über alle Decks ODER ≥ 3 featured Decks ODER ≥ 200 aktive Subscribers über alle Decks. Periodisch neu evaluiert. | -| beides | 🛡️⭐ Beide Badges | Mana + Community zusammen. | - -### 4.2 Decks + Versionen - -```sql -public_decks ( - id uuid PRIMARY KEY, - slug text UNIQUE NOT NULL, -- /decks/anna-lang/spanish-a2-vocab - title text NOT NULL, - description text, - language text, -- ISO-639-1 - license text NOT NULL, -- SPDX - price_credits integer DEFAULT 0, -- 0 = kostenlos - owner_user_id uuid NOT NULL REFERENCES public_authors(user_id), - latest_version_id uuid, -- → public_deck_versions - is_featured boolean DEFAULT false, - is_takedown boolean DEFAULT false, - takedown_at timestamptz, - takedown_reason text, - created_at timestamptz DEFAULT now(), - CONSTRAINT price_requires_license CHECK (price_credits = 0 OR license = 'Cardecky-Pro-Only-1.0') -) - -public_deck_versions ( - id uuid PRIMARY KEY, - deck_id uuid NOT NULL REFERENCES public_decks(id), - semver text NOT NULL, -- 1.0.0, 1.1.0, 2.0.0 - changelog text, - content_hash text NOT NULL, -- SHA-256 of canonicalized cards - card_count integer NOT NULL, - published_at timestamptz DEFAULT now(), - deprecated_at timestamptz, - UNIQUE (deck_id, semver) -) - -public_deck_cards ( - id uuid PRIMARY KEY, - version_id uuid NOT NULL REFERENCES public_deck_versions(id), - type text NOT NULL, -- basic, basic-reverse, cloze, type-in - fields jsonb NOT NULL, -- {front, back} oder {text, extra} - ord integer NOT NULL, - content_hash text NOT NULL, -- per Karte: ermöglicht Smart-Merge - UNIQUE (version_id, ord) -) -``` - -### 4.3 Tags + Discovery - -```sql -tag_definitions ( - id uuid PRIMARY KEY, - slug text UNIQUE NOT NULL, - name text NOT NULL, - parent_id uuid REFERENCES tag_definitions(id), -- Hierarchie - description text, - curated boolean DEFAULT false -- vom Mana-Verein gepflegt -) - -deck_tags ( - deck_id uuid REFERENCES public_decks(id), - tag_id uuid REFERENCES tag_definitions(id), - PRIMARY KEY (deck_id, tag_id) -) -``` - -### 4.4 Engagement (Stars, Subscribes, Forks) - -```sql -deck_stars ( - user_id uuid REFERENCES auth.users(id), - deck_id uuid REFERENCES public_decks(id), - starred_at timestamptz DEFAULT now(), - PRIMARY KEY (user_id, deck_id) -) - -deck_subscriptions ( - user_id uuid REFERENCES auth.users(id), - deck_id uuid REFERENCES public_decks(id), - current_version_id uuid REFERENCES public_deck_versions(id), - subscribed_at timestamptz DEFAULT now(), - notify_updates boolean DEFAULT true, - PRIMARY KEY (user_id, deck_id) -) - -deck_forks ( - user_id uuid REFERENCES auth.users(id), - source_deck_id uuid REFERENCES public_decks(id), - source_version_id uuid REFERENCES public_deck_versions(id), - forked_at timestamptz DEFAULT now(), - PRIMARY KEY (user_id, source_deck_id, source_version_id) -) - -author_follows ( - follower_user_id uuid REFERENCES auth.users(id), - author_user_id uuid REFERENCES public_authors(user_id), - since timestamptz DEFAULT now(), - PRIMARY KEY (follower_user_id, author_user_id) -) -``` - -### 4.5 Pull-Requests + Discussions - -```sql -deck_pull_requests ( - id uuid PRIMARY KEY, - deck_id uuid REFERENCES public_decks(id), - author_user_id uuid REFERENCES auth.users(id), - status text NOT NULL, -- open, merged, closed, rejected - title text NOT NULL, - body text, - diff jsonb NOT NULL, -- {add: [...], modify: [...], remove: [...]} - merged_into_version uuid REFERENCES public_deck_versions(id), - created_at timestamptz DEFAULT now(), - resolved_at timestamptz -) - -card_discussions ( - id uuid PRIMARY KEY, - card_content_hash text NOT NULL, -- bindet sich an Karte, nicht an version - deck_id uuid REFERENCES public_decks(id), - author_user_id uuid REFERENCES auth.users(id), - parent_id uuid REFERENCES card_discussions(id), - body text NOT NULL, - hidden boolean DEFAULT false, - created_at timestamptz DEFAULT now() -) -``` - -### 4.6 Moderation - -```sql -deck_reports ( - id uuid PRIMARY KEY, - deck_id uuid REFERENCES public_decks(id), - version_id uuid REFERENCES public_deck_versions(id), - card_content_hash text, -- optional: Karte spezifisch - reporter_user_id uuid REFERENCES auth.users(id), - category text NOT NULL, -- spam, copyright, nsfw, misinformation, other - body text, - status text DEFAULT 'open', -- open, dismissed, actioned - resolved_by uuid, - resolved_at timestamptz, - resolution_notes text, - created_at timestamptz DEFAULT now() -) - -ai_moderation_log ( - id uuid PRIMARY KEY, - version_id uuid REFERENCES public_deck_versions(id), - verdict text NOT NULL, -- pass, flag, block - categories text[], -- spam, csam, hate, nsfw, ... - model text, -- "claude-3-5-sonnet" etc - rationale text, - human_reviewed boolean DEFAULT false, - human_overrode boolean DEFAULT false, - created_at timestamptz DEFAULT now() -) -``` - -### 4.7 mana-credits Integration - -```sql -deck_purchases ( - id uuid PRIMARY KEY, - buyer_user_id uuid REFERENCES auth.users(id), - deck_id uuid REFERENCES public_decks(id), - version_id uuid REFERENCES public_deck_versions(id), - price_credits integer NOT NULL, -- Snapshot zum Zeitpunkt des Kaufs - author_share integer NOT NULL, -- nach Verein-Cut - mana_share integer NOT NULL, - credits_transaction text, -- mana-credits ID - purchased_at timestamptz DEFAULT now(), - refunded_at timestamptz, - UNIQUE (buyer_user_id, deck_id) -- einmal Kauf reicht für Lifetime + alle Versionen -) - -author_payouts ( - id uuid PRIMARY KEY, - author_user_id uuid REFERENCES public_authors(user_id), - source_purchase_id uuid REFERENCES deck_purchases(id), - credits_granted integer NOT NULL, - credits_grant_id text, -- mana-credits grant ID - granted_at timestamptz DEFAULT now() -) -``` - -## 5. mana-credits Integration (Detail) - -Zwei-seitiger Marktplatz. mana-credits ist Single-Source-of-Truth fürs Geld. - -### 5.1 Kauf-Flow (Buyer) - -1. User klickt „Kaufen" auf paid Deck (Preis: z.B. 50 Credits) -2. cards-server checkt: Hat User schon dieses Deck? (deck_purchases) → wenn ja, sofort Zugriff -3. cards-server reserviert Credits via `POST mana-credits/api/v1/credits/reservations` (2-phase) -4. cards-server erstellt deck_purchases-Row (committed) -5. cards-server commit-released die Reservation → Credits abgebucht -6. cards-server erstellt author_payouts-Row → ruft `POST mana-credits/api/v1/internal/credits/grant` für den Author-Anteil -7. User bekommt sofortigen Zugriff: Deck wird in private Liste verschoben (User hat eine eigene Lokal-Kopie als Author-Subscription) - -**Was passiert wenn Author gebannt nach Kauf?** → Refund-Path (Phase γ Implementation): Admin kann Refund triggern → mana-credits → Reverse-Grant → User behält das Deck nicht mehr. - -### 5.2 Author-Auszahlungs-Modell - -- **Standard-Cut**: 80 % Author / 20 % Mana-Verein (Server-, Hosting-, Moderations-Kosten) -- **Verifizierte Authoren** (verified_mana): 90 % / 10 % -- **Mindestauszahlung**: keine — Credits werden direkt im mana-credits-Account gebucht, von dort kann der Author sie selbst nutzen oder per Stripe-Payout (mana-credits-Feature, falls vorhanden) abheben -- **Pricing-Range**: Free (0 Credits), oder 10–500 Credits (entspricht ungefähr 1–50 € — exakte Conversion siehe mana-credits packages) - -### 5.3 Käufer-Lebenszyklus - -- Einmal gekauft = Lifetime-Zugriff auf alle künftigen Versionen -- Bei major Version (e.g. 1.x → 2.0.0) **kein** zweiter Kauf nötig — Author behält die Verbesserungs-Pflicht -- Refund-Window: 30 Tage, automatisch verfügbar wenn ≤ 10 % der Karten gelernt wurden (Quizlet hat das, ist Best-Practice) - -### 5.4 Buyer-Protection bei Take-Down - -- Wenn Deck per Take-Down entfernt wird, behält Buyer Zugriff auf das letzte gesehene Snapshot (DSGVO-konform) -- Refund automatisch wenn Take-Down innerhalb 90 Tagen nach Kauf - -## 6. Service-Architektur - -### 6.1 `cards-server` (neu) - -- **Stack**: Hono + Bun (Mana-Konvention) -- **Port**: 3072 -- **Deps**: PostgreSQL (`mana_platform.cards.*`), Redis (Job-Queue für Indexing/Notifications) -- **Auth**: JWT via JWKS (mana-auth) -- **Routes**: siehe §7 - -### 6.2 `cards-search` (neu, später) - -- Eigene PostgreSQL-Instance mit pg_trgm + tsvector + pgvector -- Async-Indexer hört auf cards-server-Events („deck-published", „deck-updated") -- Optional: Meilisearch wenn Postgres FTS nicht reicht - -### 6.3 mana-llm (existierend, erweitert) - -- Embeddings für semantic search (jeden Deck-Description + Karte → 1536-dim Vector) -- Moderation-First-Pass (Klassifikation in spam/csam/hate/nsfw/etc.) -- Auto-Tag-Suggestions -- Auto-Summary für Deck-Beschreibungen - -### 6.4 mana-credits (existierend, erweitert) - -- Bestehende `/credits/use` und `/credits/reservations/*` für Kauf -- Bestehender `/internal/credits/grant` für Author-Auszahlung -- Vermutlich keine API-Erweiterung nötig - -### 6.5 mana-notify (existierend, erweitert) - -- Push-Notifications für Subscribe-Updates, neue Subscribers, neue Discussions/Replies, neue Stars (vom User konfigurierbar) - -### 6.6 mana-media (existierend) - -- Bilder/Audio in published Decks landen wie heute auch -- Pro Author-Tier ein Soft-Quota: Free 100MB, Verified 1GB, Mana 5GB - -## 7. API-Endpoints (Auswahl) - -OpenAPI-Spec wird die Quelle der Wahrheit; hier die wichtigsten Routes: - -### 7.1 Authoren - -``` -POST /v1/authors/me — Profil anlegen/updaten (slug, displayName, bio, avatar, pseudonym) -GET /v1/authors/:slug — Public Profile + Decks-Liste + Stats -GET /v1/authors/me/dashboard — Eigene Stats: Subscriber, Erlöse, Mod-Inbox -POST /v1/authors/:slug/follow — Folgen -DELETE /v1/authors/:slug/follow — Entfolgen -GET /v1/authors/me/feed — Personal Activity-Feed -``` - -### 7.2 Decks - -``` -POST /v1/decks — Deck als public registrieren (Init-Flow) -GET /v1/decks/:slug — Public Deck mit latest version -GET /v1/decks/:slug/versions — Versionsliste mit Changelogs -GET /v1/decks/:slug/versions/:semver — Specific Version + alle Karten -PATCH /v1/decks/:slug — Metadaten (title, description, license, price) - -POST /v1/decks/:slug/publish — Neue Version publishen (body: cards[], semver, changelog) - → triggert AI-Mod-Pass - → setzt latest_version_id - -POST /v1/decks/:slug/star — Star setzen -DELETE /v1/decks/:slug/star — Star entfernen - -POST /v1/decks/:slug/subscribe — Subscribe (lädt + sync'd Karten in lokale DB) -DELETE /v1/decks/:slug/subscribe — Unsubscribe - -POST /v1/decks/:slug/fork — Fork (lokale Kopie + Author-Lineage) - -POST /v1/decks/:slug/buy — Paid Deck kaufen (mana-credits-Flow) -POST /v1/decks/:slug/refund — Refund anfragen -``` - -### 7.3 Pull-Requests - -``` -GET /v1/decks/:slug/pull-requests — Liste -POST /v1/decks/:slug/pull-requests — Neuer PR (body: title, body, diff) -GET /v1/pull-requests/:id — Details -POST /v1/pull-requests/:id/merge — Author merged → erstellt neue Version -POST /v1/pull-requests/:id/close — Author schließt -POST /v1/pull-requests/:id/comments — Diskussion auf PR-Ebene -``` - -### 7.4 Discussions - -``` -GET /v1/cards/:contentHash/discussions — Threads für eine Karte (über Versionen hinweg) -POST /v1/cards/:contentHash/discussions — Neuer Thread / Reply -POST /v1/discussions/:id/hide — Author/Mod versteckt -``` - -### 7.5 Discovery + Search - -``` -GET /v1/explore — Featured + Trending + Categories (curated) -GET /v1/search?q=…&tag=…&lang=…&sort=… — Volltextsuche (FTS + semantic) -GET /v1/tags — Tag-Hierarchie -GET /v1/decks?author=…&tag=…&sort=…&p=… — Filtered Browse -``` - -### 7.6 Reports + Moderation - -``` -POST /v1/decks/:slug/report — User reportet Deck -POST /v1/cards/:contentHash/report — User reportet Karte -GET /v1/admin/reports — Admin-Inbox (verifizierte Mana-Mods only) -POST /v1/admin/decks/:slug/takedown — Admin entfernt Deck -POST /v1/admin/authors/:slug/ban — Admin sperrt Author -POST /v1/admin/authors/:slug/verify-mana — Mana-Verein-Badge vergeben -``` - -### 7.7 Notifications - -``` -GET /v1/notifications — Unread + recent -POST /v1/notifications/:id/read — Mark read -PATCH /v1/notifications/preferences — Settings (welche Events triggern Push) -``` - -## 8. UI / Routes (Cardecky-Frontend) - -``` -/explore — Featured + Trending + Tag-Tree + Search-Bar -/explore/search?q=… — Search-Result-Page -/explore/tag/:slug — Tag-Page - -/u/:slug — Author-Profil (Public) -/u/:slug/follow — Follow-Button im Header - -/d/:slug — Public-Deck-Detail-View - (Description, Stats, Latest-Karten-Preview, Subscribe/Fork/Star/Buy, Discussions) -/d/:slug/v/:semver — spezifische Version -/d/:slug/discussions — Alle Discussions zum Deck -/d/:slug/pull-requests — PRs -/d/:slug/pull-requests/:id — PR-Detail mit Diff-View - -/me/decks — Eigene private Decks (heute existiert) -/me/published — Eigene published Decks + Stats -/me/subscribed — Abonnierte Decks (mit Update-Indikator) -/me/forks — Geforkte Decks -/me/dashboard — Author-Dashboard (Erlöse, Subscriber-Wachstum) - -/feed — Personal Activity-Feed (Following-Activity + Updates) - -/admin/reports — Admin-Inbox (verified-mana-only) -/admin/decks — Take-Down-UI -/admin/authors — Verify + Ban -``` - -Zusätzlich: einige bestehende Komponenten erweitern (DeckDetail bekommt Subscribe-Button etc.). - -## 9. Cold-Start-Strategie - -Marktplatz ohne Decks ist nutzlos. Drei parallele Hebel: - -1. **Verein-Seed-Decks**: 50 hochwertige Decks selbst erstellen — sprachen (Top-3000 Vokabeln pro Sprache), Geschichte (TimeLine-Karten), Allgemeinwissen, Programmierung. Vom Mana-Team published, alle mit `verified_mana`-Badge. -2. **Anki-Top-100-Import-Service**: Wir bieten an, populäre Anki-Web-Decks (mit korrekter CC-BY-Lizenz) zu importieren und mit Original-Author-Attribution als Public-Decks anzulegen. Original-Author bekommt das `verified_mana`-Badge wenn er sich registriert. -3. **Influencer-Outreach**: Direkte Ansprache von 10-20 Anki-Power-Authoren (AnKing, etc.) mit dem Angebot eines verified-Status + sehr Author-freundlichem Cut. Wenn 1-2 wechseln, kommt ein Lawineneffekt. - -## 10. Risiken + Mitigationen - -| Risiko | Mitigation | -|---|---| -| Cold-Start (Marktplatz leer) | Seed + Anki-Import + Influencer (siehe §9) | -| Spam / Junk-Decks | AI-Mod-First-Pass + Report-System + Author-Ban-Flow | -| Copyright-Klagen (Lehrbuch-Karten) | Lizenz-Pflichtangabe + DMCA-Process + Take-Down-Workflow | -| Server-Kosten (Storage von Bildern/Audio) | Soft-Quotas pro Author-Tier (§6.6) + lossy compression im mana-media | -| AnkiHub als Konkurrent (Live-Updates Medizin) | „Alle Fachgebiete + gratis" als Counter; Med-Decks aktiv akquirieren | -| Mana-Credits-Verein-Cut zu hoch oder zu niedrig | A/B-Test verschiedener Cut-Verhältnisse; Best-Practice: ~80/20 für Standard, ~90/10 für Verified | -| Author-Frustration über fehlende Mobile-App | Klarer Roadmap-Hinweis + Mobile-Push-Notifications via PWA (heute geht das schon) | -| Discussions werden Toxic | Author-Owns-Their-Discussions (kann hide); Community-Mod (Verified-User können flaggen); klar dokumentierte Community-Guidelines | -| Mining/Scraping der Decks | Rate-limit auf API + Auth-Required für full-content; offene Snippets aber paywall am Voll-Inhalt | - -## 11. Phasenplan - -> **Co-Learn explizit ausgeklammert.** Mobile-App auch. - -### Phase α — Daten-Skelett (cards-server v0.1) - -- `services/cards-server/` SvelteKit-style Service-Setup, Hono + Bun + Drizzle -- Alle Schema-Tabellen + Migrationen (§4) -- API-Routes (CRUD-Niveau): Authoren, Decks, Versionen, Stars, Subscriptions -- OpenAPI-Spec -- Integration-Tests (Drizzle + Vitest) -- mana-auth-JWT-Middleware (`@mana/shared-hono`) -- Container in `docker-compose.macmini.yml` -- Cloudflare-Tunnel-Route `cardecky-api.mana.how` → `:3072` - -### Phase β — Author-Workflow ✅ shipped - -- ✅ „Author werden"-Flow im Frontend (Profil anlegen, slug claimen) -- ✅ „Publish"-Aktion auf Deck-Detail-Seite - - ✅ Lizenz-Picker (SPDX-Auswahl) - - ✅ Optional: Preis in Credits - - ⏳ Tags: Picker fehlt im Publish-Flow; Server-Schema steht -- ✅ Versioning: semver-Eingabe (Auto-Suggest pre-fill folgt in θ) -- ✅ Changelog-Editor -- ✅ AI-First-Pass-Moderation (mana-llm classify, Verdict im Publish-Result) -- ⏳ Author-Dashboard mit Subscriber-Counts: Erlöse jetzt unter `/me/purchases`, restliche Stats fehlen - -### Phase γ — Discovery-Frontend ✅ shipped (FTS minimal) - -- ✅ `/explore`-Seite mit Featured + Trending -- 🟡 Volltext-Suche: einfaches `ILIKE` über Title/Description; tsvector-Upgrade in Phase ι -- 🟡 Tag-Hierarchie: flach implementiert; baumartige Eltern-Kind-Navigation offen -- ✅ Author-Profile (`/u/`) + Follow-Button -- ⏳ Activity-Feed (wer hat was published / merged): nicht gebaut -- ✅ Star-System - -### Phase δ — Subscribe + Updates + Smart-Merge ✅ shipped - -- ✅ „Abonnieren"-Button → lädt aktuelle Version in lokale Cardecky-DB -- 🟡 Update-Detection: Polling beim Öffnen der Deck-Page; **kein** WebSocket-Push (kommt in θ/ι) -- ✅ **Smart-Merge**: Diff zwischen Versionen → unveränderte Karten behalten FSRS-State; geänderte erben FSRS-State über Ord-Pairing-Heuristik; neue + entfernte werden korrekt behandelt -- ✅ Diff-View „+N · ~N · −N" mit Apply-Button auf der Deck-Page -- ⏳ Push-Notifications für Subscribe-Updates via mana-notify: PR-/Verkaufs-Mails sind drin (ε.3, ζ.1), Update-Mail noch nicht - -### Phase ε — Pull-Requests + Discussions ✅ shipped - -- ✅ PR-Erstellen-UI: „✏️ Verbessern" auf `/learn/[id]` für Karten aus abonnierten Decks (modify oder remove) -- ✅ PR-Diff-Preview (flach, alle drei Blöcke `add` / `modify` / `remove`) -- ✅ Author-Merge-Workflow → erstellt neue Version atomar, bumped semver-Minor by default -- ✅ Inline-Discussion-Threads: in `/learn` (Toggle) + auf `/d/` (Karten-Liste mit Comment-Counts) -- ✅ Notify: Author bei neuem PR; PR-Author bei Merge/Reject (deterministische ExternalIDs für Dedup) -- ⏳ Mention-System (@username): nicht gebaut; Schema-Änderung später trivial -- 🟡 PR-Merge ist „stale-blind": kein Rebase / Konflikt-Detection (siehe §13a) - -### Phase ζ — mana-credits Marketplace 🟡 ζ.1 shipped, ζ.2 offen - -- ✅ Paid-Deck-Workflow End-to-End: 4-step Pipeline `reserve → INSERT purchase → commit → grant author + INSERT payout`, idempotent über `(buyer, deck)` -- ✅ Author-Auszahlungs-Pipeline: 80/20 Standard, 90/10 für `verifiedMana`-Authoren, kommt aus `config.authorPayout` (Basis-Punkte) -- ✅ Buyer-Dashboard `/me/purchases` mit Käufen + Author-Auszahlungs-Historie -- ⏳ **Refund-Workflow**: bewusst out-of-scope für ζ.1 (Author-Clawback ist konzeptuell heikel — siehe §13a) -- ⏳ **Reconciler**: bei Commit-/Grant-Failure nach Schritt 2 bleibt eine Purchase-Row mit `creditsTransaction = null` bzw. ohne Payout. Code logged, niemand fegt nach. Cron-Sweep in ζ.2 -- ⏳ Author-Payouts-CSV-Export für Steuern - -### Phase η — Moderation + Trust 🟡 η.1 shipped, η.2/η.3 offen - -- ✅ Report-Buttons auf Deck (`/d/`) + Discussion-Kommentare -- ✅ Admin-Inbox-UI (`/admin/reports`) mit Abweisen / Deck-Takedown / Author-Bann -- ✅ Take-Down-Workflow: transaktional, auto-closed parallele Reports + offene PRs auf demselben Deck, Mail an Author -- 🟡 Verified-Badge-Vergabe via API (`POST /v1/admin/authors/:slug/verify`); kein dediziertes UI -- ⏳ **Community-Verified Auto-Calculation**: Schema + Schwellwerte da; Cron-Job fehlt (η.2) -- ⏳ **Public Take-Down-Changelog**: Plan erwähnt das, nicht gebaut -- ⏳ **Verified-Mana-only Mods**: aktuell nur `role === 'admin'`; Plan-Vision ist „verified-mana darf auch resolven" — feiner Cut, später -- ⏳ Author-Ban-Process: Ban kaskadiert auf Decks ✅, aber kein Self-Service-Appeal-Flow für Author -- ⏳ Report-Spam-Schutz (Rate-Limit pro User+Deck): nicht da - -### Phase θ — Deep AI - -- Auto-Tag-Suggestions beim Publish (mana-llm) -- Auto-Summary für Decks (mana-llm Markdown-Render-tauglich) -- Audio-Vertonung mit mana-tts (Author opt-in: alle Karten als Audio generieren) -- Semantic-Search via Embeddings (mana-llm + pgvector) -- Personalized-Discovery („Empfohlen für dich" basierend auf Lern-Historie) - -### Phase ι — Optimierung + Skalierung - -- Search-Service als separater Pod (Meilisearch wenn Postgres FTS limitiert) -- CDN für public-deck-content (Cache + Geo-Distribution) -- Rate-Limiting + Anti-Scraping -- Real-time-Stats-Aggregation (Materialized Views) - -### Phasen die später kommen (explizit nicht in diesem Plan) - -- **Phase λ — Co-Learn-Sessions**: WebSocket-Multiplayer, gemeinsam lernen, Sehen-was-andere-machen -- **Phase μ — Mobile-Apps**: Expo-App (Cardecky-Standalone-Mobile) -- **Phase ν — Author-Tools**: Bulk-Edit-UI für Authoren mit großen Decks, Style-Templates, Author-Analytics-Deep-Dive -- **Phase ξ — Lern-Battles**: Asynchroner Wettkampf-Modus - -## 12. Konkrete Differenzierungs-Hebel — was geht wirklich nur bei uns - -1. **Gratis Cloud-Sync + Live-Updates auf abonnierte Decks**. Niemand sonst hat beides ohne Paywall. -2. **Pull-Requests auf Decks**. AnkiHub erlaubt das nicht so flüssig, andere gar nicht. „Lerne und verbessere mit" als Modus. -3. **Card-Discussions inline** — wenn ich beim Lernen eine Karte unverständlich finde, kann ich direkt fragen / ergänzen. Anki hat Plugin dafür, RemNote auch nicht. -4. **Authoren verdienen via mana-credits** — wir behandeln Authoren als 1st-Class-Konstrukt mit Erlös-Möglichkeit. Quizlet macht das nicht, AnkiWeb macht das nicht, Brainscape paywalled stattdessen die User. -5. **Open Source PWA** mit klarer Roadmap-Transparenz — Vertrauensvorsprung vs. Quizlet (closed, Trustpilot 1.4/5) und gegenüber AnkiPro/AnkiApp (closed-source, Brand-Sniper). -6. **Doppelte Verifizierungs-Stufen** mit unterschiedlichen Badges — Anki-Foren machen das ad-hoc; wir formalisieren es. -7. **AI als Moderator + Generator + Indexer** ohne Paywall — wir haben den eigenen mana-llm-Stack, Konkurrenten zahlen OpenAI per Call. - -## 13. Was wir NICHT tun - -- **Kein Decks-Bewertungssystem mit 1-5 Sternen**. Stars (Bookmarks) ja, Bewertungen nein — die werden gegamed (Quizlet-Erfahrung), und führen zu Author-Frust + Review-Bombing. -- **Kein Reddit-Style-Voting auf Karten / PRs / Discussions**. Wirkt cool, ruiniert die Community (Hacker-News-Effekt). Lieber „helpful"-Reactions in begrenzten Kategorien. -- **Kein „Karten der Woche" allein-algorithmisch**. Editorial-Pick (Mana-Verein) + Trending-Liste, aber niemals nur Algo, das landet immer beim niederschwelligsten Content. -- **Kein Anki-Bashing im Marketing**. Anki ist OSS, ehrlich, und wir wollen nicht ihre Audience entfremden — wir wollen sie ergänzen. Bridge nicht Burning. -- **Keine Pflicht-Klarnamen**. Pseudonyme bleiben gleichberechtigt. Verifizierung ist Bonus, nicht Pflicht. -- **Kein Marketplace-Cut über 30 %**. Apple-App-Store-Hass ist real, wir bleiben fair. - -## 13a. Bekannte Limitierungen / „macht später" - -**Phase ε (Pull-Requests + Discussions)** - -- **PR-Merge ist stale-blind**: `merge()` baut die neue Version aus `currentCards` zusammen, indem es Removes anwendet, dann Modifies-by-Hash, dann Adds. Wenn der Author zwischen PR-Open und Merge selbst eine Karte geändert hat, deren `previousContentHash` der PR matched, gewinnt **stumm** der PR — kein Konflikt-Hinweis. Akzeptabel solange wir wenige PRs/Tag haben; später entweder (a) PR-rebase mit `status=stale` bei Konflikt, oder (b) optimistic locking via `baseVersionId` auf der PR-Row mit Reject bei Mismatch. -- **Keine Multi-Card-Diff-Visualisierung**: PR-Diff-Preview zeigt jeden Block (`add` / `modify` / `remove`) flach. Bei großen PRs mit 50+ Karten unübersichtlich — Side-by-side-Vergleich pro modify wäre nett. -- **Discussion-Threading ist 1-Level**: Server speichert schon `parent_id`, aber das UI rendert flach. Bei Bedarf später ein Antworten-Button + visuelle Einrückung — kein Schema-Change nötig. -- **Card-Preview-Heuristik ist roh**: `` zieht `front` → `text` → erstes nicht-leeres Feld, strippt HTML, capt bei 140 Zeichen. Bei Cloze-Karten sieht der Leser den Roh-Text mit `{{c1::…}}`-Markern statt der maskierten Lern-Form. Kein Showstopper; später kann der Server eine `searchPreview`-Spalte schreiben. - -**Phase ζ (Paid Decks)** - -- **Refunds**: bewusst weggelassen. Author-Clawback ist konzeptuell heikel, weil der Author seinen Anteil nach Grant schon ausgegeben haben kann (→ 402 beim Reverse-Charge). Empfohlene ζ.2-Variante: Admin-only Refund, Buyer kriegt vollen Preis zurück, Author-Clawback nur best-effort, AGB-Klausel über Author-Cut-Risiko bei Refund. -- **Reconciler fehlt**: Wenn `commit` oder `grant` nach Schritt 2 fehlschlägt, bleibt eine Purchase-Row mit `creditsTransaction = null` bzw. ohne `author_payout`. Code logged das, aber niemand fegt nach. Cron-Sweep in ζ.2. -- **Buyer hat keinen Refund-Self-Service**: kein 30-Tage-Window-Knopf in der UI. Plan §5.3 sieht ihn vor; warten auf ζ.2. -- **CSV-Export für Steuern**: nicht drin. Easy add-on, sobald Verein die Steuerklärung 2026 vorbereitet. - -**Phase η (Moderation)** - -- **Verified-Mana-only Mods**: Admin-Gate ist aktuell `role === 'admin'`. Plan §11 sieht vor, dass auch verified-mana-Authoren Reports abarbeiten dürfen (mit eingeschränkten Aktionen). Würde nach den ersten 50 Reports sinnvoll, vorher over-engineered. -- **Community-Verified Cron**: Schema + Schwellwerte (`COMMUNITY_VERIFY_STARS=500`, `_FEATURED=3`, `_SUBSCRIBERS=200`) sind im config, aber kein Job berechnet `verified_community`. Add-on: ein Cron-Endpoint im internal API + SystemD-Timer auf Mac mini. -- **Public Take-Down-Changelog**: Plan erwähnt eine `/transparency`-Page — nicht gebaut. Bringt Trust, niedrige Priorität. -- **Appeal-Self-Service**: Author hat keinen Self-Service-Knopf für Restore. Bewusste Entscheidung — Appeals sollen menschlich sein, kein Self-Restore. -- **Report-Spam-Schutz**: ein User kann unbegrenzt Reports gegen ein Deck filen. Rate-Limit (max 1/User+Deck+Tag) wäre billig; kommt mit Phase ι. - -**Querschnittsthemen** - -- **Disk-Space auf der Build-Maschine** (Mac mini): aktuell ~6.7 GB frei. `pnpm store prune` als nächste Notbremse, falls cards-web-Builds enge Container-Layer brauchen. - -## 14. Offene Punkte die später entschieden werden müssen - -- **Mobile-Push-Notifications** für Subscribe-Updates: native PWA-Push reicht aktuell, aber Browser-API ist hin- und her — könnte Phase ι in einen eigenen Push-Service auslagern müssen. -- **Slack/Discord-Bots für Author-Updates**: nice-to-have, irgendwann. -- **Embed-Widget**: „Lerne dieses Deck auf meiner Webseite" mit IFrame — könnte Reichweite stark boosten. -- **API-Public**: API-Keys für Drittentwickler die eigene Tools rund um Cards bauen. -- **Backup für Subscriber**: Wenn ein Author published-Deck depubliziert, behalten Subscriber das letzte Snapshot (DSGVO-pflicht eh). -- **Internationalisierung der UI** (heute nur DE): nötig fürs internationale Publikum. - -## 15. Aktueller Stand 2026-05-07 - -| Phase | Status | Was läuft | Was fehlt | -|-------|--------|-----------|-----------| -| α — Skelett | ✅ | cards-server lebt auf 3072, Schema gepushed, JWT-Auth, Container in `docker-compose.macmini.yml`, Tunnel-Route `cardecky-api.mana.how` | — | -| β — Author-Workflow | ✅ | Profil-Claim, Publish, Lizenz, Preis, AI-Mod-Verdict | Tag-Picker im Publish, Author-Dashboard-Stats | -| γ — Discovery | ✅ | `/explore`, Stars, Follows, Author-Profile, Trending | tsvector-FTS, Tag-Tree, Activity-Feed | -| δ — Subscribe + Smart-Merge | ✅ | Pull, Smart-Merge mit FSRS-State-Erhalt, Diff-View | WebSocket-Push, Update-Mails | -| ε — PRs + Discussions | ✅ | PR-Erstellen / List / Merge / Reject / Close, Discussions auf `/learn` + `/d/`, Notify-Mails | Mention-System, PR-Rebase, Multi-Card-Diff-View, Discussion-Threading | -| ζ — Paid Decks | 🟡 ζ.1 | Buy-Flow, Author-Payout, Buyer-Dashboard | Refund, Reconciler, CSV-Export | -| η — Moderation | 🟡 η.1 | Reports, Admin-Inbox, Takedown, Ban-Cascade, Verify-API | Community-Verified-Cron, Public-Changelog, Verified-Mana-Mod-Permissions, Rate-Limit | -| θ — Deep AI | ⏳ | — | Auto-Tags, Auto-Summary, TTS, Embeddings, Personalized-Discovery | -| ι — Optimierung | ⏳ | — | Search-Service, CDN, Rate-Limiting, Materialized Views | -| λ / μ / ν / ξ | ⏳ | — | später (Co-Learn, Mobile, Author-Tools, Lern-Battles) | - -**Live-Domains**: `cardecky.mana.how` (Web) · `cardecky-api.mana.how` (API). - -**Nächste sinnvolle Schritte (Empfehlung)**: - -1. **ζ.2 Reconciler + minimaler Admin-Refund** — schließt das größte operative Loch im Paid-Flow. -2. **η.2 Community-Verified-Cron** — Plan-Vision der „doppelten Verifizierung" ist sonst nur halb umgesetzt; Cron ist klein. -3. **Update-Mail in δ.4** — Subscriber bekommen sonst nichts mit, wenn Author published. Dann ist die Notify-Story rund (PR-Open + PR-Merged + PR-Rejected + Verkauf + Takedown + Update). -4. **Phase θ starten** — Auto-Tags + Auto-Summary beim Publish via mana-llm: kostet wenig Code, viel Discovery-Hebel. - ---- - -*Plan erstellt: 2026-05-07. Owner: @till. Letzter Stand-Update: 2026-05-07 nach η.1.* diff --git a/apps/cards/package.json b/apps/cards/package.json deleted file mode 100644 index b5f750efe..000000000 --- a/apps/cards/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "cards", - "version": "0.1.0", - "private": true, - "description": "Cardecky — Spaced-Repetition flashcards on cardecky.mana.how (Marketing-Landing: cardecky.com). Standalone Phase-1 frontend; data shared with the mana cards module via mana-sync.", - "scripts": { - "dev": "pnpm run --filter=@cards/* --parallel dev" - } -} diff --git a/apps/docs/astro.config.mjs b/apps/docs/astro.config.mjs index d8d4ab5d6..0f249b13e 100644 --- a/apps/docs/astro.config.mjs +++ b/apps/docs/astro.config.mjs @@ -16,10 +16,10 @@ export default defineConfig({ replacesTitle: false, }, social: { - github: 'https://github.com/mana/mana-monorepo', + github: 'https://github.com/Memo-2023/managarten', }, editLink: { - baseUrl: 'https://github.com/mana/mana-monorepo/edit/main/apps/docs/', + baseUrl: 'https://github.com/Memo-2023/managarten/edit/main/apps/docs/', }, customCss: ['./src/styles/custom.css'], sidebar: [ diff --git a/apps/docs/src/content/docs/architecture/security.mdx b/apps/docs/src/content/docs/architecture/security.mdx index 823bc3a4e..a6432a0b7 100644 --- a/apps/docs/src/content/docs/architecture/security.mdx +++ b/apps/docs/src/content/docs/architecture/security.mdx @@ -16,7 +16,7 @@ Mana encrypts user-typed content with **AES-GCM-256** before it touches IndexedD ## What's encrypted -**27 tables** ship with at-rest encryption enabled. The full list is in [`DATA_LAYER_AUDIT.md`](https://github.com/mana-how/mana-monorepo/blob/main/apps/mana/apps/web/src/lib/data/DATA_LAYER_AUDIT.md), but the highlights: +**27 tables** ship with at-rest encryption enabled. The full list is in [`DATA_LAYER_AUDIT.md`](https://github.com/mana-how/managarten/blob/main/apps/mana/apps/web/src/lib/data/DATA_LAYER_AUDIT.md), but the highlights: | Module | Fields | |--------|--------| @@ -238,7 +238,7 @@ the full standard / ZK guarantees. ## Implementation references -For the architectural deep dive, code locations, and the complete rollout history (Phases 1–9 + the backlog sweep), see [`DATA_LAYER_AUDIT.md`](https://github.com/mana-how/mana-monorepo/blob/main/apps/mana/apps/web/src/lib/data/DATA_LAYER_AUDIT.md). +For the architectural deep dive, code locations, and the complete rollout history (Phases 1–9 + the backlog sweep), see [`DATA_LAYER_AUDIT.md`](https://github.com/mana-how/managarten/blob/main/apps/mana/apps/web/src/lib/data/DATA_LAYER_AUDIT.md). Key files: diff --git a/apps/docs/src/content/docs/deployment/mac-mini-server.mdx b/apps/docs/src/content/docs/deployment/mac-mini-server.mdx index 9c4e4fbbb..aa16648ac 100644 --- a/apps/docs/src/content/docs/deployment/mac-mini-server.mdx +++ b/apps/docs/src/content/docs/deployment/mac-mini-server.mdx @@ -37,7 +37,7 @@ Requires `cloudflared` installed: `brew install cloudflare/cloudflare/cloudflare ## Directory Structure ``` -~/projects/mana-monorepo/ +~/projects/managarten/ ├── docker-compose.macmini.yml # Production compose file ├── .env.production # Production environment ├── scripts/mac-mini/ # Server management scripts @@ -53,7 +53,7 @@ Requires `cloudflared` installed: `brew install cloudflare/cloudflare/cloudflare ```bash ssh mana-server -cd ~/projects/mana-monorepo +cd ~/projects/managarten ./scripts/mac-mini/status.sh ``` @@ -71,7 +71,7 @@ chat-backend running (healthy) ```bash ssh mana-server -cd ~/projects/mana-monorepo +cd ~/projects/managarten ./scripts/mac-mini/deploy.sh ``` diff --git a/apps/docs/src/content/docs/deployment/overview.mdx b/apps/docs/src/content/docs/deployment/overview.mdx index be773e25b..6c8ca5ce0 100644 --- a/apps/docs/src/content/docs/deployment/overview.mdx +++ b/apps/docs/src/content/docs/deployment/overview.mdx @@ -92,7 +92,7 @@ pnpm deploy:docs ssh mana-server # Pull latest changes -cd ~/projects/mana-monorepo +cd ~/projects/managarten git pull # Restart services @@ -135,7 +135,7 @@ npx wrangler pages deployment tail --project-name=chat-landing ```bash ssh mana-server -cd ~/projects/mana-monorepo +cd ~/projects/managarten # Revert to previous commit git checkout HEAD~1 diff --git a/apps/docs/src/content/docs/deployment/self-hosting.mdx b/apps/docs/src/content/docs/deployment/self-hosting.mdx index 43bfe7f3b..daa4866c2 100644 --- a/apps/docs/src/content/docs/deployment/self-hosting.mdx +++ b/apps/docs/src/content/docs/deployment/self-hosting.mdx @@ -23,8 +23,8 @@ Run your own Mana instance using Docker Compose. 1. **Clone the repository** ```bash - git clone https://github.com/mana/mana-monorepo.git - cd mana-monorepo + git clone https://github.com/mana/managarten.git + cd managarten ``` 2. **Create environment file** diff --git a/apps/docs/src/content/docs/getting-started/quick-start.mdx b/apps/docs/src/content/docs/getting-started/quick-start.mdx index d42080eb8..f4dcf2877 100644 --- a/apps/docs/src/content/docs/getting-started/quick-start.mdx +++ b/apps/docs/src/content/docs/getting-started/quick-start.mdx @@ -24,8 +24,8 @@ Before you begin, ensure you have: 1. **Clone the repository** ```bash - git clone https://github.com/mana/mana-monorepo.git - cd mana-monorepo + git clone https://github.com/mana/managarten.git + cd managarten ``` 2. **Install dependencies** diff --git a/apps/docs/src/content/docs/index.mdx b/apps/docs/src/content/docs/index.mdx index c974e8202..e15d97356 100644 --- a/apps/docs/src/content/docs/index.mdx +++ b/apps/docs/src/content/docs/index.mdx @@ -12,7 +12,7 @@ hero: icon: right-arrow variant: primary - text: View on GitHub - link: https://github.com/mana/mana-monorepo + link: https://github.com/mana/managarten icon: external --- @@ -21,8 +21,8 @@ import { Card, CardGrid } from '@astrojs/starlight/components'; ## Quick Start ```bash -git clone https://github.com/mana/mana-monorepo.git -cd mana-monorepo +git clone https://github.com/mana/managarten.git +cd managarten pnpm install pnpm docker:up pnpm dev:chat:full diff --git a/apps/mana/apps/landing/src/content/blueprints/003-payment-systems-stripe-vs-direct-debit.md b/apps/mana/apps/landing/src/content/blueprints/003-payment-systems-stripe-vs-direct-debit.md index 14ced9518..b1145ae1f 100644 --- a/apps/mana/apps/landing/src/content/blueprints/003-payment-systems-stripe-vs-direct-debit.md +++ b/apps/mana/apps/landing/src/content/blueprints/003-payment-systems-stripe-vs-direct-debit.md @@ -484,7 +484,7 @@ Nach Analyse aller Optionen ist die Empfehlung: ```bash # Auf dem Server ssh mana-server - cd ~/projects/mana-monorepo + cd ~/projects/managarten git pull ./scripts/mac-mini/deploy.sh ``` diff --git a/apps/mana/apps/landing/src/content/devlog/2026-01-23-production-launch.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-23-production-launch.md similarity index 96% rename from apps/mana/apps/landing/src/content/devlog/2026-01-23-production-launch.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-23-production-launch.md index 385284c9d..e34a30bec 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-01-23-production-launch.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-23-production-launch.md @@ -31,6 +31,8 @@ workingHours: end: '2026-01-24T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Heute war ein sehr produktiver Tag mit Fokus auf die **Produktivstellung der ManaCore Apps auf dem Mac Mini Server**. Die wichtigsten Errungenschaften: - **7 Apps live** auf https://mana.how (Auth, Dashboard, Chat, Todo, Calendar, Clock, Contacts) diff --git a/apps/mana/apps/landing/src/content/devlog/2026-01-24-guest-mode-improvements.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-24-guest-mode-improvements.md similarity index 91% rename from apps/mana/apps/landing/src/content/devlog/2026-01-24-guest-mode-improvements.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-24-guest-mode-improvements.md index 448ff007f..082841be7 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-01-24-guest-mode-improvements.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-24-guest-mode-improvements.md @@ -21,6 +21,8 @@ workingHours: end: '2026-01-25T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Fokussierter Tag mit **4 Commits** zur Verbesserung des Guest-Mode-Erlebnisses: - **Clock Guest Mode** - Alarms/Timers ohne Auth-Redirect ladbar diff --git a/apps/mana/apps/landing/src/content/devlog/2026-01-25-food-monitoring-infrastructure.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-25-food-monitoring-infrastructure.md similarity index 96% rename from apps/mana/apps/landing/src/content/devlog/2026-01-25-food-monitoring-infrastructure.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-25-food-monitoring-infrastructure.md index 91a661e29..475d1584f 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-01-25-food-monitoring-infrastructure.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-25-food-monitoring-infrastructure.md @@ -33,6 +33,8 @@ workingHours: end: '2026-01-26T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Massiver Tag mit **38 Commits** und Fokus auf neue App, Monitoring und Infrastructure: - **Food** - AI-powered Nutrition Tracking App mit Gemini diff --git a/apps/mana/apps/landing/src/content/devlog/2026-01-26-monitoring-auth-ai.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-26-monitoring-auth-ai.md similarity index 95% rename from apps/mana/apps/landing/src/content/devlog/2026-01-26-monitoring-auth-ai.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-26-monitoring-auth-ai.md index deced6f13..842cb1285 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-01-26-monitoring-auth-ai.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-26-monitoring-auth-ai.md @@ -33,6 +33,9 @@ workingHours: end: '2026-01-27T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + + Produktiver Tag mit Fokus auf **Monitoring-Infrastruktur**, **Email-Authentifizierung** und **lokale AI-Services**. Die wichtigsten Errungenschaften: - **Prometheus Metrics** für alle NestJS Backends diff --git a/apps/mana/apps/landing/src/content/devlog/2026-01-27-matrix-infrastructure-self-hosting.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-27-matrix-infrastructure-self-hosting.md similarity index 96% rename from apps/mana/apps/landing/src/content/devlog/2026-01-27-matrix-infrastructure-self-hosting.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-27-matrix-infrastructure-self-hosting.md index 795781a3f..19ede2fe9 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-01-27-matrix-infrastructure-self-hosting.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-27-matrix-infrastructure-self-hosting.md @@ -34,6 +34,8 @@ workingHours: end: '2026-01-28T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Strategischer Tag mit **11 Commits** und Fokus auf GDPR-Compliance und Developer Experience: - **Matrix Self-Hosting** - Eigene Synapse-Instanz für GDPR diff --git a/apps/mana/apps/landing/src/content/devlog/2026-01-28-massive-feature-sprint.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-28-massive-feature-sprint.md similarity index 98% rename from apps/mana/apps/landing/src/content/devlog/2026-01-28-massive-feature-sprint.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-28-massive-feature-sprint.md index a439a0de3..5133cc7de 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-01-28-massive-feature-sprint.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-28-massive-feature-sprint.md @@ -34,6 +34,8 @@ workingHours: end: '2026-01-29T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Außergewöhnlich produktiver Tag (und Nacht!) mit **74 Commits** und mehreren großen neuen Features. Die wichtigsten Errungenschaften: - **SkillTree App** - Gamified Skill Tracking mit XP-System (MVP complete) diff --git a/apps/mana/apps/landing/src/content/devlog/2026-01-29-microservices-matrix-consolidation.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-29-microservices-matrix-consolidation.md similarity index 98% rename from apps/mana/apps/landing/src/content/devlog/2026-01-29-microservices-matrix-consolidation.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-29-microservices-matrix-consolidation.md index dfcb20c54..f8e8b39cf 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-01-29-microservices-matrix-consolidation.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-29-microservices-matrix-consolidation.md @@ -37,6 +37,8 @@ workingHours: end: '2026-01-30T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Außergewöhnlich produktiver Tag mit **55 Commits** - aufgeteilt in drei große Bereiche: - **3 neue Microservices** - mana-llm (LLM Abstraction), mana-crawler (Web Crawler), mana-notify (Notifications) diff --git a/apps/mana/apps/landing/src/content/devlog/2026-01-30-matrix-bots-llm-playground.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-30-matrix-bots-llm-playground.md similarity index 95% rename from apps/mana/apps/landing/src/content/devlog/2026-01-30-matrix-bots-llm-playground.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-30-matrix-bots-llm-playground.md index c8d08e9d5..1bafbcf2b 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-01-30-matrix-bots-llm-playground.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-30-matrix-bots-llm-playground.md @@ -31,6 +31,8 @@ workingHours: end: '2026-01-31T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Produktiver Tag mit **41 Commits** und Fokus auf Matrix Bot Expansion und Developer Experience: - **8 neue Matrix Bots** - Spezialisierte Bots für Planta, Cards, Contacts, Picture, Chat, SkillTree, Presi, Questions diff --git a/apps/mana/apps/landing/src/content/devlog/2026-01-31-matrix-consolidation-voice-support.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-31-matrix-consolidation-voice-support.md similarity index 96% rename from apps/mana/apps/landing/src/content/devlog/2026-01-31-matrix-consolidation-voice-support.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-31-matrix-consolidation-voice-support.md index 3390bc741..5f76cad83 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-01-31-matrix-consolidation-voice-support.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-01-31-matrix-consolidation-voice-support.md @@ -33,6 +33,8 @@ workingHours: end: '2026-02-01T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Intensiver Tag (und Nacht!) mit **52 Commits** - der Fokus lag auf der Konsolidierung der Matrix Bot Infrastruktur: - **@manacore/matrix-bot-common** - Neues Shared Package für alle 19 Matrix Bots diff --git a/apps/mana/apps/landing/src/content/devlog/2026-02-01-ssd-migration-i18n-cicd.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-01-ssd-migration-i18n-cicd.md similarity index 95% rename from apps/mana/apps/landing/src/content/devlog/2026-02-01-ssd-migration-i18n-cicd.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-01-ssd-migration-i18n-cicd.md index 15181193c..a20c7a3b7 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-02-01-ssd-migration-i18n-cicd.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-01-ssd-migration-i18n-cicd.md @@ -33,6 +33,9 @@ workingHours: end: '2026-02-02T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + + Produktiver Tag mit **42 Commits** und Fokus auf Infrastructure und Production Readiness: - **SSD Migration** - PostgreSQL und MinIO auf externe SSD verschoben diff --git a/apps/mana/apps/landing/src/content/devlog/2026-02-02-cross-domain-sso-mana-media.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-02-cross-domain-sso-mana-media.md similarity index 97% rename from apps/mana/apps/landing/src/content/devlog/2026-02-02-cross-domain-sso-mana-media.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-02-cross-domain-sso-mana-media.md index 44737c9b2..231cb6ab7 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-02-02-cross-domain-sso-mana-media.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-02-cross-domain-sso-mana-media.md @@ -33,6 +33,8 @@ workingHours: end: '2026-02-03T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Produktiver Tag mit **40 Commits** und Fokus auf nahtlose Authentifizierung über alle Apps: - **Cross-Domain SSO** - Single Sign-On für alle .mana.how Subdomains diff --git a/apps/mana/apps/landing/src/content/devlog/2026-02-11-photos-app-stt-tts-admin.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-11-photos-app-stt-tts-admin.md similarity index 97% rename from apps/mana/apps/landing/src/content/devlog/2026-02-11-photos-app-stt-tts-admin.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-11-photos-app-stt-tts-admin.md index c990b3d59..a662a130e 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-02-11-photos-app-stt-tts-admin.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-11-photos-app-stt-tts-admin.md @@ -34,6 +34,8 @@ workingHours: end: '2026-02-12T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Nach einer Woche Pause: **28 Commits** mit Fokus auf neue Apps und API-Infrastruktur: - **Photos App** - Neue App mit mana-media EXIF-Integration diff --git a/apps/mana/apps/landing/src/content/devlog/2026-02-12-gdpr-matrix-mac-mini.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-12-gdpr-matrix-mac-mini.md similarity index 97% rename from apps/mana/apps/landing/src/content/devlog/2026-02-12-gdpr-matrix-mac-mini.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-12-gdpr-matrix-mac-mini.md index 39df18810..3d00bab61 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-02-12-gdpr-matrix-mac-mini.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-12-gdpr-matrix-mac-mini.md @@ -33,6 +33,8 @@ workingHours: end: '2026-02-13T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + **22 Commits** mit Fokus auf DSGVO-Compliance, Mobile UX und Server-Stabilität: - **GDPR Self-Service** - Neue Endpoints für User Data Export diff --git a/apps/mana/apps/landing/src/content/devlog/2026-02-13-gift-codes-stripe-quotes.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-13-gift-codes-stripe-quotes.md similarity index 98% rename from apps/mana/apps/landing/src/content/devlog/2026-02-13-gift-codes-stripe-quotes.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-13-gift-codes-stripe-quotes.md index 6e1fe36d1..985f71cfb 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-02-13-gift-codes-stripe-quotes.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-13-gift-codes-stripe-quotes.md @@ -34,6 +34,8 @@ workingHours: end: '2026-02-14T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Ein massiver Tag mit **55 Commits** und mehreren Major Features: - **Gift Codes** - Credit-Gutscheine mit Code-Einlösung diff --git a/apps/mana/apps/landing/src/content/devlog/2026-02-14-matrix-stt-bot-gift-codes-onboarding.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-14-matrix-stt-bot-gift-codes-onboarding.md similarity index 97% rename from apps/mana/apps/landing/src/content/devlog/2026-02-14-matrix-stt-bot-gift-codes-onboarding.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-14-matrix-stt-bot-gift-codes-onboarding.md index 489e19e79..18d71c4ee 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-02-14-matrix-stt-bot-gift-codes-onboarding.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-14-matrix-stt-bot-gift-codes-onboarding.md @@ -35,6 +35,8 @@ workingHours: end: '2026-02-15T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Produktiver Valentinstag mit **56 Commits** und vielen neuen Features: - **Matrix STT Bot** - Speech-to-Text direkt in Matrix diff --git a/apps/mana/apps/landing/src/content/devlog/2026-02-15-onboarding-polish-calendar-views.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-15-onboarding-polish-calendar-views.md similarity index 94% rename from apps/mana/apps/landing/src/content/devlog/2026-02-15-onboarding-polish-calendar-views.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-15-onboarding-polish-calendar-views.md index 7d18fabd1..434b2c7ec 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-02-15-onboarding-polish-calendar-views.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-15-onboarding-polish-calendar-views.md @@ -21,6 +21,8 @@ workingHours: end: '2026-02-16T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Fokussierter Tag mit **7 Commits** für UI-Polish und kleinere Features: - **Onboarding Modal** - Kompaktes Design statt Fullscreen diff --git a/apps/mana/apps/landing/src/content/devlog/2026-02-16-lightwrite-pwa-stripe-sepa.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-16-lightwrite-pwa-stripe-sepa.md similarity index 95% rename from apps/mana/apps/landing/src/content/devlog/2026-02-16-lightwrite-pwa-stripe-sepa.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-16-lightwrite-pwa-stripe-sepa.md index 9b43ee471..da2d80b75 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-02-16-lightwrite-pwa-stripe-sepa.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-16-lightwrite-pwa-stripe-sepa.md @@ -32,6 +32,8 @@ workingHours: end: '2026-02-17T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Massiver Tag mit **35 Commits** – neuer App-Launch, Monetarisierung und PWA-Rollout: - **LightWrite** - Beat/Lyrics Editor als Full-Stack App gelauncht diff --git a/apps/mana/apps/landing/src/content/devlog/2026-02-17-spiral-db-planta-bot-mana-values.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-17-spiral-db-planta-bot-mana-values.md similarity index 96% rename from apps/mana/apps/landing/src/content/devlog/2026-02-17-spiral-db-planta-bot-mana-values.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-17-spiral-db-planta-bot-mana-values.md index c92828f0c..aed2a5727 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-02-17-spiral-db-planta-bot-mana-values.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-02-17-spiral-db-planta-bot-mana-values.md @@ -21,6 +21,9 @@ workingHours: end: '2026-02-18T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + + Produktiver Tag mit **26 Commits** – neue Packages, Bot-Features und Dokumentation: - **spiral-db** - Pixel-basierte Spiral-Datenbank-Visualisierung diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-06-matrix-mobile-manalink.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-06-matrix-mobile-manalink.md similarity index 94% rename from apps/mana/apps/landing/src/content/devlog/2026-03-06-matrix-mobile-manalink.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-06-matrix-mobile-manalink.md index 4e8aa3798..fd95cc3d3 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-06-matrix-mobile-manalink.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-06-matrix-mobile-manalink.md @@ -21,6 +21,8 @@ workingHours: end: '2026-03-07T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Fokussierter Tag mit **7 Commits** für den neuen Matrix Mobile Client: - **Manalink** - Expo React Native App für Matrix Chat diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-07-manalink-sdk55-fixes.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-07-manalink-sdk55-fixes.md similarity index 94% rename from apps/mana/apps/landing/src/content/devlog/2026-03-07-manalink-sdk55-fixes.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-07-manalink-sdk55-fixes.md index a546e4f44..1b17da77c 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-07-manalink-sdk55-fixes.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-07-manalink-sdk55-fixes.md @@ -21,6 +21,8 @@ workingHours: end: '2026-03-08T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Bugfix-Tag mit **5 Commits** – Expo SDK Upgrade und kritische Fixes: - **SDK 55** - Manalink auf Expo SDK 55 aktualisiert diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-11-cd-pipeline-mac-mini-runner.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-11-cd-pipeline-mac-mini-runner.md similarity index 95% rename from apps/mana/apps/landing/src/content/devlog/2026-03-11-cd-pipeline-mac-mini-runner.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-11-cd-pipeline-mac-mini-runner.md index e03b7eca2..3ad8f8b7b 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-11-cd-pipeline-mac-mini-runner.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-11-cd-pipeline-mac-mini-runner.md @@ -21,6 +21,8 @@ workingHours: end: '2026-03-12T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Infrastruktur-Tag mit **6 Commits** für die Deployment-Pipeline: - **CD Pipeline** - GitHub Actions mit self-hosted Runner diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-12-manalink-eas-build-fixes.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-12-manalink-eas-build-fixes.md similarity index 92% rename from apps/mana/apps/landing/src/content/devlog/2026-03-12-manalink-eas-build-fixes.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-12-manalink-eas-build-fixes.md index 40a5d92b8..43122a5ce 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-12-manalink-eas-build-fixes.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-12-manalink-eas-build-fixes.md @@ -21,6 +21,8 @@ workingHours: end: '2026-03-13T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Fokussierter Tag mit **5 Commits** für EAS Build Stabilität und Expo SDK 55 Kompatibilität: - **EAS Build Pre-Install Hook** - pnpm hoisted Mode für Monorepo Builds diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-15-traces-app-calendar-hardening.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-15-traces-app-calendar-hardening.md similarity index 94% rename from apps/mana/apps/landing/src/content/devlog/2026-03-15-traces-app-calendar-hardening.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-15-traces-app-calendar-hardening.md index df6da2c64..3cf730203 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-15-traces-app-calendar-hardening.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-15-traces-app-calendar-hardening.md @@ -21,6 +21,8 @@ workingHours: end: '2026-03-16T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Intensiver Tag mit **8 Commits** für eine neue App-Integration und Production Hardening: - **Traces App** - AI City Guides mit NestJS Backend und Expo Mobile diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-17-mukke-mobile-e2e-tests-pre-commit.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-17-mukke-mobile-e2e-tests-pre-commit.md similarity index 95% rename from apps/mana/apps/landing/src/content/devlog/2026-03-17-mukke-mobile-e2e-tests-pre-commit.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-17-mukke-mobile-e2e-tests-pre-commit.md index e78c1d684..cc4126ef2 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-17-mukke-mobile-e2e-tests-pre-commit.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-17-mukke-mobile-e2e-tests-pre-commit.md @@ -21,6 +21,8 @@ workingHours: end: '2026-03-18T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Produktiver Tag mit **9 Commits** über mehrere Apps hinweg: - **Mukke** - Offline-first iOS Music Player mit Expo und SQLite diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-18-test-coverage-contacts-todo.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-18-test-coverage-contacts-todo.md similarity index 94% rename from apps/mana/apps/landing/src/content/devlog/2026-03-18-test-coverage-contacts-todo.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-18-test-coverage-contacts-todo.md index 36b64b114..71cf45693 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-18-test-coverage-contacts-todo.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-18-test-coverage-contacts-todo.md @@ -21,6 +21,8 @@ workingHours: end: '2026-03-19T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Fokussierter Tag mit **5 Commits** für Test Coverage und Code Cleanup: - **Contacts Web** - 62 Unit Tests für Stores, Utils und API Client diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-19-security-hardening-error-tracking.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-19-security-hardening-error-tracking.md similarity index 97% rename from apps/mana/apps/landing/src/content/devlog/2026-03-19-security-hardening-error-tracking.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-19-security-hardening-error-tracking.md index 2fe804439..35c3198d7 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-19-security-hardening-error-tracking.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-19-security-hardening-error-tracking.md @@ -33,6 +33,8 @@ workingHours: end: '2026-03-20T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Massiver Infrastruktur- und Security-Tag mit **74 Commits** über das gesamte Monorepo: - **Cross-App SSO Fix** - Fehlende trusted origins repariert, 47 SSO Contract Tests diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-20-cross-app-sso-infrastructure-fixes.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-20-cross-app-sso-infrastructure-fixes.md similarity index 95% rename from apps/mana/apps/landing/src/content/devlog/2026-03-20-cross-app-sso-infrastructure-fixes.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-20-cross-app-sso-infrastructure-fixes.md index 9ace8912e..dcacaf3ef 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-20-cross-app-sso-infrastructure-fixes.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-20-cross-app-sso-infrastructure-fixes.md @@ -34,6 +34,8 @@ workingHours: end: '2026-03-20T21:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Intensiver Infrastruktur- und Bugfix-Tag mit **48 Commits**: Cross-App SSO endlich funktionsfähig, Calendar-Web von stale GHCR Image auf Local Build migriert, und eine Kette von Routing/CORS/Cookie-Problemen systematisch aufgelöst. - **SSO Cookie-Fix** - `SameSite=None` + Session-Cookie bei Login setzen diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-21-whopixels-refactor-storage-picture.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-21-whopixels-refactor-storage-picture.md similarity index 96% rename from apps/mana/apps/landing/src/content/devlog/2026-03-21-whopixels-refactor-storage-picture.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-21-whopixels-refactor-storage-picture.md index 310ed3142..5b4d2f79d 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-21-whopixels-refactor-storage-picture.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-21-whopixels-refactor-storage-picture.md @@ -33,6 +33,8 @@ workingHours: end: '2026-03-21T18:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Feature-reicher Tag über drei Projekte hinweg mit **7 Commits** und knapp **5.000 hinzugefügten Zeilen**: WhoPixels-Spiel komplett refactored (21 Verbesserungen), Storage mit Drag-Animationen und Integration Tests ausgebaut, Picture-Backend gehärtet. - ✅ **WhoPixels** — Monolithische RPGScene (1210 Zeilen) in 8 Module aufgeteilt diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-22-analytics-pwa-visualizer-error-tracking.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-22-analytics-pwa-visualizer-error-tracking.md similarity index 96% rename from apps/mana/apps/landing/src/content/devlog/2026-03-22-analytics-pwa-visualizer-error-tracking.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-22-analytics-pwa-visualizer-error-tracking.md index 42a4366d7..587e94346 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-22-analytics-pwa-visualizer-error-tracking.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-22-analytics-pwa-visualizer-error-tracking.md @@ -37,6 +37,8 @@ workingHours: end: '2026-03-23T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Produktiver Tag mit **26 Commits** über das gesamte Monorepo: - **Analytics** - Umami-Tracking zentralisiert + Event-Tracking für alle Apps und Landing Pages diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-23-abend-context-menus-llm-onboarding.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-23-abend-context-menus-llm-onboarding.md similarity index 96% rename from apps/mana/apps/landing/src/content/devlog/2026-03-23-abend-context-menus-llm-onboarding.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-23-abend-context-menus-llm-onboarding.md index cbd0a687b..666586816 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-23-abend-context-menus-llm-onboarding.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-23-abend-context-menus-llm-onboarding.md @@ -34,6 +34,8 @@ workingHours: end: '2026-03-23T23:30' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Intensive Abend-Session mit **27 Commits** über **180 Dateien** und netto **+7.234 Zeilen**: - **Context Menus Everywhere** - Shared `ContextMenu`-Component aus `@manacore/shared-ui` in 12 Apps integriert diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-23-vormittag-manalink-prod-ready.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-23-vormittag-manalink-prod-ready.md similarity index 97% rename from apps/mana/apps/landing/src/content/devlog/2026-03-23-vormittag-manalink-prod-ready.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-23-vormittag-manalink-prod-ready.md index 7becb35b2..e6e91ac76 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-23-vormittag-manalink-prod-ready.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-23-vormittag-manalink-prod-ready.md @@ -38,6 +38,8 @@ workingHours: end: '2026-03-23T13:30' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Extrem produktiver Vormittag mit **61 Commits** über **695 Dateien** und netto **+17.282 Zeilen**: - **Manalink Live** - Matrix-Chat-Client auf Production gebracht und auf link.mana.how deployed diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-24-abend-pwa-todo-infra.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-24-abend-pwa-todo-infra.md similarity index 96% rename from apps/mana/apps/landing/src/content/devlog/2026-03-24-abend-pwa-todo-infra.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-24-abend-pwa-todo-infra.md index bebab7932..26c265df6 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-24-abend-pwa-todo-infra.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-24-abend-pwa-todo-infra.md @@ -35,6 +35,8 @@ workingHours: end: '2026-03-24T23:59' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Intensive Abend-Session mit **41 Commits** über **302 Dateien** und netto **+14.112 Zeilen**: - **PWA für alle Apps** — Icons, Meta-Tags und Offline-Prerender für 20 Web-Apps diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-24-vormittag-credits-dashboard-infra.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-24-vormittag-credits-dashboard-infra.md similarity index 96% rename from apps/mana/apps/landing/src/content/devlog/2026-03-24-vormittag-credits-dashboard-infra.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-24-vormittag-credits-dashboard-infra.md index 20bf13b24..c62c1f9c3 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-24-vormittag-credits-dashboard-infra.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-24-vormittag-credits-dashboard-infra.md @@ -36,6 +36,9 @@ workingHours: end: '2026-03-24T12:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + + Intensive Vormittags-Session mit **48 Commits** über **248 Dateien** und netto **+10.622 Zeilen**: - **Credits-Kostenübersicht** — Neuer "Kosten"-Tab zeigt alle 40+ Credit-Operationen nach App gruppiert diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-25-storage-auth-tags-analytics.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-25-storage-auth-tags-analytics.md similarity index 93% rename from apps/mana/apps/landing/src/content/devlog/2026-03-25-storage-auth-tags-analytics.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-25-storage-auth-tags-analytics.md index 7ef0d0023..9323208e2 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-25-storage-auth-tags-analytics.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-25-storage-auth-tags-analytics.md @@ -33,6 +33,9 @@ workingHours: end: '2026-03-25T21:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + + Breiter Arbeitstag mit **44 Commits** über viele Bereiche — Analytics, Help, CityCorners, ManaLink, Infra und Dashboard: - **Analytics überall** — Custom Event Tracking in 8+ Apps, neuer ManaScore-Metrik diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-26-local-first-architektur-planung.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-26-local-first-architektur-planung.md similarity index 96% rename from apps/mana/apps/landing/src/content/devlog/2026-03-26-local-first-architektur-planung.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-26-local-first-architektur-planung.md index 186911688..16e098333 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-26-local-first-architektur-planung.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-26-local-first-architektur-planung.md @@ -35,6 +35,9 @@ workingHours: end: '2026-03-26T22:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + + Massiver Tag mit **61 Commits** über **329 Dateien** und netto **+16.715 Zeilen** — Storage-Sprint, Auth-Features, Tag-System, Quotes-Polish, Infra und Architekturplanung: - **Storage-App komplett** — Von Score 72 auf 87: Previews, Audio-Player, Skeleton Screens, Sharing, Tagging, i18n, E2E-Tests diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-27-matrix-bot-go-consolidation.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-27-matrix-bot-go-consolidation.md similarity index 97% rename from apps/mana/apps/landing/src/content/devlog/2026-03-27-matrix-bot-go-consolidation.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-27-matrix-bot-go-consolidation.md index 780580122..c3ebb4ab2 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-27-matrix-bot-go-consolidation.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-27-matrix-bot-go-consolidation.md @@ -31,6 +31,8 @@ workingHours: end: '2026-03-27T17:30' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + Einer der größten Architektur-Umbauten im ManaCore-Monorepo: **21 separate NestJS Matrix-Bot-Prozesse** komplett ersetzt durch **ein einziges Go-Binary mit Plugin-Architektur**. Dazu Legacy-Code aufgeräumt, CI/CD migriert und Docker-Compose aktualisiert. --- diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-28-gpu-offload-colima-migration.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-28-gpu-offload-colima-migration.md similarity index 95% rename from apps/mana/apps/landing/src/content/devlog/2026-03-28-gpu-offload-colima-migration.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-28-gpu-offload-colima-migration.md index 3cbbc611e..4f149a6f1 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-28-gpu-offload-colima-migration.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-28-gpu-offload-colima-migration.md @@ -28,6 +28,9 @@ contributors: handle: 'Till-JS' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + + ## Das Problem: 16 GB reichen nicht Unser Mac Mini M4 hostet das komplette ManaCore-Oekosystem: 33 Docker-Container, PostgreSQL, Redis, MinIO, Forgejo, Matrix — plus Ollama (LLM), FLUX.2 (Bildgenerierung) und STT/TTS. Das Problem: diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-28-local-first-nestjs-elimination.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-28-local-first-nestjs-elimination.md similarity index 98% rename from apps/mana/apps/landing/src/content/devlog/2026-03-28-local-first-nestjs-elimination.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-28-local-first-nestjs-elimination.md index 6f03c1f20..a1ef0a413 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-28-local-first-nestjs-elimination.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-28-local-first-nestjs-elimination.md @@ -33,6 +33,8 @@ workingHours: end: '2026-03-28T22:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + ## Was passiert ist In zwei intensiven Sessions wurde die gesamte ManaCore-Backend-Architektur grundlegend transformiert. Der Kern: **Daten gehören dem Client, nicht dem Server.** Und: **NestJS ist zu viel Overhead für das, was wir brauchen.** diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-30-todo-ux-keyboard-navigation.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-30-todo-ux-keyboard-navigation.md similarity index 95% rename from apps/mana/apps/landing/src/content/devlog/2026-03-30-todo-ux-keyboard-navigation.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-30-todo-ux-keyboard-navigation.md index 900376dc9..1934e4671 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-30-todo-ux-keyboard-navigation.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-30-todo-ux-keyboard-navigation.md @@ -21,6 +21,8 @@ workingHours: end: '2026-03-30T18:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + ## Highlights - Notizblock-Design mit liniertem Papier-Hintergrund und roter Margin-Linie diff --git a/apps/mana/apps/landing/src/content/devlog/2026-03-31-memoro-statuspage-todo-infra.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-31-memoro-statuspage-todo-infra.md similarity index 97% rename from apps/mana/apps/landing/src/content/devlog/2026-03-31-memoro-statuspage-todo-infra.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-31-memoro-statuspage-todo-infra.md index 0bed08379..ef23f7742 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-03-31-memoro-statuspage-todo-infra.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-03-31-memoro-statuspage-todo-infra.md @@ -35,6 +35,8 @@ workingHours: end: '2026-03-31T21:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + ## Highlights - **Memoro** vollständig im Monorepo: Web-App (local-first, shared-auth-stores), Hono/Bun Server + Audio-Server ersetzen NestJS diff --git a/apps/mana/apps/landing/src/content/devlog/2026-04-01-unified-app-migration-phases-1-7.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-01-unified-app-migration-phases-1-7.md similarity index 96% rename from apps/mana/apps/landing/src/content/devlog/2026-04-01-unified-app-migration-phases-1-7.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-01-unified-app-migration-phases-1-7.md index f3f9b79a2..2ccd315fb 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-04-01-unified-app-migration-phases-1-7.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-01-unified-app-migration-phases-1-7.md @@ -34,6 +34,9 @@ workingHours: end: '2026-04-01T23:05' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + + ## Highlights - **ManaCore Unified App gestartet** — Same-Origin Web-App, die alle 26 Module unter einem Build/Domain bündelt. 7 Phasen in einem Tag. diff --git a/apps/mana/apps/landing/src/content/devlog/2026-04-02-unified-api-server-archive-25-apps.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-02-unified-api-server-archive-25-apps.md similarity index 97% rename from apps/mana/apps/landing/src/content/devlog/2026-04-02-unified-api-server-archive-25-apps.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-02-unified-api-server-archive-25-apps.md index 92789578e..0f44342f3 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-04-02-unified-api-server-archive-25-apps.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-02-unified-api-server-archive-25-apps.md @@ -33,6 +33,8 @@ workingHours: end: '2026-04-02T23:59' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + ## Highlights - **Unified API Server** (`@manacore/api`): 17 Module-Server zusammengeführt, 25 standalone Web-Apps archiviert diff --git a/apps/mana/apps/landing/src/content/devlog/2026-04-03-habits-automations-stalwart-undo.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-03-habits-automations-stalwart-undo.md similarity index 97% rename from apps/mana/apps/landing/src/content/devlog/2026-04-03-habits-automations-stalwart-undo.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-03-habits-automations-stalwart-undo.md index b3e83fe27..5614d1352 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-04-03-habits-automations-stalwart-undo.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-03-habits-automations-stalwart-undo.md @@ -34,6 +34,8 @@ workingHours: end: '2026-04-03T21:39' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + ## Highlights - **Vier neue Module**: habits, automations, notes, finance, places (GPS) diff --git a/apps/mana/apps/landing/src/content/devlog/2026-04-04-mana-media-cas-effect-depth-fix.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-04-mana-media-cas-effect-depth-fix.md similarity index 93% rename from apps/mana/apps/landing/src/content/devlog/2026-04-04-mana-media-cas-effect-depth-fix.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-04-mana-media-cas-effect-depth-fix.md index 60842326a..3ff33b260 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-04-04-mana-media-cas-effect-depth-fix.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-04-mana-media-cas-effect-depth-fix.md @@ -22,6 +22,8 @@ workingHours: end: '2026-04-04T11:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + ## Highlights - **mana-media** wird zentrale Bild-Pipeline: Picture, Contacts, Planta, Storage, Food laden Bilder darüber hoch diff --git a/apps/mana/apps/landing/src/content/devlog/2026-04-05-timeblocks-mukke-music-rename-pwa.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-05-timeblocks-mukke-music-rename-pwa.md similarity index 96% rename from apps/mana/apps/landing/src/content/devlog/2026-04-05-timeblocks-mukke-music-rename-pwa.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-05-timeblocks-mukke-music-rename-pwa.md index 25edb6e5d..5cd9e24c0 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-04-05-timeblocks-mukke-music-rename-pwa.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-05-timeblocks-mukke-music-rename-pwa.md @@ -34,6 +34,8 @@ workingHours: end: '2026-04-05T21:14' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + ## Highlights - **TimeBlocks** als unified time model — Calendar, Habits, Tasks, Focus-Mode teilen sich eine Tabelle diff --git a/apps/mana/apps/landing/src/content/devlog/2026-04-07-encryption-phases-1-9-period-dreams-events.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-07-encryption-phases-1-9-period-dreams-events.md similarity index 97% rename from apps/mana/apps/landing/src/content/devlog/2026-04-07-encryption-phases-1-9-period-dreams-events.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-07-encryption-phases-1-9-period-dreams-events.md index fb71c9d55..7998eddc4 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-04-07-encryption-phases-1-9-period-dreams-events.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-07-encryption-phases-1-9-period-dreams-events.md @@ -34,6 +34,8 @@ workingHours: end: '2026-04-07T23:57' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + ## Highlights - **At-Rest Encryption** in 9 Phasen ausgerollt: AES-GCM-256 für 27 Tabellen diff --git a/apps/mana/apps/landing/src/content/devlog/2026-04-08-voice-quick-add-llm-parsing.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-08-voice-quick-add-llm-parsing.md similarity index 98% rename from apps/mana/apps/landing/src/content/devlog/2026-04-08-voice-quick-add-llm-parsing.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-08-voice-quick-add-llm-parsing.md index a5f7d8205..1105798e4 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-04-08-voice-quick-add-llm-parsing.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-08-voice-quick-add-llm-parsing.md @@ -36,6 +36,9 @@ workingHours: end: '2026-04-08T17:30' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + + ## Highlights - **Geteilte ``** ersetzt zwei kopierte MediaRecorder-Implementierungen in Dreams + Memoro und kommt jetzt in Notes, Todo workbench, Memoro workbench und standalone /memoro zum Einsatz diff --git a/apps/mana/apps/landing/src/content/devlog/2026-04-09-news-body-nutriphi-workbench-sprint.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-09-news-body-nutriphi-workbench-sprint.md similarity index 97% rename from apps/mana/apps/landing/src/content/devlog/2026-04-09-news-body-nutriphi-workbench-sprint.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-09-news-body-nutriphi-workbench-sprint.md index 53a71b8c8..c21c7868f 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-04-09-news-body-nutriphi-workbench-sprint.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-09-news-body-nutriphi-workbench-sprint.md @@ -35,6 +35,8 @@ workingHours: end: '2026-04-09T23:59' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + ## Highlights - **News-Modul** — Backend-Ingester-Service, kuratierte RSS-Feeds, Client-Data-Layer, Workbench-ListView + Dashboard-Widget, "Interested"-Bookmark, Onboarding-Flow diff --git a/apps/mana/apps/landing/src/content/devlog/2026-04-10-journal-sync-billing-geocoding-a11y.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-10-journal-sync-billing-geocoding-a11y.md similarity index 96% rename from apps/mana/apps/landing/src/content/devlog/2026-04-10-journal-sync-billing-geocoding-a11y.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-10-journal-sync-billing-geocoding-a11y.md index 32cb40632..88cf20117 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-04-10-journal-sync-billing-geocoding-a11y.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-10-journal-sync-billing-geocoding-a11y.md @@ -34,6 +34,8 @@ workingHours: end: '2026-04-10T22:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + ## Highlights - **Journal-Modul** — Tagebuch mit Voice-Capture, Mood-Tracking und Encryption diff --git a/apps/mana/apps/landing/src/content/devlog/2026-04-11-settings-geocoding-deploy-monitoring.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-11-settings-geocoding-deploy-monitoring.md similarity index 94% rename from apps/mana/apps/landing/src/content/devlog/2026-04-11-settings-geocoding-deploy-monitoring.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-11-settings-geocoding-deploy-monitoring.md index 76fa984b9..232978922 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-04-11-settings-geocoding-deploy-monitoring.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-11-settings-geocoding-deploy-monitoring.md @@ -33,6 +33,8 @@ workingHours: end: '2026-04-11T19:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + ## Highlights - **Settings-Page Redesign** mit PillNav-Compute-Selector diff --git a/apps/mana/apps/landing/src/content/devlog/2026-04-12-ui-redesign-wallpaper-floating-input-drink.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-12-ui-redesign-wallpaper-floating-input-drink.md similarity index 94% rename from apps/mana/apps/landing/src/content/devlog/2026-04-12-ui-redesign-wallpaper-floating-input-drink.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-12-ui-redesign-wallpaper-floating-input-drink.md index d151b11f6..33f580597 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-04-12-ui-redesign-wallpaper-floating-input-drink.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-12-ui-redesign-wallpaper-floating-input-drink.md @@ -34,6 +34,8 @@ workingHours: end: '2026-04-12T20:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + ## Highlights - **Wallpaper-System** + sticky PageHeader — individualisierbare Hintergrundbilder für die App diff --git a/apps/mana/apps/landing/src/content/devlog/2026-04-13-companion-brain-sleep-stretch-recipes-mail.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-13-companion-brain-sleep-stretch-recipes-mail.md similarity index 96% rename from apps/mana/apps/landing/src/content/devlog/2026-04-13-companion-brain-sleep-stretch-recipes-mail.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-13-companion-brain-sleep-stretch-recipes-mail.md index bb3e0256f..78a948bd4 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-04-13-companion-brain-sleep-stretch-recipes-mail.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-13-companion-brain-sleep-stretch-recipes-mail.md @@ -36,6 +36,8 @@ workingHours: end: '2026-04-13T23:00' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + ## Highlights - **Companion Brain** — komplettes AI-Subsystem in 7 Phasen aufgebaut: Event Bus → Projections → Goals → Tools → Chat → Rituals → Memory diff --git a/apps/mana/apps/landing/src/content/devlog/2026-04-14-ai-workbench-actor-missions-grants.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-14-ai-workbench-actor-missions-grants.md similarity index 97% rename from apps/mana/apps/landing/src/content/devlog/2026-04-14-ai-workbench-actor-missions-grants.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-14-ai-workbench-actor-missions-grants.md index 2e27d2a39..0abc1bd16 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-04-14-ai-workbench-actor-missions-grants.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-14-ai-workbench-actor-missions-grants.md @@ -35,6 +35,8 @@ workingHours: end: '2026-04-14T23:59' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + ## Highlights - **Actor-Attribution** auf allen Records, PendingChanges und DomainEvents — jeder Schreibvorgang ist einem Actor (user/ai/system) zugeordnet diff --git a/apps/mana/apps/landing/src/content/devlog/2026-04-15-multi-agent-workbench-news-research-settings-theme.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-15-multi-agent-workbench-news-research-settings-theme.md similarity index 97% rename from apps/mana/apps/landing/src/content/devlog/2026-04-15-multi-agent-workbench-news-research-settings-theme.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-15-multi-agent-workbench-news-research-settings-theme.md index b7cae424d..e21606317 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-04-15-multi-agent-workbench-news-research-settings-theme.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-15-multi-agent-workbench-news-research-settings-theme.md @@ -36,6 +36,8 @@ workingHours: end: '2026-04-15T23:59' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + ## Highlights - **Multi-Agent Phase 4-7** — Policy pro Agent, Agents UI + Scene-Binding, Workbench Agent-Filter, Tag-basiertes Scoping diff --git a/apps/mana/apps/landing/src/content/devlog/2026-04-16-library-wetter-voice-interview-research-lab.md b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-16-library-wetter-voice-interview-research-lab.md similarity index 96% rename from apps/mana/apps/landing/src/content/devlog/2026-04-16-library-wetter-voice-interview-research-lab.md rename to apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-16-library-wetter-voice-interview-research-lab.md index f4d7d3417..6fa92a823 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-04-16-library-wetter-voice-interview-research-lab.md +++ b/apps/mana/apps/landing/src/content/devlog/_legacy-sessions/2026-04-16-library-wetter-voice-interview-research-lab.md @@ -35,6 +35,8 @@ workingHours: end: '2026-04-16T23:59' --- +> **Legacy-Format.** Dieser Eintrag stammt aus dem Session-basierten Devlog vor der Umstellung auf das Tages-Modell (Cutover 2026-05-09). Bestand bleibt erhalten und unverändert; neue Einträge folgen der Tages-Konvention mit `spieler.md` + `macher.md` pro 06–06-Bucket. Spec: [`mana/docs/DEVLOG.md`](https://github.com/mana-ev/mana/blob/main/docs/DEVLOG.md). + ## Highlights - **Bibliothek-Modul** — Bücher, Filme, Serien, Comics mit Progress-Tracking, CRUD, Grid-View diff --git a/apps/mana/apps/landing/src/pages/devlog/[slug].astro b/apps/mana/apps/landing/src/pages/devlog/[slug].astro index 847e0f355..0306152c9 100644 --- a/apps/mana/apps/landing/src/pages/devlog/[slug].astro +++ b/apps/mana/apps/landing/src/pages/devlog/[slug].astro @@ -12,7 +12,7 @@ import { Icon } from 'astro-icon/components'; export async function getStaticPaths() { const posts = await getCollection('devlog'); return posts.map((post) => ({ - params: { slug: post.slug }, + params: { slug: post.slug.replace(/^_legacy-sessions\//, '') }, props: { post }, })); } diff --git a/apps/mana/apps/landing/src/pages/devlog/activity.astro b/apps/mana/apps/landing/src/pages/devlog/activity.astro index 9a7f25cdb..1350179c6 100644 --- a/apps/mana/apps/landing/src/pages/devlog/activity.astro +++ b/apps/mana/apps/landing/src/pages/devlog/activity.astro @@ -38,7 +38,7 @@ for (const post of sortedPosts) { linesAdded: existing.linesAdded + (post.data.stats?.linesAdded || 0), linesRemoved: existing.linesRemoved + (post.data.stats?.linesRemoved || 0), posts: [...existing.posts, post.data.title], - slug: post.slug, + slug: post.slug.replace(/^_legacy-sessions\//, ''), }); } @@ -602,7 +602,7 @@ const avgCommitsPerDay = totalDays > 0 ? Math.round(totalCommits / totalDays) : .reverse() .map((post, index) => (
diff --git a/apps/mana/apps/landing/src/pages/devlog/index.astro b/apps/mana/apps/landing/src/pages/devlog/index.astro index fdee4c7c7..ad0b78a4f 100644 --- a/apps/mana/apps/landing/src/pages/devlog/index.astro +++ b/apps/mana/apps/landing/src/pages/devlog/index.astro @@ -113,15 +113,27 @@ const formatDate = (date: Date) => { class="group flex items-center justify-between bg-gradient-to-r from-green-500/10 to-emerald-500/10 hover:from-green-500/20 hover:to-emerald-500/20 border border-green-500/30 rounded-xl p-4 transition-all" > @@ -135,7 +147,7 @@ const formatDate = (date: Date) => { class={`absolute -inset-0.5 bg-gradient-to-r ${colors.bg} rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 blur`} />
diff --git a/apps/mana/apps/web/package.json b/apps/mana/apps/web/package.json index 58bd77768..12cc32d57 100644 --- a/apps/mana/apps/web/package.json +++ b/apps/mana/apps/web/package.json @@ -59,6 +59,7 @@ "@mana/shared-ai": "workspace:*", "@mana/shared-auth": "workspace:*", "@mana/shared-auth-ui": "workspace:*", + "@simplewebauthn/browser": "^13.3.0", "@mana/shared-branding": "workspace:*", "@mana/shared-crypto": "workspace:*", "@mana/shared-error-tracking": "workspace:*", @@ -67,7 +68,6 @@ "@mana/shared-links": "workspace:*", "@mana/shared-llm": "workspace:*", "@mana/shared-privacy": "workspace:*", - "@mana/cards-core": "workspace:*", "@mana/shared-stores": "workspace:*", "@mana/shared-tags": "workspace:*", "@mana/shared-tailwind": "workspace:*", diff --git a/apps/mana/apps/web/src/hooks.server.ts b/apps/mana/apps/web/src/hooks.server.ts index 5c3ee4325..31b249e23 100644 --- a/apps/mana/apps/web/src/hooks.server.ts +++ b/apps/mana/apps/web/src/hooks.server.ts @@ -27,6 +27,8 @@ import { setSecurityHeaders } from '@mana/shared-utils/security-headers'; const PUBLIC_MANA_AUTH_URL_CLIENT = process.env.PUBLIC_MANA_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_AUTH_URL || ''; +const PUBLIC_AUTH_WEB_URL_CLIENT = + process.env.PUBLIC_AUTH_WEB_URL_CLIENT || process.env.PUBLIC_AUTH_WEB_URL || ''; const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; const PUBLIC_SYNC_SERVER_URL_CLIENT = @@ -227,6 +229,7 @@ export const handle: Handle = async ({ event, resolve }) => { transformPageChunk: ({ html }) => { const envScript = ` - - diff --git a/apps/mana/apps/web/src/lib/components/layout/RouteTierGate.svelte b/apps/mana/apps/web/src/lib/components/layout/RouteTierGate.svelte index de027e8db..8a76ea7e9 100644 --- a/apps/mana/apps/web/src/lib/components/layout/RouteTierGate.svelte +++ b/apps/mana/apps/web/src/lib/components/layout/RouteTierGate.svelte @@ -7,6 +7,7 @@ import { goto } from '$app/navigation'; import { locale } from 'svelte-i18n'; import { authStore } from '$lib/stores/auth.svelte'; + import { redirectToPortal } from '$lib/auth/portal-redirect'; interface Props { appName: string; @@ -71,7 +72,7 @@ class="w-full cursor-pointer rounded-lg border px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-90" style:border-color="hsl(var(--color-border, 0 0% 90%))" style:color="hsl(var(--color-foreground, 0 0% 9%))" - onclick={() => goto('/login')} + onclick={() => redirectToPortal()} > {isDE ? 'Anmelden' : 'Sign in'} diff --git a/apps/mana/apps/web/src/lib/components/settings/sections/SecuritySection.svelte b/apps/mana/apps/web/src/lib/components/settings/sections/SecuritySection.svelte index 3a9229805..e92ba7eff 100644 --- a/apps/mana/apps/web/src/lib/components/settings/sections/SecuritySection.svelte +++ b/apps/mana/apps/web/src/lib/components/settings/sections/SecuritySection.svelte @@ -4,25 +4,66 @@ import { ShieldCheck } from '@mana/shared-icons'; import { PasskeyManager, TwoFactorSetup, AuditLog, SessionManager } from '@mana/shared-auth-ui'; import { authStore } from '$lib/stores/auth.svelte'; + import { + passkeys as passkeysClient, + sessions as sessionsClient, + twoFactor as twoFactorClient, + audit as auditClient, + type PasskeyEntry, + type SessionEntry, + type SecurityEvent, + } from '$lib/auth/settings-client'; import SettingsPanel from '../SettingsPanel.svelte'; import SettingsSectionHeader from '../SettingsSectionHeader.svelte'; import VaultSection from './VaultSection.svelte'; - let passkeys = $state([]); - let sessions = $state([]); + let passkeys = $state([]); + let passkeyAvailable = $state(false); + let sessions = $state([]); let sessionsLoading = $state(false); - let securityEvents = $state([]); + let securityEvents = $state([]); let securityEventsLoading = $state(false); + // Adapter: die UI-Komponenten erwarten `{ success, error? }`-Returns, + // unsere Settings-Client-Methoden werfen. Hier einmal zentral übersetzen. + function asResult(p: Promise): Promise<{ success: boolean; error?: string }> { + return p + .then(() => ({ success: true as const })) + .catch((e: unknown) => ({ + success: false as const, + error: e instanceof Error ? e.message : String(e), + })); + } + + async function handleEnable(password: string) { + try { + const r = await twoFactorClient.enable(password); + return { success: true as const, totpURI: r.uri, backupCodes: r.backupCodes }; + } catch (e) { + return { success: false as const, error: e instanceof Error ? e.message : String(e) }; + } + } + + async function handleGenerateBackupCodes(password: string) { + try { + const r = await twoFactorClient.generateBackupCodes(password); + return { success: true as const, backupCodes: r.backupCodes }; + } catch (e) { + return { success: false as const, error: e instanceof Error ? e.message : String(e) }; + } + } + onMount(async () => { if (!authStore.isAuthenticated) return; try { - passkeys = await authStore.listPasskeys(); + const cap = await passkeysClient.capability(); + passkeyAvailable = cap.available; + passkeys = await passkeysClient.list(); sessionsLoading = true; - sessions = await authStore.listSessions(); + sessions = await sessionsClient.list(); sessionsLoading = false; securityEventsLoading = true; - securityEvents = await authStore.getSecurityEvents(); + securityEvents = await auditClient.getSecurityEvents(); securityEventsLoading = false; } catch (e) { console.error('SecuritySection load failed:', e); @@ -44,12 +85,12 @@ authStore.registerPasskey(name)} - onDelete={(id) => authStore.deletePasskey(id)} - onRename={(id, name) => authStore.renamePasskey(id, name)} + {passkeyAvailable} + onRegister={(name) => asResult(passkeysClient.register(name))} + onDelete={(id) => asResult(passkeysClient.delete(id))} + onRename={(id, name) => asResult(passkeysClient.rename(id, name))} onRefresh={async () => { - passkeys = await authStore.listPasskeys(); + passkeys = await passkeysClient.list(); }} primaryColor="hsl(var(--color-primary))" /> @@ -59,10 +100,10 @@ authStore.revokeSession(id)} + onRevoke={(id) => asResult(sessionsClient.revoke(id))} onRefresh={async () => { sessionsLoading = true; - sessions = await authStore.listSessions(); + sessions = await sessionsClient.list(); sessionsLoading = false; }} primaryColor="hsl(var(--color-primary))" @@ -72,9 +113,9 @@ authStore.enableTwoFactor(password)} - onDisable={(password) => authStore.disableTwoFactor(password)} - onGenerateBackupCodes={(password) => authStore.generateBackupCodes(password)} + onEnable={handleEnable} + onDisable={(password) => asResult(twoFactorClient.disable(password))} + onGenerateBackupCodes={handleGenerateBackupCodes} primaryColor="hsl(var(--color-primary))" /> @@ -89,7 +130,7 @@ loading={securityEventsLoading} onRefresh={async () => { securityEventsLoading = true; - securityEvents = await authStore.getSecurityEvents(); + securityEvents = await auditClient.getSecurityEvents(); securityEventsLoading = false; }} primaryColor="hsl(var(--color-primary))" diff --git a/apps/mana/apps/web/src/lib/data/cross-app-queries.ts b/apps/mana/apps/web/src/lib/data/cross-app-queries.ts index c10d2ae4a..60e7734fb 100644 --- a/apps/mana/apps/web/src/lib/data/cross-app-queries.ts +++ b/apps/mana/apps/web/src/lib/data/cross-app-queries.ts @@ -19,7 +19,6 @@ import type { LocalAlarm, LocalCountdownTimer } from '$lib/modules/times/types'; import type { LocalFile } from '$lib/modules/storage/types'; import type { LocalSong, LocalPlaylist } from '$lib/modules/music/types'; import type { LocalDeck as LocalPresiDeck } from '$lib/modules/presi/types'; -import type { LocalDeck as LocalCardDeck, LocalCard } from '$lib/modules/cards/types'; // ─── Todo Queries ─────────────────────────────────────────── @@ -278,43 +277,7 @@ export function useRecentDecks(limit = 5) { } // ─── Cards Queries ───────────────────────────────────────── - -interface CardsProgress { - totalDecks: number; - totalCards: number; - cardsLearned: number; - dueForReview: number; - decks: LocalCardDeck[]; -} - -/** Cards learning progress. */ -export function useCardsProgress() { - return useLiveQueryWithDefault( - async (): Promise => { - const decks = await db.table('cardDecks').toArray(); - const cards = await db.table('cards').toArray(); - const activeDecks = decks.filter((d) => !d.deletedAt); - const activeCards = cards.filter((c) => !c.deletedAt); - const now = new Date().toISOString(); - const dueCards = activeCards.filter((c) => c.nextReview && c.nextReview <= now); - // Phase 6: cardDecks.name is encrypted — the widget renders the - // deck names so they need decryption. Counts work plaintext. - const { decryptRecords } = await import('./crypto'); - const decryptedDecks = await decryptRecords('cardDecks', activeDecks); - return { - totalDecks: activeDecks.length, - totalCards: activeCards.length, - cardsLearned: activeCards.filter((c) => (c.reviewCount ?? 0) > 0).length, - dueForReview: dueCards.length, - decks: decryptedDecks, - }; - }, - { - totalDecks: 0, - totalCards: 0, - cardsLearned: 0, - dueForReview: 0, - decks: [] as LocalCardDeck[], - } - ); -} +// Cards-Modul ist 2026-05-08 dekommissioniert (eigenständig auf +// cardecky.mana.how). Cross-App-Progress-Widgets, die Cards-Daten +// gezeigt haben, müssen entweder entfernt werden oder gegen die +// Cardecky-API queren — heute kein Konsument im mana-Frontend. diff --git a/apps/mana/apps/web/src/lib/data/module-registry.ts b/apps/mana/apps/web/src/lib/data/module-registry.ts index 8721b4fd6..f8ed22923 100644 --- a/apps/mana/apps/web/src/lib/data/module-registry.ts +++ b/apps/mana/apps/web/src/lib/data/module-registry.ts @@ -56,7 +56,6 @@ import { calendarModuleConfig } from '$lib/modules/calendar/module.config'; import { contactsModuleConfig } from '$lib/modules/contacts/module.config'; import { chatModuleConfig } from '$lib/modules/chat/module.config'; import { pictureModuleConfig } from '$lib/modules/picture/module.config'; -import { cardsModuleConfig } from '$lib/modules/cards/module.config'; import { quotesModuleConfig } from '$lib/modules/quotes/module.config'; import { musicModuleConfig } from '$lib/modules/music/module.config'; import { storageModuleConfig } from '$lib/modules/storage/module.config'; @@ -119,7 +118,6 @@ export const MODULE_CONFIGS: readonly ModuleConfig[] = [ contactsModuleConfig, chatModuleConfig, pictureModuleConfig, - cardsModuleConfig, quotesModuleConfig, musicModuleConfig, storageModuleConfig, diff --git a/apps/mana/apps/web/src/lib/data/privacy/exposed-records.ts b/apps/mana/apps/web/src/lib/data/privacy/exposed-records.ts index 257e6aeb2..523fc5318 100644 --- a/apps/mana/apps/web/src/lib/data/privacy/exposed-records.ts +++ b/apps/mana/apps/web/src/lib/data/privacy/exposed-records.ts @@ -211,18 +211,6 @@ const TABLES: TableConfig[] = [ return memosStore.setVisibility(id, next); }, }, - { - module: 'cards', - collection: 'cardDecks', - moduleLabel: 'Karten (Decks)', - encrypted: true, - title: (r) => asString(r.name), - href: (id) => `/cards/deck/${id}`, - setVisibility: async (id, next) => { - const { deckStore } = await import('$lib/modules/cards/stores/decks.svelte'); - return deckStore.setVisibility(id, next); - }, - }, { module: 'presi', collection: 'presiDecks', diff --git a/apps/mana/apps/web/src/lib/data/seed-registry.ts b/apps/mana/apps/web/src/lib/data/seed-registry.ts index f33ba1da5..04c492345 100644 --- a/apps/mana/apps/web/src/lib/data/seed-registry.ts +++ b/apps/mana/apps/web/src/lib/data/seed-registry.ts @@ -22,7 +22,6 @@ import { MOODLIT_GUEST_SEED } from '$lib/modules/moodlit/collections'; import { CONTACTS_GUEST_SEED } from '$lib/modules/contacts/collections'; import { CALENDAR_GUEST_SEED } from '$lib/modules/calendar/collections'; import { CHAT_GUEST_SEED } from '$lib/modules/chat/collections'; -import { CARDS_GUEST_SEED } from '$lib/modules/cards/collections'; import { SKILLTREE_GUEST_SEED } from '$lib/modules/skilltree/collections'; import { TODO_GUEST_SEED } from '$lib/modules/todo/collections'; import { NOTES_GUEST_SEED } from '$lib/modules/notes/collections'; @@ -63,7 +62,6 @@ register(MOODLIT_GUEST_SEED); register(CONTACTS_GUEST_SEED); register(CALENDAR_GUEST_SEED); register(CHAT_GUEST_SEED); -register(CARDS_GUEST_SEED); register(SKILLTREE_GUEST_SEED); register(TODO_GUEST_SEED); register(NOTES_GUEST_SEED); diff --git a/apps/mana/apps/web/src/lib/data/tools/init.ts b/apps/mana/apps/web/src/lib/data/tools/init.ts index cf9dfaba5..4ada90270 100644 --- a/apps/mana/apps/web/src/lib/data/tools/init.ts +++ b/apps/mana/apps/web/src/lib/data/tools/init.ts @@ -16,7 +16,6 @@ import { contactsTools } from '$lib/modules/contacts/tools'; import { bodyTools } from '$lib/modules/body/tools'; import { financeTools } from '$lib/modules/finance/tools'; import { dreamsTools } from '$lib/modules/dreams/tools'; -import { cardsTools } from '$lib/modules/cards/tools'; import { timesTools } from '$lib/modules/times/tools'; import { socialEventsTools } from '$lib/modules/events/tools'; import { musicTools } from '$lib/modules/music/tools'; @@ -68,7 +67,6 @@ export function initTools(): void { registerTools(bodyTools); registerTools(financeTools); registerTools(dreamsTools); - registerTools(cardsTools); registerTools(timesTools); registerTools(socialEventsTools); registerTools(musicTools); diff --git a/apps/mana/apps/web/src/lib/i18n/locales/cards/de.json b/apps/mana/apps/web/src/lib/i18n/locales/cards/de.json deleted file mode 100644 index 4a07a9990..000000000 --- a/apps/mana/apps/web/src/lib/i18n/locales/cards/de.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "app": { - "name": "Cards", - "description": "KI-Lernkarten" - }, - "nav": { - "decks": "Decks", - "study": "Lernen", - "stats": "Statistiken", - "settings": "Einstellungen" - }, - "deck": { - "create": "Deck erstellen", - "edit": "Deck bearbeiten", - "delete": "Deck löschen", - "empty": "Noch keine Decks", - "cards": "Karten", - "study": "Lernen starten", - "addCard": "Karte hinzufügen", - "importCards": "Karten importieren", - "generateWithAI": "Mit KI generieren" - }, - "card": { - "front": "Vorderseite", - "back": "Rückseite", - "edit": "Karte bearbeiten", - "delete": "Karte löschen", - "hint": "Hinweis" - }, - "study": { - "again": "Nochmal", - "hard": "Schwer", - "good": "Gut", - "easy": "Einfach", - "showAnswer": "Antwort zeigen", - "complete": "Abgeschlossen!", - "cardsRemaining": "Karten übrig", - "streak": "Serie" - }, - "stats": { - "studied": "Gelernt", - "mastered": "Gemeistert", - "accuracy": "Genauigkeit", - "reviewsDue": "Fällige Wiederholungen" - }, - "common": { - "save": "Speichern", - "cancel": "Abbrechen", - "delete": "Löschen", - "back": "Zurück", - "loading": "Lädt...", - "error": "Fehler", - "success": "Erfolgreich" - }, - "progress": { - "page_title_html": "Fortschritt - Cards - Mana", - "heading": "Fortschritt", - "subtitle": "Verfolge deinen Lernfortschritt", - "stat_decks": "Decks", - "stat_total_cards": "Karten gesamt", - "stat_due": "Fällig zur Wiederholung", - "section_overview": "Decks Übersicht", - "empty_title": "Noch keine Lernsitzungen.", - "empty_hint": "Erstelle ein Deck und beginne zu lernen!", - "deck_cards": "{n} Karten" - }, - "detail": { - "not_found": "Deck nicht gefunden", - "confirm_delete": "Deck wirklich löschen?", - "toast_deleted": "Deck gelöscht", - "placeholder_name": "Deck-Name...", - "name_fallback": "Unbenannt", - "prop_color": "Farbe", - "prop_visibility": "Sichtbarkeit", - "prop_cards": "Karten", - "prop_last_studied": "Zuletzt gelernt", - "section_description": "Beschreibung", - "placeholder_description": "Beschreibung hinzufügen...", - "meta_created": "Erstellt: {date}", - "meta_updated": "Bearbeitet: {date}" - } -} diff --git a/apps/mana/apps/web/src/lib/i18n/locales/cards/en.json b/apps/mana/apps/web/src/lib/i18n/locales/cards/en.json deleted file mode 100644 index 3c8fcfcf0..000000000 --- a/apps/mana/apps/web/src/lib/i18n/locales/cards/en.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "app": { - "name": "Cards", - "description": "AI Flashcards" - }, - "nav": { - "decks": "Decks", - "study": "Study", - "stats": "Statistics", - "settings": "Settings" - }, - "deck": { - "create": "Create Deck", - "edit": "Edit Deck", - "delete": "Delete Deck", - "empty": "No decks yet", - "cards": "Cards", - "study": "Start Studying", - "addCard": "Add Card", - "importCards": "Import Cards", - "generateWithAI": "Generate with AI" - }, - "card": { - "front": "Front", - "back": "Back", - "edit": "Edit Card", - "delete": "Delete Card", - "hint": "Hint" - }, - "study": { - "again": "Again", - "hard": "Hard", - "good": "Good", - "easy": "Easy", - "showAnswer": "Show Answer", - "complete": "Complete!", - "cardsRemaining": "cards remaining", - "streak": "Streak" - }, - "stats": { - "studied": "Studied", - "mastered": "Mastered", - "accuracy": "Accuracy", - "reviewsDue": "Reviews due" - }, - "common": { - "save": "Save", - "cancel": "Cancel", - "delete": "Delete", - "back": "Back", - "loading": "Loading...", - "error": "Error", - "success": "Success" - }, - "progress": { - "page_title_html": "Progress - Cards - Mana", - "heading": "Progress", - "subtitle": "Track your learning progress", - "stat_decks": "Decks", - "stat_total_cards": "Total cards", - "stat_due": "Due for review", - "section_overview": "Decks overview", - "empty_title": "No study sessions yet.", - "empty_hint": "Create a deck and start studying!", - "deck_cards": "{n} cards" - }, - "detail": { - "not_found": "Deck not found", - "confirm_delete": "Really delete this deck?", - "toast_deleted": "Deck deleted", - "placeholder_name": "Deck name...", - "name_fallback": "Untitled", - "prop_color": "Color", - "prop_visibility": "Visibility", - "prop_cards": "Cards", - "prop_last_studied": "Last studied", - "section_description": "Description", - "placeholder_description": "Add a description...", - "meta_created": "Created: {date}", - "meta_updated": "Edited: {date}" - } -} diff --git a/apps/mana/apps/web/src/lib/i18n/locales/cards/es.json b/apps/mana/apps/web/src/lib/i18n/locales/cards/es.json deleted file mode 100644 index 86bc42470..000000000 --- a/apps/mana/apps/web/src/lib/i18n/locales/cards/es.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "app": { - "name": "Cards", - "description": "Flashcards con IA" - }, - "nav": { - "decks": "Mazos", - "study": "Estudiar", - "stats": "Estadísticas", - "settings": "Ajustes" - }, - "deck": { - "create": "Crear mazo", - "edit": "Editar mazo", - "delete": "Eliminar mazo", - "empty": "Aún no hay mazos", - "cards": "Tarjetas", - "study": "Empezar a estudiar", - "addCard": "Añadir tarjeta", - "importCards": "Importar tarjetas", - "generateWithAI": "Generar con IA" - }, - "card": { - "front": "Anverso", - "back": "Reverso", - "edit": "Editar tarjeta", - "delete": "Eliminar tarjeta", - "hint": "Pista" - }, - "study": { - "again": "Otra vez", - "hard": "Difícil", - "good": "Bien", - "easy": "Fácil", - "showAnswer": "Mostrar respuesta", - "complete": "¡Completado!", - "cardsRemaining": "tarjetas restantes", - "streak": "Racha" - }, - "stats": { - "studied": "Estudiadas", - "mastered": "Dominadas", - "accuracy": "Precisión", - "reviewsDue": "Repasos pendientes" - }, - "common": { - "save": "Guardar", - "cancel": "Cancelar", - "delete": "Eliminar", - "back": "Atrás", - "loading": "Cargando...", - "error": "Error", - "success": "Éxito" - }, - "progress": { - "page_title_html": "Progreso - Cards - Mana", - "heading": "Progreso", - "subtitle": "Sigue tu progreso de aprendizaje", - "stat_decks": "Mazos", - "stat_total_cards": "Tarjetas en total", - "stat_due": "Pendientes de repasar", - "section_overview": "Resumen de mazos", - "empty_title": "Aún no hay sesiones de estudio.", - "empty_hint": "¡Crea un mazo y empieza a estudiar!", - "deck_cards": "{n} tarjetas" - }, - "detail": { - "not_found": "Mazo no encontrado", - "confirm_delete": "¿Eliminar realmente este mazo?", - "toast_deleted": "Mazo eliminado", - "placeholder_name": "Nombre del mazo...", - "name_fallback": "Sin título", - "prop_color": "Color", - "prop_visibility": "Visibilidad", - "prop_cards": "Tarjetas", - "prop_last_studied": "Último estudio", - "section_description": "Descripción", - "placeholder_description": "Añadir una descripción...", - "meta_created": "Creado: {date}", - "meta_updated": "Editado: {date}" - } -} diff --git a/apps/mana/apps/web/src/lib/i18n/locales/cards/fr.json b/apps/mana/apps/web/src/lib/i18n/locales/cards/fr.json deleted file mode 100644 index a567b2173..000000000 --- a/apps/mana/apps/web/src/lib/i18n/locales/cards/fr.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "app": { - "name": "Cards", - "description": "Flashcards IA" - }, - "nav": { - "decks": "Paquets", - "study": "Étudier", - "stats": "Statistiques", - "settings": "Paramètres" - }, - "deck": { - "create": "Créer un paquet", - "edit": "Modifier le paquet", - "delete": "Supprimer le paquet", - "empty": "Pas encore de paquets", - "cards": "Cartes", - "study": "Commencer à étudier", - "addCard": "Ajouter une carte", - "importCards": "Importer des cartes", - "generateWithAI": "Générer avec l'IA" - }, - "card": { - "front": "Recto", - "back": "Verso", - "edit": "Modifier la carte", - "delete": "Supprimer la carte", - "hint": "Indice" - }, - "study": { - "again": "Encore", - "hard": "Difficile", - "good": "Bien", - "easy": "Facile", - "showAnswer": "Afficher la réponse", - "complete": "Terminé !", - "cardsRemaining": "cartes restantes", - "streak": "Série" - }, - "stats": { - "studied": "Étudiées", - "mastered": "Maîtrisées", - "accuracy": "Précision", - "reviewsDue": "Révisions à faire" - }, - "common": { - "save": "Enregistrer", - "cancel": "Annuler", - "delete": "Supprimer", - "back": "Retour", - "loading": "Chargement...", - "error": "Erreur", - "success": "Succès" - }, - "progress": { - "page_title_html": "Progression - Cards - Mana", - "heading": "Progression", - "subtitle": "Suis ta progression d'apprentissage", - "stat_decks": "Paquets", - "stat_total_cards": "Cartes au total", - "stat_due": "À réviser", - "section_overview": "Vue d'ensemble des paquets", - "empty_title": "Pas encore de sessions d'étude.", - "empty_hint": "Crée un paquet et commence à étudier !", - "deck_cards": "{n} cartes" - }, - "detail": { - "not_found": "Paquet introuvable", - "confirm_delete": "Vraiment supprimer ce paquet ?", - "toast_deleted": "Paquet supprimé", - "placeholder_name": "Nom du paquet...", - "name_fallback": "Sans titre", - "prop_color": "Couleur", - "prop_visibility": "Visibilité", - "prop_cards": "Cartes", - "prop_last_studied": "Dernière révision", - "section_description": "Description", - "placeholder_description": "Ajouter une description...", - "meta_created": "Créé : {date}", - "meta_updated": "Modifié : {date}" - } -} diff --git a/apps/mana/apps/web/src/lib/i18n/locales/cards/it.json b/apps/mana/apps/web/src/lib/i18n/locales/cards/it.json deleted file mode 100644 index 194bee77c..000000000 --- a/apps/mana/apps/web/src/lib/i18n/locales/cards/it.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "app": { - "name": "Cards", - "description": "Flashcard IA" - }, - "nav": { - "decks": "Mazzi", - "study": "Studia", - "stats": "Statistiche", - "settings": "Impostazioni" - }, - "deck": { - "create": "Crea mazzo", - "edit": "Modifica mazzo", - "delete": "Elimina mazzo", - "empty": "Nessun mazzo ancora", - "cards": "Carte", - "study": "Inizia a studiare", - "addCard": "Aggiungi carta", - "importCards": "Importa carte", - "generateWithAI": "Genera con IA" - }, - "card": { - "front": "Fronte", - "back": "Retro", - "edit": "Modifica carta", - "delete": "Elimina carta", - "hint": "Suggerimento" - }, - "study": { - "again": "Ancora", - "hard": "Difficile", - "good": "Bene", - "easy": "Facile", - "showAnswer": "Mostra risposta", - "complete": "Completato!", - "cardsRemaining": "carte rimanenti", - "streak": "Serie" - }, - "stats": { - "studied": "Studiate", - "mastered": "Padroneggiate", - "accuracy": "Precisione", - "reviewsDue": "Ripetizioni in scadenza" - }, - "common": { - "save": "Salva", - "cancel": "Annulla", - "delete": "Elimina", - "back": "Indietro", - "loading": "Caricamento...", - "error": "Errore", - "success": "Successo" - }, - "progress": { - "page_title_html": "Progresso - Cards - Mana", - "heading": "Progresso", - "subtitle": "Segui i tuoi progressi di studio", - "stat_decks": "Mazzi", - "stat_total_cards": "Carte totali", - "stat_due": "Da ripassare", - "section_overview": "Panoramica mazzi", - "empty_title": "Ancora nessuna sessione di studio.", - "empty_hint": "Crea un mazzo e inizia a studiare!", - "deck_cards": "{n} carte" - }, - "detail": { - "not_found": "Mazzo non trovato", - "confirm_delete": "Eliminare davvero questo mazzo?", - "toast_deleted": "Mazzo eliminato", - "placeholder_name": "Nome del mazzo...", - "name_fallback": "Senza titolo", - "prop_color": "Colore", - "prop_visibility": "Visibilità", - "prop_cards": "Carte", - "prop_last_studied": "Ultimo studio", - "section_description": "Descrizione", - "placeholder_description": "Aggiungi una descrizione...", - "meta_created": "Creato: {date}", - "meta_updated": "Modificato: {date}" - } -} diff --git a/apps/mana/apps/web/src/lib/modules/cards/ListView.svelte b/apps/mana/apps/web/src/lib/modules/cards/ListView.svelte deleted file mode 100644 index a866faf59..000000000 --- a/apps/mana/apps/web/src/lib/modules/cards/ListView.svelte +++ /dev/null @@ -1,126 +0,0 @@ - - - -{#if !standaloneHintDismissed} -
- - Cardecky gibt es jetzt auch als eigenständige App auf - cardecky.mana.how — gleiche Daten, fokussierte UI. - - -
-{/if} - d.id} emptyTitle="Keine Decks"> - {#snippet header()} - {decks.length} Decks - {totalDue} fällig - {/snippet} - - {#snippet item(deck)} - {@const due = dueByDeck.get(deck.id) ?? 0} - - {/snippet} - diff --git a/apps/mana/apps/web/src/lib/modules/cards/card-reviews.ts b/apps/mana/apps/web/src/lib/modules/cards/card-reviews.ts deleted file mode 100644 index 15870809f..000000000 --- a/apps/mana/apps/web/src/lib/modules/cards/card-reviews.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Cards module — review fan-out is now sourced from `@mana/cards-core`. - * Thin re-export so existing local imports keep working. - */ - -export { subIndexesFor } from '@mana/cards-core'; diff --git a/apps/mana/apps/web/src/lib/modules/cards/cloze.ts b/apps/mana/apps/web/src/lib/modules/cards/cloze.ts deleted file mode 100644 index a0f026aab..000000000 --- a/apps/mana/apps/web/src/lib/modules/cards/cloze.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Cards module — cloze parser is now sourced from `@mana/cards-core`. - * Thin re-export so existing local imports keep working. - */ - -export { - tokenize, - clusterIndexes, - clusters, - renderCloze, - type ClozeCluster, - type RenderedCloze, -} from '@mana/cards-core'; diff --git a/apps/mana/apps/web/src/lib/modules/cards/collections.ts b/apps/mana/apps/web/src/lib/modules/cards/collections.ts deleted file mode 100644 index 9a1bb029e..000000000 --- a/apps/mana/apps/web/src/lib/modules/cards/collections.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Cards module — collection accessors and guest seed data. - * - * Tables in the unified DB: cardDecks, cards, cardReviews, cardStudyBlocks. - */ - -import { db } from '$lib/data/database'; -import type { LocalDeck, LocalCard, LocalCardReview, LocalCardStudyBlock } from './types'; - -// ─── Collection Accessors ────────────────────────────────── - -export const cardDeckTable = db.table('cardDecks'); -export const cardTable = db.table('cards'); -export const cardReviewTable = db.table('cardReviews'); -export const cardStudyBlockTable = db.table('cardStudyBlocks'); - -// ─── Guest Seed ──────────────────────────────────────────── - -const ONBOARDING_DECK_ID = 'onboarding-deck'; - -export const CARDS_GUEST_SEED = { - cardDecks: [ - { - id: ONBOARDING_DECK_ID, - name: 'Erste Schritte', - description: 'Lerne Cards kennen mit diesen Beispiel-Karteikarten.', - color: '#6366f1', - cardCount: 3, - }, - ], - cards: [ - { - id: 'card-1', - deckId: ONBOARDING_DECK_ID, - front: 'Was ist Cards?', - back: 'Cards ist eine Karteikarten-App zum effizienten Lernen mit Spaced Repetition.', - difficulty: 1, - reviewCount: 0, - order: 0, - }, - { - id: 'card-2', - deckId: ONBOARDING_DECK_ID, - front: 'Wie funktioniert Spaced Repetition?', - back: 'Karten, die du gut kennst, werden seltener gezeigt. Schwierige Karten erscheinen haufiger, bis du sie beherrschst.', - difficulty: 2, - reviewCount: 0, - order: 1, - }, - { - id: 'card-3', - deckId: ONBOARDING_DECK_ID, - front: 'Wie erstelle ich ein neues Deck?', - back: 'Klicke auf den + Button auf der Decks-Seite, um ein neues Deck mit eigenen Karteikarten zu erstellen.', - difficulty: 1, - reviewCount: 0, - order: 2, - }, - ], -}; diff --git a/apps/mana/apps/web/src/lib/modules/cards/components/CardFace.svelte b/apps/mana/apps/web/src/lib/modules/cards/components/CardFace.svelte deleted file mode 100644 index 3c3538628..000000000 --- a/apps/mana/apps/web/src/lib/modules/cards/components/CardFace.svelte +++ /dev/null @@ -1,95 +0,0 @@ - - -
-
- {@html view.prompt} -
- - {#if isTypeIn} - onTypedAnswer?.((e.currentTarget as HTMLInputElement).value)} - disabled={showBack} - /> - {/if} - - {#if showBack} -
- {@html view.answer} -
- {/if} -
diff --git a/apps/mana/apps/web/src/lib/modules/cards/components/CreateDeckModal.svelte b/apps/mana/apps/web/src/lib/modules/cards/components/CreateDeckModal.svelte deleted file mode 100644 index 04331d7fe..000000000 --- a/apps/mana/apps/web/src/lib/modules/cards/components/CreateDeckModal.svelte +++ /dev/null @@ -1,140 +0,0 @@ - - -{#if open} -
e.key === 'Escape' && handleClose()} - tabindex="-1" - role="presentation" - > - - -
e.stopPropagation()} - > -

Neues Deck erstellen

- -
{ - e.preventDefault(); - handleSubmit(); - }} - class="space-y-4" - > -
- - -
- -
- - -
- -
- Tags - (selectedTagIds = ids)} - /> -
- -
- Farbe - (color = c)} - size="sm" - /> -
- - {#if deckStore.error} -
- {deckStore.error} -
- {/if} - -
- - -
-
-
-
-{/if} diff --git a/apps/mana/apps/web/src/lib/modules/cards/components/DeckCard.svelte b/apps/mana/apps/web/src/lib/modules/cards/components/DeckCard.svelte deleted file mode 100644 index c3c488965..000000000 --- a/apps/mana/apps/web/src/lib/modules/cards/components/DeckCard.svelte +++ /dev/null @@ -1,51 +0,0 @@ - - - -
- -
- - -

{deck.title}

- - - {#if deck.description} -

- {deck.description} -

- {/if} - - -
-
- {deck.cardCount || 0} Karten - {#if deck.visibility === 'public'} - - Öffentlich - - {/if} -
- {formatDate(deck.updatedAt)} -
-
-
diff --git a/apps/mana/apps/web/src/lib/modules/cards/fsrs.ts b/apps/mana/apps/web/src/lib/modules/cards/fsrs.ts deleted file mode 100644 index bf1264557..000000000 --- a/apps/mana/apps/web/src/lib/modules/cards/fsrs.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Cards module — FSRS wrapper is now sourced from `@mana/cards-core`. - * Thin re-export so existing local imports keep working. - */ - -export { newReview, gradeReview } from '@mana/cards-core'; diff --git a/apps/mana/apps/web/src/lib/modules/cards/index.ts b/apps/mana/apps/web/src/lib/modules/cards/index.ts deleted file mode 100644 index f063df7aa..000000000 --- a/apps/mana/apps/web/src/lib/modules/cards/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Cards module — barrel exports. - */ - -export { deckStore } from './stores/decks.svelte'; -export { cardStore } from './stores/cards.svelte'; -export { - useAllDecks, - useDeck, - useCardsByDeck, - toDeck, - toCard, - getDeckById, - getPublicDecks, - getCardCountForDeck, - getDueCards, -} from './queries'; -export { cardDeckTable, cardTable, CARDS_GUEST_SEED } from './collections'; -export type { - LocalDeck, - LocalCard, - Deck, - Card, - CreateDeckInput, - UpdateDeckInput, - CreateCardInput, - UpdateCardInput, -} from './types'; diff --git a/apps/mana/apps/web/src/lib/modules/cards/module.config.ts b/apps/mana/apps/web/src/lib/modules/cards/module.config.ts deleted file mode 100644 index 98818d4d5..000000000 --- a/apps/mana/apps/web/src/lib/modules/cards/module.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { ModuleConfig } from '$lib/data/module-registry'; - -export const cardsModuleConfig: ModuleConfig = { - appId: 'cards', - tables: [ - { name: 'cardDecks', syncName: 'decks' }, - { name: 'cards' }, - { name: 'deckTags' }, - { name: 'cardReviews' }, - { name: 'cardStudyBlocks' }, - ], -}; diff --git a/apps/mana/apps/web/src/lib/modules/cards/queries.ts b/apps/mana/apps/web/src/lib/modules/cards/queries.ts deleted file mode 100644 index 166e18145..000000000 --- a/apps/mana/apps/web/src/lib/modules/cards/queries.ts +++ /dev/null @@ -1,201 +0,0 @@ -/** - * Reactive queries & pure helpers for Cards — uses Dexie liveQuery on the unified DB. - * - * Uses table names: cardDecks, cards. - */ - -import { liveQuery } from 'dexie'; -import { deriveUpdatedAt } from '$lib/data/sync'; -import { db } from '$lib/data/database'; -import { scopedForModule } from '$lib/data/scope'; -import { decryptRecord, decryptRecords } from '$lib/data/crypto'; -import type { - CardFields, - CardType, - LocalDeck, - LocalCard, - LocalCardReview, - Deck, - Card, - CardReview, -} from './types'; - -// ─── Type Converters ─────────────────────────────────────── - -export function toDeck(local: LocalDeck): Deck { - return { - id: local.id, - title: local.name, - description: local.description ?? undefined, - color: local.color, - visibility: local.visibility ?? 'space', - tags: [], - cardCount: local.cardCount, - createdAt: local.createdAt ?? new Date().toISOString(), - updatedAt: deriveUpdatedAt(local), - }; -} - -/** - * Promote any LocalCard row — including legacy pre-Phase-0 ones — to - * the canonical {type, fields} shape. Readers must go through this so - * the rest of the app sees one schema. - * - * - Phase-0+ rows: returned as-is, with `front`/`back` derived from - * fields for the convenience accessors on the DTO. - * - Legacy rows (only `front`/`back` set): synthesised as - * {type: 'basic', fields: {front, back}}. - */ -export function toLogicalCard(local: LocalCard): { - type: CardType; - fields: CardFields; - front: string; - back: string; -} { - const type: CardType = local.type ?? 'basic'; - const fields: CardFields = local.fields ?? { - front: local.front ?? '', - back: local.back ?? '', - }; - const front = fields.front ?? local.front ?? ''; - const back = fields.back ?? local.back ?? ''; - return { type, fields, front, back }; -} - -export function toCard(local: LocalCard): Card { - const { type, fields, front, back } = toLogicalCard(local); - return { - id: local.id, - deckId: local.deckId, - type, - fields, - front, - back, - order: local.order, - createdAt: local.createdAt ?? new Date().toISOString(), - updatedAt: deriveUpdatedAt(local), - // Legacy fields surfaced for pre-Phase-0 UI. Populated only when the - // underlying row carries them. - difficulty: local.difficulty, - nextReview: local.nextReview ?? undefined, - reviewCount: local.reviewCount, - }; -} - -// ─── Live Queries ────────────────────────────────────────── - -/** All decks, auto-updates on any change. */ -export function useAllDecks() { - return liveQuery(async () => { - const visible = ( - await scopedForModule('cards', 'cardDecks').toArray() - ).filter((d) => !d.deletedAt); - const decrypted = await decryptRecords('cardDecks', visible); - return decrypted.map(toDeck); - }); -} - -/** Single deck by ID. Auto-updates on any change. */ -export function useDeck(deckId: string) { - return liveQuery(async () => { - const local = await db.table('cardDecks').get(deckId); - if (!local || local.deletedAt) return null; - const decrypted = await decryptRecord('cardDecks', { ...local }); - return toDeck(decrypted); - }); -} - -/** All cards for a specific deck, sorted by order. Auto-updates on any change. */ -export function useCardsByDeck(deckId: string) { - return liveQuery(async () => { - const visible = ( - await db.table('cards').where('deckId').equals(deckId).sortBy('order') - ).filter((c) => !c.deletedAt); - const decrypted = await decryptRecords('cards', visible); - return decrypted.map(toCard); - }); -} - -/** - * All reviews that are due now (or overdue), optionally filtered by - * deck. Joined with the parent card so the UI can render the prompt - * immediately without a second lookup. - * - * Sorted by `due` ascending so the oldest-due learnable unit comes - * first — that's the natural session order. - */ -export function useDueReviews(deckId?: string) { - return liveQuery(async () => { - const nowIso = new Date().toISOString(); - const due = await db - .table('cardReviews') - .where('due') - .belowOrEqual(nowIso) - .toArray(); - const live = due.filter((r) => !r.deletedAt); - if (live.length === 0) return [] as { review: CardReview; card: Card }[]; - - const cardIds = [...new Set(live.map((r) => r.cardId))]; - const cardRows = await db.table('cards').where('id').anyOf(cardIds).toArray(); - const decryptedCards = await decryptRecords( - 'cards', - cardRows.filter((c) => !c.deletedAt) - ); - const cardById = new Map(decryptedCards.map((c) => [c.id, toCard(c)] as const)); - - return live - .filter((r) => { - const c = cardById.get(r.cardId); - if (!c) return false; - if (deckId && c.deckId !== deckId) return false; - return true; - }) - .sort((a, b) => (a.due < b.due ? -1 : a.due > b.due ? 1 : 0)) - .map((r) => ({ review: toCardReview(r), card: cardById.get(r.cardId)! })); - }); -} - -/** Just the reviews row, no card join — useful in the session UI mid-grade. */ -export function useReview(reviewId: string) { - return liveQuery(async () => { - const r = await db.table('cardReviews').get(reviewId); - if (!r || r.deletedAt) return null; - return toCardReview(r); - }); -} - -function toCardReview(r: LocalCardReview): CardReview { - return { - id: r.id, - cardId: r.cardId, - subIndex: r.subIndex, - state: r.state, - stability: r.stability, - difficulty: r.difficulty, - due: r.due, - reps: r.reps, - lapses: r.lapses, - lastReview: r.lastReview, - elapsedDays: r.elapsedDays, - scheduledDays: r.scheduledDays, - }; -} - -// ─── Pure Helper Functions ───────────────────────────────── - -export function getDeckById(decks: Deck[], id: string): Deck | undefined { - return decks.find((d) => d.id === id); -} - -export function getPublicDecks(decks: Deck[]): Deck[] { - return decks.filter((d) => d.visibility === 'public'); -} - -export function getCardCountForDeck(cards: Card[], deckId: string): number { - return cards.filter((c) => c.deckId === deckId).length; -} - -export function getDueCards(cards: Card[]): Card[] { - const now = new Date().toISOString(); - return cards.filter((c) => c.nextReview && c.nextReview <= now); -} diff --git a/apps/mana/apps/web/src/lib/modules/cards/render.ts b/apps/mana/apps/web/src/lib/modules/cards/render.ts deleted file mode 100644 index d37a1fcb5..000000000 --- a/apps/mana/apps/web/src/lib/modules/cards/render.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Cards module — Markdown render helper is now sourced from - * `@mana/cards-core`. Thin re-export so existing local imports keep - * working. - */ - -export { renderMarkdown, type RenderOptions } from '@mana/cards-core'; diff --git a/apps/mana/apps/web/src/lib/modules/cards/stores/cards.svelte.ts b/apps/mana/apps/web/src/lib/modules/cards/stores/cards.svelte.ts deleted file mode 100644 index bde4e6ff0..000000000 --- a/apps/mana/apps/web/src/lib/modules/cards/stores/cards.svelte.ts +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Card Store — Mutations Only - * - * Reads come from liveQuery hooks in queries.ts. - * This store only handles writes to IndexedDB via the unified database. - * - * Phase 0+: writes the new {type, fields} shape AND mirrors basic-card - * content to the legacy front/back columns so older mana builds keep - * rendering. Every create/update fans out to cardReviews via - * reviewStore.ensureReviewsForCard(). - */ - -import { CardsEvents } from '@mana/shared-utils/analytics'; -import { cardTable, cardDeckTable } from '../collections'; -import { toCard, toLogicalCard } from '../queries'; -import { encryptRecord, decryptRecord } from '$lib/data/crypto'; -import { emitDomainEvent } from '$lib/data/events'; -import type { - CardFields, - CardType, - LocalCard, - Card, - CreateCardInput, - UpdateCardInput, -} from '../types'; -import { reviewStore } from './reviews.svelte'; - -let error = $state(null); - -/** - * Build the {type, fields} pair from a CreateCardInput. Accepts the - * convenience `front`/`back` shortcut for basic cards and falls back - * to an explicit `fields` map for cloze and friends. - */ -function resolveTypeAndFields(input: CreateCardInput): { - type: CardType; - fields: CardFields; -} { - const type = input.type ?? 'basic'; - if (input.fields) return { type, fields: input.fields }; - if (type === 'cloze') return { type, fields: { text: input.front ?? '' } }; - return { type, fields: { front: input.front ?? '', back: input.back ?? '' } }; -} - -/** Mirror basic-card text into the legacy columns for older clients. */ -function legacyMirror(type: CardType, fields: CardFields): { front?: string; back?: string } { - if (type === 'basic' || type === 'basic-reverse' || type === 'type-in') { - return { front: fields.front ?? '', back: fields.back ?? '' }; - } - if (type === 'cloze') { - // Surface the cloze source on `front` so legacy list-views show - // something meaningful rather than an empty row. - return { front: fields.text ?? '', back: '' }; - } - return {}; -} - -export const cardStore = { - get error() { - return error; - }, - - async createCard(input: CreateCardInput, currentCardCount: number = 0): Promise { - error = null; - try { - const { type, fields } = resolveTypeAndFields(input); - const legacy = legacyMirror(type, fields); - - const newLocal: LocalCard = { - id: crypto.randomUUID(), - deckId: input.deckId, - type, - fields, - order: currentCardCount, - ...legacy, - }; - - const plaintextSnapshot = toCard(newLocal); - await encryptRecord('cards', newLocal); - await cardTable.add(newLocal); - - const deck = await cardDeckTable.get(input.deckId); - if (deck) { - await cardDeckTable.update(input.deckId, { - cardCount: (deck.cardCount || 0) + 1, - }); - } - - await reviewStore.ensureReviewsForCard({ id: newLocal.id, type, fields }); - - emitDomainEvent('CardCreated', 'cards', 'cards', newLocal.id, { - cardId: newLocal.id, - deckId: input.deckId, - }); - CardsEvents.cardCreated(); - return plaintextSnapshot; - } catch (err: any) { - error = err.message || 'Failed to create card'; - console.error('Create card error:', err); - return null; - } - }, - - async updateCard(id: string, updates: UpdateCardInput) { - error = null; - try { - const existing = await cardTable.get(id); - if (!existing) return; - - const decrypted = await decryptRecord('cards', { ...existing }); - const current = toLogicalCard(decrypted as LocalCard); - const nextType: CardType = updates.type ?? current.type; - const nextFields: CardFields = updates.fields - ? updates.fields - : updates.front !== undefined || updates.back !== undefined - ? nextType === 'cloze' - ? { ...current.fields, text: updates.front ?? current.fields.text ?? '' } - : { - ...current.fields, - front: updates.front ?? current.fields.front ?? '', - back: updates.back ?? current.fields.back ?? '', - } - : current.fields; - - const legacy = legacyMirror(nextType, nextFields); - const diff: Partial = { - type: nextType, - fields: nextFields, - ...legacy, - }; - if (updates.order !== undefined) diff.order = updates.order; - if (updates.difficulty !== undefined) diff.difficulty = updates.difficulty; - - await encryptRecord('cards', diff); - await cardTable.update(id, diff); - - const structuralChange = - updates.type !== undefined || - updates.fields !== undefined || - (nextType === 'cloze' && updates.front !== undefined); - if (structuralChange) { - await reviewStore.ensureReviewsForCard({ id, type: nextType, fields: nextFields }); - } - } catch (err: any) { - error = err.message || 'Failed to update card'; - console.error('Update card error:', err); - } - }, - - async deleteCard(id: string, deckId?: string) { - error = null; - try { - const now = new Date().toISOString(); - await cardTable.update(id, { deletedAt: now }); - await reviewStore.softDeleteForCard(id); - CardsEvents.cardDeleted(); - - if (deckId) { - const deck = await cardDeckTable.get(deckId); - if (deck) { - await cardDeckTable.update(deckId, { - cardCount: Math.max(0, (deck.cardCount || 0) - 1), - }); - } - } - } catch (err: any) { - error = err.message || 'Failed to delete card'; - console.error('Delete card error:', err); - } - }, - - async reorderCards(cardIds: string[]) { - error = null; - try { - for (let i = 0; i < cardIds.length; i++) { - await cardTable.update(cardIds[i], { order: i }); - } - } catch (err: any) { - error = err.message || 'Failed to reorder cards'; - console.error('Reorder cards error:', err); - } - }, - - clearError() { - error = null; - }, -}; diff --git a/apps/mana/apps/web/src/lib/modules/cards/stores/decks.svelte.ts b/apps/mana/apps/web/src/lib/modules/cards/stores/decks.svelte.ts deleted file mode 100644 index 1f0ad27bb..000000000 --- a/apps/mana/apps/web/src/lib/modules/cards/stores/decks.svelte.ts +++ /dev/null @@ -1,167 +0,0 @@ -/** - * Deck Store — Mutations Only - * - * Reads come from liveQuery hooks in queries.ts. - * This store only handles writes to IndexedDB via the unified database. - */ - -import { CardsEvents } from '@mana/shared-utils/analytics'; -import { db } from '$lib/data/database'; -import { cardDeckTable, cardTable } from '../collections'; -import { toDeck } from '../queries'; -import { encryptRecord, decryptRecord } from '$lib/data/crypto'; -import { emitDomainEvent } from '$lib/data/events'; -import { getActiveSpace } from '$lib/data/scope'; -import { getEffectiveUserId } from '$lib/data/current-user'; -import { defaultVisibilityFor, type VisibilityLevel } from '@mana/shared-privacy'; -import { createBlock, updateBlock } from '$lib/data/time-blocks/service'; -import type { LocalDeck } from '../types'; -import type { Deck, CreateDeckInput, UpdateDeckInput } from '../types'; - -let error = $state(null); - -export const deckStore = { - get error() { - return error; - }, - - async createDeck(input: CreateDeckInput): Promise { - error = null; - try { - const newLocal: LocalDeck = { - id: crypto.randomUUID(), - name: input.title, - description: input.description ?? null, - color: '#6366f1', - cardCount: 0, - visibility: defaultVisibilityFor(getActiveSpace()?.type), - }; - - const plaintextSnapshot = toDeck(newLocal); - await encryptRecord('cardDecks', newLocal); - await cardDeckTable.add(newLocal); - CardsEvents.deckCreated(); - return plaintextSnapshot; - } catch (err: any) { - error = err.message || 'Failed to create deck'; - console.error('Create deck error:', err); - return null; - } - }, - - async updateDeck(id: string, updates: UpdateDeckInput) { - error = null; - try { - const localUpdates: Partial = {}; - if (updates.title !== undefined) localUpdates.name = updates.title; - if (updates.description !== undefined) localUpdates.description = updates.description; - - const diff: Partial = { - ...localUpdates, - }; - await encryptRecord('cardDecks', diff); - await cardDeckTable.update(id, diff); - } catch (err: any) { - error = err.message || 'Failed to update deck'; - console.error('Update deck error:', err); - } - }, - - /** - * Flip a deck's visibility. Public decks surface in the cards - * embed-resolver on the user's website. - */ - async setVisibility(id: string, next: VisibilityLevel) { - const existing = await cardDeckTable.get(id); - if (!existing) throw new Error(`Deck ${id} not found`); - const before: VisibilityLevel = existing.visibility ?? 'space'; - if (before === next) return; - - const stamp = new Date().toISOString(); - await cardDeckTable.update(id, { - visibility: next, - visibilityChangedAt: stamp, - visibilityChangedBy: getEffectiveUserId(), - updatedAt: stamp, - }); - - emitDomainEvent('VisibilityChanged', 'cards', 'cardDecks', id, { - recordId: id, - collection: 'cardDecks', - before, - after: next, - }); - }, - - async deleteDeck(id: string) { - error = null; - try { - const now = new Date().toISOString(); - - // Atomic cascade: deck + all child cards are soft-deleted in one - // Dexie transaction. If any write fails, the whole operation aborts — - // no orphaned cards left pointing at a deleted deck. - await db.transaction('rw', cardDeckTable, cardTable, async () => { - const cards = await cardTable.where('deckId').equals(id).toArray(); - for (const card of cards) { - await cardTable.update(card.id, { deletedAt: now }); - } - await cardDeckTable.update(id, { deletedAt: now }); - }); - CardsEvents.deckDeleted(); - } catch (err: any) { - error = err.message || 'Failed to delete deck'; - console.error('Delete deck error:', err); - } - }, - - async startStudySession(deckId: string): Promise { - const deck = await cardDeckTable.get(deckId); - if (!deck) return null; - - // Don't start a second session if one is already active - if (deck.activeStudyBlockId) return deck.activeStudyBlockId; - - const decrypted = await decryptRecord('cardDecks', { ...deck }); - const deckName = decrypted?.name ?? 'Deck'; - const now = new Date().toISOString(); - - const timeBlockId = await createBlock({ - startDate: now, - endDate: null, - isLive: true, - kind: 'logged', - type: 'study', - sourceModule: 'cards', - sourceId: deckId, - title: `${deckName} lernen`, - color: '#0ea5e9', - }); - - await cardDeckTable.update(deckId, { - activeStudyBlockId: timeBlockId, - lastStudied: now, - }); - - return timeBlockId; - }, - - async endStudySession(deckId: string): Promise { - const deck = await cardDeckTable.get(deckId); - if (!deck?.activeStudyBlockId) return; - - const now = new Date().toISOString(); - await updateBlock(deck.activeStudyBlockId, { - endDate: now, - isLive: false, - }); - - await cardDeckTable.update(deckId, { - activeStudyBlockId: null, - }); - }, - - clearError() { - error = null; - }, -}; diff --git a/apps/mana/apps/web/src/lib/modules/cards/stores/reviews.svelte.ts b/apps/mana/apps/web/src/lib/modules/cards/stores/reviews.svelte.ts deleted file mode 100644 index a7b3e69d0..000000000 --- a/apps/mana/apps/web/src/lib/modules/cards/stores/reviews.svelte.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Card-Review Store — FSRS scheduling state. - * - * Reviews are plaintext (no encryptRecord) — cardReviews is in - * `plaintext-allowlist.ts` because the scheduler must query by `due` - * to find what's fällig today. - * - * Three operations the rest of the module needs: - * - ensureReviewsForCard: create the right number of subIndex rows - * for a card, soft-delete obsolete ones (e.g. when a cloze cluster - * gets removed). Idempotent — safe to call after every card edit. - * - grade: apply a user rating, persist the next FSRS state. - * - softDeleteForCard: cascade soft-delete when a card is deleted. - */ - -import { cardReviewTable } from '../collections'; -import { newReview, gradeReview as fsrsGrade } from '../fsrs'; -import { subIndexesFor } from '../card-reviews'; -import type { CardFields, CardType, LocalCardReview, ReviewGrade } from '../types'; - -let error = $state(null); - -export const reviewStore = { - get error() { - return error; - }, - - /** - * Reconcile the cardReviews rows for a card with what the card - * structurally needs. New subIndexes get a fresh review; obsolete - * ones get soft-deleted. Returns the live set of reviews. - */ - async ensureReviewsForCard(card: { - id: string; - type: CardType; - fields: CardFields; - }): Promise { - error = null; - try { - const existing = await cardReviewTable.where('cardId').equals(card.id).toArray(); - const live = existing.filter((r) => !r.deletedAt); - const liveByIdx = new Map(live.map((r) => [r.subIndex, r])); - - const wanted = subIndexesFor(card); - const wantedSet = new Set(wanted); - const nowIso = new Date().toISOString(); - - for (const subIndex of wanted) { - if (!liveByIdx.has(subIndex)) { - const r = newReview({ cardId: card.id, subIndex }); - await cardReviewTable.add(r); - liveByIdx.set(subIndex, r); - } - } - - for (const r of live) { - if (!wantedSet.has(r.subIndex)) { - await cardReviewTable.update(r.id, { deletedAt: nowIso }); - liveByIdx.delete(r.subIndex); - } - } - - return [...liveByIdx.values()].sort((a, b) => a.subIndex - b.subIndex); - } catch (err: any) { - error = err.message || 'Failed to ensure reviews'; - console.error('Ensure reviews error:', err); - return []; - } - }, - - async grade(reviewId: string, grade: ReviewGrade): Promise { - error = null; - try { - const existing = await cardReviewTable.get(reviewId); - if (!existing) return null; - const next = fsrsGrade(existing, grade); - await cardReviewTable.put(next); - return next; - } catch (err: any) { - error = err.message || 'Failed to grade review'; - console.error('Grade review error:', err); - return null; - } - }, - - async softDeleteForCard(cardId: string): Promise { - const reviews = await cardReviewTable.where('cardId').equals(cardId).toArray(); - const now = new Date().toISOString(); - for (const r of reviews) { - if (!r.deletedAt) await cardReviewTable.update(r.id, { deletedAt: now }); - } - }, - - clearError() { - error = null; - }, -}; diff --git a/apps/mana/apps/web/src/lib/modules/cards/stores/study-blocks.svelte.ts b/apps/mana/apps/web/src/lib/modules/cards/stores/study-blocks.svelte.ts deleted file mode 100644 index 888d537b0..000000000 --- a/apps/mana/apps/web/src/lib/modules/cards/stores/study-blocks.svelte.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Study-Block Store — daily aggregate of learning activity. - * - * One row per local date with counters. The streak query walks back - * from today; finding a gap (no row, or cardsReviewed=0) ends the - * streak. Plaintext, no encryption. - * - * Why a daily aggregate row instead of just summing cardReviews? - * Because the streak is a UI-hot read — we want it cheap (≤ 30 row - * lookups) regardless of how many reviews exist in total. - */ - -import { cardStudyBlockTable } from '../collections'; -import type { LocalCardStudyBlock } from '../types'; - -let error = $state(null); - -function localDateKey(d: Date = new Date()): string { - // YYYY-MM-DD in the user's local timezone — matches LocalCardStudyBlock.date. - const y = d.getFullYear(); - const m = `${d.getMonth() + 1}`.padStart(2, '0'); - const day = `${d.getDate()}`.padStart(2, '0'); - return `${y}-${m}-${day}`; -} - -export const studyBlockStore = { - get error() { - return error; - }, - - /** - * Record one review against today's block. Creates the row on the - * first review of the day. Idempotent across concurrent calls only - * within a Dexie transaction — for now we accept the small chance of - * an off-by-one race; real users grade one card at a time. - */ - async recordReview(durationMs: number, count: number = 1): Promise { - error = null; - try { - const date = localDateKey(); - const existing = await cardStudyBlockTable.where('date').equals(date).first(); - if (existing && !existing.deletedAt) { - await cardStudyBlockTable.update(existing.id, { - cardsReviewed: existing.cardsReviewed + count, - durationMs: existing.durationMs + durationMs, - }); - } else { - const row: LocalCardStudyBlock = { - id: crypto.randomUUID(), - date, - cardsReviewed: count, - durationMs, - }; - await cardStudyBlockTable.add(row); - } - } catch (err: any) { - error = err.message || 'Failed to record review'; - console.error('Record review error:', err); - } - }, - - /** - * Walk back from today; return how many consecutive days have at - * least one reviewed card. Stops at the first gap. Caps at 365 days - * to keep the worst case bounded. - */ - async getRecentStreak(): Promise { - const today = new Date(); - let streak = 0; - for (let i = 0; i < 365; i++) { - const d = new Date(today); - d.setDate(d.getDate() - i); - const row = await cardStudyBlockTable.where('date').equals(localDateKey(d)).first(); - if (!row || row.deletedAt || row.cardsReviewed <= 0) break; - streak++; - } - return streak; - }, - - clearError() { - error = null; - }, -}; diff --git a/apps/mana/apps/web/src/lib/modules/cards/stores/tags.svelte.ts b/apps/mana/apps/web/src/lib/modules/cards/stores/tags.svelte.ts deleted file mode 100644 index d960a888c..000000000 --- a/apps/mana/apps/web/src/lib/modules/cards/stores/tags.svelte.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Ucards Tags — Uses shared global tags + module-specific junction table. - */ - -import { db } from '$lib/data/database'; -import { createTagLinkOps } from '@mana/shared-stores'; - -export { - tagMutations, - useAllTags, - getTagById, - getTagsByIds, - getTagColor, -} from '@mana/shared-stores'; - -export const deckTagOps = createTagLinkOps({ - table: () => db.table('deckTags'), - entityIdField: 'deckId', -}); diff --git a/apps/mana/apps/web/src/lib/modules/cards/tools.ts b/apps/mana/apps/web/src/lib/modules/cards/tools.ts deleted file mode 100644 index 8133eb6da..000000000 --- a/apps/mana/apps/web/src/lib/modules/cards/tools.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { ModuleTool } from '$lib/data/tools/types'; -import { cardStore } from './stores/cards.svelte'; - -export const cardsTools: ModuleTool[] = [ - { - name: 'create_card', - module: 'cards', - description: 'Erstellt eine neue Lernkarte (Flashcard)', - parameters: [ - { name: 'deckId', type: 'string', description: 'ID des Decks', required: true }, - { name: 'front', type: 'string', description: 'Vorderseite (Frage)', required: true }, - { name: 'back', type: 'string', description: 'Rueckseite (Antwort)', required: true }, - ], - async execute(params) { - const card = await cardStore.createCard({ - deckId: params.deckId as string, - front: params.front as string, - back: params.back as string, - }); - return card - ? { success: true, data: card, message: 'Lernkarte erstellt' } - : { success: false, message: 'Fehler beim Erstellen der Karte' }; - }, - }, -]; diff --git a/apps/mana/apps/web/src/lib/modules/cards/types.ts b/apps/mana/apps/web/src/lib/modules/cards/types.ts deleted file mode 100644 index 263c01487..000000000 --- a/apps/mana/apps/web/src/lib/modules/cards/types.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Cardecky / cards module — types are now sourced from `@mana/cards-core` - * so the standalone cardecky.mana.how app and this in-mana module stay in sync. - * - * This file is a thin re-export to keep existing - * `from './types'` / `from '$lib/modules/cards/types'` imports working. - */ - -export type { - CardType, - CardFields, - LocalDeck, - LocalCard, - LocalCardReview, - LocalCardStudyBlock, - Deck, - Card, - CardReview, - CreateDeckInput, - UpdateDeckInput, - CreateCardInput, - UpdateCardInput, - ReviewGrade, -} from '@mana/cards-core'; diff --git a/apps/mana/apps/web/src/lib/modules/cards/views/DetailView.svelte b/apps/mana/apps/web/src/lib/modules/cards/views/DetailView.svelte deleted file mode 100644 index f3746c84b..000000000 --- a/apps/mana/apps/web/src/lib/modules/cards/views/DetailView.svelte +++ /dev/null @@ -1,147 +0,0 @@ - - - - - detail.deleteWithUndo({ - label: $_('cards.detail.toast_deleted'), - delete: () => deckStore.deleteDeck(deckId), - goBack, - })} -> - {#snippet body(deck)} - - -
-
- {$_('cards.detail.prop_color')} - -
- -
- {$_('cards.detail.prop_visibility')} - deckStore.setVisibility(deckId, next)} - disabledLevels={['unlisted']} - /> -
- -
- {$_('cards.detail.prop_cards')} - {cardCount} -
- - {#if deck.lastStudied} -
- {$_('cards.detail.prop_last_studied')} - {formatDate(new Date(deck.lastStudied))} -
- {/if} -
- -
- - -
- -
- {$_('cards.detail.meta_created', { - values: { date: formatDate(new Date(deck.createdAt ?? '')) }, - })} - {#if deck.updatedAt} - {$_('cards.detail.meta_updated', { - values: { date: formatDate(new Date(deck.updatedAt)) }, - })} - {/if} -
- {/snippet} -
diff --git a/apps/mana/apps/web/src/lib/modules/feedback/views/DetailView.svelte b/apps/mana/apps/web/src/lib/modules/feedback/views/DetailView.svelte index 70b5c6d5f..aaa00518e 100644 --- a/apps/mana/apps/web/src/lib/modules/feedback/views/DetailView.svelte +++ b/apps/mana/apps/web/src/lib/modules/feedback/views/DetailView.svelte @@ -9,6 +9,7 @@ import ItemCard from '../components/ItemCard.svelte'; import { authStore } from '$lib/stores/auth.svelte'; import { feedbackService } from '$lib/api/feedback'; + import { portalHref } from '$lib/auth/portal-redirect'; interface Props { id: string; @@ -118,7 +119,7 @@
{:else} {/if} diff --git a/apps/mana/apps/web/src/lib/modules/feedback/views/ListView.svelte b/apps/mana/apps/web/src/lib/modules/feedback/views/ListView.svelte index 8da737e80..79e821a40 100644 --- a/apps/mana/apps/web/src/lib/modules/feedback/views/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/feedback/views/ListView.svelte @@ -10,6 +10,7 @@ import { useCommunityFeed, toggleReactionOnItem } from '../queries.svelte'; import ItemCard from '../components/ItemCard.svelte'; import { authStore } from '$lib/stores/auth.svelte'; + import { portalHref } from '$lib/auth/portal-redirect'; interface Props { /** Optional initial moduleContext filter — passed by the @@ -82,7 +83,7 @@ Noch keine Stimmen — sei der erste, der was reinwirft. {#if !authStore.user}
- Login, um mitzumachen. + Login, um mitzumachen. {/if}
{:else} diff --git a/apps/mana/apps/web/src/lib/modules/profile/ListView.svelte b/apps/mana/apps/web/src/lib/modules/profile/ListView.svelte index 05270e579..bc91db851 100644 --- a/apps/mana/apps/web/src/lib/modules/profile/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/profile/ListView.svelte @@ -7,6 +7,7 @@ import { authStore } from '$lib/stores/auth.svelte'; import { toast } from '$lib/stores/toast.svelte'; import { goto } from '$app/navigation'; + import { redirectToPortal } from '$lib/auth/portal-redirect'; import { EditProfileModal, ChangePasswordModal, @@ -69,7 +70,7 @@ async function handleAccountDeleted() { toast.info($_('profile.hub.toast_account_deleting')); await authStore.signOut(); - goto('/login'); + redirectToPortal({ next: '/' }); } @@ -232,7 +233,7 @@ class="account-btn" onclick={async () => { await authStore.signOut(); - goto('/login'); + redirectToPortal({ next: '/' }); }} > {$_('profile.logout')} diff --git a/apps/mana/apps/web/src/lib/modules/website/embeds.ts b/apps/mana/apps/web/src/lib/modules/website/embeds.ts index 6e9f52247..c13677eb4 100644 --- a/apps/mana/apps/web/src/lib/modules/website/embeds.ts +++ b/apps/mana/apps/web/src/lib/modules/website/embeds.ts @@ -35,7 +35,6 @@ import type { LocalHabit, LocalHabitLog } from '$lib/modules/habits/types'; import type { LocalQuiz } from '$lib/modules/quiz/types'; import type { LocalSocialEvent } from '$lib/modules/events/types'; import type { LocalMemo } from '$lib/modules/memoro/types'; -import type { LocalDeck as LocalCardDeck } from '$lib/modules/cards/types'; import type { LocalDeck as LocalPresiDeck } from '$lib/modules/presi/types'; import type { LocalAugurEntry } from '$lib/modules/augur/types'; import type { LocalTimeBlock } from '$lib/data/time-blocks/types'; @@ -91,9 +90,11 @@ export async function resolveEmbed(props: ModuleEmbedProps): Promise { }); } -/** - * Card-decks: shareable-flashcard-collection teaser. Returns decks - * flipped to 'public' with their card count as subtitle. - * - * Whitelist: title + "N Karten". Card fronts/backs, difficulty - * scores, and review history all stay private — the deck is a - * unit; its cards belong to the play-experience (future - * unlisted-share flow), not the public teaser. - */ -async function resolveCardDecks(_props: ModuleEmbedProps): Promise { - let decks = await db.table('cardDecks').toArray(); - decks = decks.filter((d) => !d.deletedAt && canEmbedOnWebsite(d.visibility ?? 'private')); - - if (decks.length === 0) return []; - - const decrypted = (await decryptRecords('cardDecks', decks)) as LocalCardDeck[]; - - // Newest first. - decrypted.sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? '')); - - return decrypted.map((d) => { - const count = d.cardCount ?? 0; - return { - title: d.name, - subtitle: `${count} ${count === 1 ? 'Karte' : 'Karten'}`, - }; - }); -} +// resolveCardDecks: dekommissioniert 2026-05-08, Cards lebt eigenständig +// auf cardecky.mana.how. Public-Deck-Embeds für Cardecky kommen später +// über die Cardecky-API. /** * Presi-decks: "talks I've given" teaser. Returns decks flipped to diff --git a/apps/mana/apps/web/src/lib/search/providers/cards.ts b/apps/mana/apps/web/src/lib/search/providers/cards.ts deleted file mode 100644 index 12ee0046e..000000000 --- a/apps/mana/apps/web/src/lib/search/providers/cards.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { db } from '$lib/data/database'; -import { getManaApp } from '@mana/shared-branding'; -import { scoreRecord, truncateSubtitle } from '../scoring'; -import type { SearchProvider, SearchResult, SearchOptions } from '../types'; - -const app = getManaApp('cards'); - -export const cardsSearchProvider: SearchProvider = { - appId: 'cards', - appName: 'Cards', - appIcon: app?.icon, - appColor: app?.color, - searchableTypes: ['deck', 'card'], - - async search(query: string, options?: SearchOptions): Promise { - const limit = options?.limit ?? 5; - const results: SearchResult[] = []; - - // Search decks - const decks = await db.table('cardDecks').toArray(); - for (const deck of decks) { - if (deck.deletedAt) continue; - const { score, matchedField } = scoreRecord( - [ - { name: 'name', value: deck.name, weight: 1.0 }, - { name: 'description', value: deck.description, weight: 0.7 }, - ], - query - ); - if (score > 0) { - results.push({ - id: deck.id, - type: 'deck', - appId: 'cards', - title: deck.name, - subtitle: truncateSubtitle(deck.description) || 'Deck', - appIcon: app?.icon, - appColor: app?.color, - href: `/cards/${deck.id}`, - score, - matchedField, - }); - } - } - - // Search cards (front/back) - const cards = await db.table('cards').toArray(); - for (const card of cards) { - if (card.deletedAt) continue; - const { score, matchedField } = scoreRecord( - [ - { name: 'front', value: card.front, weight: 1.0 }, - { name: 'back', value: card.back, weight: 0.8 }, - ], - query - ); - if (score > 0) { - results.push({ - id: card.id, - type: 'card', - appId: 'cards', - title: truncateSubtitle(card.front, 60) || 'Karte', - subtitle: truncateSubtitle(card.back, 60), - appIcon: app?.icon, - appColor: app?.color, - href: `/cards/${card.deckId}`, - score, - matchedField, - }); - } - } - - return results.sort((a, b) => b.score - a.score).slice(0, limit); - }, -}; diff --git a/apps/mana/apps/web/src/lib/search/providers/index.ts b/apps/mana/apps/web/src/lib/search/providers/index.ts index f97ca975d..9b600e682 100644 --- a/apps/mana/apps/web/src/lib/search/providers/index.ts +++ b/apps/mana/apps/web/src/lib/search/providers/index.ts @@ -20,7 +20,7 @@ export function registerAllProviders(registry: SearchRegistry): void { ); registry.registerLazy('chat', () => import('./chat').then((m) => m.chatSearchProvider)); registry.registerLazy('storage', () => import('./storage').then((m) => m.storageSearchProvider)); - registry.registerLazy('cards', () => import('./cards').then((m) => m.cardsSearchProvider)); + // 'cards': dekommissioniert 2026-05-08 — Cards eigenständig auf cardecky.mana.how. registry.registerLazy('picture', () => import('./picture').then((m) => m.pictureSearchProvider)); registry.registerLazy('presi', () => import('./presi').then((m) => m.presiSearchProvider)); registry.registerLazy('music', () => import('./music').then((m) => m.musicSearchProvider)); diff --git a/apps/mana/apps/web/src/lib/stores/auth.svelte.ts b/apps/mana/apps/web/src/lib/stores/auth.svelte.ts index 5b3423b04..81664a5ad 100644 --- a/apps/mana/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/mana/apps/web/src/lib/stores/auth.svelte.ts @@ -1,7 +1,28 @@ /** - * Auth Store — uses centralized Mana auth factory. + * Auth Store — re-export der neuen Session-Klasse. + * + * Bis 2026-05-12 war das ein Wrapper über `createManaAuthStore()` aus + * `@mana/shared-auth-ui`, der 47 Methoden mitbrachte (Login, Register, + * Reset, Passkey-CRUD, 2FA-Setup, Sessions, Audit). Mit dem Wechsel + * auf den zentralen Auth-Portal sind die Login-/Register-/Reset-Flows + * komplett rausgeflogen — sie leben jetzt auf `auth.mana.how`. + * + * Was bleibt: + * - `authStore.user`, `authStore.isAuthenticated`, `authStore.initialized`, + * `authStore.loading` — der reaktive Session-State. + * - `authStore.initialize()` — Boot-Pass. + * - `authStore.signOut()` — Logout (kein Redirect; den macht der Caller). + * - `authStore.getValidToken()`, `authStore.getAccessToken()` — Token-Access. + * + * Was wegfällt (Settings → `$lib/auth/settings-client.ts`): + * - Passkey-CRUD → `import { passkeys } from '$lib/auth/settings-client'` + * - 2FA-Setup → `import { twoFactor } from '$lib/auth/settings-client'` + * - Sessions / Audit → `import { sessions, audit } from '$lib/auth/settings-client'` + * + * Was ganz wegfällt (passiert jetzt im Portal): + * - `signIn`, `signUp`, `resetPassword`, `resetPasswordWithToken`, + * `resendVerificationEmail`, `verifyTwoFactor`, `verifyBackupCode`, + * `sendMagicLink`, `signInWithPasskey`. */ -import { createManaAuthStore } from '@mana/shared-auth-ui'; - -export const authStore = createManaAuthStore(); +export { session as authStore } from '$lib/auth/session.svelte'; diff --git a/apps/mana/apps/web/src/lib/stores/guest-prompt.svelte.ts b/apps/mana/apps/web/src/lib/stores/guest-prompt.svelte.ts index 2fd43fcec..6e0b709ae 100644 --- a/apps/mana/apps/web/src/lib/stores/guest-prompt.svelte.ts +++ b/apps/mana/apps/web/src/lib/stores/guest-prompt.svelte.ts @@ -25,12 +25,15 @@ */ import type { BottomNotification } from '@mana/shared-ui'; +import { portalHref } from '$lib/auth/portal-redirect'; let prompts = $state([]); -/** Default action target — the login page already exposes both login - * and register flows so we point at one URL and let the user pick. */ -const DEFAULT_LOGIN_HREF = '/login'; +/** Default action target — the central auth portal (auth.mana.how) handles + * both login and register flows in one UI. */ +function defaultLoginHref(): string { + return portalHref(); +} /** Navigates the browser. Kept as a small wrapper so unit tests can * swap it out without pulling SvelteKit's `goto`. */ @@ -79,7 +82,7 @@ export const guestPrompt = { action: { label: actionLabel, onClick: () => { - navigate(DEFAULT_LOGIN_HREF); + navigate(defaultLoginHref()); guestPrompt.dismiss(id); }, }, diff --git a/apps/mana/apps/web/src/lib/types/dashboard.test.ts b/apps/mana/apps/web/src/lib/types/dashboard.test.ts index 8c3328aa8..fe31962ed 100644 --- a/apps/mana/apps/web/src/lib/types/dashboard.test.ts +++ b/apps/mana/apps/web/src/lib/types/dashboard.test.ts @@ -75,7 +75,6 @@ describe('WIDGET_REGISTRY', () => { expect(types).toContain('contacts-favorites'); expect(types).toContain('quotes-quote'); expect(types).toContain('picture-recent'); - expect(types).toContain('cards-progress'); expect(types).toContain('clock-timers'); expect(types).toContain('storage-usage'); expect(types).toContain('music-library'); diff --git a/apps/mana/apps/web/src/lib/types/dashboard.ts b/apps/mana/apps/web/src/lib/types/dashboard.ts index 3ae0f7402..820abc01a 100644 --- a/apps/mana/apps/web/src/lib/types/dashboard.ts +++ b/apps/mana/apps/web/src/lib/types/dashboard.ts @@ -19,7 +19,6 @@ export type WidgetType = | 'contacts-recent' // Contacts: recently updated | 'quotes-quote' // Quotes API: daily inspiration quote | 'picture-recent' // Picture API: recent generations - | 'cards-progress' // Cards API: learning progress | 'clock-timers' // Clock: active timers and alarms | 'storage-usage' // Storage: file storage stats | 'music-library' // Music: music library stats @@ -232,15 +231,6 @@ export const WIDGET_REGISTRY: WidgetMeta[] = [ allowMultiple: false, requiredBackend: 'picture', }, - { - type: 'cards-progress', - nameKey: 'dashboard.widgets.cards.title', - descriptionKey: 'dashboard.widgets.cards.description', - icon: '🎴', - defaultSize: 'medium', - allowMultiple: false, - requiredBackend: 'cards', - }, { type: 'clock-timers', nameKey: 'dashboard.widgets.clock.title', diff --git a/apps/mana/apps/web/src/routes/(app)/+layout.svelte b/apps/mana/apps/web/src/routes/(app)/+layout.svelte index 800fcb1d3..3fff16241 100644 --- a/apps/mana/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/mana/apps/web/src/routes/(app)/+layout.svelte @@ -7,6 +7,7 @@ import { goto } from '$app/navigation'; import { page } from '$app/stores'; + import { redirectToPortal, portalHref } from '$lib/auth/portal-redirect'; import type { Component, Snippet } from 'svelte'; import ToastContainer from '$lib/components/ToastContainer.svelte'; import FeedbackQuickModal from '$lib/components/feedback/FeedbackQuickModal.svelte'; @@ -497,7 +498,7 @@ guestMode?.destroy(); setErrorTrackingUser(null); await authStore.signOut(); - goto('/login'); + redirectToPortal({ next: '/' }); } // ── Guest Mode ────────────────────────────────────────── @@ -739,7 +740,7 @@ markAsGuest(); guestMode = createGuestMode('mana', { nudgeDelayMinutes: 3, - onRegister: () => goto('/register'), + onRegister: () => redirectToPortal({ target: 'register' }), }); } } @@ -1062,7 +1063,7 @@ {languageItems} {currentLanguageLabel} showLogout={authStore.isAuthenticated} - loginHref="/login" + loginHref={portalHref()} primaryColor="hsl(var(--color-primary))" showAppSwitcher={false} showAiTierSelector={true} @@ -1152,8 +1153,8 @@ appId="mana" visible={guestMode.showWelcome} onClose={() => guestMode?.dismissWelcome()} - onLogin={() => goto('/login')} - onRegister={() => goto('/register')} + onLogin={() => redirectToPortal()} + onRegister={() => redirectToPortal({ target: 'register' })} locale={($locale || 'de') === 'de' ? 'de' : 'en'} /> {/if} diff --git a/apps/mana/apps/web/src/routes/(app)/cards/+layout.svelte b/apps/mana/apps/web/src/routes/(app)/cards/+layout.svelte deleted file mode 100644 index 7e0db5dc8..000000000 --- a/apps/mana/apps/web/src/routes/(app)/cards/+layout.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - -{@render children()} diff --git a/apps/mana/apps/web/src/routes/(app)/cards/+page.svelte b/apps/mana/apps/web/src/routes/(app)/cards/+page.svelte deleted file mode 100644 index ae85d9971..000000000 --- a/apps/mana/apps/web/src/routes/(app)/cards/+page.svelte +++ /dev/null @@ -1,78 +0,0 @@ - - - - Cards - Mana - - - -
-
-

Cards

-

Karteikarten & Spaced Repetition

-
- - -
-
-
- -
-
-
Lerne effizienter
-
- Karteikarten erstellen, organisieren und mit Spaced Repetition lernen -
-
-
-
- - -
- {#each quickLinks as link} - -
-
- -
-
-
{link.label}
-
{link.description}
-
-
-
- {/each} -
-
-
diff --git a/apps/mana/apps/web/src/routes/(app)/cards/decks/+page.svelte b/apps/mana/apps/web/src/routes/(app)/cards/decks/+page.svelte deleted file mode 100644 index dde873bdc..000000000 --- a/apps/mana/apps/web/src/routes/(app)/cards/decks/+page.svelte +++ /dev/null @@ -1,75 +0,0 @@ - - - - Meine Decks - Cards - Mana - - - -
- -
-
-

Meine Decks

-

Organisiere deine Lernmaterialien in Decks

-
- -
- - - {#if deckStore.error} -
-

{$_('common.error_loading')}

-

{deckStore.error}

-
- {:else if (allDecks?.value ?? []).length === 0} - -
-
📚
-

Noch keine Decks

-

- Erstelle dein erstes Deck, um mit dem Lernen zu beginnen. -

- -
- {:else} - -
- {#each allDecks.value as deck (deck.id)} - handleDeckClick(deck.id)} /> - {/each} -
- {/if} -
- - - -
diff --git a/apps/mana/apps/web/src/routes/(app)/cards/decks/[id]/+page.svelte b/apps/mana/apps/web/src/routes/(app)/cards/decks/[id]/+page.svelte deleted file mode 100644 index fb1ca5de6..000000000 --- a/apps/mana/apps/web/src/routes/(app)/cards/decks/[id]/+page.svelte +++ /dev/null @@ -1,410 +0,0 @@ - - - - {deck?.title || 'Deck'} — Cards — Mana - - - - {#if deck} -
- - - -
-
-
-
-

{deck.title}

-
- {#if deck.description} -

{deck.description}

- {/if} -
- -
- {#if deck.visibility === 'public'} - Öffentlich - {/if} - - -
-
- - -
- - {#if dueCount === 0 && cards.length > 0} - - Heute alles gelernt — schau später wieder rein. - - {/if} -
- - -
-
-
{cards.length}
-
Karten gesamt
-
-
-
{dueCount}
-
Fällig
-
-
- - -
- -
- - - {#if showNewCardForm} -
-

Neue Karte

- - -
- {#each cardTypeOptions as opt} - - {/each} -
- -
- {#if newCardType === 'cloze'} -
- - - -

- Markiere mit - {{c1::Wort}} - — optional Hinweis: ::Hinweis. -

-
- {:else} -
- - - -
-
- - -
- {/if} -
- - -
-
-
- {/if} - - -
-

- Karten ({cards.length}) -

- {#if cards.length === 0} -
-
📝
-

Noch keine Karten. Erstelle deine erste Karte!

- -
- {:else} -
- {#each cards as card, i (card.id)} - {@const preview = previewSummary(card)} -
- {i + 1}. -
-
- {@html renderMarkdown(preview.primary)} -
- {#if preview.secondary} -
- {@html renderMarkdown(preview.secondary)} -
- {/if} -
-
- - {typeBadge(card.type)} - - -
-
- {/each} -
- {/if} -
- - - {#if showDeleteConfirm} -
(showDeleteConfirm = false)} - onkeydown={(e) => e.key === 'Escape' && (showDeleteConfirm = false)} - tabindex="-1" - role="presentation" - > - - -
e.stopPropagation()} - > -

Deck löschen?

-

- Möchtest du "{deck.title}" wirklich löschen? Diese Aktion kann nicht rückgängig - gemacht werden und löscht auch alle Karten in diesem Deck. -

-
- - -
-
-
- {/if} -
- {:else} -
-

Deck nicht gefunden

- -
- {/if} - - (showShare = false)} - url={shareUrl} - title={deck?.title ?? ''} - source="cards" - description={deck?.description ?? ''} - /> -
diff --git a/apps/mana/apps/web/src/routes/(app)/cards/explore/+page.svelte b/apps/mana/apps/web/src/routes/(app)/cards/explore/+page.svelte deleted file mode 100644 index 147dd12f5..000000000 --- a/apps/mana/apps/web/src/routes/(app)/cards/explore/+page.svelte +++ /dev/null @@ -1,33 +0,0 @@ - - - - Entdecken - Cards - Mana - - - -
-
-

Entdecken

-

- Offentliche Decks aus der Community entdecken -

-
- -
-
-
- -
-

Entdecken-Feature

-

- Offentliche Decks durchsuchen und entdecken — kommt bald! -

-
-
-
-
diff --git a/apps/mana/apps/web/src/routes/(app)/cards/learn/[deckId]/+page.svelte b/apps/mana/apps/web/src/routes/(app)/cards/learn/[deckId]/+page.svelte deleted file mode 100644 index 5a16ba8dc..000000000 --- a/apps/mana/apps/web/src/routes/(app)/cards/learn/[deckId]/+page.svelte +++ /dev/null @@ -1,175 +0,0 @@ - - -
-
-
- -

Lernen

-
- {#if queue.length > 0 && !finished} -
- {Math.min(currentIndex + 1, queue.length)} / {queue.length} -
- {/if} -
- - {#if empty} -
-
Alles gelernt
-

- Komm später wieder — fällige Karten erscheinen automatisch. -

- -
- {:else if finished} -
-
Session abgeschlossen
-

- {sessionCount} Karten in {Math.round((Date.now() - sessionStartedAt) / 1000)} s. -

- -
- {:else if current} - (typedAnswer = v)} - /> - - {#if !showBack} - - {:else} -
- - - - -
- {/if} - {:else} -
Lade…
- {/if} -
diff --git a/apps/mana/apps/web/src/routes/(app)/cards/progress/+page.svelte b/apps/mana/apps/web/src/routes/(app)/cards/progress/+page.svelte deleted file mode 100644 index ccaa516df..000000000 --- a/apps/mana/apps/web/src/routes/(app)/cards/progress/+page.svelte +++ /dev/null @@ -1,84 +0,0 @@ - - - - {$_('cards.progress.page_title_html')} - - - -
-
-

{$_('cards.progress.heading')}

-

{$_('cards.progress.subtitle')}

-
- - -
-
-
{decks.length}
-
{$_('cards.progress.stat_decks')}
-
-
-
{totalCards}
-
{$_('cards.progress.stat_total_cards')}
-
-
-
0
-
{$_('cards.progress.stat_due')}
-
-
- - -
-

- - - {$_('cards.progress.section_overview')} - -

- {#if decks.length === 0} -
-
🎯
-

{$_('cards.progress.empty_title')}

-

{$_('cards.progress.empty_hint')}

-
- {:else} -
- {#each decks as deck (deck.id)} -
-
-
-
-
{deck.title}
-
- {$_('cards.progress.deck_cards', { values: { n: deck.cardCount || 0 } })} -
-
-
-
-
- {formatDate(new Date(deck.updatedAt), { - day: '2-digit', - month: 'short', - })} -
-
-
- {/each} -
- {/if} -
-
-
diff --git a/apps/mana/apps/web/src/routes/(app)/citycorners/favorites/+page.svelte b/apps/mana/apps/web/src/routes/(app)/citycorners/favorites/+page.svelte index 95259e557..a63994607 100644 --- a/apps/mana/apps/web/src/routes/(app)/citycorners/favorites/+page.svelte +++ b/apps/mana/apps/web/src/routes/(app)/citycorners/favorites/+page.svelte @@ -2,6 +2,7 @@ import { Heart } from '@mana/shared-icons'; import { _ } from 'svelte-i18n'; import { authStore } from '$lib/stores/auth.svelte'; + import { portalHref } from '$lib/auth/portal-redirect'; import { RoutePage } from '$lib/components/shell'; import { favoritesStore, @@ -38,7 +39,7 @@

{$_('favorites.loginRequired')}

{$_('settings.login')} diff --git a/apps/mana/apps/web/src/routes/(app)/quotes/favorites/+page.svelte b/apps/mana/apps/web/src/routes/(app)/quotes/favorites/+page.svelte index 38b1021ce..fb8a7a2da 100644 --- a/apps/mana/apps/web/src/routes/(app)/quotes/favorites/+page.svelte +++ b/apps/mana/apps/web/src/routes/(app)/quotes/favorites/+page.svelte @@ -1,8 +1,8 @@ - - - - -{@render children()} diff --git a/apps/mana/apps/web/src/routes/(auth)/forgot-password/+page.svelte b/apps/mana/apps/web/src/routes/(auth)/forgot-password/+page.svelte deleted file mode 100644 index 3005fdda4..000000000 --- a/apps/mana/apps/web/src/routes/(auth)/forgot-password/+page.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - - - {#snippet appSlider()} - - {/snippet} - diff --git a/apps/mana/apps/web/src/routes/(auth)/login/+page.svelte b/apps/mana/apps/web/src/routes/(auth)/login/+page.svelte deleted file mode 100644 index 494d69c6f..000000000 --- a/apps/mana/apps/web/src/routes/(auth)/login/+page.svelte +++ /dev/null @@ -1,62 +0,0 @@ - - - authStore.signInWithPasskey()} - onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)} - onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)} - onSendMagicLink={(email) => authStore.sendMagicLink(email)} - {goto} - successRedirect="/" - registerPath="/register" - forgotPasswordPath="/forgot-password" - lightBackground="#f3f4f6" - darkBackground="#121212" - {translations} - {verified} - {initialEmail} - {isDark} - version={APP_VERSION} - buildTime={BUILD_TIME} -> - {#snippet headerControls()} - - {/snippet} - {#snippet appSlider()} - - {/snippet} - diff --git a/apps/mana/apps/web/src/routes/(auth)/register/+page.svelte b/apps/mana/apps/web/src/routes/(auth)/register/+page.svelte deleted file mode 100644 index bd28dde7a..000000000 --- a/apps/mana/apps/web/src/routes/(auth)/register/+page.svelte +++ /dev/null @@ -1,55 +0,0 @@ - - - - {translations.title} | Mana - - - - {#snippet appSlider()} - - {/snippet} - diff --git a/apps/mana/apps/web/src/routes/(auth)/reset-password/+page.svelte b/apps/mana/apps/web/src/routes/(auth)/reset-password/+page.svelte deleted file mode 100644 index daf10c97c..000000000 --- a/apps/mana/apps/web/src/routes/(auth)/reset-password/+page.svelte +++ /dev/null @@ -1,168 +0,0 @@ - - -
-
-

Reset Password

-

- {#if success} - Password reset successfully - {:else if hasToken} - Enter your new password - {:else} - Invalid or missing reset token - {/if} -

-
- - {#if success} - -
- - {:else if hasToken} - -
- {#if error} -
- {error} -
- {/if} - -
-
- - -

- Must be at least 12 characters -

-
- -
- - -
- -
- -
-
-
-
- {:else} - -
-
⚠️
-

- This password reset link is invalid or has expired. -

- - Request a new reset link - -
-
- {/if} -
diff --git a/apps/mana/apps/web/src/routes/auth/callback/+page.svelte b/apps/mana/apps/web/src/routes/auth/callback/+page.svelte index 325345916..d9aaa6c06 100644 --- a/apps/mana/apps/web/src/routes/auth/callback/+page.svelte +++ b/apps/mana/apps/web/src/routes/auth/callback/+page.svelte @@ -1,88 +1,71 @@ - Authenticating... - Mana + Anmeldung wird abgeschlossen – Mana
- {#if processing} -
-

Authenticating...

-

Please wait while we complete your sign-in.

- {:else if error} + {#if error}
⚠️
-

Authentication Error

+

+ Authentifizierungsfehler +

{error}

-

Redirecting you back to login...

+

+ Du wirst zurück zur Anmeldung geleitet… +

+ {:else} +
+

Anmeldung wird abgeschlossen…

+

Einen Moment bitte.

{/if}
diff --git a/apps/mana/apps/web/src/routes/auth/reset-password/+page.svelte b/apps/mana/apps/web/src/routes/auth/reset-password/+page.svelte deleted file mode 100644 index ff15f6e4c..000000000 --- a/apps/mana/apps/web/src/routes/auth/reset-password/+page.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -
-
-
-

Redirecting...

-
-
diff --git a/apps/mana/apps/web/src/routes/email-verified/+page.svelte b/apps/mana/apps/web/src/routes/email-verified/+page.svelte index c99361c89..c937da4d4 100644 --- a/apps/mana/apps/web/src/routes/email-verified/+page.svelte +++ b/apps/mana/apps/web/src/routes/email-verified/+page.svelte @@ -1,6 +1,6 @@ @@ -26,7 +26,7 @@