ki integration

This commit is contained in:
2026-08-15 13:43:52 +02:00
parent 82d98bd8cf
commit 9ecb292c26
78 changed files with 12055 additions and 55 deletions

188
scripts/seed-demo.mjs Normal file
View File

@@ -0,0 +1,188 @@
/**
* 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}`);