/* Bild der Trennung Normal / Premium in der Scout-Pipeline (06.10.2026) Filipe: "ich will dass es da in der mitte eine trennung gibt. links sollen normale creator sein und rechts premium creator." Die Zahlen sind gruen (pruef-scouting-felder, 63 Pruefungen) -- das sagt, dass die Kaesten an der richtigen Stelle liegen, aber nicht, wie es AUSSIEHT. Dafuer ist dieses Werkzeug da. Aufrufen mit: node server/bild-arten.mjs */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const ordner = mkdtempSync(join(tmpdir(), "ws-arten-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); const { portMussFreiSein } = await import("./helfer-port.mjs"); const PORT = await portMussFreiSein(4336, "das Bildwerkzeug Arten"); process.env.PORT = String(PORT); process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; const express = (await import("express")).default; const ec = express.response.cookie; express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; await import("./index.js"); await new Promise((r) => setTimeout(r, 1200)); const BASIS = `http://127.0.0.1:${PORT}`; const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes } = await import("node:crypto"); for (let i = 0; i < 60; i++) { try { const p = new DatabaseSync(process.env.WORKSPACE_DB); try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); } } catch { await new Promise((r) => setTimeout(r, 250)); } } const d = new DatabaseSync(process.env.WORKSPACE_DB); const j = new Date().toISOString(); const heute = j.slice(0, 10); function anlegen(name, rolle, code) { const salt = randomBytes(16).toString("hex"); const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); d.prepare("INSERT INTO personen (name,rolle,code_hash,code_salt,code_n,aktiv,erstellt) VALUES (?,?,?,?,?,1,?)") .run(name, rolle, hash, salt, 32768, j); return d.prepare("SELECT last_insert_rowid() AS id").get().id; } const dogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); const scout = anlegen("BananaStift", "scout", "CODE-SCOU-0001"); /* Ein realistischer Stand: beide Arten, mehrere Stufen, eine leere Spalte -- genau die Faelle, die im Alltag vorkommen. */ const lead = (name, handle, status, art, prio, land, follower, netz, followup) => d.prepare(`INSERT INTO leads (name, plattform, handle, status, prioritaet, scout_id, erstellt, erstellt_von, creator_art, land, follower, netzwerk, naechster_followup, live_zeiten) VALUES (?,'TikTok',?,?,?,?,?,?,?,?,?,?,?,?)`) .run(name, handle, status, prio, scout, j, dogi, art, land, follower, netz, followup, "Mo-Fr ab 21 Uhr"); lead("Tamy", "@coolemama1991", "neu", "normal", "hoch", "DE", 12400, "nein", heute); lead("Mira", "@mira.live", "neu", "normal", "mittel", "AT", 3200, "unbekannt", null); lead("Nala", "@nala", "neu", "premium", "hoch", "CH", 184000, "nein", null); lead("Jo", "@jo_streamt", "angesprochen", "normal", "mittel", "DE", 8700, "nein", null); lead("Elif", "@elif", "angesprochen", "premium", "hoch", "DE", 92000, "nein", heute); lead("Sam", "@samsam", "gespraech", "normal", "niedrig", "LU", 1500, "ja", null); d.close(); const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); const browser = await pw.chromium.launch(); const fehlerAufSeite = []; const anmelden = async (seite, rolle, code) => { await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" }); await seite.click(`.rolle[data-rolle="${rolle}"]`); await seite.fill("#code", code); await seite.click("button[type=submit]"); await seite.waitForURL("**/start.html", { timeout: 25000 }); }; /* ---- 1. Am Rechner ---------------------------------------------------- */ const ctx = await browser.newContext({ viewport: { width: 1400, height: 1100 }, deviceScaleFactor: 2 }); const seite = await ctx.newPage(); seite.on("console", (m) => { if (m.type() === "error") fehlerAufSeite.push(m.text()); }); seite.on("pageerror", (f) => fehlerAufSeite.push(String(f))); await anmelden(seite, "scout", "CODE-SCOU-0001"); await seite.goto(BASIS + "/workspace/scouting.html", { waitUntil: "networkidle" }); await seite.waitForTimeout(1800); console.log("\n=== 1. Am Rechner (1400 px) ==="); const spalten = await seite.$$eval(".art-spalte", (ks) => ks.map((k) => ({ stufe: k.closest(".stufe")?.dataset.stufe, art: k.dataset.art, titel: k.querySelector(".art-spalte__titel")?.textContent.trim(), zahl: k.querySelector(".art-spalte__zahl")?.textContent.trim(), x: Math.round(k.getBoundingClientRect().x), breite: Math.round(k.getBoundingClientRect().width), linie: getComputedStyle(k).borderLeftWidth, namen: [...k.querySelectorAll(".kk__name")].map((n) => n.textContent.trim()).join(", ") || k.querySelector(".art-spalte__leer")?.textContent.trim(), }))); for (const s of spalten) { console.log(` ${String(s.stufe).padEnd(14)} ${String(s.titel).padEnd(8)} ${String(s.zahl).padStart(2)} ` + `x=${String(s.x).padStart(4)} b=${String(s.breite).padStart(4)} Linie=${s.linie.padStart(5)} ${s.namen}`); } const stufe1 = await seite.$('.stufe[data-stufe="neu"]'); await stufe1.screenshot({ path: "server/bild-arten-rechner.png" }); await seite.screenshot({ path: "server/bild-arten-rechner-ganz.png", fullPage: false }); /* ---- 2. Das Formular -------------------------------------------------- */ await seite.click("#neu-auf"); await seite.waitForTimeout(600); const feld = await seite.$("#neu-block"); await feld.screenshot({ path: "server/bild-arten-formular.png" }); console.log("\n=== 2. Im Formular ==="); console.log(" Auswahl:", (await seite.$$eval("#n-art option", (os) => os.map((o) => o.textContent.trim()))).join(" | ")); await seite.click("#neu-zu"); /* ---- 3. Auf dem Handy (mit Finger!) ----------------------------------- */ const handyKontext = await browser.newContext({ viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true, deviceScaleFactor: 3, }); await handyKontext.addCookies(await ctx.cookies()); const handy = await handyKontext.newPage(); await handy.goto(BASIS + "/workspace/scouting.html", { waitUntil: "networkidle" }); await handy.waitForTimeout(1800); console.log("\n=== 3. Auf dem Handy (390 px) ==="); const schmal = await handy.$$eval('.stufe[data-stufe="neu"] .art-spalte', (ks) => ks.map((k) => ({ art: k.dataset.art, y: Math.round(k.getBoundingClientRect().y), breite: Math.round(k.getBoundingClientRect().width), ueberRand: k.getBoundingClientRect().right > window.innerWidth, }))); for (const s of schmal) { console.log(` ${String(s.art).padEnd(8)} y=${String(s.y).padStart(5)} b=${s.breite}` + (s.ueberRand ? " UEBER DEN RAND" : "")); } const stufeH = await handy.$('.stufe[data-stufe="neu"]'); await stufeH.screenshot({ path: "server/bild-arten-handy.png" }); /* ---- 4. Die Personenseite nach einer Uebergabe ------------------------- */ const ctxD = await browser.newContext({ viewport: { width: 1400, height: 1100 }, deviceScaleFactor: 2 }); const dogiSeite = await ctxD.newPage(); await anmelden(dogiSeite, "admin", "CODE-DOGI-0001"); await dogiSeite.goto(BASIS + "/workspace/scouting.html", { waitUntil: "networkidle" }); await dogiSeite.waitForTimeout(1500); /* Elif (Premium) durch die Pipeline bis zur Uebergabe und ins Onboarding. */ const elifId = await dogiSeite.evaluate(async () => { const a = await fetch("/workspace/api/leads", { credentials: "same-origin" }); const d2 = await a.json(); return d2.leads.find((l) => l.name === "Elif")?.id; }); await dogiSeite.evaluate(async (id) => { await fetch("/workspace/api/leads/" + id, { method: "PATCH", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: "uebergeben" }), }); await fetch(`/workspace/api/leads/${id}/onboarding`, { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "Elif" }), }); }, elifId); await dogiSeite.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" }); await dogiSeite.waitForTimeout(1800); console.log("\n=== 4. Auf der Personenseite ==="); /* Die Rollengruppen sind zugeklappt (Vorgabe: nur DogFather offen). Ohne diesen Klick steht die Creator-Kachel gar nicht im Dokument -- beim ersten Lauf meldete dieser Abschnitt deshalb nichts, und das sah aus wie ein Fehler an der Art. */ for (const kopf of await dogiSeite.$$(".gruppe__kopf--klappbar")) { const t = (await kopf.textContent()) || ""; if (t.toLowerCase().includes("creator")) { await kopf.click(); await dogiSeite.waitForTimeout(800); } } const creator = await dogiSeite.$$eval('.person[data-rolle="creator"]', (ks) => ks.map((k) => ({ name: k.querySelector(".person__name")?.textContent.trim(), marken: [...k.querySelectorAll(".marke-rolle")].map((m) => m.textContent.trim()).join(" - "), art: k.querySelector('select[id^="creator-art-"]')?.value, }))); for (const c of creator) console.log(` ${String(c.name).padEnd(14)} [${c.marken}] Auswahl: ${c.art}`); const kachel = await dogiSeite.$('.person[data-rolle="creator"]'); if (kachel) await kachel.screenshot({ path: "server/bild-arten-person.png" }); console.log("\nFehler auf der Seite:", fehlerAufSeite.length ? fehlerAufSeite.join(" | ") : "keine"); await browser.close(); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ } process.exit(0);