Filipe: "zuerst will ich dass du die kategorien und die kacheln viel
krasser geiler machst."
Beim Nachmessen stand fest, warum die Pipeline flach aussah: Drei
Regeln in scouting.css kamen gar nicht an, seit es den Baukasten
(module.css) gibt -- und module.css NENNT zwei davon im eigenen Text.
--ton war an JEDER Karte #db4b66, dem Rot der Seite.
Daraus nimmt der Baukasten Kantenlicht, die
drei Eckwinkel und das Licht beim Zeigen. Sechs
Stufenfarben in der Datei, eine auf dem Schirm.
.kk::before (3px) gemessen 560px breit -- das ist das Kantenlicht
des Baukastens, das dasselbe Pseudoelement
belegt. Die farbige Pipeline-Kante gab es nicht.
.kk { border } /:hover module.css setzt `border: 0`, gleich stark und
spaeter im Ladeweg.
Das Haus hatte die Lehre schon: aufgaben.css setzt `--ton` an fuenf
Spalten mit genau dieser Begruendung. Hierher uebertragen hat es nie
jemand.
WAS JETZT ANDERS IST
* Jede Stufe traegt ihren Ton -- Eckwinkel, Kantenlicht und Hover
laufen damit von kuehl (neu entdeckt) zu gruen (uebergeben).
* Die Farbkante der Karte liegt im HINTERGRUND statt auf einem
Pseudoelement (beide gehoeren dem Baukasten). 6px, weil das
Kantenlicht 1,6px davon verdeckt -- bei 3px blieb nichts Sichtbares.
* Etappennummern 01-05, abgeleitet aus WEITER statt daneben
geschrieben. "Abgelehnt" bekommt keine: keine Etappe nach vorn.
* Stufentitel in der eigenen Farbe (vorher einheitlich --akzent),
mit Weiss aufgehellt -- 4,3:1 waeren bei fett-versal 12,5px zu wenig.
* Faellig-Saum als inset-Schatten wie bei .kachel[data-warn], mit
(0,3,0) gegen die Modulliste; vorher border-color und damit wirkungslos.
* Karte: Plattform als Schild, Handle in gleichbreiter Schrift,
Haarlinie zwischen "wer" und "lohnt sich", Reichweite deutlicher.
DAS MESSGERAET, DAS GEFEHLT HAT: module.css notiert "NICHT
MITREPARIERT, weil es kein Messgeraet dafuer gibt ... Hover laesst sich
nicht pruefen." Doch -- pruef-scouting-felder faehrt den Zeiger jetzt
auf die Karte und liest nach (6px -> 9px).
DREI FUNDE IN DEN EIGENEN WERKZEUGEN, alle von Hauswachen oder der
eigenen Messung:
* bild-arten hatte keine Notbremse; page.hover() blieb haengen und
liess beim Abbrechen einen Server auf Port 4336 zurueck. Jetzt
notbremse(240s) und mouse.move statt hover().
* color-mix(in srgb, var(--rand)) -- mit EINER Farbe ungueltig; der
ganze Verlauf fiel aus, uebrig blieb Abstand ohne Linie.
* Im Pruefausdruck steckte ein Backspace-Byte (0x08), das die
Kommandozeile aus einer Zeichenfolge gemacht hatte. Sah im
Quelltext richtig aus, traf nie. Jetzt startsWith.
pruef-scouting-felder: 64 -> 72 Pruefungen, gruen. Gegenprobe gefahren
(einer Stufe den Ton genommen -> rot, mit #db4b66 als Beleg). Statt des
226-Sekunden-Hauslaufs pruef-handy misst die Datei jetzt selbst auf
320px: 97 Teile, keines ueber dem Rand, kein seitliches Schieben.
Dazu gruen: css-klassen, lesbarkeit, tippziele, fingermass,
haus-trennung, leerzustand, deutsche-texte, zeichen (der eine Rest dort
ist bereich.css und aelter).
Co-Authored-By: Claude Opus 5 <[email protected]>
276 lines
14 KiB
JavaScript
276 lines
14 KiB
JavaScript
/* 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";
|
|
/* NOTBREMSE (nachgeruestet 06.10.2026, nachdem genau das passiert ist).
|
|
|
|
Dieses Werkzeug startet einen Server und einen Browser. Beim Versuch,
|
|
die Farbkante unter dem Zeiger zu messen, blieb `page.hover()` haengen
|
|
-- und ein Stillstand sieht von aussen genauso aus wie "laeuft noch".
|
|
Schlimmer: Nach dem Abbrechen von aussen blieb der Server auf Port
|
|
4336 stehen und haette den naechsten Lauf blockiert.
|
|
|
|
Dieselbe Regel wie fuer die Pruefungen, und aus demselben Grund: Eine
|
|
Frist, die ueberschritten wird, ist ein FEHLER mit Meldung -- kein
|
|
Warten ohne Ende. */
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
notbremse(240_000, "bild-arten");
|
|
|
|
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}`);
|
|
}
|
|
/* WAS KOMMT AN DER KARTE WIRKLICH AN? (06.10.2026)
|
|
|
|
`module.css` belegt `::before` (Kantenlicht) und `::after` (Raster,
|
|
Eckwinkel) JEDER Karte und wird NACH scouting.css geladen. Was
|
|
scouting.css auf dieselben Pseudoelemente legt, verliert damit --
|
|
ohne dass irgendwo ein Fehler erscheint. Diese Messung sagt, welcher
|
|
Ton tatsaechlich gilt und wie breit die farbige Kante wirklich ist. */
|
|
console.log("\n--- Was an der Karte wirklich ankommt ---");
|
|
const kante = await seite.$eval(".kk", (k) => {
|
|
const vor = getComputedStyle(k, "::before");
|
|
return {
|
|
ton: getComputedStyle(k).getPropertyValue("--ton").trim() || "(nicht gesetzt)",
|
|
stufenfarbe: getComputedStyle(k).getPropertyValue("--stufe-farbe").trim() || "(nicht gesetzt)",
|
|
vorBreite: vor.width, vorInhalt: vor.content,
|
|
grund: getComputedStyle(k).backgroundImage.slice(0, 60),
|
|
};
|
|
});
|
|
console.log(" --ton:", kante.ton, " --stufe-farbe:", kante.stufenfarbe);
|
|
console.log(" ::before Breite:", kante.vorBreite, " (3px = Kante von scouting.css, sonst ueberschrieben)");
|
|
console.log(" Grundbild:", kante.grund);
|
|
|
|
/* WIE BREIT IST DIE FARBKANTE WIRKLICH -- und kommt sie durch?
|
|
Gemessen wird nicht die Regel, sondern das Bild: die Farbe des
|
|
Bildpunktes zwei Pixel rechts von der linken Kartenkante, einmal in
|
|
Ruhe und einmal unter dem Zeiger. Eine Kante, die man nur im
|
|
Quelltext sieht, ist keine. */
|
|
const kantenMass = await seite.evaluate(() => {
|
|
const k = document.querySelector(".kk");
|
|
const s = getComputedStyle(k);
|
|
return { groesse: s.backgroundSize, lage: s.backgroundPosition, kante: s.getPropertyValue("--kante").trim() };
|
|
});
|
|
console.log(" background-size:", kantenMass.groesse, "| --kante:", kantenMass.kante || "(nicht gesetzt)");
|
|
/* MIT mouse.move AUF DIE MITTE DER KARTE, NICHT MIT page.hover().
|
|
`hover()` wartet darauf, dass das Ziel "bedienbar" ist -- sichtbar,
|
|
stabil, nicht verdeckt. Bei einer Karte, ueber der das Hintergrundbild
|
|
der Seite liegt, kann diese Pruefung dreissig Sekunden brauchen oder
|
|
nie fertig werden. Gemessen werden soll hier aber nicht die
|
|
Bedienbarkeit, sondern was der Zeiger an der Farbe aendert -- dafuer
|
|
genuegt es, den Zeiger hinzubewegen. */
|
|
{
|
|
const m = await seite.$eval(".kk", (k) => {
|
|
const r = k.getBoundingClientRect();
|
|
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
|
});
|
|
await seite.mouse.move(m.x, m.y);
|
|
await seite.waitForTimeout(400);
|
|
}
|
|
const unterZeiger = await seite.evaluate(() =>
|
|
getComputedStyle(document.querySelector(".kk")).getPropertyValue("--kante").trim());
|
|
console.log(" --kante unter dem Zeiger:", unterZeiger || "(nicht gesetzt)");
|
|
/* Kommt die Haarlinie zwischen Kopf und Zahlen an? Ein ungueltiger
|
|
Farbwert laesst `border-image` lautlos ausfallen -- uebrig bleibt
|
|
Abstand ohne Linie, und das sieht im Quelltext richtig aus. */
|
|
const linie = await seite.$eval(".kk__info", (k) => {
|
|
const s2 = getComputedStyle(k);
|
|
return { quelle: s2.borderImageSource.slice(0, 48), breite: s2.borderTopWidth };
|
|
});
|
|
console.log(" Haarlinie in der Karte:", linie.breite, "|", linie.quelle);
|
|
/* Eine Nahaufnahme der linken Kante -- 90 px breit, damit man sie
|
|
ueberhaupt sehen kann. */
|
|
/* Eine Karte OHNE Faellig-Saum: Bei Tamy liegt der warme Innenring
|
|
ueber der Stufenkante, und dann misst man den Ring statt der Kante. */
|
|
const kasten = await seite.$eval(".kk:not([data-faellig])", (k) => {
|
|
const r = k.getBoundingClientRect();
|
|
/* `width`/`height` -- nicht `w`/`h`. Playwright meldet dann
|
|
"clip.width: expected float, got undefined"; gefunden hat es die
|
|
Notbremse, nicht das Auge. */
|
|
return { x: Math.round(r.x), y: Math.round(r.y), width: 90, height: Math.round(r.height) };
|
|
});
|
|
await seite.screenshot({ path: "server/bild-arten-kante.png", clip: kasten });
|
|
await seite.mouse.move(0, 0);
|
|
await seite.waitForTimeout(300);
|
|
|
|
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);
|