Files
dogfather-universe/server/bild-arten.mjs
T
DogFatherGitandClaude Opus 5 ea4078740e Pipeline: die Stufenfarbe kommt endlich auf dem Bildschirm an
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]>
2026-10-07 00:04:49 +02:00

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);