chore: seed-test-decks browser-console snippet
Some checks are pending
CI / validate (push) Waiting to run

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>
This commit is contained in:
Till JS 2026-05-09 18:02:15 +02:00
parent 870e2aea85
commit 9626200616

246
scripts/seed-test-decks.js Normal file
View file

@ -0,0 +1,246 @@
/**
* 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',
);
})();