Filipe, mit dem Fenster im Bild: "perfektionnier auch diese kachel die sieht so scheisse aus." Er hatte recht, und der Grund war nicht die Farbe, sondern die REIHENFOLGE. Vor dem Feld, in das man einfuegt, standen fuenf lange Schritte und zwei Absaetze -- rund 1400 px Text, bevor die eigentliche Arbeit anfing. Wer das Fenster zum zwanzigsten Mal oeffnet, scrollt jedes Mal an einer Anleitung vorbei, die er laengst kennt; wer es zum ersten Mal oeffnet, liest eine Wand, bevor er weiss, worum es geht. Das habe ich selbst gebaut, heute Vormittag, auf den Wunsch "ich brauch auch eine erklaerung immer dabei". Die Erklaerung war richtig -- ihr Platz war falsch. JETZT: ein Satz oben, sofort das Feld, dann Tag und Vorschau. Die ausfuehrliche Anleitung liegt unter einer aufklappbaren Zeile, die zugeklappt 37 px braucht statt 560. DIE UEBERSCHRIFTENZEILE BLEIBT OBEN, ausserhalb des Aufklappers. Sie ist die einzige Angabe, ohne die es gar nicht geht -- den haeufigsten Fehlgriff hinter einen Klick zu legen waere genau der falsche Tausch. Nebenbei: Der Erklaersatz zum Tag stand IN der Feldbeschriftung und machte sie zweizeilig -- eine Beschriftung, die man lesen muss, ist keine mehr. Er steht jetzt darunter. Das Datumsfeld erbt die Schrift des Hauses (ohne Angabe nimmt der Browser seine eigene, und das sah aus wie ein vergessener Rest) und ist auf die Breite gedeckelt, die ein Datum braucht. Und das Fenster rollt INNEN, damit "Uebernehmen" immer erreichbar bleibt. pruef-backstage-import 72 -> 77. Die neuen Zeilen messen die POSITION in Pixeln, nicht den Text: Die bestehenden Pruefungen lesen `textContent` des ganzen Dialogs und waeren auch dann gruen, wenn die Anleitung wieder nach vorn rutscht. Dazu: zugeklappt beim Oeffnen, Ueberschriftenzeile trotzdem sichtbar, die Zeile nimmt unter 90 px, und der Aufklapper klappt wirklich auf (ein Aufklapper, der klemmt, versteckt die Anleitung endgueltig). Mit SCHIRM=1 legt die Pruefung drei Bilder ab: leer, zugeklappt, aufgeklappt. Co-Authored-By: Claude Opus 5 <[email protected]>
711 lines
34 KiB
JavaScript
711 lines
34 KiB
JavaScript
/* =====================================================================
|
|
DER BACKSTAGE-IMPORT — eine Tabelle, alle Creator auf einmal.
|
|
|
|
Filipe (10.09.2026): "wie sehr wir diese seite hier mit dem backstage
|
|
verbinden könnten von tiktok. ohne probleme und ganz schnell."
|
|
|
|
Die Recherche ergab: Backstage hat keine Schnittstelle und keinen
|
|
Export. Was es hat, ist eine Tabelle im Browser. Also: markieren,
|
|
kopieren, einfügen -- und der Workspace ordnet die Zeilen den
|
|
Creators zu.
|
|
|
|
DIE ZUORDNUNG IST DIE GANZE ARBEIT, und sie ist auch die ganze
|
|
Gefahr: Eine falsch zugeordnete Zahl ist schlimmer als eine
|
|
fehlende, weil die fehlende auffällt. Deshalb prüft diese Datei vor
|
|
allem, was NICHT passiert:
|
|
|
|
- eine unbekannte Person wird gemeldet, nicht geraten
|
|
- eine Zeile ohne Zahlen wird übersprungen, nicht als Null
|
|
geschrieben (sie würde sonst einen echten Tag überschreiben)
|
|
- ein Datum in der Zukunft wird abgelehnt
|
|
- ein Scout kommt gar nicht erst an diesen Weg
|
|
|
|
Aufruf: node server/pruef-backstage-import.mjs
|
|
===================================================================== */
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { spawn } from "node:child_process";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
import { scryptSync, randomBytes } from "node:crypto";
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
|
|
notbremse(240_000, "pruef-backstage-import");
|
|
|
|
const HIER = dirname(fileURLToPath(import.meta.url));
|
|
const PORT = 4389;
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-backstage-"));
|
|
const DB = join(ordner, "workspace.db");
|
|
const jetzt = new Date().toISOString();
|
|
const heute = new Date().toISOString().slice(0, 10);
|
|
const gestern = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
|
|
const morgen = new Date(Date.now() + 86400000).toISOString().slice(0, 10);
|
|
|
|
let gemacht = 0, fehler = 0;
|
|
const ok = (b, text) => {
|
|
gemacht++;
|
|
if (b) console.log(` ok ${text}`);
|
|
else { fehler++; console.log(` FEHL ${text}`); }
|
|
};
|
|
|
|
let kind = null;
|
|
async function starten() {
|
|
kind = spawn(process.execPath, [join(HIER, "index.js")], {
|
|
env: { ...process.env, WORKSPACE_DB: DB, PORT: String(PORT),
|
|
SITE_ACCESS_SECRET: "lokaler-test",
|
|
SITE_PUBLIC_LAUNCH_AT: "2020-01-01T00:00:00+01:00" },
|
|
stdio: ["ignore", "pipe", "pipe"] });
|
|
let aus = "";
|
|
kind.stdout.on("data", (d) => { aus += d; });
|
|
kind.stderr.on("data", (d) => { aus += d; });
|
|
for (let i = 0; i < 80; i++) {
|
|
await new Promise((r) => setTimeout(r, 250));
|
|
if (kind.exitCode !== null) break;
|
|
try { const a = await fetch(BASIS + "/workspace/", { redirect: "manual" });
|
|
if (a.status < 500) return; } catch { /* noch nicht oben */ }
|
|
}
|
|
throw new Error("Server startete nicht:\n" + aus.slice(-1500));
|
|
}
|
|
await starten();
|
|
kind.kill();
|
|
await new Promise((r) => setTimeout(r, 600));
|
|
|
|
let idNova, idKiro, idLumi, idSam, idMira;
|
|
{
|
|
const d = new DatabaseSync(DB);
|
|
const anlegen = (name, rolle, code, tiktok) => {
|
|
const salz = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(code, salz, 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, tiktok) VALUES (?,?,?,?,?,1,?,?)")
|
|
.run(name, rolle, hash, salz, 32768, jetzt, tiktok ?? null);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
};
|
|
anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
|
idSam = anlegen("Sam", "scout", "CODE-SAM-00001");
|
|
/* Seit dem 11.09.2026 kommen auch Manager und Spicy Media auf diese
|
|
Seite. Beide sehen andere Creator als ein Scout -- Spicy Media
|
|
alle (siehtAlles), ein Manager nur seine eigenen und die seiner
|
|
Scouts. Ohne beide im Bestand waere das ungeprueft. */
|
|
idMira = anlegen("Mira", "manager", "CODE-MIRA-0001");
|
|
anlegen("Sasha", "spicy", "CODE-SASHA-0001");
|
|
/* Nova hat ein Handle, Kiro NICHT -- er wird ueber den Namen
|
|
gefunden. Lumi hat ein Handle mit anderer Schreibweise als in der
|
|
Tabelle: Genau daran zeigt sich, ob normalisiert wird. */
|
|
idNova = anlegen("Nova", "creator", "CODE-NOVA-0001", "nova_live");
|
|
idKiro = anlegen("Kiro", "creator", "CODE-KIRO-0001", null);
|
|
idLumi = anlegen("Lumi", "creator", "CODE-LUMI-0001", "LumiStreamt");
|
|
/* Sam betreut Nova. Ohne diese Zeile haette der Scout gar keinen
|
|
Creator -- die Seite waere leer, und eine Pruefung auf "der Scout
|
|
sieht den Backstage-Satz NICHT" waere gruen, weil er ueberhaupt
|
|
nichts sieht. Das ist der Unterschied zwischen einem Beweis und
|
|
einem Zufall. */
|
|
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)")
|
|
.run(idNova, idSam, jetzt);
|
|
/* Und Sam gehoert zu Mira -- damit der Manager ueber seinen Scout
|
|
an Nova kommt. Sonst saehe er eine leere Seite, und die Pruefung
|
|
unten wuerde messen, dass nichts da ist, statt dass es stimmt. */
|
|
d.prepare("INSERT INTO scout_zuteilung (scout_id, manager_id, seit) VALUES (?,?,?)")
|
|
.run(idSam, idMira, jetzt);
|
|
d.close();
|
|
}
|
|
await starten();
|
|
|
|
const anmelden = async (rolle, code) => {
|
|
const a = await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rolle, code }) });
|
|
const keks = (a.headers.getSetCookie?.() || []).map((k) => k.split(";")[0]).join("; ");
|
|
if (!keks) throw new Error(`Anmeldung ${rolle} fehlgeschlagen (${a.status})`);
|
|
return keks;
|
|
};
|
|
const keksDogi = await anmelden("admin", "CODE-DOGI-0001");
|
|
const keksScout = await anmelden("scout", "CODE-SAM-00001");
|
|
|
|
const ruf = async (weg, keks, koerper) => {
|
|
const a = await fetch(BASIS + weg, { method: "POST",
|
|
headers: { Cookie: keks, "Content-Type": "application/json", Origin: BASIS },
|
|
body: JSON.stringify(koerper) });
|
|
let daten = null; try { daten = await a.json(); } catch { /* leer erlaubt */ }
|
|
return { status: a.status, daten };
|
|
};
|
|
|
|
/* EINE TABELLE, WIE SIE AUS DEM BROWSER KOMMT: mit TABULATOREN.
|
|
Genau das ist der Fall, den der alte Zerleger nicht kannte -- er
|
|
suchte nur Komma und Semikolon. */
|
|
const TABELLE = [
|
|
"Creator\tHandle\tDiamanten\tLIVE-Dauer (Minuten)\tØ Zuschauer\tPeak Zuschauer\tNeue Follower",
|
|
`Nova\t@Nova_Live\t12.480\t184\t42\t310\t23`,
|
|
`Kiro\t\t3.900\t95\t18\t74\t6`,
|
|
`Lumi\t@lumistreamt\t22.010\t240\t88\t512\t41`,
|
|
`Fremde Person\t@niemand_hier\t500\t30\t5\t12\t1`,
|
|
].join("\n");
|
|
|
|
/* =======================================================================
|
|
1. Die Vorschau ordnet zu -- und raet nicht
|
|
======================================================================= */
|
|
console.log("=== Die Vorschau ===");
|
|
{
|
|
const { status, daten } = await ruf("/workspace/api/leistung/netzwerk-import/vorschau",
|
|
keksDogi, { text: TABELLE, tag: gestern });
|
|
ok(status === 200, `die Tabelle wird gelesen (HTTP ${status})`);
|
|
/* DER TABULATOR IST DER PUNKT: Ohne ihn stuende alles in Spalte 1
|
|
und es waere nichts erkannt worden. */
|
|
ok(daten?.zugeordnet === 3, `drei von vier Zeilen zugeordnet (${daten?.zugeordnet})`);
|
|
ok(daten?.unbekannt?.length === 1 && /niemand_hier/.test(daten.unbekannt[0]),
|
|
`die vierte wird beim Namen genannt (${(daten?.unbekannt || []).join(", ")})`);
|
|
ok(daten?.neu === 3 && daten?.ersetzt === 0, `drei neu, keine ersetzt (${daten?.neu}/${daten?.ersetzt})`);
|
|
const sp = Object.keys(daten?.spalten || {});
|
|
ok(sp.includes("diamanten") && sp.includes("dauer_min") && sp.includes("zuschauer_avg")
|
|
&& sp.includes("zuschauer_max") && sp.includes("follower_neu"),
|
|
`fuenf Zahlenspalten erkannt (${sp.join(", ")})`);
|
|
/* "12.480" ist deutsch geschrieben und muss 12480 sein, nicht 12,48. */
|
|
const nova = (daten?.beispiele || []).find((b) => b.name === "Nova");
|
|
ok(nova?.diamanten === 12480, `"12.480" wurde zu 12480 (${nova?.diamanten})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
2. Ohne Datum in der Tabelle UND ohne Vorgabe: eine Frage, kein Unsinn
|
|
======================================================================= */
|
|
console.log("\n=== Wenn der Tag fehlt ===");
|
|
{
|
|
const { status, daten } = await ruf("/workspace/api/leistung/netzwerk-import/vorschau",
|
|
keksDogi, { text: TABELLE });
|
|
ok(status === 400 && daten?.braucht_tag === true,
|
|
`es wird nach dem Tag gefragt statt geraten (HTTP ${status})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. Uebernehmen -- und was dabei NICHT passiert
|
|
======================================================================= */
|
|
console.log("\n=== Übernehmen ===");
|
|
{
|
|
const { status, daten } = await ruf("/workspace/api/leistung/netzwerk-import",
|
|
keksDogi, { text: TABELLE, tag: gestern });
|
|
ok(status === 200, `die Übernahme läuft (HTTP ${status})`);
|
|
ok(daten?.geschrieben === 3, `drei Zeilen geschrieben (${daten?.geschrieben})`);
|
|
|
|
const d = new DatabaseSync(DB);
|
|
const z = d.prepare("SELECT creator_id, diamanten, dauer_min, gueltiger_tag, quelle FROM leistung WHERE tag = ? ORDER BY creator_id").all(gestern);
|
|
ok(z.length === 3, `drei Zeilen stehen in der Datenbank (${z.length})`);
|
|
ok(z.every((x) => x.quelle === "import"),
|
|
"alle mit Quelle 'import' -- von Hand getippte sind unterscheidbar");
|
|
ok(z.every((x) => x.gueltiger_tag === 1),
|
|
"alle als gültiger Tag gezählt, weil LIVE-Dauer vorhanden ist");
|
|
const lumi = z.find((x) => x.creator_id === idLumi);
|
|
ok(lumi?.diamanten === 22010,
|
|
`Lumi wurde über "@lumistreamt" gegen "LumiStreamt" gefunden (${lumi?.diamanten})`);
|
|
const kiro = z.find((x) => x.creator_id === idKiro);
|
|
ok(kiro?.diamanten === 3900, `Kiro ohne Handle über den Namen (${kiro?.diamanten})`);
|
|
/* DIE FREMDE PERSON DARF NICHT ENTSTANDEN SEIN. Ein Import, der
|
|
nebenbei Leute anlegt, waere die schlimmste Variante von
|
|
"hilfreich". */
|
|
const wieViele = d.prepare("SELECT COUNT(*) AS n FROM personen WHERE rolle = 'creator'").get().n;
|
|
ok(wieViele === 3, `es wurde niemand neu angelegt (${wieViele} Creator)`);
|
|
d.close();
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. Noch einmal dieselbe Tabelle: ersetzen statt verdoppeln
|
|
======================================================================= */
|
|
console.log("\n=== Dieselbe Tabelle ein zweites Mal ===");
|
|
{
|
|
const { daten } = await ruf("/workspace/api/leistung/netzwerk-import/vorschau",
|
|
keksDogi, { text: TABELLE, tag: gestern });
|
|
ok(daten?.ersetzt === 3 && daten?.neu === 0,
|
|
`jetzt drei ersetzt, keine neu (${daten?.neu}/${daten?.ersetzt})`);
|
|
const { status } = await ruf("/workspace/api/leistung/netzwerk-import",
|
|
keksDogi, { text: TABELLE, tag: gestern });
|
|
ok(status === 200, "und sie laeuft anstandslos durch");
|
|
const d = new DatabaseSync(DB);
|
|
const n = d.prepare("SELECT COUNT(*) AS n FROM leistung WHERE tag = ?").get(gestern).n;
|
|
ok(n === 3, `es sind immer noch drei Zeilen, keine sechs (${n})`);
|
|
d.close();
|
|
}
|
|
|
|
/* =======================================================================
|
|
5. Eine von Hand geschriebene Notiz ueberlebt
|
|
======================================================================= */
|
|
console.log("\n=== Was von Hand kam, bleibt ===");
|
|
{
|
|
const d = new DatabaseSync(DB);
|
|
d.prepare("UPDATE leistung SET notiz = ? WHERE creator_id = ? AND tag = ?")
|
|
.run("Technikprobleme ab 22 Uhr", idNova, gestern);
|
|
d.close();
|
|
await ruf("/workspace/api/leistung/netzwerk-import", keksDogi, { text: TABELLE, tag: gestern });
|
|
const d2 = new DatabaseSync(DB);
|
|
const n = d2.prepare("SELECT notiz FROM leistung WHERE creator_id = ? AND tag = ?")
|
|
.get(idNova, gestern);
|
|
ok(n?.notiz === "Technikprobleme ab 22 Uhr",
|
|
`die Notiz steht nach dem Import noch da ("${n?.notiz}")`);
|
|
d2.close();
|
|
}
|
|
|
|
/* =======================================================================
|
|
6. Gegenproben -- was abgelehnt wird
|
|
======================================================================= */
|
|
console.log("\n=== Gegenproben ===");
|
|
{
|
|
const { status } = await ruf("/workspace/api/leistung/netzwerk-import/vorschau",
|
|
keksDogi, { text: TABELLE, tag: morgen });
|
|
/* Ein Tag in der Zukunft laesst die Zeilen durchfallen -- die
|
|
Vorschau antwortet zwar 200, ordnet aber nichts zu. */
|
|
const { daten } = await ruf("/workspace/api/leistung/netzwerk-import/vorschau",
|
|
keksDogi, { text: TABELLE, tag: morgen });
|
|
ok(status === 200 && daten?.zugeordnet === 0 && daten?.probleme_gesamt >= 3,
|
|
`ein Datum in der Zukunft wird abgelehnt (${daten?.zugeordnet} zugeordnet, `
|
|
+ `${daten?.probleme_gesamt} Probleme)`);
|
|
}
|
|
{
|
|
const ohneZahlen = ["Creator\tHandle\tDiamanten", "Nova\t@Nova_Live\t"].join("\n");
|
|
const { daten } = await ruf("/workspace/api/leistung/netzwerk-import/vorschau",
|
|
keksDogi, { text: ohneZahlen, tag: gestern });
|
|
ok(daten?.zugeordnet === 0,
|
|
"eine Zeile ganz ohne Zahl wird uebersprungen statt als Null geschrieben");
|
|
}
|
|
{
|
|
const ohneNamen = ["Datum\tDiamanten", `${gestern}\t100`].join("\n");
|
|
const { status, daten } = await ruf("/workspace/api/leistung/netzwerk-import/vorschau",
|
|
keksDogi, { text: ohneNamen });
|
|
ok(status === 400 && /Name oder Handle/.test(daten?.fehler || ""),
|
|
`ohne Personenspalte wird es abgelehnt ("${daten?.fehler}")`);
|
|
}
|
|
{
|
|
const { status } = await ruf("/workspace/api/leistung/netzwerk-import",
|
|
keksScout, { text: TABELLE, tag: gestern });
|
|
ok(status === 403, `ein Scout kommt an diesen Weg nicht (HTTP ${status})`);
|
|
}
|
|
{
|
|
/* GEGENPROBE ZUR GEGENPROBE: Das Richtige geht weiterhin durch.
|
|
Ohne sie hiesse "alles wird abgelehnt" auch "alles wird
|
|
abgelehnt". */
|
|
const { status } = await ruf("/workspace/api/leistung/netzwerk-import/vorschau",
|
|
keksDogi, { text: TABELLE, tag: heute });
|
|
ok(status === 200, `heute als Tag geht dagegen durch (HTTP ${status})`);
|
|
}
|
|
|
|
/* DIE SCHNITTSTELLE ZUR TIKTOK-DATEI GIBT ES NICHT MEHR (11.09.2026).
|
|
|
|
Filipe: "ich will keine daten von denen kriegen, nur wir."
|
|
|
|
Ein ausgebauter Knopf ist keine Entfernung -- die Schnittstelle
|
|
waere weiterhin erreichbar, und wer die Adresse kennt, koennte
|
|
weiter eine Creator-Datei hochladen. Gemessen wird deshalb hier,
|
|
mit DogFathers Zugang, also dem staerksten, den es gibt: Bekommt
|
|
ER 404, bekommt ihn jeder.
|
|
|
|
GEGENPROBE DANEBEN: Der Backstage-Weg antwortet weiterhin. Sonst
|
|
bewiese das 404 nur, dass ich mich vertippt habe. */
|
|
for (const weg of ["/workspace/api/leistung/1/tiktok-datei",
|
|
"/workspace/api/leistung/1/tiktok-datei/vorschau"]) {
|
|
const { status } = await ruf(weg, keksDogi, { inhalt: {} });
|
|
ok(status === 404, `${weg.split("/").slice(-2).join("/")} gibt es nicht mehr (HTTP ${status})`);
|
|
}
|
|
{
|
|
const { status } = await ruf("/workspace/api/leistung/netzwerk-import/vorschau",
|
|
keksDogi, { text: TABELLE, tag: gestern });
|
|
ok(status === 200,
|
|
`Gegenprobe: der Backstage-Weg antwortet weiterhin (HTTP ${status})`);
|
|
}
|
|
|
|
/* WER SIEHT WEN -- direkt an der Schnittstelle gemessen, bevor der
|
|
Browser ins Spiel kommt. Wird der Auswahlliste auf der Seite etwas
|
|
fehlen, steht hier, ob es schon hier fehlte. */
|
|
for (const [rolle, code, wer] of [
|
|
["manager", "CODE-MIRA-0001", "über seinen Scout"],
|
|
["spicy", "CODE-SASHA-0001", "alle"],
|
|
]) {
|
|
const keks = await anmelden(rolle, code);
|
|
const a = await fetch(BASIS + "/workspace/api/leistung", { headers: { Cookie: keks } });
|
|
const d = await a.json().catch(() => ({}));
|
|
const namen = (d.creator || []).map((c) => c.name).join(", ");
|
|
ok(a.status === 200 && namen.length > 0,
|
|
`${rolle} bekommt Creator (${wer}): ${namen || `LEER, HTTP ${a.status}`}`);
|
|
}
|
|
|
|
/* Vorbedingung fuer die Gegenprobe weiter unten: Sam MUSS Nova sehen.
|
|
Sonst waere "der Scout sieht den Backstage-Satz nicht" nur die
|
|
Auskunft, dass seine Seite leer ist. */
|
|
{
|
|
const a = await fetch(BASIS + "/workspace/api/leistung", { headers: { Cookie: keksScout } });
|
|
const d = await a.json().catch(() => ({}));
|
|
const namen = (d.creator || []).map((c) => c.name).join(", ");
|
|
ok(/Nova/.test(namen),
|
|
`der Scout sieht seinen Creator in der Liste (${namen || "leer"})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
7. Und die Seite selbst
|
|
======================================================================= */
|
|
console.log("\n=== Die Seite ===");
|
|
{
|
|
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await pw.chromium.launch();
|
|
const ctx = await browser.newContext({ viewport: { width: 1400, height: 1000 } });
|
|
const seite = await ctx.newPage();
|
|
const skriptfehler = [];
|
|
seite.on("pageerror", (e) => skriptfehler.push(String(e).slice(0, 140)));
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
|
await seite.click('.rolle[data-rolle="admin"]');
|
|
await seite.fill("#code", "CODE-DOGI-0001");
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 25000 });
|
|
await seite.goto(BASIS + "/workspace/leistung.html", { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1800);
|
|
|
|
ok(skriptfehler.length === 0, `kein Skriptfehler (${skriptfehler.join(" | ") || "keiner"})`);
|
|
const sichtbar = await seite.$eval("#netz-oeffnen", (e) => !e.hidden).catch(() => false);
|
|
ok(sichtbar === true, "DogFather sieht den Knopf „Backstage-Tabelle einfügen\"");
|
|
|
|
/* Einfuegen, Vorschau abwarten, Meldung lesen -- der ganze Weg. */
|
|
await seite.click("#netz-oeffnen");
|
|
await seite.waitForTimeout(400);
|
|
await seite.fill("#netz-tag", gestern);
|
|
await seite.fill("#netz-text", TABELLE);
|
|
await seite.dispatchEvent("#netz-text", "change");
|
|
await seite.waitForTimeout(1200);
|
|
const vorschau = await seite.$eval("#netz-vorschau", (e) => e.textContent).catch(() => "");
|
|
ok(/3 von 4 Zeilen zugeordnet/.test(vorschau),
|
|
`die Vorschau nennt die Zahl (${vorschau.slice(0, 60)}…)`);
|
|
ok(/niemand_hier/.test(vorschau), "und den Namen, der nicht zugeordnet wurde");
|
|
const losFrei = await seite.$eval("#netz-los", (e) => !e.disabled);
|
|
ok(losFrei === true, "der Übernehmen-Knopf ist danach frei");
|
|
|
|
/* ---- DIE ERKLÄRUNG MUSS DA SEIN (11.09.2026) --------------------
|
|
Filipe: "ich brauch auch eine erklärung immer dabei wo die scouts
|
|
und manager das finden." Ein Text, den niemand prüft, verschwindet
|
|
beim nächsten Umbau -- und niemandem fällt es auf, weil ein
|
|
fehlender Satz nichts kaputtmacht. */
|
|
const schritte = await seite.$$eval("#netz-dialog .anleitung li", (ls) => ls.length);
|
|
ok(schritte >= 5, `die Backstage-Anleitung hat ${schritte} Schritte`);
|
|
const anleitung = await seite.$eval("#netz-dialog", (e) => e.textContent);
|
|
ok(/Überschriftenzeile/.test(anleitung),
|
|
"und sagt, dass die Überschriftenzeile mitmarkiert werden muss");
|
|
ok(/Steckbrief/.test(anleitung),
|
|
"und woher die Zuordnung kommt (TikTok-Name im Steckbrief)");
|
|
|
|
/* ---- ARBEIT ZUERST, ANLEITUNG DAHINTER (11.09.2026) -------------
|
|
Filipe: "perfektionnier auch diese kachel die sieht so scheisse
|
|
aus." Fuenf lange Schritte standen VOR dem Einfuegefeld.
|
|
|
|
DIE ZEILEN OBEN WUERDEN AUCH GRUEN BLEIBEN, WENN DIE ANLEITUNG
|
|
WIEDER NACH VORN RUTSCHT -- `textContent` liest den ganzen Dialog
|
|
und kennt keine Reihenfolge. Deshalb wird hier die POSITION
|
|
gemessen, in Pixeln, an der laufenden Seite.
|
|
|
|
Und die eine Angabe, ohne die es gar nicht geht (die
|
|
Ueberschriftenzeile), muss SICHTBAR sein, ohne dass jemand
|
|
aufklappt. Sie hinter einen Klick zu legen hiesse, den
|
|
haeufigsten Fehlgriff zu verstecken. */
|
|
const bau = await seite.evaluate(() => {
|
|
const o = (id) => document.getElementById(id)?.getBoundingClientRect().top ?? -1;
|
|
const hilfe = document.getElementById("netz-hilfe");
|
|
const sichtbar = [...document.querySelectorAll("#netz-form > *")]
|
|
.filter((e) => e !== hilfe && !e.hidden)
|
|
.map((e) => e.textContent).join(" ");
|
|
return {
|
|
feld: o("netz-text"), hilfe: o("netz-hilfe"),
|
|
offen: hilfe?.open === true,
|
|
sichtbar,
|
|
zeileHoch: document.getElementById("netz-hilfe")?.offsetHeight ?? 0,
|
|
};
|
|
});
|
|
ok(bau.feld > 0 && bau.hilfe > bau.feld,
|
|
`das Einfügefeld steht VOR der Anleitung (${Math.round(bau.feld)} px zu `
|
|
+ `${Math.round(bau.hilfe)} px)`);
|
|
ok(bau.offen === false, "die Anleitung ist zugeklappt -- sie steht im Weg, wer sie kennt");
|
|
ok(/Überschriftenzeile/.test(bau.sichtbar),
|
|
"trotzdem steht die Überschriftenzeile schon oben, ohne Aufklappen");
|
|
ok(bau.zeileHoch > 0 && bau.zeileHoch < 90,
|
|
`zugeklappt nimmt sie kaum Platz (${bau.zeileHoch} px)`);
|
|
|
|
/* Und sie laesst sich wirklich oeffnen -- ein Aufklapper, der nicht
|
|
aufklappt, versteckt die Anleitung endgueltig. */
|
|
await seite.click("#netz-hilfe > summary");
|
|
await seite.waitForTimeout(300);
|
|
const auf = await seite.evaluate(() => {
|
|
const h = document.getElementById("netz-hilfe");
|
|
const ol = document.querySelector("#netz-hilfe .anleitung");
|
|
return { offen: h?.open === true, hoch: ol?.offsetHeight ?? 0 };
|
|
});
|
|
ok(auf.offen === true && auf.hoch > 80,
|
|
`aufgeklappt stehen die Schritte da (${auf.hoch} px)`);
|
|
await seite.click("#netz-hilfe > summary");
|
|
await seite.waitForTimeout(200);
|
|
if (process.env.SCHIRM) {
|
|
await seite.locator("#netz-dialog").screenshot(
|
|
{ path: join(HIER, "pruef-netz-zu.png") });
|
|
await seite.click("#netz-hilfe > summary");
|
|
await seite.waitForTimeout(300);
|
|
await seite.locator("#netz-dialog").screenshot(
|
|
{ path: join(HIER, "pruef-netz-auf.png") });
|
|
await seite.click("#netz-hilfe > summary");
|
|
await seite.waitForTimeout(200);
|
|
/* Und der Zustand, den man beim OEFFNEN sieht -- genau der, ueber
|
|
den Filipe gestolpert ist. */
|
|
await seite.evaluate(() => {
|
|
document.getElementById("netz-text").value = "";
|
|
document.getElementById("netz-vorschau").hidden = true;
|
|
document.getElementById("netz-tag").value = "";
|
|
});
|
|
await seite.waitForTimeout(200);
|
|
await seite.locator("#netz-dialog").screenshot(
|
|
{ path: join(HIER, "pruef-netz-leer.png") });
|
|
}
|
|
|
|
await seite.click("#netz-zu");
|
|
await seite.waitForTimeout(300);
|
|
/* Gemessen wird SATZ FUER SATZ, nicht der Block. Ein Block, der
|
|
als Ganzes erscheint, erklaert einem Scout auch den Knopf, den er
|
|
nicht hat -- das ist keine Hilfe, sondern eine Absage. */
|
|
const satz = (id) => seite.$eval(id, (e) => (e.hidden ? "" : e.textContent))
|
|
.catch(() => "");
|
|
const netzSatz = await satz("#quelle-netz");
|
|
ok(/Backstage/.test(netzSatz),
|
|
"unter den Knöpfen steht dauerhaft, woher die Zahlen kommen");
|
|
|
|
/* ---- EINE EXCEL-DATEI IST EINE SACKGASSE MIT WEGWEISER ----------
|
|
Filipe: "meine excel datei kann ich nicht auswaehlen wieso??"
|
|
|
|
Sie liess sich nicht einmal AUSWAEHLEN -- ausgegraut, ohne ein
|
|
Wort dazu. Jetzt laesst sie sich waehlen und bekommt eine Antwort,
|
|
die weiterhilft.
|
|
|
|
GEPRUEFT WIRD AN EINEM ECHTEN ZIP-KOPF ("PK"), denn genau das ist
|
|
eine .xlsx. Die Erkennung haengt am INHALT, nicht an der Endung --
|
|
deshalb heisst die Testdatei hier bewusst ".xlsx" UND traegt den
|
|
richtigen Kopf; eine Pruefung ueber die Endung waere gruen, ohne
|
|
die Erkennung je zu beruehren. */
|
|
await seite.click("#import-oeffnen");
|
|
await seite.waitForTimeout(300);
|
|
await seite.setInputFiles("#datei", {
|
|
name: "backstage.xlsx",
|
|
mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
buffer: Buffer.from([0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x06, 0x00, 0x08, 0x00]),
|
|
});
|
|
await seite.waitForTimeout(600);
|
|
const xlsx = await seite.evaluate(() => ({
|
|
text: document.getElementById("import-fehler")?.textContent || "",
|
|
vorschau: document.getElementById("vorschau")?.hidden,
|
|
los: document.getElementById("import-los")?.disabled,
|
|
feld: document.getElementById("datei")?.value || "",
|
|
}));
|
|
ok(/Excel/.test(xlsx.text), `es steht da, dass es eine Excel-Datei ist (${xlsx.text.slice(0, 50)}…)`);
|
|
ok(/Backstage-Tabelle einfügen/.test(xlsx.text),
|
|
"und welcher Knopf stattdessen hilft");
|
|
ok(/CSV/.test(xlsx.text), "und wie man sie umspeichert");
|
|
ok(xlsx.vorschau === true && xlsx.los === true,
|
|
"es entsteht KEINE Vorschau und der Übernehmen-Knopf bleibt gesperrt");
|
|
ok(xlsx.feld === "",
|
|
"und das Feld ist wieder leer -- eine Datei, die nicht geht, bleibt nicht stehen");
|
|
if (process.env.SCHIRM) {
|
|
await seite.locator("#import-dialog").screenshot(
|
|
{ path: join(HIER, "pruef-import-excel.png") });
|
|
}
|
|
|
|
/* GEGENPROBE: Eine echte Text-Tabelle geht durch denselben Weg
|
|
weiterhin hindurch. Sonst hiesse das oben nur, dass gar nichts
|
|
mehr eingelesen wird. */
|
|
await seite.setInputFiles("#datei", {
|
|
name: "tage.csv",
|
|
mimeType: "text/csv",
|
|
buffer: Buffer.from(`Datum;Diamanten;LIVE-Dauer (Minuten)
|
|
${gestern};1234;90
|
|
`, "utf8"),
|
|
});
|
|
await seite.waitForTimeout(900);
|
|
const csv = await seite.evaluate(() => ({
|
|
fehler: document.getElementById("import-fehler")?.textContent || "",
|
|
vorschau: document.getElementById("vorschau")?.hidden,
|
|
}));
|
|
ok(csv.vorschau === false && !/Excel/.test(csv.fehler),
|
|
`eine CSV kommt weiterhin durch (Vorschau da: ${csv.vorschau === false})`);
|
|
await seite.click("#import-zu");
|
|
await seite.waitForTimeout(250);
|
|
|
|
/* ---- DER WEG UEBER DIE TIKTOK-DATEI IST WEG (11.09.2026) --------
|
|
Filipe: "ich will keine daten von denen kriegen, nur wir."
|
|
|
|
GEPRUEFT WIRD DIE ABWESENHEIT, und zwar an DREI Stellen -- sonst
|
|
hiesse "der Knopf ist weg" nur, dass er versteckt ist. Ein
|
|
versteckter Weg ist ein Weg: Wer die Adresse kennt, benutzt ihn.
|
|
Deshalb muss auch die Schnittstelle verschwunden sein.
|
|
|
|
Und der Text, der einen Creator um seine Daten bittet, darf
|
|
nirgends mehr auf der Seite stehen -- auch nicht in einem
|
|
Fenster, das niemand oeffnet. */
|
|
const spuren = await seite.evaluate(() => ({
|
|
knopf: !!document.getElementById("tt-oeffnen"),
|
|
fenster: !!document.getElementById("tt-dialog"),
|
|
text: document.body.textContent.includes("Deine Daten herunterladen"),
|
|
}));
|
|
ok(spuren.knopf === false, "der Knopf „TikTok-Datei des Creators\" ist weg");
|
|
ok(spuren.fenster === false, "und sein Fenster auch");
|
|
ok(spuren.text === false,
|
|
"und die Bitte an den Creator steht nirgends mehr auf der Seite");
|
|
|
|
/* ---- DIESELBE SEITE, ANDERE ROLLE (11.09.2026) -----------------
|
|
Filipe, mit der Kachel im Bild: "jetzt soll jede rolle diese
|
|
kachel sehen. spicy, dogfather, manager, scout und creator. aber
|
|
die creator kriegen dan quasi nur ihre daten zu sehen und der
|
|
manager scout dogfather oder spicy koennen eintragen."
|
|
|
|
Bis heute war die Seite auf ["admin"] gesperrt. Dass ich das
|
|
ueberhaupt gemerkt habe, verdanke ich dieser Gegenprobe: Sie
|
|
wurde rot, weil Sam auf der Startseite landete -- waehrend ich
|
|
nebenan eine Erklaerung "fuer Scouts und Manager" schrieb.
|
|
|
|
GEPRUEFT WIRD DER UNTERSCHIED, NICHT DIE ANWESENHEIT. Ein Scout
|
|
und ein Creator sehen dieselbe Seite; was sie unterscheidet, sind
|
|
die Knoepfe. Deshalb wird bei beiden JEDER Knopf einzeln gemessen
|
|
-- ein "sieht etwas" waere bei beiden gruen und wertlos. */
|
|
const rollenBild = async (rolle, code) => {
|
|
const c = await browser.newContext({ viewport: { width: 1400, height: 1000 } });
|
|
const p = await c.newPage();
|
|
const fehler = [];
|
|
p.on("pageerror", (e) => fehler.push(String(e).slice(0, 140)));
|
|
await p.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
|
await p.click(`.rolle[data-rolle="${rolle}"]`);
|
|
await p.fill("#code", code);
|
|
await p.click("button[type=submit]");
|
|
await p.waitForURL("**/start.html", { timeout: 25000 });
|
|
/* Erst die Kachel: Ein Weg, den man nur ueber die Adresse findet,
|
|
ist kein Weg. Filipe hat ausdruecklich die KACHEL gezeigt.
|
|
|
|
ABWARTEN, BIS SIE DA SIND. Der erste Entwurf las sofort nach
|
|
waitForURL -- da hatte das Skript die Kacheln noch gar nicht
|
|
gebaut, und die Pruefung meldete "keine Kachel" fuer beide
|
|
Rollen. Das war kein Befund, das war eine zu frueh gestellte
|
|
Frage. Und weil KEINE Kachel gefunden wurde, haette ein
|
|
schlichtes "mindestens eine" den Fehler auch nicht bemerkt. */
|
|
await p.waitForSelector(".kachel__link", { timeout: 20000 });
|
|
const kachel = await p.evaluate(() =>
|
|
[...document.querySelectorAll("a[href]")]
|
|
.some((a) => (a.getAttribute("href") || "").includes("leistung.html")));
|
|
await p.goto(BASIS + "/workspace/leistung.html", { waitUntil: "networkidle" });
|
|
await p.waitForTimeout(1800);
|
|
const bild = await p.evaluate(() => {
|
|
const h = (id) => {
|
|
const e = document.getElementById(id);
|
|
return e ? e.hidden : "fehlt";
|
|
};
|
|
return {
|
|
weg: location.pathname,
|
|
kacheln: 0,
|
|
import: h("import-oeffnen"), netz: h("netz-oeffnen"),
|
|
ziele: h("ziele-aendern"),
|
|
block: h("quellen-hinweis"), sNetz: h("quelle-netz"),
|
|
/* Wie viele Creator stehen zur Auswahl? Ein Creator darf hier
|
|
kein Auswahlfeld haben -- es haette nur ihn selbst drin. */
|
|
wahl: document.getElementById("creator-wahl")?.hidden === false
|
|
? document.querySelectorAll("#creator option").length : 0,
|
|
aendern: document.querySelectorAll(".tagzeile__knopf").length,
|
|
};
|
|
});
|
|
bild.kachel = kachel;
|
|
bild.fehler = fehler.join(" | ");
|
|
/* Bilder nur auf Zuruf (SCHIRM=1). Eine Pruefung, die bei jedem
|
|
Lauf Dateien ablegt, muellt das Verzeichnis zu -- und ein Bild
|
|
beweist ohnehin nichts, das misst der Code darueber. Es ist zum
|
|
HINSEHEN da, wenn Filipe fragt, wie es fuer eine Rolle aussieht. */
|
|
if (process.env.SCHIRM) {
|
|
await p.screenshot({ path: join(HIER, `pruef-zahlen-${rolle}.png`), fullPage: false });
|
|
}
|
|
await c.close();
|
|
return bild;
|
|
};
|
|
|
|
const scoutBild = await rollenBild("scout", "CODE-SAM-00001");
|
|
ok(scoutBild.kachel === true && /leistung\.html$/.test(scoutBild.weg),
|
|
`der Scout findet die Kachel und kommt auf die Seite (${scoutBild.weg})`);
|
|
ok(scoutBild.fehler === "", `kein Skriptfehler beim Scout (${scoutBild.fehler || "keiner"})`);
|
|
ok(scoutBild.import === false && scoutBild.ziele === false,
|
|
"der Scout darf eintragen: eigene Datei und Ziele sind da");
|
|
ok(scoutBild.netz === true,
|
|
"die Backstage-Tabelle bleibt ihm zu -- sie gilt für ALLE Creator");
|
|
ok(scoutBild.sNetz === true && scoutBild.block === true,
|
|
"die Backstage-Erklärung bekommt er nicht -- den Weg hat er nicht");
|
|
ok(scoutBild.aendern > 0,
|
|
`er kann einen Tag eintragen (${scoutBild.aendern} Knöpfe in der Liste)`);
|
|
|
|
/* MANAGER UND SPICY MEDIA -- die beiden, die ich beinahe nur
|
|
mitgedacht haette. Sie gehoeren zur Leitung, also sollten sie
|
|
alles koennen; "sollte" ist aber kein Messwert. Sie sehen
|
|
ausserdem UNTERSCHIEDLICH viele Creator: Spicy Media alle
|
|
(siehtAlles), ein Manager nur seine eigenen und die seiner
|
|
Scouts. Genau das wird hier verglichen -- eine Pruefung, die bei
|
|
beiden nur "sieht etwas" sagt, haette den Unterschied nie
|
|
bemerkt. */
|
|
for (const [rolle, code, mindestens] of [
|
|
["manager", "CODE-MIRA-0001", 1],
|
|
["spicy", "CODE-SASHA-0001", 3],
|
|
]) {
|
|
const b = await rollenBild(rolle, code);
|
|
ok(b.kachel === true && /leistung\.html$/.test(b.weg),
|
|
`${rolle}: findet die Kachel und kommt auf die Seite`);
|
|
ok(b.fehler === "", `${rolle}: kein Skriptfehler (${b.fehler || "keiner"})`);
|
|
ok(b.netz === false && b.import === false && b.ziele === false,
|
|
`${rolle}: alle drei Wege sind da -- auch die Backstage-Tabelle`);
|
|
ok(b.sNetz === false,
|
|
`${rolle}: und die Erklärung dazu`);
|
|
ok(b.wahl >= mindestens,
|
|
`${rolle}: sieht ${b.wahl} Creator zur Auswahl (mindestens ${mindestens})`);
|
|
}
|
|
/* Und der Unterschied selbst, als eigene Aussage: Spicy Media sieht
|
|
MEHR als der Manager. Waeren beide gleich, waere oben alles gruen
|
|
und die Haustrennung trotzdem kaputt. */
|
|
{
|
|
const m = await rollenBild("manager", "CODE-MIRA-0001");
|
|
const sp = await rollenBild("spicy", "CODE-SASHA-0001");
|
|
ok(sp.wahl > m.wahl,
|
|
`Spicy Media sieht mehr Creator als der Manager (${sp.wahl} zu ${m.wahl})`);
|
|
}
|
|
|
|
const creatorBild = await rollenBild("creator", "CODE-NOVA-0001");
|
|
ok(creatorBild.kachel === true && /leistung\.html$/.test(creatorBild.weg),
|
|
`der Creator findet die Kachel und kommt auf die Seite (${creatorBild.weg})`);
|
|
ok(creatorBild.fehler === "", `kein Skriptfehler beim Creator (${creatorBild.fehler || "keiner"})`);
|
|
ok(creatorBild.import === true && creatorBild.netz === true
|
|
&& creatorBild.ziele === true,
|
|
"der Creator sieht KEINEN einzigen Eintrag-Knopf");
|
|
ok(creatorBild.aendern === 0,
|
|
`auch nicht an den einzelnen Tagen (${creatorBild.aendern} Knöpfe)`);
|
|
ok(creatorBild.block === true,
|
|
"und keinen Quellen-Hinweis -- eine Erklärung ohne Knopf ist Rauschen");
|
|
ok(creatorBild.wahl === 0,
|
|
`und kein Auswahlfeld für fremde Creator (${creatorBild.wahl} Einträge)`);
|
|
|
|
/* GEGENPROBE ZUR GEGENPROBE: Dass der Creator nichts sieht, darf
|
|
nicht daran liegen, dass die Seite bei ihm leer bleibt. Seine
|
|
eigenen Zahlen muessen da sein -- sonst waere alles oben gruen,
|
|
weil nichts geladen hat. */
|
|
const creatorZahlen = await (async () => {
|
|
const c = await browser.newContext({ viewport: { width: 1400, height: 1000 } });
|
|
const p = await c.newPage();
|
|
await p.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
|
await p.click('.rolle[data-rolle="creator"]');
|
|
await p.fill("#code", "CODE-NOVA-0001");
|
|
await p.click("button[type=submit]");
|
|
await p.waitForURL("**/start.html", { timeout: 25000 });
|
|
await p.goto(BASIS + "/workspace/leistung.html", { waitUntil: "networkidle" });
|
|
await p.waitForTimeout(1800);
|
|
const t = await p.evaluate(() => document.getElementById("tagetabelle")?.textContent || "");
|
|
await c.close();
|
|
return t;
|
|
})();
|
|
ok(/\d/.test(creatorZahlen) && creatorZahlen.length > 20,
|
|
`seine eigenen Zahlen stehen trotzdem da (${creatorZahlen.length} Zeichen)`);
|
|
|
|
await browser.close();
|
|
}
|
|
|
|
kind.kill();
|
|
await new Promise((r) => setTimeout(r, 400));
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Zugabe */ }
|
|
|
|
console.log(`\n${gemacht} Pruefungen`);
|
|
console.log(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`);
|
|
process.exit(fehler ? 1 : 0);
|