Files
videogen/scripts/seed-demo.mjs
2026-08-15 13:43:52 +02:00

189 lines
8.6 KiB
JavaScript
Raw Permalink 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.
/**
* Legt ein Demo-Konto mit Ordner und drei Modellen (inkl. echter Bild-Uploads)
* an damit sich die Anzeige in der App gegen echte Daten prüfen lässt.
*
* Läuft als Client (Cookie-Session), nicht mit dem Server-Key: so wird
* nebenbei belegt, dass die Tabellen-Rechte aus E2 wirklich ausreichen.
*/
import { readFileSync } from 'node:fs';
import { basename } from 'node:path';
const ENDPOINT = 'https://appwrite.webklar.com/v1';
const PROJECT = '6a5cee34002bb8360c34';
const DB = 'brandloop';
const UP = 'A:/developer/GitHub-desktop/videogen/uploads';
const email = process.argv[2] ?? 'demo@brandloop.test';
const password = process.argv[3] ?? 'Demo-2026-brandloop';
const label = 'Modaily';
const jar = new Map();
async function call(method, path, body, isForm = false) {
const headers = { 'X-Appwrite-Project': PROJECT };
if (!isForm) headers['Content-Type'] = 'application/json';
if (jar.size) headers.Cookie = [...jar].map(([k, v]) => `${k}=${v}`).join('; ');
const res = await fetch(`${ENDPOINT}${path}`, {
method, headers, body: isForm ? body : body ? JSON.stringify(body) : undefined,
});
for (const c of res.headers.getSetCookie?.() ?? []) {
const [pair] = c.split(';');
const i = pair.indexOf('=');
if (i > 0) jar.set(pair.slice(0, i).trim(), pair.slice(i + 1).trim());
}
const t = await res.text();
let j; try { j = JSON.parse(t); } catch { j = { message: t }; }
if (res.status >= 300) throw new Error(`${method} ${path} -> ${res.status} ${j.message}`);
return j;
}
// --- Konto -----------------------------------------------------------------
try {
await call('POST', '/account', { userId: 'unique()', email, password, name: label });
console.log('Konto angelegt:', email);
} catch (e) {
if (!String(e.message).includes('already exists')) throw e;
console.log('Konto existiert bereits:', email);
}
await call('POST', '/account/sessions/email', { email, password });
const teamsVorhanden = await call('GET', '/teams');
const team = teamsVorhanden.teams[0] ?? (await call('POST', '/teams', { teamId: 'unique()', name: label }));
const rolle = `team:${team.$id}`;
const rechte = [`read("${rolle}")`, `update("${rolle}")`, `delete("${rolle}")`];
const brandsVorhanden = await call('GET', `/tablesdb/${DB}/tables/brands/rows`);
const brand = brandsVorhanden.rows[0] ?? (await call('POST', `/tablesdb/${DB}/tables/brands/rows`, {
rowId: 'unique()',
data: { team_id: team.$id, label_name: label, status: 'trial', plan: 'trial', nische: 'beauty_kosmetik' },
permissions: rechte,
}));
console.log(`Marke: ${brand.label_name} (${brand.$id}), Team ${team.$id}`);
// --- Kategorien + Attribute + brand-weite Scores ---------------------------
// Das erzeugt später P2 aus dem Onboarding. Hier von Hand, damit ein Ordner
// mit Startwert `erben` überhaupt etwas zu erben hat.
const WISSEN = {
location: ['badezimmer morgens', 'studio grauer hintergrund', 'kueche tageslicht'],
licht: ['weiches seitenlicht', 'hartes sonnenlicht', 'flaches studiolicht'],
farben: ['warme erdtoene', 'kuehles monochrom', 'kraeftiger rotakzent'],
kamera: ['augenhoehe frontal', 'leichte aufsicht', 'detail makro'],
};
const katVorhanden = await call('GET', `/tablesdb/${DB}/tables/categories/rows`);
if (!katVorhanden.rows.length) {
let attrCount = 0;
for (const [kat, attribute] of Object.entries(WISSEN)) {
const c = await call('POST', `/tablesdb/${DB}/tables/categories/rows`, {
rowId: 'unique()',
data: { brand_id: brand.$id, name: kat, avg_score: 5000, attribute_count: attribute.length },
permissions: rechte,
});
for (const name of attribute) {
const a = await call('POST', `/tablesdb/${DB}/tables/attributes/rows`, {
rowId: 'unique()',
data: {
brand_id: brand.$id, category_id: c.$id, name,
slug: name.replace(/\s+/g, '-'), status: 'aktiv',
beschreibung_md: `Aus dem Onboarding abgeleitet: ${name}.`,
},
permissions: rechte,
});
// Brand-weite Ebene: folder_id bleibt leer.
await call('POST', `/tablesdb/${DB}/tables/attribute_scores/rows`, {
rowId: 'unique()',
data: {
brand_id: brand.$id, attribute_id: a.$id, category_id: c.$id,
score: 5000, start_value: 5000, k_factor: 32, start_quelle: 'neutral',
used_count: 0, wins: 0, losses: 0,
},
permissions: rechte,
});
attrCount++;
}
}
console.log(`Wissen: ${Object.keys(WISSEN).length} Kategorien, ${attrCount} Attribute (brand-weit)`);
} else {
console.log(`Wissen: ${katVorhanden.rows.length} Kategorien vorhanden`);
}
// --- Ordner ----------------------------------------------------------------
const ordnerVorhanden = await call('GET', `/tablesdb/${DB}/tables/folders/rows`);
if (!ordnerVorhanden.rows.length) {
for (const o of [
{ name: 'Sommerkampagne', zweck: 'wissens_scope', startwert_modus: 'erben' },
{ name: 'Cleane Studioshots', zweck: 'wissens_scope', startwert_modus: 'aus_posts' },
]) {
const r = await call('POST', `/tablesdb/${DB}/tables/folders/rows`, {
rowId: 'unique()',
data: { brand_id: brand.$id, ...o, ist_default: o.name === 'Sommerkampagne', post_count: 0, signal_count: 0 },
permissions: rechte,
});
// Gleiche Regel wie ordnerInitialisieren() im Client: nur `erben` kopiert
// die brand-weite Ebene, alles andere startet leer.
let kopiert = 0;
if (o.startwert_modus === 'erben') {
const q = encodeURIComponent(JSON.stringify({ method: 'isNull', attribute: 'folder_id' }));
const l = encodeURIComponent(JSON.stringify({ method: 'limit', values: [200] }));
const quelle = await call('GET', `/tablesdb/${DB}/tables/attribute_scores/rows?queries[]=${q}&queries[]=${l}`);
for (const z of quelle.rows) {
await call('POST', `/tablesdb/${DB}/tables/attribute_scores/rows`, {
rowId: 'unique()',
data: {
brand_id: brand.$id, attribute_id: z.attribute_id, category_id: z.category_id,
folder_id: r.$id, score: z.score, start_value: z.score, k_factor: 32,
start_quelle: 'geerbt', used_count: 0, wins: 0, losses: 0,
},
permissions: rechte,
});
kopiert++;
}
}
console.log(`Ordner: ${r.name} (${o.zweck}, ${o.startwert_modus}) ${kopiert} Scores geerbt`);
}
} else {
console.log(`Ordner: ${ordnerVorhanden.rows.length} vorhanden`);
}
// --- Modelle mit echten Referenzbildern ------------------------------------
async function bildHochladen(datei) {
const bytes = readFileSync(`${UP}/${datei}`);
const fd = new FormData();
fd.append('fileId', 'unique()');
fd.append('file', new Blob([bytes], { type: 'image/jpeg' }), basename(datei));
for (const p of rechte) fd.append('permissions[]', p);
const f = await call('POST', '/storage/buckets/asset-references/files', fd, true);
return f.$id;
}
const MODELLE = [
{ typ: 'produkt', name: 'Refine & Renew B3 Serum', datei: 'pasted-1784663200459-0.jpg', beschreibung: 'Weiße Pumpflasche, 30 ml, rote Typo, Aufschrift MODAILY vertikal links.' },
{ typ: 'produkt', name: 'Age Decoder Essence', datei: 'pasted-1784643165286-0.jpg', beschreibung: 'Pipettenflasche, rosé-transparentes Glas, mattgraue Kappe, Schriftzug okolo.' },
{ typ: 'kulisse', name: 'U-Bahn, Türbereich', datei: 'pasted-1784643111353-0.jpg', beschreibung: 'Metallische U-Bahn-Türen, Haltestangen, weiches Kunstlicht, gedämpfte Erdtöne.' },
{ typ: 'kulisse', name: 'Halle, Metallwand', datei: 'pasted-1784663261701-0.jpg', beschreibung: 'Minimalistischer Innenraum, Stahlwand mit kühlen Reflexen, polierter Boden.' },
];
const assetsVorhanden = await call('GET', `/tablesdb/${DB}/tables/assets/rows`);
if (assetsVorhanden.rows.length) {
console.log(`Modelle: ${assetsVorhanden.rows.length} vorhanden, kein Neuanlegen`);
} else {
for (const m of MODELLE) {
const fileId = await bildHochladen(m.datei);
const asset = await call('POST', `/tablesdb/${DB}/tables/assets/rows`, {
rowId: 'unique()',
data: { brand_id: brand.$id, typ: m.typ, name: m.name, ist_teilbar: false, nische: 'beauty_kosmetik' },
permissions: rechte,
});
const version = await call('POST', `/tablesdb/${DB}/tables/asset_versions/rows`, {
rowId: 'unique()',
data: { asset_id: asset.$id, version_no: 1, status: 'freigegeben', beschreibung_md: m.beschreibung, reference_file_ids: [fileId] },
permissions: rechte,
});
await call('PATCH', `/tablesdb/${DB}/tables/assets/rows/${asset.$id}`, {
data: { released_version_id: version.$id },
});
console.log(`Modell: ${m.name} [${m.typ}] Bild ${fileId}`);
}
}
console.log(`\nFertig. Anmelden mit ${email} / ${password}`);