fix(wordeck): pre-existing test drifts + L-2 cleanup vor Deploy
Some checks are pending
CI / validate (push) Waiting to run
Some checks are pending
CI / validate (push) Waiting to run
- tests cards→wordeck rebrand-drift: app-name in health/search/tools/dsgvo, envelope to.app + service-key env-var WORDECK_DSGVO_SERVICE_KEY (war: CARDS_*). Test-Suite jetzt 83/83 grün. - dsgvo.ts: ENV-Name auf WORDECK_DSGVO_SERVICE_KEY (war CARDS_*) — passt zum Test-Setup + wordeck-Branding - decks.ts (web): generateDeckFromImage routet URL-only-Pfad auf generateDeck, File-Upload-Pfad wirft klaren Fehler (Server-Route existiert nicht). UI-Komponenten unverändert - migrate-db-to-events.ts: Stub als „nicht benötigt" markiert. Wordeck-Production hat keine User-Daten in den obsoleten Tabellen; Marketplace-Decks (cardecky-User) leben in eigenem pgSchema und sind vom Cutover nicht betroffen Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
375a6af86e
commit
cba37c3c37
8 changed files with 52 additions and 50 deletions
|
|
@ -1,42 +1,35 @@
|
|||
/**
|
||||
* Migration-Skript für den Big-Bang-Cutover L-2 (2026-05-20).
|
||||
* Migration-Skript-Stub für DB→Event-Sync (L-2, 2026-05-20).
|
||||
*
|
||||
* Liest pro User alle Decks + Cards + Reviews aus der Wordeck-DB und
|
||||
* publiziert sie als event-sourced Append-Sequence an
|
||||
* sync2.mana.how/sync/wordeck. Idempotent über `idempotencyKey =
|
||||
* 'migration:<row-id>:<event-type>'`.
|
||||
* **Aktueller Status (2026-05-20): NICHT BENÖTIGT.**
|
||||
*
|
||||
* Verwendung (immer dry-run probieren bevor echter Run):
|
||||
* Wordeck-Production hat keine User-Daten in den (jetzt obsoleten)
|
||||
* wordeck.decks/cards/reviews-Tabellen. Die einzigen aktiven Daten
|
||||
* sind die öffentlichen Marketplace-Decks des cardecky-Plattform-Users,
|
||||
* die in `marketplace.public_decks` + `marketplace.deck_versions` leben
|
||||
* und vom Big-Bang nicht betroffen sind.
|
||||
*
|
||||
* Dieses Skript bleibt als Bauplan stehen, falls künftig vor einem
|
||||
* weiteren Cutover (z.B. Schema-V2-Migration) ein DB→Events-Lift
|
||||
* gebraucht wird. **Bei Reaktivierung dringend Code-Review +
|
||||
* Dry-Run-Tests, das Skript ist heute ungetestet.**
|
||||
*
|
||||
* Offene Implementation-Punkte falls Reaktivierung kommt:
|
||||
* - `mintToken()` braucht entweder Service-Key-Token-Mint in mana-auth
|
||||
* (gibt's heute nicht) oder Service-Key-Auth-Mode in mana-sync
|
||||
* (gibt's heute nicht) — beides Plattform-Arbeit
|
||||
* - Encryption: Plaintext-Migration ist akzeptabel solange Trust-
|
||||
* Domain stimmt; Re-Encrypt nach User-Login als Folge-Task
|
||||
*
|
||||
* Verwendung (sobald aktiviert):
|
||||
*
|
||||
* pnpm tsx scripts/migrate-db-to-events.ts --dry-run [--user-id <id>]
|
||||
* pnpm tsx scripts/migrate-db-to-events.ts --commit [--user-id <id>]
|
||||
*
|
||||
* Ohne --user-id wird über alle User iteriert. --dry-run gibt Counts
|
||||
* aus ohne POST. --commit POSTet tatsächlich.
|
||||
*
|
||||
* Voraussetzungen:
|
||||
* - DATABASE_URL gesetzt (Wordeck-DB)
|
||||
* - MANA_SYNC_URL (default https://sync2.mana.how)
|
||||
* - MANA_SERVICE_KEY (für service-side User-JWT-Mint oder per-User-Token)
|
||||
* - User-JWTs: Skript ruft mana-auth-`POST /api/v1/service/mint-token`
|
||||
* mit Service-Key + user_id für temporäre JWT (Service-Key-Pattern,
|
||||
* siehe shared-auth)
|
||||
*
|
||||
* Encryption: Daten werden im **Plaintext** an sync2 gesendet — wir
|
||||
* haben keinen User-Master-Key zur Migration-Zeit. Server speichert
|
||||
* sie als wire-format string (NoOp-kompatibel). Wenn der User sich
|
||||
* später einloggt + Vault-Key bootstrappt, kann ein Re-Encrypt-Pass
|
||||
* folgen. Für den Big-Bang akzeptieren wir Plaintext-Migration, weil:
|
||||
* - die Daten waren vorher schon in Postgres plaintext
|
||||
* - der Sync-Server liegt in derselben Trust-Domain (Mac Mini)
|
||||
* - User kann nach Login encrypted-Versionen drüberspielen
|
||||
*
|
||||
* **Status: Skript-Stub, ungetestet.** Vor echtem Run:
|
||||
* 1. Code-Review durch Till
|
||||
* 2. Snapshot von mana_sync_v2.wordeck.* + Wordeck-DB
|
||||
* 3. Dry-run für 1-2 User
|
||||
* 4. Manuelle Verifikation des sync2-States
|
||||
* 5. Erst dann --commit
|
||||
* - MANA_SERVICE_KEY (für Token-Mint)
|
||||
*/
|
||||
|
||||
import { eq, sql } from 'drizzle-orm';
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ export function dsgvoRouter(deps: DsgvoDeps = {}): Hono {
|
|||
// IP-Rate-Limit als Defense-in-Depth — der Service-Key ist ohnehin
|
||||
// Pflicht, aber 10/min stoppt einen brute-force-Versuch auf den Key.
|
||||
r.use('*', rateLimit({ scope: 'dsgvo', windowMs: 60_000, max: 10, keyOf: ipKey }));
|
||||
r.use('*', serviceKeyAuth({ envVar: 'CARDS_DSGVO_SERVICE_KEY' }));
|
||||
r.use('*', serviceKeyAuth({ envVar: 'WORDECK_DSGVO_SERVICE_KEY' }));
|
||||
|
||||
/**
|
||||
* Voll-Export aller Cards-Daten eines Users. Liefert serialisier-
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ describe('dsgvoRouter — Service-Key-Gate', () => {
|
|||
data: { decks: unknown[]; cards: unknown[]; reviews: unknown[] };
|
||||
};
|
||||
expect(body.user_id).toBe('u-1');
|
||||
expect(body.app).toBe('cards');
|
||||
expect(body.app).toBe('wordeck');
|
||||
expect(Array.isArray(body.data.decks)).toBe(true);
|
||||
expect(Array.isArray(body.data.cards)).toBe(true);
|
||||
expect(Array.isArray(body.data.reviews)).toBe(true);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ describe('health routes', () => {
|
|||
const res = await app.request('/version');
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { app: string; version: string; build: string };
|
||||
expect(body.app).toBe('cards');
|
||||
expect(body.app).toBe('wordeck');
|
||||
expect(body.version).toBeTruthy();
|
||||
expect(body.build).toBeTruthy();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ describe('searchRouter — Query-Validation', () => {
|
|||
};
|
||||
expect(body.envelope_version).toBe('0.1');
|
||||
expect(body.query).toBe('Konfuzius');
|
||||
expect(body.app).toBe('cards');
|
||||
expect(body.app).toBe('wordeck');
|
||||
expect(Array.isArray(body.results)).toBe(true);
|
||||
expect(body.partial).toBe(false);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ function envelope(overrides: Record<string, unknown> = {}) {
|
|||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
to: {
|
||||
app: 'cards',
|
||||
app: 'wordeck',
|
||||
user_id: userA,
|
||||
},
|
||||
type: 'mana/quote',
|
||||
|
|
@ -61,7 +61,7 @@ describe('shareRouter — Envelope-Validation', () => {
|
|||
it('Cross-User-Share ist 422 (envelope_invalid)', async () => {
|
||||
const { app } = buildApp();
|
||||
const env = envelope({
|
||||
to: { app: 'cards', user_id: userB }, // anderer User → Cross-User
|
||||
to: { app: 'wordeck', user_id: userB }, // anderer User → Cross-User
|
||||
});
|
||||
const res = await app.request('/api/v1/share/receive', {
|
||||
method: 'POST',
|
||||
|
|
@ -77,7 +77,7 @@ describe('shareRouter — Envelope-Validation', () => {
|
|||
const { app } = buildApp();
|
||||
const env = envelope({
|
||||
from: { ...envelope().from, user_id: userB },
|
||||
to: { app: 'cards', user_id: userB },
|
||||
to: { app: 'wordeck', user_id: userB },
|
||||
});
|
||||
const res = await app.request('/api/v1/share/receive', {
|
||||
method: 'POST',
|
||||
|
|
|
|||
|
|
@ -91,6 +91,6 @@ describe('toolsRouter — Tool-Dispatch', () => {
|
|||
const body = (await res.json()) as { query: string; results: unknown[]; app: string };
|
||||
expect(body.query).toBe('Konfuzius');
|
||||
expect(Array.isArray(body.results)).toBe(true);
|
||||
expect(body.app).toBe('cards');
|
||||
expect(body.app).toBe('wordeck');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import type { Deck, DeckCreate, DeckUpdate } from '@wordeck/domain';
|
|||
import type { WordeckDeckState as DeckProjection } from '@mana/shared-schemas';
|
||||
import { ulid } from 'ulid';
|
||||
|
||||
import { api, apiForm } from './client.ts';
|
||||
import { api } from './client.ts';
|
||||
import { deckAggregateId, deckStateToRow } from './event-adapters.ts';
|
||||
import { emitEvent } from './event-builder.ts';
|
||||
import { getSync } from '../sync.svelte.ts';
|
||||
|
|
@ -188,21 +188,30 @@ export function fetchDistractors(
|
|||
return api<{ distractors: string[] }>(`/api/v1/decks/${deckId}/distractors${qs}`);
|
||||
}
|
||||
|
||||
export function generateDeckFromImage(
|
||||
/**
|
||||
* generateDeckFromImage — Stub seit 2026-05-20 (L-2g).
|
||||
*
|
||||
* Die Server-Route `/api/v1/decks/from-image` existiert nicht und wurde
|
||||
* nie ausgerollt. UI nutzt diese Funktion für File-Upload + URL-only-Pfad.
|
||||
* Wir routen URL-only-Aufrufe transparent auf `generateDeck` (das den
|
||||
* URL-Kontext bereits unterstützt). File-Uploads werfen einen klaren
|
||||
* Fehler bis Server-Implementation kommt.
|
||||
*/
|
||||
export async function generateDeckFromImage(
|
||||
files: File | File[],
|
||||
opts: { language?: string; count?: number; url?: string },
|
||||
) {
|
||||
): Promise<{ deck: Deck; cards_created: number }> {
|
||||
const arr = Array.isArray(files) ? files : [files];
|
||||
if (arr.length === 0) {
|
||||
return api<{ deck: Deck; cards_created: number }>('/api/v1/decks/from-image', {
|
||||
method: 'POST',
|
||||
body: { language: opts.language, count: opts.count, url: opts.url },
|
||||
if (arr.length === 0 && opts.url) {
|
||||
// URL-only: über generateDeck routen (Server hat URL-Support)
|
||||
return generateDeck({
|
||||
prompt: opts.url,
|
||||
language: opts.language,
|
||||
count: opts.count,
|
||||
url: opts.url,
|
||||
});
|
||||
}
|
||||
const form = new FormData();
|
||||
for (const f of arr) form.append('file', f);
|
||||
if (opts.language) form.append('language', opts.language);
|
||||
if (opts.count != null) form.append('count', String(opts.count));
|
||||
if (opts.url) form.append('url', opts.url);
|
||||
return apiForm<{ deck: Deck; cards_created: number }>('/api/v1/decks/from-image', form);
|
||||
throw new Error(
|
||||
'Bild-zu-Karten ist nach dem event-sync-Cutover noch nicht verfügbar. Nutze die URL-Eingabe oder erstelle Karten manuell.',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue