cards/scripts/seed-test-decks.js
Till JS 9626200616
Some checks are pending
CI / validate (push) Waiting to run
chore: seed-test-decks browser-console snippet
Lokales Test-Daten-Tool: 7 Decks gemischt (verschiedene Farben +
Karten-Counts inkl. Empty-Stack für Empty-State-Test) anlegbar
via Browser-Console-Paste.

Auth liest TOKEN/STUB-User-ID aus localStorage, hits cards-api
auf localhost:3081 (lokal) oder cardecky-api.mana.how (live).

Wiederholtes Ausführen erstellt zusätzliche Decks — keine
Unique-Constraint auf Deck-Name. Sauberer Reset über die UI oder
docker exec ... TRUNCATE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:02:15 +02:00

246 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Test-Decks-Seed via Browser-Console.
*
* Einfachste Nutzung — keine Token-Konfiguration, keine ENV-Vars:
*
* 1. Cards-App im Browser öffnen (lokal http://localhost:3082 oder
* live https://cardecky.mana.how) und sicherstellen dass du
* eingeloggt bist (Decks-Seite ist erreichbar).
* 2. F12 → Console-Tab.
* 3. Den GESAMTEN Inhalt dieser Datei kopieren und einfügen.
* Browser fragt evtl. einmal "Wirklich Code einfügen?" → "Allow
* pasting" tippen.
* 4. Enter.
*
* Das Skript liest die Auth-Daten aus dem Browser (echtes JWT oder
* Dev-Stub-User-ID), zeigt einen Fortschritts-Log und legt 7
* Test-Decks mit gemischten Größen + Farben an. Lauf dauert ~3-5s.
*
* Wiederholtes Ausführen erstellt zusätzliche Decks (gleiche Namen
* dürfen mehrfach vorkommen). Wenn du sauber starten willst: vorher
* vorhandene Test-Decks löschen oder ein frisches DB-Reset machen.
*/
(async () => {
const TOKEN = localStorage.getItem('cards.auth.accessToken');
const STUB = localStorage.getItem('cards.dev.userId');
const headers = { 'Content-Type': 'application/json' };
if (TOKEN) headers['Authorization'] = `Bearer ${TOKEN}`;
else if (STUB) headers['X-User-Id'] = STUB;
else {
console.error('Kein Auth-Token gefunden. Bitte erst auf /einloggen.');
return;
}
const origin = location.origin;
const API = origin.includes('localhost:3082')
? 'http://localhost:3081'
: origin.includes('cardecky.mana.how')
? 'https://cardecky-api.mana.how'
: origin.replace(/cardecky/, 'cardecky-api');
console.log(`%cSeed-Skript läuft gegen ${API}`, 'color:#16a34a; font-weight:600');
async function api(path, body, method = 'POST') {
const r = await fetch(API + path, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
if (!r.ok) {
const text = await r.text();
throw new Error(`${method} ${path}${r.status}: ${text}`);
}
return r.json();
}
// Verschiedene Größen + Farben → Stack-Hint, Fan-Spread, Empty-State alle testbar.
const DECKS = [
{
name: 'Spanisch — Alltagsvokabular',
description: 'Wörter und kurze Wendungen für die ersten Reisetage.',
color: '#16a34a',
cards: makeBasic([
['hola', 'hallo'],
['gracias', 'danke'],
['por favor', 'bitte'],
['¿dónde está…?', 'wo ist …?'],
['la cuenta, por favor', 'die Rechnung, bitte'],
['no hablo bien español', 'ich spreche nicht gut Spanisch'],
['¿habla inglés?', 'sprechen Sie Englisch?'],
['una mesa para dos', 'einen Tisch für zwei'],
['agua sin gas', 'stilles Wasser'],
['una cerveza', 'ein Bier'],
['¿cuánto cuesta?', 'wie viel kostet das?'],
['demasiado caro', 'zu teuer'],
['está bien', 'ist gut'],
['perdón', 'entschuldigung'],
['lo siento', 'tut mir leid'],
['ayuda', 'Hilfe'],
['estoy perdido', 'ich habe mich verlaufen'],
['la estación', 'der Bahnhof'],
['el aeropuerto', 'der Flughafen'],
['el hotel', 'das Hotel'],
['la habitación', 'das Zimmer'],
['la llave', 'der Schlüssel'],
['mañana', 'morgen'],
['ayer', 'gestern'],
]),
},
{
name: 'Deutsch ↔ Niederländisch',
description: 'Falsche Freunde und überraschend nahe Verwandte.',
color: '#FF6600',
cards: makeBasicReverse([
['Bahnhof', 'station'],
['Käse', 'kaas'],
['Fenster', 'raam'],
['Tisch', 'tafel'],
['Brot', 'brood'],
['Buch', 'boek'],
['Auto', 'auto'],
['Haus', 'huis'],
['Frau', 'vrouw'],
['Mann', 'man'],
['Kind', 'kind'],
['Hund', 'hond'],
]),
},
{
name: 'JavaScript ES2026',
description: 'Neue Features — Pattern-Matching, Decorators, Pipe.',
color: '#6366F1',
cards: [
...makeBasic([
['Optional Chaining', '?.'],
['Nullish Coalescing', '??'],
['Logical Assignment', '||=, &&=, ??='],
['Top-Level Await', 'await außerhalb von async-Funktion in ES-Modules'],
['Pipeline Operator (Stage 2)', 'value |> fn()'],
]),
...makeCloze([
'Der `using`-Keyword aus Stage 3 ruft automatisch `Symbol.{{c1::dispose}}` auf, wenn der Scope verlassen wird.',
'Mit Pattern-Matching schreibst du `{{c1::match}} (value) { when … -> … }` statt verschachtelter `if`-Ketten.',
'`Array.prototype.{{c1::with}}(index, value)` gibt eine neue Kopie zurück — immutable Update.',
]),
],
},
{
name: 'Geschichte 19. Jh.',
description: 'Schlüsselereignisse 18151900.',
color: '#DC2626',
cards: makeBasic([
['1815', 'Wiener Kongress — Neuordnung Europas nach Napoleon'],
['1848', 'Märzrevolution + Frankfurter Nationalversammlung'],
['18611865', 'Amerikanischer Bürgerkrieg'],
['1871', 'Deutsche Reichsgründung in Versailles'],
['18841885', 'Berliner Kongokonferenz — Aufteilung Afrikas'],
['1889', 'Pariser Weltausstellung + Eiffelturm'],
['1895', 'Erste Filmvorführung der Brüder Lumière'],
]),
},
{
name: 'Gitarre — Akkord-Grundlagen',
description: 'Vier Akkorde, mit denen 80 % der Pop-Songs gehen.',
color: '#07D6FF',
cards: makeBasic([
['G-Dur', 'Tonika in vielen Singer-Songwriter-Stücken'],
['D-Dur', 'helle Dominante zu G'],
['Em', 'parallele Moll zu G — melancholische Färbung'],
['C-Dur', 'Subdominante in G — auflösend'],
]),
},
{
name: 'Pflanzenkunde — heimische Bäume',
description:
'Erkennungsmerkmale, Standort, Holz-Eigenschaften. Großer Stapel zum Testen.',
color: '#F8D62B',
cards: makeBasic(
[
['Eiche', 'Stieleiche oder Traubeneiche, gelappte Blätter, Eicheln'],
['Buche', 'Rotbuche, ovale Blätter mit gewelltem Rand, Bucheckern'],
['Linde', 'herzförmige Blätter, duftende Blüten im Juni'],
['Birke', 'weiße Rinde, kleine dreieckige Blätter'],
['Fichte', 'kurze Nadeln einzeln am Zweig, hängende Zapfen'],
['Kiefer', 'lange Nadeln paarweise, rotbraune Rinde'],
['Tanne', 'flache Nadeln mit zwei weißen Streifen unten, stehende Zapfen'],
['Lärche', 'einziger heimischer Nadelbaum, der im Herbst Nadeln verliert'],
['Esche', 'unpaarig gefiederte Blätter, schwarze Knospen'],
['Ahorn', 'Spitzahorn, Bergahorn, Feldahorn — gelappte Blätter, Flügelfrüchte'],
['Erle', 'eiförmige Blätter, Standort an Bachläufen'],
['Hainbuche', 'doppelt gesägte Blätter, glatte graue Rinde'],
['Pappel', 'dreieckige Blätter, sehr schnellwüchsig'],
['Weide', 'lanzettliche Blätter, Standort an Wasserläufen'],
['Ulme', 'asymmetrischer Blattgrund — Erkennungsmerkmal'],
['Robinie', 'gefiederte Blätter, weiße Schmetterlingsblüten, sehr hartes Holz'],
['Walnuss', 'gefiederte Blätter mit aromatischem Geruch'],
['Edelkastanie', 'lange gesägte Blätter, stachelige Fruchthülle'],
['Rosskastanie', 'handförmig gefiederte Blätter, glatte Kastanien'],
['Vogelbeere', 'gefiederte Blätter, leuchtend rote Beeren-Doldentrauben'],
['Holunder', 'gefiederte Blätter, weiße Doldenblüten, schwarze Beeren'],
['Hasel', 'rundliche Blätter mit Spitze, Haselnüsse'],
['Schlehe', 'dornig, weiße Blüten vor dem Laubaustrieb'],
['Weißdorn', 'tief gelappte Blätter, weiße Blüten, rote Apfelfrüchte'],
['Eberesche', 'andere Bezeichnung für Vogelbeere'],
['Kirschbaum', 'glänzende Rinde mit horizontalen Streifen'],
['Apfelbaum', 'eiförmige Blätter, kugelige Frucht'],
['Birnbaum', 'eiförmige Blätter, längliche Frucht'],
['Eibe', 'flache Nadeln, rote fleischige Samenmäntel — sonst alles giftig'],
['Wacholder', 'spitze Nadeln, schwarzblaue Beerenzapfen für Gin'],
].slice(0, 30),
),
},
{
name: 'Architekten der Moderne',
description: 'Leerer Stapel zum Testen des Empty-States.',
color: '#6B7280',
cards: [],
},
];
function makeBasic(pairs) {
return pairs.map(([front, back]) => ({
type: 'basic',
fields: { front, back },
}));
}
function makeBasicReverse(pairs) {
return pairs.map(([front, back]) => ({
type: 'basic-reverse',
fields: { front, back },
}));
}
function makeCloze(texts) {
return texts.map((text) => ({
type: 'cloze',
fields: { text },
}));
}
let totalDecks = 0;
let totalCards = 0;
for (const spec of DECKS) {
const deck = await api('/api/v1/decks', {
name: spec.name,
description: spec.description,
color: spec.color,
});
totalDecks++;
console.log(`%c✓ Deck: ${deck.name}`, 'color:#16a34a', `(${deck.id.slice(0, 8)})`);
for (const card of spec.cards) {
await api('/api/v1/cards', { deck_id: deck.id, ...card });
totalCards++;
}
if (spec.cards.length > 0) {
console.log(` + ${spec.cards.length} Karten`);
}
}
console.log(
`%c✓ Fertig — ${totalDecks} Decks, ${totalCards} Karten angelegt. Jetzt /decks öffnen.`,
'color:#16a34a; font-weight:600; font-size:1.1em',
);
})();