Wunsch: "diese Kisten sind jetzt ueberall zu viel, die sollen weg." Nur die Content-Seite (nachgefragt): Vorrat, Veroeffentlicht, Ideen, ohne Hook, ueberfaellig. Die Reihe auf der Startseite und die Kisten im Report bleiben -- letztere sind der Report. Sie hatte auch inhaltlich kein Recht mehr: Die Strecke direkt darunter zeigt Ideen, Produktion und Veroeffentlichtes ohnehin mit Zahl an jeder Spalte. Zwei Zaehler fuer dieselbe Sache auf einem Bildschirm sind einer zu viel -- und laufen frueher oder spaeter auseinander. WAS BLEIBT UND WARUM: Die Abfrage /workspace/api/content/kennzahlen wird NICHT entfernt. Aus derselben Antwort speist sich der Saeulen-Balken darunter (70/20/10). Die Funktion heisst jetzt zahlenLaden() statt kennzahlenLaden() -- der alte Name zeigte auf etwas, das es nicht mehr gibt. Entfernt sind neben der Reihe auch .kachel, .kachel__wert, .kachel__name und .kachel__zusatz aus content.css. Achtung fuer spaeter: "kachel" gibt es auch in start.css und report.css, mit ganz anderen Regeln. Diese drei Kopien haben nichts miteinander zu tun; ein Kommentar an der Fundstelle sagt das jetzt. DIE PRUEFUNG WURDE UMGEDREHT, NICHT GELOESCHT. pruef-content-ansicht.mjs sicherte bisher "fuenf Kennzahlen" -- jetzt sichert sie, dass keine da ist. Eine geloeschte Pruefung merkt niemand, wenn jemand die Reihe spaeter versehentlich wieder einbaut. Die Zahlen selbst bleiben geprueft: pruef-content.mjs prueft Vorrat, ohne Hook und ueberfaellig weiterhin an der Schnittstelle (Abschnitt 5 und 6). Entfallen ist ihre Anzeige, nicht ihre Richtigkeit. Co-Authored-By: Claude Opus 5 <[email protected]>
226 lines
12 KiB
JavaScript
226 lines
12 KiB
JavaScript
/* Browserpruefung der Content-Planung: Strecke, Kennzahlen, Balance,
|
||
Ueberfuehren -- und was ein Creator davon zu sehen bekommt. */
|
||
import { mkdtempSync, rmSync } from "node:fs";
|
||
import { tmpdir } from "node:os";
|
||
import { join } from "node:path";
|
||
|
||
const ordner = mkdtempSync(join(tmpdir(), "ws-content-b-"));
|
||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||
process.env.PORT = "4185";
|
||
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
|
||
process.env.SITE_ACCESS_CODE_VANVAN = "VANVAN-TEST-1";
|
||
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, 700));
|
||
const BASIS = "http://127.0.0.1:4185";
|
||
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 200_000).unref?.();
|
||
|
||
let fehler = 0;
|
||
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||
|
||
await fetch(BASIS + "/workspace/api/anmelden", {
|
||
method: "POST", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
|
||
|
||
const { DatabaseSync } = await import("node:sqlite");
|
||
const { scryptSync, randomBytes } = await import("node:crypto");
|
||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||
const jetzt = new Date().toISOString();
|
||
const heute = jetzt.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, jetzt);
|
||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||
}
|
||
const idDogi = anlegen("Dogfather", "admin", "CODE-DOGI-0001");
|
||
const idAnna = anlegen("Anna Sturm", "creator", "CODE-ANNA-0001");
|
||
|
||
/* Vier Saeulen -- damit vier der fuenf Farbplaetze im Bild sind. */
|
||
const saeulen = {};
|
||
for (const [i, [name, ziel]] of [["Tutorials", 55], ["Community", 25], ["Hinter den Kulissen", 10], ["Eigenwerbung", 10]].entries()) {
|
||
d.prepare("INSERT INTO content_saeulen (creator_id, name, ziel, slot, erstellt) VALUES (?,?,?,?,?)")
|
||
.run(idAnna, name, ziel, i + 1, jetzt);
|
||
saeulen[name] = d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||
}
|
||
|
||
const video = (art, titel, hook, saeule, format, geplant) =>
|
||
d.prepare(`INSERT INTO eintraege (bereich, art, titel, hook, format, saeule_id, geplant, datum, status, creator_id, erstellt, erstellt_von)
|
||
VALUES ('content',?,?,?,?,?,?,?,'offen',?,?,?)`)
|
||
.run(art, titel, hook, format, saeule ? saeulen[saeule] : null, geplant, heute, idAnna, jetzt, idDogi);
|
||
|
||
video("idee", "3 Fehler beim ersten LIVE", null, "Tutorials", null, null);
|
||
video("idee", "Warum ich fast aufgehört hätte", null, "Hinter den Kulissen", null, null);
|
||
video("skript", "Setup für unter 100 €", "Das hier kostet weniger als dein Handy.", "Tutorials", "Tutorial", null);
|
||
video("skript", "Antwort auf eure Fragen", null, "Community", "Talking Head", null);
|
||
video("gedreht", "Mein Tag als Creator", "Um 6 klingelt der Wecker – freiwillig.", "Hinter den Kulissen", "Story", null);
|
||
video("gedreht", "Die 3 häufigsten Fragen", "Nummer 2 fragt mir wirklich jeder.", "Community", "Talking Head", null);
|
||
video("fertig", "Licht richtig setzen", "Ein Fenster reicht. Ehrlich.", "Tutorials", "Tutorial",
|
||
new Date(Date.now() + 2 * 86400_000).toISOString().slice(0, 10));
|
||
video("fertig", "Verpasst und überfällig", "Guck bis zum Schluss.", "Tutorials", "Tutorial", "2026-01-05");
|
||
for (let i = 0; i < 9; i++) video("veroeffentlicht", "Draußen " + i, "x", "Tutorials", "Tutorial", null);
|
||
for (let i = 0; i < 4; i++) video("veroeffentlicht", "Community " + i, "x", "Community", "Talking Head", null);
|
||
for (let i = 0; i < 2; i++) video("veroeffentlicht", "Kulissen " + i, "x", "Hinter den Kulissen", "Story", null);
|
||
video("veroeffentlicht", "Ohne Zuordnung", "x", null, null, null);
|
||
d.close();
|
||
|
||
const { chromium } = await import(
|
||
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||
const browser = await chromium.launch();
|
||
|
||
async function seiteFuer(kontext, rolle, code) {
|
||
const seite = await kontext.newPage();
|
||
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
||
await seite.click(`.rolle[data-rolle="${rolle}"]`);
|
||
await seite.fill("#code", code);
|
||
await seite.click("button[type=submit]");
|
||
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
||
await seite.goto(BASIS + "/workspace/content.html", { waitUntil: "networkidle" });
|
||
await seite.waitForTimeout(700);
|
||
return seite;
|
||
}
|
||
|
||
const lesen = (seite) => seite.evaluate(() => ({
|
||
spalten: [...document.querySelectorAll(".spalte")].map((s) => ({
|
||
name: s.querySelector(".spalte__name")?.textContent,
|
||
zahl: s.querySelector(".spalte__zahl")?.textContent,
|
||
karten: s.querySelectorAll(".video").length,
|
||
})),
|
||
kacheln: [...document.querySelectorAll(".kachel")].map((k) => ({
|
||
wert: k.querySelector(".kachel__wert")?.textContent,
|
||
name: k.querySelector(".kachel__name")?.textContent,
|
||
zusatz: k.querySelector(".kachel__zusatz")?.textContent,
|
||
stufe: k.dataset.stufe,
|
||
})),
|
||
stuecke: [...document.querySelectorAll(".balance__stueck")].map((s) => ({
|
||
slot: s.dataset.slot, breite: s.style.width,
|
||
farbe: getComputedStyle(s).backgroundColor,
|
||
})),
|
||
legende: [...document.querySelectorAll(".legende__punkt")].map((l) => l.textContent),
|
||
hooks: [...document.querySelectorAll(".video__hook")].map((h) => h.textContent).slice(0, 3),
|
||
fehlendeHooks: document.querySelectorAll(".video__hook--fehlt").length,
|
||
anlegenDa: !document.getElementById("neu-oeffnen")?.hidden,
|
||
saeulenKnopfDa: !document.getElementById("saeulen-oeffnen")?.hidden,
|
||
/* Nur SICHTBARE zaehlen -- versteckte Knoepfe waeren zwar unschoen,
|
||
aber die Frage ist, was der Creator vor sich hat. Gezaehlt wird
|
||
zusaetzlich, was ueberhaupt im Dokument steht (siehe knoepfeGesamt):
|
||
Ein Knopf, den nur CSS versteckt, ist mit der Tastatur erreichbar. */
|
||
knoepfe: [...document.querySelectorAll(".tu-knopf")].filter((b) => b.offsetParent !== null).length,
|
||
knoepfeGesamt: document.querySelectorAll(".tu-knopf").length,
|
||
mehrKnopf: document.querySelectorAll(".spalte__mehr").length,
|
||
unterzeile: document.getElementById("unterzeile")?.textContent,
|
||
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||
ueberstehend: [...document.querySelectorAll("main *")]
|
||
.filter((e) => e.getBoundingClientRect().right > window.innerWidth + 1)
|
||
.map((e) => String(e.className).slice(0, 40)).slice(0, 4),
|
||
}));
|
||
|
||
for (const [breite, wie] of [[1440, "Computer"], [390, "Handy"]]) {
|
||
console.log(`\n=== DogFather · ${wie}`);
|
||
const kontext = await browser.newContext({ viewport: { width: breite, height: 1100 } });
|
||
const konsole = [];
|
||
const seite = await seiteFuer(kontext, "admin", "CODE-DOGI-0001");
|
||
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
|
||
seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message));
|
||
await seite.reload({ waitUntil: "networkidle" });
|
||
await seite.waitForTimeout(700);
|
||
|
||
const r = await lesen(seite);
|
||
console.log(" Spalten:", r.spalten.map((s) => `${s.name}=${s.zahl}`).join(" · "));
|
||
|
||
ok(r.spalten.length === 5, `fünf Stufen (${r.spalten.length})`);
|
||
ok(r.spalten[0].zahl === "2" && r.spalten[4].zahl === "16",
|
||
`Ideen 2, Veröffentlicht 16 (${r.spalten[0].zahl}/${r.spalten[4].zahl})`);
|
||
/* DIE KENNZAHLEN-REIHE IST WEG (01.09.2026, auf Wunsch: "diese Kisten
|
||
sind jetzt ueberall zu viel, die sollen weg").
|
||
|
||
Hier stand vorher, dass es genau fuenf davon gibt. Die Pruefung wird
|
||
nicht geloescht, sondern UMGEDREHT: Sie sichert jetzt, dass sie
|
||
nicht zurueckkommen. Eine geloeschte Pruefung merkt niemand, wenn
|
||
jemand die Reihe spaeter versehentlich wieder einbaut.
|
||
|
||
Die Zahlen selbst (Vorrat, ohne Hook, ueberfaellig) sind dadurch
|
||
NICHT ungeprueft: pruef-content.mjs prueft sie weiterhin an der
|
||
Schnittstelle, Abschnitt 5 und 6. Nur ihre Anzeige ist entfallen. */
|
||
ok(r.kacheln.length === 0, `keine Kennzahlen-Kisten mehr (${r.kacheln.length})`);
|
||
ok(r.spalten.length === 5,
|
||
"die Strecke zeigt die Zahlen weiterhin an jeder Spalte – dort gehoeren sie hin");
|
||
|
||
/* Die Balance -- vier Farben, jede eine andere, keine zwei gleich. */
|
||
ok(r.stuecke.length === 4, `vier Stücke im Balken (${r.stuecke.length})`);
|
||
const farben = new Set(r.stuecke.map((s) => s.farbe));
|
||
ok(farben.size === r.stuecke.length, `jede Farbe kommt genau einmal vor (${farben.size})`);
|
||
console.log(" Farben:", [...farben].join(" "));
|
||
ok(r.legende.some((l) => /Ziel 55/.test(l)), "die Legende nennt das Ziel");
|
||
ok(r.legende.some((l) => /über|unter/.test(l)), "und die Abweichung davon");
|
||
|
||
ok(r.fehlendeHooks === 1, `genau eine Karte sagt „kein Hook" (${r.fehlendeHooks})`);
|
||
ok(r.hooks.some((h) => /„/.test(h)), "Hooks stehen in Anführungszeichen");
|
||
ok(r.anlegenDa && r.saeulenKnopfDa, "DogFather kann anlegen und Säulen verwalten");
|
||
ok(r.ueber <= 0, `nichts steht über (${r.ueber}px, ${r.ueberstehend.join(", ")})`);
|
||
ok(konsole.length === 0, `keine Fehler in der Konsole${konsole.length ? " -> " + konsole.join(" | ") : ""}`);
|
||
|
||
/* Die lange Spalte darf nicht alles erschlagen -- und der Aufklapper
|
||
muss sie wirklich vollstaendig zeigen. */
|
||
ok(r.mehrKnopf === 1, `ein Aufklapper an der zu langen Spalte (${r.mehrKnopf})`);
|
||
ok(r.spalten[4].karten === 6, `Veröffentlicht zeigt 6 von 16 (${r.spalten[4].karten})`);
|
||
await seite.click(".spalte__mehr");
|
||
await seite.waitForTimeout(300);
|
||
const offen = await lesen(seite);
|
||
ok(offen.spalten[4].karten === 16, `aufgeklappt sind es alle 16 (${offen.spalten[4].karten})`);
|
||
await seite.click(".spalte__mehr");
|
||
await seite.waitForTimeout(300);
|
||
ok((await lesen(seite)).spalten[4].karten === 6, "und wieder zu");
|
||
|
||
if (breite === 1440) {
|
||
await seite.screenshot({ path: "pruef-content-computer.png", fullPage: true });
|
||
|
||
console.log("\n Überführen im Browser");
|
||
/* Die Karte mit dem geplanten Termin -- dort gibt es alle drei Wege. */
|
||
const knopf = seite.locator('.video:has-text("Licht richtig setzen") .tu-knopf', { hasText: "→ Aufgabe" });
|
||
await knopf.first().click();
|
||
await seite.waitForTimeout(1200);
|
||
const meldung = await seite.evaluate(() => document.getElementById("fehler")?.textContent);
|
||
ok(/Aufgabe angelegt/.test(meldung || ""), `Rückmeldung erscheint ("${meldung}")`);
|
||
const link = await seite.evaluate(() => document.querySelector("#fehler a")?.getAttribute("href"));
|
||
ok(link === "aufgaben.html", `mit Weg dorthin (${link})`);
|
||
} else {
|
||
await seite.screenshot({ path: "pruef-content-handy.png", fullPage: true });
|
||
}
|
||
await kontext.close();
|
||
}
|
||
|
||
console.log("\n=== Creator sieht, ändert aber nichts");
|
||
{
|
||
const kontext = await browser.newContext({ viewport: { width: 1440, height: 1100 } });
|
||
const seite = await seiteFuer(kontext, "creator", "CODE-ANNA-0001");
|
||
const r = await lesen(seite);
|
||
ok(r.spalten.length === 5, `sieht die ganze Strecke (${r.spalten.length} Spalten)`);
|
||
/* 2+2+2+2 plus die gekuerzten 6 der letzten Spalte = 14. Die Kuerzung
|
||
gilt fuer JEDE Rolle -- sie ist eine Frage der Lesbarkeit, nicht der
|
||
Rechte. Der Rest ist einen Klick entfernt. */
|
||
ok(r.spalten.reduce((n, s) => n + s.karten, 0) === 14,
|
||
`und seine Videos, letzte Spalte gekürzt (${r.spalten.reduce((n, s) => n + s.karten, 0)})`);
|
||
ok(r.mehrKnopf === 1, `mit einem Aufklapper an der langen Spalte (${r.mehrKnopf})`);
|
||
ok(r.kacheln.length === 0, "sieht keine Kennzahlen-Kisten mehr");
|
||
ok(r.stuecke.length === 4, "sieht die Verteilung");
|
||
ok(!r.anlegenDa, "aber keinen Anlegen-Knopf");
|
||
ok(!r.saeulenKnopfDa, "und keine Säulenverwaltung");
|
||
ok(r.knoepfe === 0, `keinen einzigen sichtbaren Knopf (${r.knoepfe})`);
|
||
ok(r.knoepfeGesamt === 0, `und auch keinen versteckten im Dokument (${r.knoepfeGesamt})`);
|
||
ok(/deine Betreuung/.test(r.unterzeile || ""), `und die Unterzeile erklärt es ("${r.unterzeile}")`);
|
||
await seite.screenshot({ path: "pruef-content-creator.png", fullPage: true });
|
||
await kontext.close();
|
||
}
|
||
|
||
await browser.close();
|
||
console.log(`\n${fehler === 0 ? "Alles in Ordnung." : fehler + " Prüfung(en) fehlgeschlagen."}`);
|
||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||
process.exit(fehler === 0 ? 0 : 1);
|