/** * 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}`);