refactor(big-bang): cards → wordeck im gesamten Code-Layer
Some checks are pending
CI / validate (push) Waiting to run

Phase 2 des cards→wordeck Big-Bang-Rebrand:

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-17 22:39:42 +02:00
parent c77100e85a
commit 372832d266
90 changed files with 213 additions and 3716 deletions

View file

@ -340,7 +340,7 @@ sub_index 1 = back→front). Cloze hat 1 Review pro Cluster-Index.
Beim Card-Insert werden alle initialen Reviews in **einer Transaktion**
mit angelegt — siehe `apps/api/src/routes/cards.ts` POST-Handler.
`subIndexCount(type)` in `@cards/domain` ist die SoT für statische
`subIndexCount(type)` in `@wordeck/domain` ist die SoT für statische
Typen. Für Cloze siehe Subtilität #6`subIndexCountForCloze(text)` ist
die SoT, weil die Anzahl text-abhängig ist.
@ -391,14 +391,14 @@ Swap auf mana-auth:
### 6. Cloze-Karten haben N Reviews — sub_index pro Cluster
`subIndexCount('cloze')` wirft bewusst, weil die Anzahl text-abhängig
ist. Caller müssen `subIndexCountForCloze(text)` aus `@cards/domain`
ist. Caller müssen `subIndexCountForCloze(text)` aus `@wordeck/domain`
nutzen. Cluster werden nach numerischer ID aufsteigend sortiert
(`{{c1::…}}` = sub_index 0, `{{c2::…}}` = 1, …). Der Card-POST-Handler
lehnt `type=cloze` ohne mindestens ein Cluster mit 422 ab — eine Cloze
ohne `{{cN::…}}`-Markup ist sinnlos.
Render-Helpers (`renderClozePrompt` / `renderClozeAnswer`) leben in
`@cards/domain/src/cloze.ts`, sind 12-fach unit-getestet und werden
`@wordeck/domain/src/cloze.ts`, sind 12-fach unit-getestet und werden
vom Study-View dünn konsumiert. Hint-Markup (`{{c1::answer::hint}}`)
wird MVP-stumm gedroppt — Hint-Anzeige ist Phase-9-Polish.
@ -470,7 +470,7 @@ Jede neue Karte bekommt einen SHA-256-`content_hash` über
genutzt vom Anki-Re-Import-Dedupe (`/api/v1/cards/hashes` lädt
nur die Hash-Liste, der Importer dedupliziert clientseitig).
`cardContentHash()` in `@cards/domain` ist deterministisch und
`cardContentHash()` in `@wordeck/domain` ist deterministisch und
field-order-invariant. Cluster-Markup, Whitespace und Hint-
Annotationen zählen mit — bewusst, weil zwei Cards mit dem Text
`Paris ist Hauptstadt` und `{{c1::Paris}} ist Hauptstadt` sind

View file

@ -1,9 +1,9 @@
{
"name": "@cards/api",
"name": "@wordeck/api",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Cards-API — Hono+Bun-Backend für die Greenfield-Cards-App. Spricht mana-Plattform-Services über HTTP.",
"description": "Cards-API \u2014 Hono+Bun-Backend f\u00fcr die Greenfield-Cards-App. Spricht mana-Plattform-Services \u00fcber HTTP.",
"scripts": {
"dev": "bun run --hot src/index.ts",
"start": "bun run src/index.ts",
@ -20,7 +20,7 @@
"drizzle:bootstrap-tracking": "bun run scripts/bootstrap-drizzle-tracking.ts"
},
"dependencies": {
"@cards/domain": "workspace:*",
"@wordeck/domain": "workspace:*",
"drizzle-orm": "0.38",
"hono": "^4.6.0",
"jose": "^6.2.3",

View file

@ -4,4 +4,4 @@
import { pgSchema } from 'drizzle-orm/pg-core';
export const cardsSchema = pgSchema('cards');
export const cardsSchema = pgSchema('wordeck');

View file

@ -6,7 +6,7 @@ import { cardsSchema } from './_schema.ts';
/**
* Decks Sammlungen von Karten. Eine Karte gehört zu genau einem Deck.
* `fsrs_settings` ist ein JSONB-Slot für per-Deck-Overrides der globalen
* FSRS-Defaults (siehe @cards/domain `FsrsSettings`).
* FSRS-Defaults (siehe @wordeck/domain `FsrsSettings`).
*/
export const decks = cardsSchema.table(
'decks',

View file

@ -29,7 +29,7 @@ import { marketplaceSchema } from './_schema.ts';
import { authors } from './authors.ts';
/**
* Spiegelt `CardType` aus `@cards/domain`. Enum lebt im
* Spiegelt `CardType` aus `@wordeck/domain`. Enum lebt im
* `marketplace`-pgSchema (nicht im default `public`), damit
* drizzle-kit-Push mit `schemaFilter: ['cards', 'marketplace']`
* keine spurious CREATE-TYPE-Versuche macht.
@ -125,7 +125,7 @@ export const publicDeckCards = marketplaceSchema.table(
versionId: uuid('version_id')
.notNull()
.references(() => publicDeckVersions.id, { onDelete: 'cascade' }),
// Spiegelt @cards/domain CardType.
// Spiegelt @wordeck/domain CardType.
type: cardTypeEnum('type').notNull(),
// Free-form key/value bag.
// basic / basic-reverse / type-in: { front, back }
@ -135,7 +135,7 @@ export const publicDeckCards = marketplaceSchema.table(
// SHA-256 über canonical(type, fields). Subscriber nutzen den
// Hash für per-Karten-Smart-Merge — unveränderte Karten behalten
// ihren FSRS-State über Versions-Pulls hinweg. **Identisch zur
// Berechnung in `@cards/domain` `cardContentHash`** — das ist der
// Berechnung in `@wordeck/domain` `cardContentHash`** — das ist der
// SoT, dieser Hash ist nur die persistierte Form.
contentHash: text('content_hash').notNull(),
},

View file

@ -43,7 +43,7 @@ app.use(
if (/^https?:\/\/localhost(:\d+)?$/.test(origin)) return origin;
if (/^https?:\/\/127\.0\.0\.1(:\d+)?$/.test(origin)) return origin;
}
if (origin === 'https://cardecky.mana.how') return origin;
if (origin === 'https://wordeck.com') return origin;
return null;
},
allowHeaders: ['Content-Type', 'X-User-Id', 'Authorization', 'X-Service-Key'],
@ -83,15 +83,15 @@ app.route('/api/v1/marketplace/decks', marketplaceDecksRouter());
app.get('/', (c) =>
c.json({
app: 'cards',
version: process.env.CARDS_API_VERSION ?? '0.0.0',
app: 'wordeck',
version: process.env.WORDECK_API_VERSION ?? '0.0.0',
see: '/.well-known/mana-app.json',
})
);
const port = Number(process.env.CARDS_API_PORT ?? 3081);
const port = Number(process.env.WORDECK_API_PORT ?? 3081);
console.log(`[cards-api] listening on http://localhost:${port}`);
console.log(`[wordeck-api] listening on http://localhost:${port}`);
export default {
port,

View file

@ -1,6 +1,6 @@
/**
* Per-Version-Content-Hash. Pro Karte wird `cardContentHash` aus
* `@cards/domain` benutzt (gemeinsame Source-of-Truth zwischen privatem
* `@wordeck/domain` benutzt (gemeinsame Source-of-Truth zwischen privatem
* Lern-Stack und Marketplace) pro Version hashen wir die geordnete
* Liste von Karten-Hashes plus deren Ordinal-Position.
*
@ -17,7 +17,7 @@
* privaten `cards.cards.content_hash` matchen können soll.
*/
import { cardContentHash } from '@cards/domain';
import { cardContentHash } from '@wordeck/domain';
export interface OrderedCardForHash {
type: string;

View file

@ -1,4 +1,4 @@
import { newReview } from '@cards/domain';
import { newReview } from '@wordeck/domain';
export function makeInitialReviewRows(params: {
userId: string;

View file

@ -3,7 +3,7 @@ import { and, eq, sql } from 'drizzle-orm';
import type { CardsDb } from '../db/connection.ts';
import { cards, decks } from '../db/schema/index.ts';
const APP_BASE_URL = process.env.CARDS_PUBLIC_URL ?? 'https://cardecky.mana.how';
const APP_BASE_URL = process.env.WORDECK_PUBLIC_URL ?? 'https://cardecky.mana.how';
export type SearchHit = {
id: string;

View file

@ -38,7 +38,7 @@ export function tierAtLeast(have: Tier, need: Tier): boolean {
const MANA_AUTH_URL = process.env.MANA_AUTH_URL ?? 'https://auth.mana.how';
// Fail-secure: opt-in, nicht opt-out. Vergessene env-var ⇒ Bypass AUS.
// Tests setzen die Variable via vitest.config.ts → tests/setup.ts.
const ALLOW_DEV_STUB = process.env.CARDS_AUTH_DEV_STUB === 'true';
const ALLOW_DEV_STUB = process.env.WORDECK_AUTH_DEV_STUB === 'true';
let jwksCache: ReturnType<typeof createRemoteJWKSet> | null = null;
function getJwks() {
@ -118,7 +118,7 @@ export function getUserId(c: Context<{ Variables: AuthVars }>): string {
* Tier-Gate für Premium-Features. Nutze als Hono-Middleware:
* r.post('/premium-thing', requireTier('beta'), async (c) => )
* Liefert 403 wenn der eingeloggte User nicht mindestens den
* verlangten Tier hat. Aktuell auf cards-api nicht aktiv genutzt
* verlangten Tier hat. Aktuell auf wordeck-api nicht aktiv genutzt
* (Cards-MVP ist tier-frei für authentifizierte User), aber das
* Plumbing ist da für künftige Premium-Pfade (z.B. AI-Bulk-Convert).
*/

View file

@ -5,7 +5,7 @@ import type { MiddlewareHandler } from 'hono';
* service-zu-service-Calls.
*
* Heute (Phase 5): vergleicht `X-Service-Key`-Header per
* constant-time-Compare gegen `process.env.CARDS_DSGVO_SERVICE_KEY`.
* constant-time-Compare gegen `process.env.WORDECK_DSGVO_SERVICE_KEY`.
*
* Phase F-1: ersetzt durch Verifikation gegen mana-auth's
* `apps.app_service_keys` Tabelle (caller-App = `mana-admin`).

View file

@ -7,7 +7,7 @@ import {
cardContentHash,
subIndexCount,
subIndexCountForCloze,
} from '@cards/domain';
} from '@wordeck/domain';
import { makeInitialReviewRows } from '../lib/reviews.ts';
import { toCardDto } from '../lib/dto.ts';

View file

@ -2,7 +2,7 @@ import { eq } from 'drizzle-orm';
import { Hono } from 'hono';
import { z } from 'zod';
import { cardContentHash, subIndexCount } from '@cards/domain';
import { cardContentHash, subIndexCount } from '@wordeck/domain';
import { makeInitialReviewRows } from '../lib/reviews.ts';

View file

@ -2,7 +2,7 @@ import { and, eq, isNotNull, isNull, ne } from 'drizzle-orm';
import { sql } from 'drizzle-orm';
import { Hono } from 'hono';
import { DeckCreateSchema, DeckUpdateSchema } from '@cards/domain';
import { DeckCreateSchema, DeckUpdateSchema } from '@wordeck/domain';
import { getDb, type CardsDb } from '../db/connection.ts';
import { cards, decks, publicDecks } from '../db/schema/index.ts';

View file

@ -42,7 +42,7 @@ export async function buildUserExport(db: CardsDb, userId: string) {
user_id: userId,
exported_at: new Date().toISOString(),
app: 'wordeck',
app_version: process.env.CARDS_API_VERSION ?? '0.0.0',
app_version: process.env.WORDECK_API_VERSION ?? '0.0.0',
data: {
decks: decksRows.map((d) => ({
...d,

View file

@ -25,7 +25,7 @@ healthRoute.get('/healthz/details', async (c) => {
{
status: allOk ? 'ok' : 'degraded',
app: 'wordeck',
version: process.env.CARDS_API_VERSION ?? '0.0.0',
version: process.env.WORDECK_API_VERSION ?? '0.0.0',
uptime_s: Math.floor(process.uptime()),
checks: {
db: dbProbe,
@ -38,8 +38,8 @@ healthRoute.get('/healthz/details', async (c) => {
healthRoute.get('/version', (c) =>
c.json({
app: 'wordeck',
version: process.env.CARDS_API_VERSION ?? '0.0.0',
build: process.env.CARDS_BUILD_SHA ?? 'dev',
version: process.env.WORDECK_API_VERSION ?? '0.0.0',
build: process.env.WORDECK_BUILD_SHA ?? 'dev',
})
);

View file

@ -2,7 +2,7 @@ import { and, eq } from 'drizzle-orm';
import { Hono } from 'hono';
import { z } from 'zod';
import { cardContentHash } from '@cards/domain';
import { cardContentHash } from '@wordeck/domain';
import { getDb, type CardsDb } from '../../db/connection.ts';
import {
@ -34,7 +34,7 @@ import { hashVersionCards } from '../../lib/marketplace/version-hash.ts';
*
* Geschichte: ported aus
* `cards-decommission-base:services/cards-server/src/{routes,services}/decks.ts`,
* mit Greenfield-Anpassungen (Hashing über `@cards/domain`,
* mit Greenfield-Anpassungen (Hashing über `@wordeck/domain`,
* AI-Mod-Stub statt mana-llm-Call, Auth-Vars-Shape).
*/

View file

@ -7,7 +7,7 @@ import {
newReview,
subIndexCount,
subIndexCountForCloze,
} from '@cards/domain';
} from '@wordeck/domain';
import { getDb, type CardsDb } from '../../db/connection.ts';
import { cards, decks, reviews } from '../../db/schema/index.ts';
@ -42,7 +42,7 @@ import { computeDiff } from '../../lib/marketplace/diff.ts';
* Architektur-Begründung:
* - **content_hash-basierter Dedupe** ist die Smart-Merge-Magic:
* `cards.cards.content_hash` matcht `marketplace.deck_cards.content_hash`,
* identisch berechnet via `@cards/domain.cardContentHash`.
* identisch berechnet via `@wordeck/domain.cardContentHash`.
* Unveränderte Karten = identisches Hash schon da Insert
* übersprungen FSRS bleibt.
* - **Removed-Cards bleiben lokal**: anders als das alte Dexie-Sync
@ -305,7 +305,7 @@ export function forkRouter(deps: MarketplaceForkDeps = {}): Hono<{ Variables: Au
// Marketplace-Karte). Unveränderte Karten existieren schon
// privat — wir können den Insert sicher überspringen, weil
// (deck_id, content_hash) im privaten cards.cards einzigartig
// ist (gleiche Hash-Funktion via @cards/domain).
// ist (gleiche Hash-Funktion via @wordeck/domain).
// Removed-Cards bleiben lokal (server-authoritative User-Choice).
const now = new Date();

View file

@ -2,7 +2,7 @@ import { and, asc, desc, eq } from 'drizzle-orm';
import { Hono } from 'hono';
import { z } from 'zod';
import { cardContentHash } from '@cards/domain';
import { cardContentHash } from '@wordeck/domain';
import { getDb, type CardsDb } from '../../db/connection.ts';
import {

View file

@ -5,7 +5,7 @@ import {
GradeReviewInputSchema,
gradeReview,
type Review as DomainReview,
} from '@cards/domain';
} from '@wordeck/domain';
import { getDb, type CardsDb } from '../db/connection.ts';
import { cards, decks, reviews } from '../db/schema/index.ts';

View file

@ -3,13 +3,13 @@ import { Hono } from 'hono';
import {
SEARCH_ENVELOPE_VERSION,
type SearchResultEnvelope,
} from '@cards/domain';
} from '@wordeck/domain';
import { getDb, type CardsDb } from '../db/connection.ts';
import { authMiddleware, type AuthVars } from '../middleware/auth.ts';
import { searchUserCards } from '../lib/search.ts';
const APP_VERSION = process.env.CARDS_API_VERSION ?? '0.0.0';
const APP_VERSION = process.env.WORDECK_API_VERSION ?? '0.0.0';
export type SearchDeps = { db?: CardsDb };
@ -43,7 +43,7 @@ export function searchRouter(deps: SearchDeps = {}): Hono<{ Variables: AuthVars
const envelope: SearchResultEnvelope = {
envelope_version: SEARCH_ENVELOPE_VERSION,
query: q,
app: 'cards',
app: 'wordeck',
app_version: APP_VERSION,
results: hits.map((h) => ({
id: h.id,

View file

@ -1,6 +1,6 @@
import { Hono } from 'hono';
import { parseEnvelope, validatePayloadForType } from '@cards/domain';
import { parseEnvelope, validatePayloadForType } from '@wordeck/domain';
import manifest from '../../../../app-manifest.json' with { type: 'json' };

View file

@ -7,7 +7,7 @@ import {
cardContentHash,
subIndexCount,
subIndexCountForCloze,
} from '@cards/domain';
} from '@wordeck/domain';
import { makeInitialReviewRows } from '../lib/reviews.ts';
@ -17,8 +17,8 @@ import { authMiddleware, type AuthVars } from '../middleware/auth.ts';
import { ulid } from '../lib/ulid.ts';
import { searchUserCards } from '../lib/search.ts';
const APP_BASE_URL = process.env.CARDS_PUBLIC_URL ?? 'https://wordeck.com';
const APP_VERSION = process.env.CARDS_API_VERSION ?? '0.0.0';
const APP_BASE_URL = process.env.WORDECK_PUBLIC_URL ?? 'https://wordeck.com';
const APP_VERSION = process.env.WORDECK_API_VERSION ?? '0.0.0';
export type ToolsDeps = { db?: CardsDb };
@ -132,7 +132,7 @@ export function toolsRouter(deps: ToolsDeps = {}): Hono<{ Variables: AuthVars }>
})),
total: hits.length,
took_ms: tookMs,
app: 'cards',
app: 'wordeck',
app_version: APP_VERSION,
base_url: APP_BASE_URL,
});

View file

@ -17,7 +17,7 @@
const CREDITS_URL = process.env.MANA_CREDITS_URL ?? 'https://credits.mana.how';
function authHeader(): Record<string, string> {
const key = process.env.CARDS_MANA_SERVICE_KEY;
const key = process.env.WORDECK_MANA_SERVICE_KEY;
if (!key) return {};
return { 'X-Service-Key': key };
}

View file

@ -13,7 +13,7 @@
*/
const LLM_URL = process.env.MANA_LLM_URL ?? 'https://llm.mana.how';
const LLM_API_KEY = process.env.CARDS_LLM_API_KEY ?? '';
const LLM_API_KEY = process.env.WORDECK_LLM_API_KEY ?? '';
export interface ChatMessage {
role: 'system' | 'user' | 'assistant';

View file

@ -1,5 +1,5 @@
import { newReview, subIndexCount } from '@cards/domain';
import type { QuotePayload, TextPayload, LinkPayload } from '@cards/domain';
import { newReview, subIndexCount } from '@wordeck/domain';
import type { QuotePayload, TextPayload, LinkPayload } from '@wordeck/domain';
import type { CardsDb } from '../db/connection.ts';
import { cards, reviews } from '../db/schema/index.ts';
@ -15,7 +15,7 @@ export type HandlerResult = {
resulting_id: string;
};
const APP_BASE_URL = process.env.CARDS_PUBLIC_URL ?? 'https://wordeck.com';
const APP_BASE_URL = process.env.WORDECK_PUBLIC_URL ?? 'https://wordeck.com';
/**
* Legt eine basic-Karte mit (front,back) im Inbox-Deck an, inkl.

View file

@ -4,18 +4,18 @@ import { Hono } from 'hono';
import { dsgvoRouter } from '../src/routes/dsgvo.ts';
import type { CardsDb } from '../src/db/connection.ts';
const ORIG_ENV = process.env.CARDS_DSGVO_SERVICE_KEY;
const ORIG_ENV = process.env.WORDECK_DSGVO_SERVICE_KEY;
const TEST_KEY = 'msk_test_dsgvo_key_42';
beforeEach(() => {
process.env.CARDS_DSGVO_SERVICE_KEY = TEST_KEY;
process.env.WORDECK_DSGVO_SERVICE_KEY = TEST_KEY;
});
afterEach(() => {
if (ORIG_ENV === undefined) {
delete process.env.CARDS_DSGVO_SERVICE_KEY;
delete process.env.WORDECK_DSGVO_SERVICE_KEY;
} else {
process.env.CARDS_DSGVO_SERVICE_KEY = ORIG_ENV;
process.env.WORDECK_DSGVO_SERVICE_KEY = ORIG_ENV;
}
});
@ -98,7 +98,7 @@ describe('dsgvoRouter — Service-Key-Gate', () => {
describe('dsgvoRouter — Key-not-configured Pfad', () => {
it('Wenn ENV fehlt → 500 service_key_not_configured', async () => {
delete process.env.CARDS_DSGVO_SERVICE_KEY;
delete process.env.WORDECK_DSGVO_SERVICE_KEY;
const { app } = buildApp();
const res = await app.request('/api/v1/dsgvo/export?user_id=u-1', {
headers: { 'X-Service-Key': 'whatever' },

View file

@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { cardContentHash } from '@cards/domain';
import { cardContentHash } from '@wordeck/domain';
import { hashVersionCards } from '../src/lib/marketplace/version-hash.ts';
@ -38,7 +38,7 @@ describe('hashVersionCards', () => {
expect(inOrder).toBe(reversedInput);
});
it('uses cardContentHash for per-card identity (consumes @cards/domain SoT)', async () => {
it('uses cardContentHash for per-card identity (consumes @wordeck/domain SoT)', async () => {
// Smoke: changing fields without changing the type must change the
// per-card hash (cardContentHash semantics) and therefore the
// version hash.

View file

@ -4,6 +4,6 @@
// Aktiviert den X-User-Id-Dev-Stub für Tests, weil die Suiten ohne
// echten mana-auth-JWKS laufen. In Produktion ist der Stub fail-secure
// AUS (siehe apps/api/src/middleware/auth.ts).
if (process.env.CARDS_AUTH_DEV_STUB === undefined) {
process.env.CARDS_AUTH_DEV_STUB = 'true';
if (process.env.WORDECK_AUTH_DEV_STUB === undefined) {
process.env.WORDECK_AUTH_DEV_STUB = 'true';
}

View file

@ -1,41 +1,41 @@
{
"name": "@cards/web",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Cards-Web — SvelteKit 2 + Svelte 5 Frontend für cardecky.mana.how.",
"scripts": {
"dev": "vite dev --port 3082 --host",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"test": "vitest run --passWithNoTests",
"lint": "echo 'lint configured later (eslint flat-config)'",
"clean": "rm -rf .svelte-kit build .turbo"
},
"dependencies": {
"@cards/domain": "workspace:*",
"dompurify": "^3.4.2",
"jszip": "^3.10.1",
"marked": "^18.0.3",
"sql.js": "^1.14.1",
"@mana/themes": "^0.1.0",
"@mana/shared-ui-2": "^0.1.0",
"@mana/shared-icons": "^1.0.0"
},
"devDependencies": {
"@sveltejs/adapter-node": "^5.2.0",
"@sveltejs/kit": "^2.8.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@tailwindcss/vite": "^4.2.4",
"@types/dompurify": "^3.2.0",
"@types/jszip": "^3.4.1",
"@types/sql.js": "^1.4.11",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.2.4",
"vite": "^5.4.0",
"vitest": "^2.1.0"
}
"name": "@wordeck/web",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Cards-Web \u2014 SvelteKit 2 + Svelte 5 Frontend f\u00fcr cardecky.mana.how.",
"scripts": {
"dev": "vite dev --port 3082 --host",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"test": "vitest run --passWithNoTests",
"lint": "echo 'lint configured later (eslint flat-config)'",
"clean": "rm -rf .svelte-kit build .turbo"
},
"dependencies": {
"@wordeck/domain": "workspace:*",
"dompurify": "^3.4.2",
"jszip": "^3.10.1",
"marked": "^18.0.3",
"sql.js": "^1.14.1",
"@mana/themes": "^0.1.0",
"@mana/shared-ui-2": "^0.1.0",
"@mana/shared-icons": "^1.0.0"
},
"devDependencies": {
"@sveltejs/adapter-node": "^5.2.0",
"@sveltejs/kit": "^2.8.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@tailwindcss/vite": "^4.2.4",
"@types/dompurify": "^3.2.0",
"@types/jszip": "^3.4.1",
"@types/sql.js": "^1.4.11",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.2.4",
"vite": "^5.4.0",
"vitest": "^2.1.0"
}
}

View file

@ -37,7 +37,7 @@ export const handle: Handle = async ({ event, resolve }) => {
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
const cspHeader =
process.env.CARDS_CSP_ENFORCE === 'true'
process.env.WORDECK_CSP_ENFORCE === 'true'
? 'Content-Security-Policy'
: 'Content-Security-Policy-Report-Only';
response.headers.set(cspHeader, CSP_POLICY);

View file

@ -14,7 +14,7 @@
* geladen, Duplikate werden gezählt und übersprungen.
*/
import { cardContentHash } from '@cards/domain';
import { cardContentHash } from '@wordeck/domain';
import { createDeck } from '$lib/api/decks.ts';
import { createCard, listCardHashes } from '$lib/api/cards.ts';
import { sanitizeAnkiHtml, type ParsedAnki } from './parse.ts';

View file

@ -18,14 +18,14 @@
* mana-monorepo/apps/cards/apps/web/src/lib/anki/parse.ts (commit
* ~Mai 2026). Anki-Format-Logik ist standalone Parser-Code ohne
* Architektur-Übernahme die Kopie spart 2-3 Tage Re-Implementierung
* bei null Strategy-Risiko. CardType-Import auf @cards/domain
* bei null Strategy-Risiko. CardType-Import auf @wordeck/domain
* umgestellt, Doc-Kommentar an Phase-8-Scope angepasst.
* --------------------------------------------------------------------
*/
import JSZip, { type JSZipObject } from 'jszip';
import initSqlJs, { type Database } from 'sql.js';
import type { CardType } from '@cards/domain';
import type { CardType } from '@wordeck/domain';
export interface ParsedDeck {
ankiId: string; // Anki's numeric deck id, stringified

View file

@ -1,4 +1,4 @@
import type { Card, CardCreate, CardUpdate } from '@cards/domain';
import type { Card, CardCreate, CardUpdate } from '@wordeck/domain';
import { api } from './client.ts';
export function listCards(deckId?: string) {

View file

@ -20,7 +20,7 @@ export const API_BASE = (() => {
const fromPublic = publicEnv.PUBLIC_CARDS_API_URL;
if (fromPublic) return fromPublic;
if (typeof window !== 'undefined') return 'http://localhost:3081';
return process.env.CARDS_API_URL ?? 'http://localhost:3081';
return process.env.WORDECK_API_URL ?? 'http://localhost:3081';
})();
export class ApiError extends Error {
@ -29,7 +29,7 @@ export class ApiError extends Error {
readonly body: unknown,
message?: string
) {
super(message ?? `cards-api ${status}`);
super(message ?? `wordeck-api ${status}`);
}
}

View file

@ -1,4 +1,4 @@
import type { Deck, DeckCreate, DeckUpdate } from '@cards/domain';
import type { Deck, DeckCreate, DeckUpdate } from '@wordeck/domain';
import { api, apiForm } from './client.ts';
export function listDecks(opts: { forkedFromMarketplace?: boolean; archived?: boolean } = {}) {

View file

@ -1,4 +1,4 @@
import type { Deck } from '@cards/domain';
import type { Deck } from '@wordeck/domain';
import { listDecks } from './decks.ts';
import { listCards } from './cards.ts';

View file

@ -1,4 +1,4 @@
import type { Card, Rating, Review } from '@cards/domain';
import type { Card, Rating, Review } from '@wordeck/domain';
import { api } from './client.ts';
export type DueReview = Review & {

View file

@ -1,7 +1,7 @@
/**
* Auth-Session für cards-web.
* Auth-Session für wordeck-web.
*
* Login-Flow: cards-web redirectet zu auth.mana.how (mana-auth-web),
* Login-Flow: wordeck-web redirectet zu auth.mana.how (mana-auth-web),
* dort setzt mana-auth den SSO-Cookie, dann Redirect zurück zu
* /auth/callback, welcher via `tryRefresh()` + `loadUserFromToken()`
* den JWT holt und in localStorage speichert.

View file

@ -1,5 +1,5 @@
<script lang="ts">
import type { DeckCategoryId } from '@cards/domain';
import type { DeckCategoryId } from '@wordeck/domain';
import {
Globe,
Heartbeat,

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { Card, Deck } from '@cards/domain';
import type { Card, Deck } from '@wordeck/domain';
import { t, tn } from '$lib/i18n/index.svelte.ts';
import { stackLayers } from '$lib/utils/deck-tilt';
import CardSurface from './CardSurface.svelte';

View file

@ -1,5 +1,5 @@
<script lang="ts">
import type { Deck } from '@cards/domain';
import type { Deck } from '@wordeck/domain';
import DeckStack from './DeckStack.svelte';
import NewDeckCard from './NewDeckCard.svelte';
import { t } from '$lib/i18n/index.svelte.ts';

View file

@ -1,6 +1,6 @@
<script lang="ts">
import type { Deck } from '@cards/domain';
import { DECK_CATEGORY_LABELS } from '@cards/domain';
import type { Deck } from '@wordeck/domain';
import { DECK_CATEGORY_LABELS } from '@wordeck/domain';
import { stackLayers } from '$lib/utils/deck-tilt';
import { t, tn } from '$lib/i18n/index.svelte.ts';
import { archiveDeck, unarchiveDeck } from '$lib/api/decks.ts';

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { Rating } from '@cards/domain';
import type { Rating } from '@wordeck/domain';
import { fetchDistractors } from '$lib/api/decks.ts';
let {

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { type DeckCategoryId, DECK_CATEGORY_IDS, DECK_CATEGORY_LABELS } from '@cards/domain';
import { type DeckCategoryId, DECK_CATEGORY_IDS, DECK_CATEGORY_LABELS } from '@wordeck/domain';
import { createDeck, generateDeck, generateDeckFromImage } from '$lib/api/decks.ts';
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
import { toasts } from '$lib/stores/toasts.svelte.ts';

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { checkTypingAnswer, type TypingMatchResult } from '@cards/domain';
import type { Rating } from '@cards/domain';
import { checkTypingAnswer, type TypingMatchResult } from '@wordeck/domain';
import type { Rating } from '@wordeck/domain';
let {
promptHtml,

View file

@ -1,7 +1,7 @@
<script lang="ts">
import type { DeckListEntry } from '$lib/api/marketplace.ts';
import type { DeckCategoryId } from '@cards/domain';
import { DECK_CATEGORY_IDS } from '@cards/domain';
import type { DeckCategoryId } from '@wordeck/domain';
import { DECK_CATEGORY_IDS } from '@wordeck/domain';
import { stackLayers, deterministicRandoms } from '$lib/utils/deck-tilt';
import { Star } from '@mana/shared-icons';
import CardSurface from '$lib/components/CardSurface.svelte';

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { Card, Deck } from '@cards/domain';
import type { Card, Deck } from '@wordeck/domain';
import {
getMyAuthorProfile,
getMarketplaceDeck,

View file

@ -1,4 +1,4 @@
import type { Card } from '@cards/domain';
import type { Card } from '@wordeck/domain';
export function exportToCsv(cards: Card[]): string {
const rows: string[] = ['type,front,back'];

View file

@ -1,4 +1,4 @@
import { cardContentHash } from '@cards/domain';
import { cardContentHash } from '@wordeck/domain';
import { createDeck } from '$lib/api/decks.ts';
import { createCard, listCardHashes } from '$lib/api/cards.ts';
import type { ParsedCsvCard } from './parse.ts';

View file

@ -7,7 +7,7 @@
renderClozePrompt,
type Card,
type CardType,
} from '@cards/domain';
} from '@wordeck/domain';
import { apiErrorMessage } from '$lib/api/error.ts';
import { getCard, updateCard, deleteCard } from '$lib/api/cards.ts';
import { devUser } from '$lib/auth/dev-stub.svelte.ts';

View file

@ -6,7 +6,7 @@
extractClusterIds,
renderClozePrompt,
type CardType,
} from '@cards/domain';
} from '@wordeck/domain';
import { createCard } from '$lib/api/cards.ts';
import { apiErrorMessage } from '$lib/api/error.ts';
import { listDecks, getDeck } from '$lib/api/decks.ts';

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import type { Deck } from '@cards/domain';
import type { Deck } from '@wordeck/domain';
import { listDecks } from '$lib/api/decks.ts';
import { listCards } from '$lib/api/cards.ts';
import { listDueReviews } from '$lib/api/reviews.ts';

View file

@ -2,8 +2,8 @@
import { onMount } from 'svelte';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import type { Card, Deck } from '@cards/domain';
import { type DeckCategoryId, DECK_CATEGORY_IDS, DECK_CATEGORY_LABELS } from '@cards/domain';
import type { Card, Deck } from '@wordeck/domain';
import { type DeckCategoryId, DECK_CATEGORY_IDS, DECK_CATEGORY_LABELS } from '@wordeck/domain';
import { getDeck, updateDeck } from '$lib/api/decks.ts';
import { listCards, deleteCard } from '$lib/api/cards.ts';
import { listDueReviews } from '$lib/api/reviews.ts';

View file

@ -2,8 +2,8 @@
import { onMount } from 'svelte';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import type { Deck } from '@cards/domain';
import { type DeckCategoryId, DECK_CATEGORY_IDS, DECK_CATEGORY_LABELS } from '@cards/domain';
import type { Deck } from '@wordeck/domain';
import { type DeckCategoryId, DECK_CATEGORY_IDS, DECK_CATEGORY_LABELS } from '@wordeck/domain';
import {
getDeck,
updateDeck,

View file

@ -2,7 +2,7 @@
import { onMount } from 'svelte';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import type { Card, Deck } from '@cards/domain';
import type { Card, Deck } from '@wordeck/domain';
import { getDeck } from '$lib/api/decks.ts';
import { listCards } from '$lib/api/cards.ts';
import { devUser } from '$lib/auth/dev-stub.svelte.ts';

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import { type DeckCategoryId, DECK_CATEGORY_IDS, DECK_CATEGORY_LABELS } from '@cards/domain';
import { type DeckCategoryId, DECK_CATEGORY_IDS, DECK_CATEGORY_LABELS } from '@wordeck/domain';
import { createDeck, generateDeck, generateDeckFromImage } from '$lib/api/decks.ts';
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
import { toasts } from '$lib/stores/toasts.svelte.ts';

View file

@ -5,7 +5,7 @@
import { listDecks } from '$lib/api/decks.ts';
import { pullUpdate } from '$lib/api/marketplace.ts';
import type { Deck } from '@cards/domain';
import type { Deck } from '@wordeck/domain';
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
import EmptyState from '$lib/components/marketplace/EmptyState.svelte';
import { toasts } from '$lib/stores/toasts.svelte.ts';

View file

@ -7,7 +7,7 @@
renderClozePrompt,
renderClozeAnswer,
type Rating,
} from '@cards/domain';
} from '@wordeck/domain';
import { apiErrorMessage } from '$lib/api/error.ts';
import { getDeck } from '$lib/api/decks.ts';
import { listDueReviews, gradeReview, undoReview, type DueReview } from '$lib/api/reviews.ts';

View file

@ -5,7 +5,7 @@ import { defineConfig } from 'vite';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
server: {
port: Number(process.env.CARDS_WEB_PORT ?? 3082),
port: Number(process.env.WORDECK_WEB_PORT ?? 3082),
host: true,
},
// @mana/* aus Verdaccio publishen Raw-.ts (main: src/index.ts).

View file

@ -6,7 +6,7 @@
## TL;DR
- **Domain-Library separieren** (`@cards/domain`, framework-agnostic)
- **Domain-Library separieren** (`@wordeck/domain`, framework-agnostic)
- **Card-Type als discriminated union** mit `fields: Record<string,string>` + `subIndex`-Granularität
- **FSRS via `ts-fsrs` v5.3.2** in dünnem Adapter, Reviews bleiben PLAINTEXT
- **Markdown-Editor**, kein Rich-Text/WYSIWYG
@ -24,7 +24,7 @@ mana-monorepo hat `packages/cards-core/` mit Pure-TS:
- Keine Dexie-, Sync-, oder UI-Abhängigkeiten
- Konsumiert von App-UI-Modul UND von `services/cards-server`
**Bei uns:** `packages/cards-domain/` analog. Bewusst kein `@cards/core` (Name-Konflikt), sondern `@cards/domain`. Steht.
**Bei uns:** `packages/cards-domain/` analog. Bewusst kein `@cards/core` (Name-Konflikt), sondern `@wordeck/domain`. Steht.
## 2. Card-Type-Modell
@ -154,7 +154,7 @@ mana-monorepos `(app)/cards/`-Routen:
mana-monorepo nutzt `@mana/shared-privacy` mit `VisibilityLevel = 'private' | 'space' | 'public'`. Bei Änderungen wird `visibilityChangedAt` + `visibilityChangedBy` getrackt.
**Bei uns:** Gleiches Enum als TypeScript-Type in `@cards/domain`. Audit-Felder optional ab Phase 9 (DSGVO-Polish).
**Bei uns:** Gleiches Enum als TypeScript-Type in `@wordeck/domain`. Audit-Felder optional ab Phase 9 (DSGVO-Polish).
## 13. Tests mit fake-indexeddb
@ -185,7 +185,7 @@ mana-monorepos Marketplace hat content-hash auf jeder Karte + jedem Deck-Version
## 5 Kern-Entscheidungen für unser Greenfield
1. **`@cards/domain` als Pure-TS-Workspace-Package** — keine Framework-Bindings
1. **`@wordeck/domain` als Pure-TS-Workspace-Package** — keine Framework-Bindings
2. **Card-Type discriminated union mit `fields`-Slot + `subIndex`-Granularität** — auch wenn MVP nur basic-Karten hat
3. **`ts-fsrs` v5.3.2 hinter dünnem Adapter** — Reviews plaintext, indiziert auf `due`
4. **Cloze als Token-Parser, wenn implementiert** — Anki-kompatible Syntax

View file

@ -103,7 +103,7 @@ export class PullRequestService {
}
private deckUrl(slug: string): string {
const base = process.env.CARDS_WEB_URL || 'https://cardecky.mana.how';
const base = process.env.WORDECK_WEB_URL || 'https://cardecky.mana.how';
return `${base}/d/${slug}`;
}

View file

@ -160,13 +160,13 @@ mit drei Anpassungen:
echte mana-auth-Login-Flow ausgerollt ist (siehe `STATUS.md`),
weicht der Stub für `@mana/shared-auth`.
### 5. Hash-Implementierung: bestehende `@cards/domain` benutzen
### 5. Hash-Implementierung: bestehende `@wordeck/domain` benutzen
Alter `cards-server/src/lib/hash.ts`: eigenständige SHA-256-Implementierung.
Greenfield: `cardContentHash` in `@cards/domain` (Web-Crypto, deterministisch).
Greenfield: `cardContentHash` in `@wordeck/domain` (Web-Crypto, deterministisch).
Beim Restore: `lib/hash.ts` **nicht** mit-übernehmen, sondern
Marketplace-Code auf `cardContentHash` aus `@cards/domain` umbiegen.
Marketplace-Code auf `cardContentHash` aus `@wordeck/domain` umbiegen.
Eine Hash-Definition für die ganze App.
### 6. mana-Service-Calls: identische Pattern
@ -304,9 +304,9 @@ sofort umsetzbar mit dem bestehenden Skill.
(`mana/docs/MIGRATION_DRIZZLE_ZOD.md`). Marketplace-Schema kommt mit
Drizzle 0.38 — sollte mit upgradeen, idealerweise atomar.
- **Karten-Hash-Konsistenz zwischen Greenfield und Marketplace.**
Greenfield-`@cards/domain` `cardContentHash` ist die SoT. Marketplace-
Greenfield-`@wordeck/domain` `cardContentHash` ist die SoT. Marketplace-
`deck_cards.content_hash` muss mit demselben Algorithmus berechnet
werden — sonst funktioniert Smart-Merge nicht. Beim R2-Port-Pass
testen, dass `cardContentHash({type,fields})` aus `@cards/domain`
testen, dass `cardContentHash({type,fields})` aus `@wordeck/domain`
byte-identisch ist mit dem alten `cards-server/src/lib/hash.ts`. Wenn
nicht: alten Code anpassen, nicht `@cards/domain` brechen.
nicht: alten Code anpassen, nicht `@wordeck/domain` brechen.

View file

@ -1,20 +1,19 @@
# Cards-Production-Env-Skeleton. Auf dem Mac Mini liegt die echte
# Wordeck-Production-Env-Skeleton. Auf dem Mac Mini liegt die echte
# `.env.production` mit den richtigen Secrets — diese Datei ist nur
# das Template (committet, ohne Secrets).
#
# Generiere die Secrets z.B. mit `openssl rand -hex 32`.
CARDS_DB_PASSWORD=change-me-strong-random
CARDS_S3_SECRET_KEY=change-me-32-bytes-base64
CARDS_DSGVO_SERVICE_KEY=change-me-msk-prefix
CARDS_API_VERSION=1.0.0
WORDECK_DB_PASSWORD=change-me-strong-random
WORDECK_DSGVO_SERVICE_KEY=change-me-msk-prefix
WORDECK_API_VERSION=1.0.0
# Fail-secure: in Produktion MUSS dieser Wert 'false' bleiben (oder die
# Variable ganz weggelassen werden — der Compose-Default ist 'false').
# Nur für gezielte Diagnose temporär auf 'true' setzen und sofort
# wieder zurückstellen. Wenn 'true' aktiv ist, akzeptiert die API
# `X-User-Id`-Header als vollwertige Auth (founder-Tier).
CARDS_AUTH_DEV_STUB=false
WORDECK_AUTH_DEV_STUB=false
# Verdaccio-Token für @mana/* — nutze denselben claudebot-Token, den
# auch die Plattform verwendet (siehe ~/.cloudflared/.npmrc oder

View file

@ -1,23 +1,19 @@
# Production-Stack für Cards auf dem Mac Mini.
# Production-Stack für Wordeck auf dem Mac Mini.
#
# Lebt unter ~/projects/cards/ auf mana-server (Forgejo-Klon von
# git.mana.how/till/cards). Build-Contexte zeigen relativ in den
# Repo, kein externes Image-Registry — Cards ist Greenfield-eigenständig
# Lebt unter ~/projects/wordeck/ auf mana-server (Forgejo-Klon von
# git.mana.how/till/wordeck). Build-Contexte zeigen relativ in den
# Repo, kein externes Image-Registry — Wordeck ist Greenfield-eigenständig
# (Strategie B), kein Plattform-Coupling.
#
# Ports auf dem Mac Mini:
# cards-postgres: 5436 (Plattform 5432, Dev 5435 sind belegt)
# cards-minio S3: 9210 (cadvisor hat 9110 belegt)
# cards-minio UI: 9211
# cards-api: 3191 (alt war 3072 → cards-api.mana.how)
# cards-web: 5181 (alt war 5180 → cards.mana.how)
# Ports auf dem Mac Mini (unverändert seit Cards-Greenfield):
# wordeck-postgres: 5436 (Plattform 5432, Dev 5435 sind belegt)
# wordeck-api: 3191 (cardecky-api.mana.how alt → api.wordeck.com neu)
# wordeck-web: 5181 (cardecky.mana.how alt → wordeck.com neu)
#
# Cutover (2026-05-08): cardecky.mana.how + cardecky-api.mana.how
# zeigen via Cloudflare-Tunnel auf diese Container. Alte Hostnames
# cards.mana.how / cards-api.mana.how → nginx :4400 → 301 zu
# cardecky.* (User-Bookmark-Erhalt).
# Cutover (2026-05-17): cards → wordeck Big-Bang über DB+Volume+Container+
# Verzeichnis (analog Manaspur→Viadocu-Rebrand vom selben Tag).
#
# Start (von ~/projects/cards/ auf mana-server):
# Start (von ~/projects/wordeck/ auf mana-server):
# docker compose -f infrastructure/docker-compose.production.yml \
# --env-file infrastructure/.env.production up -d --build
#
@ -25,27 +21,27 @@
# docker compose -f infrastructure/docker-compose.production.yml down
services:
cards-postgres:
wordeck-postgres:
image: postgres:16-alpine
container_name: cards-postgres
container_name: wordeck-postgres
restart: unless-stopped
environment:
POSTGRES_USER: cards
POSTGRES_PASSWORD: ${CARDS_DB_PASSWORD:?missing CARDS_DB_PASSWORD}
POSTGRES_DB: cards
POSTGRES_USER: wordeck
POSTGRES_PASSWORD: ${WORDECK_DB_PASSWORD:?missing WORDECK_DB_PASSWORD}
POSTGRES_DB: wordeck
ports:
- '127.0.0.1:5436:5432'
volumes:
- /Volumes/ManaData/cards/postgres:/var/lib/postgresql/data
- /Volumes/ManaData/wordeck/postgres:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U cards -d cards']
test: ['CMD-SHELL', 'pg_isready -U wordeck -d wordeck']
interval: 5s
timeout: 3s
retries: 20
cards-api:
image: cards-api:local
container_name: cards-api
wordeck-api:
image: wordeck-api:local
container_name: wordeck-api
build:
context: ../
dockerfile: apps/api/Dockerfile
@ -53,28 +49,28 @@ services:
NPM_AUTH_TOKEN: ${NPM_AUTH_TOKEN:?missing NPM_AUTH_TOKEN}
restart: unless-stopped
depends_on:
cards-postgres:
wordeck-postgres:
condition: service_healthy
environment:
DATABASE_URL: 'postgresql://cards:${CARDS_DB_PASSWORD}@cards-postgres:5432/cards'
CARDS_API_PORT: 3081
CARDS_API_VERSION: ${CARDS_API_VERSION:-1.0.0}
CARDS_PUBLIC_URL: https://wordeck.com
CARDS_DSGVO_SERVICE_KEY: ${CARDS_DSGVO_SERVICE_KEY:?missing CARDS_DSGVO_SERVICE_KEY}
DATABASE_URL: 'postgresql://wordeck:${WORDECK_DB_PASSWORD}@wordeck-postgres:5432/wordeck'
WORDECK_API_PORT: 3081
WORDECK_API_VERSION: ${WORDECK_API_VERSION:-1.0.0}
WORDECK_PUBLIC_URL: https://wordeck.com
WORDECK_DSGVO_SERVICE_KEY: ${WORDECK_DSGVO_SERVICE_KEY:?missing WORDECK_DSGVO_SERVICE_KEY}
MANA_AUTH_URL: https://auth.mana.how
MANA_CREDITS_URL: https://credits.mana.how
CARDS_MANA_SERVICE_KEY: ${CARDS_MANA_SERVICE_KEY:-}
WORDECK_MANA_SERVICE_KEY: ${WORDECK_MANA_SERVICE_KEY:-}
# Fail-secure: opt-in. Auf der Prod-Box gar nicht setzen
# ⇒ Bypass AUS. Nur für gezielte lokale Diagnose temporär
# auf 'true' setzen (und sofort wieder rausnehmen).
CARDS_AUTH_DEV_STUB: ${CARDS_AUTH_DEV_STUB:-false}
WORDECK_AUTH_DEV_STUB: ${WORDECK_AUTH_DEV_STUB:-false}
NODE_ENV: production
ports:
- '127.0.0.1:3191:3081'
cards-web:
image: cards-web:local
container_name: cards-web
wordeck-web:
image: wordeck-web:local
container_name: wordeck-web
build:
context: ../
dockerfile: apps/web/Dockerfile
@ -82,19 +78,19 @@ services:
NPM_AUTH_TOKEN: ${NPM_AUTH_TOKEN:?missing NPM_AUTH_TOKEN}
restart: unless-stopped
depends_on:
- cards-api
- wordeck-api
environment:
# SvelteKit `$env/dynamic/public` liest zur Runtime — daher
# hier statt als Build-Arg. Wert landet im SSR-Init-Snapshot
# und in client-fetches.
PUBLIC_CARDS_API_URL: https://api.wordeck.com
PUBLIC_WORDECK_API_URL: https://api.wordeck.com
PUBLIC_MANA_AUTH_URL: https://auth.mana.how
PUBLIC_AUTH_WEB_URL: https://auth.mana.how
# mana e.V. Apple-Developer-Team-ID. Wird ausgeliefert in
# /.well-known/apple-app-site-association für die cards-native
# Universal-Links (applinks:cardecky.mana.how).
# /.well-known/apple-app-site-association für die Wordeck-Native
# Universal-Links (applinks:wordeck.com).
PUBLIC_APPLE_TEAM_ID: QP3GLU8PH3
CARDS_API_URL: https://api.wordeck.com
WORDECK_API_URL: https://api.wordeck.com
NODE_ENV: production
ports:
- '127.0.0.1:5181:3000'

View file

@ -1,5 +1,4 @@
# Lokales Cards-Dev-Setup. Postgres-Container auf 5435 + MinIO auf
# 9100/9101 (Plattform nutzt 9000/9001 — wir bleiben isoliert).
# Lokales Wordeck-Dev-Setup. Postgres-Container auf 5435.
#
# Start: pnpm docker:up (vom Repo-Root)
# Logs: pnpm docker:logs
@ -8,39 +7,20 @@
# Daten persistieren in `infrastructure/.volumes/`, git-ignored.
services:
cards-postgres:
wordeck-postgres:
image: postgres:16-alpine
container_name: cards-postgres
container_name: wordeck-postgres
restart: unless-stopped
environment:
POSTGRES_USER: cards
POSTGRES_PASSWORD: cards
POSTGRES_DB: cards
POSTGRES_USER: wordeck
POSTGRES_PASSWORD: wordeck
POSTGRES_DB: wordeck
ports:
- '5435:5432'
volumes:
- ./.volumes/cards-postgres:/var/lib/postgresql/data
- ./.volumes/wordeck-postgres:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U cards -d cards']
interval: 5s
timeout: 3s
retries: 10
cards-minio:
image: minio/minio:latest
container_name: cards-minio
restart: unless-stopped
command: server /data --console-address ':9001'
environment:
MINIO_ROOT_USER: cardsadmin
MINIO_ROOT_PASSWORD: cardsadmin
ports:
- '9100:9000' # S3-API
- '9101:9001' # Web-Console
volumes:
- ./.volumes/cards-minio:/data
healthcheck:
test: ['CMD', 'mc', 'ready', 'local']
test: ['CMD-SHELL', 'pg_isready -U wordeck -d wordeck']
interval: 5s
timeout: 3s
retries: 10

View file

@ -1,9 +1,9 @@
{
"name": "cards",
"name": "wordeck",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Cards — eigenständige föderierte Spaced-Repetition-App des mana e.V. Greenfield-Build (Strategie B), redet über mana-share/-mcp/-search/-events mit den anderen Verein-Apps.",
"description": "Cards \u2014 eigenst\u00e4ndige f\u00f6derierte Spaced-Repetition-App des mana e.V. Greenfield-Build (Strategie B), redet \u00fcber mana-share/-mcp/-search/-events mit den anderen Verein-Apps.",
"packageManager": "pnpm@9.15.9",
"engines": {
"node": ">=20",

View file

@ -1,9 +1,9 @@
{
"name": "@cards/domain",
"name": "@wordeck/domain",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Cards-Domain Pure-TS-Modell: Card-Types, FSRS-Adapter, Cloze-Parser, Anki-Import-Helpers, zod-Schemas. Keine DB-, Framework- oder Hono/SvelteKit-Abhängigkeiten.",
"description": "Cards-Domain \u2014 Pure-TS-Modell: Card-Types, FSRS-Adapter, Cloze-Parser, Anki-Import-Helpers, zod-Schemas. Keine DB-, Framework- oder Hono/SvelteKit-Abh\u00e4ngigkeiten.",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {

View file

@ -149,7 +149,7 @@ export function subIndexCount(type: string): number {
return 1;
case 'cloze':
throw new Error(
'subIndexCount("cloze") not supported — use subIndexCountForCloze(text) from @cards/domain'
'subIndexCount("cloze") not supported — use subIndexCountForCloze(text) from @wordeck/domain'
);
default:
return 1;

View file

@ -1,4 +1,4 @@
// @cards/domain — public API
// @wordeck/domain — public API
//
// Pure-TS, keine DB/Framework-Abhängigkeiten. Wird vom apps/api
// (Drizzle-Schemas) und apps/web (UI) gleichermaßen konsumiert.

3496
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff