Scouting: links die normalen Creator, rechts die Premium
Filipe: "ich will dass es da in der mitte eine trennung gibt. links sollen normale creator sein und rechts premium creator. es gibt bei tiktok naemlich diese zwei optionen von streamer ... auch dass wenn man die eintraegt soll man aussuchen koennen." Jede Stufe der Pipeline hat jetzt zwei Spalten mit einer Trennlinie dazwischen. Die Trennung sitzt IN der Stufe, nicht einmal ueber der ganzen Seite: Zwei komplette Pipelines nebeneinander haetten jede Stufenueberschrift verdoppelt und die beiden Seiten waeren nie auf gleicher Hoehe gewesen. GENAU ZWEI WERTE, kein "weiss nicht". Anders als bei `netzwerk`, wo "noch nicht gefragt" eine eigene gueltige Antwort ist: Dort wird eine fremde Tatsache festgehalten, hier eine eigene Absicht -- und deren Normalfall ist "normal". NULL wird als "normal" gelesen, der eine vorhandene Lead steht damit links, ohne dass ihm etwas unterstellt wird. DIE ART UEBERLEBT DIE UEBERGABE. Beim Creator-Onboarding wandert sie auf die Person und ist in der Personenverwaltung aenderbar. Ohne das endet die Angabe genau dort, wo sie zum ersten Mal vertraglich zaehlt. Die Schwelle misst den STUFENKASTEN (@container), nicht das Fenster -- der Fehler vom 06.09.2026 im selben Haus. Auf dem Handy liegen die Spalten untereinander, die senkrechte Linie faellt weg. ZWEI FUNDE BEIM BAUEN, beide von Hauswachen: * `pruef-fingermass` fand, dass meine neue Handy-Messung mit `setViewportSize` statt eigenem Kontext lief -- ohne `hasTouch` greift keine einzige Regel aus `@media (pointer: coarse)`. * Die Klassen hiessen zuerst `.spalte__*` -- die gehoeren `aufgaben.css`, und scouting.html laedt die VOR scouting.css. `margin-left: auto` aus dem Aufgabenbrett zog die Zahl 560 px von ihrem Wort weg, ohne dass eine Pruefung rot wurde. Jetzt `.art-spalte__*`, und der Abstand wird gemessen. pruef-scouting-felder: 28 -> 64 Pruefungen, gruen. Zwei Gegenproben gefahren (Trennung deaktiviert, Spaltenreihenfolge gedreht) -- beide schlugen an. Dazu gruen: haus-trennung, css-klassen, personen-liste, personen-kachel, hand-personen, schranke, formulare, lesbarkeit, agentur, deutsche-texte, tippziele, leerzustand, fingermass. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
/* 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);
|
||||
Reference in New Issue
Block a user