/* ===================================================================== WIE DIE ANLEITUNG AUSSIEHT (06.10.2026, Etappe 1) SIE PRUEFT NICHTS und meldet keinen Fehlschlag -- sie macht Bilder, auf denen man nachsehen kann, und misst vier Dinge, die man auf einem Bild schlecht sieht: waagerechtes Schieben, Beruehrziele unter 40 px, Fehler in der Browserkonsole und ob der Filter wirklich etwas tut. Deshalb `mess-` und nicht `pruef-`. ZWEI BREITEN, und die kleinere ist die wichtigere: 412 px ist die haeufigste Android-Breite. Am 06.09.2026 hat genau diese Breite im Creator Workspace eine Ueberdeckung aufgedeckt, bei der ein Knopf ueber einem anderen lag -- auf dem Computer war nichts zu sehen. EIGENE WEGWERF-DATENBANK in einem mktemp-Ordner. In einem laufenden System waeren das Testdaten, und die sind verboten. Aufrufen mit: node server/mess-anleitung.mjs ===================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { request as httpAnfrage } from "node:http"; import { eigenerPort } from "./helfer-port.mjs"; const PORT = await eigenerPort(import.meta, "mess-anleitung"); const ordner = mkdtempSync(join(tmpdir(), "ws-anl-bild-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); 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 }); }; import { notbremse } from "./helfer-notbremse.mjs"; await import("./index.js"); notbremse(240_000, "mess-anleitung"); await new Promise((r) => setTimeout(r, 900)); process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); }); /* ---------- Leute ----------------------------------------------------- */ const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetztIso = new Date().toISOString(); const kenn = randomBytes(32).toString("hex"); d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") .run("code_kennung_schluessel", kenn, jetztIso); 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," + " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,1,?)") .run(name, rolle, hash, salt, 32768, createHmac("sha256", kenn).update(code).digest("hex"), jetztIso); } anlegen("Luna", "creator", "CODE-LUNA-0001"); anlegen("Filipe", "admin", "CODE-DOGI-0001"); d.close(); function anfrage(pfad, methode, koerper, keks) { return new Promise((fertig, schief) => { const daten = koerper ? JSON.stringify(koerper) : null; const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: pfad, method: methode, headers: { Origin: `http://127.0.0.1:${PORT}`, ...(keks ? { Cookie: keks } : {}), ...(daten ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(daten) } : {}), }, }, (antwort) => { let text = ""; antwort.on("data", (t) => { text += t; }); antwort.on("end", () => fertig({ code: antwort.statusCode, kopf: antwort.headers, text })); }); a.on("error", schief); if (daten) a.write(daten); a.end(); }); } async function keksFuer(rolle, code) { const an = await anfrage("/workspace/api/anmelden", "POST", { rolle, code }); if (an.code !== 200) { console.log(`ABBRUCH: Anmeldung ${rolle} ${an.code}`); process.exit(2); } return (an.kopf["set-cookie"] || []).map((x) => x.split(";")[0]).join("; "); } const keksCreator = await keksFuer("creator", "CODE-LUNA-0001"); const keksDogi = await keksFuer("admin", "CODE-DOGI-0001"); /* ---------- Bilder ---------------------------------------------------- */ const pw = await import( "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); const browser = await pw.chromium.launch(); const nachmessen = () => ({ schiebtQuer: document.documentElement.scrollWidth > window.innerWidth + 2, zuKlein: [...document.querySelectorAll("button, a[href], summary")] .filter((e) => { const k = e.getBoundingClientRect(); return k.width > 0 && k.height > 0 && k.height < 40; }) .map((e) => `${e.tagName.toLowerCase()}.${(e.className || "-")}: ${Math.round(e.getBoundingClientRect().height)}px`), karten: document.querySelectorAll(".an-karte").length, /* Je Stufe: wie viele Karten, und ist der Kasten offen? */ stufen: [...document.querySelectorAll(".an-stufe")].map((s) => ({ stufe: s.dataset.stufe, zahl: s.querySelector(".an-stufe__zahl")?.textContent, offen: !!s.querySelector(".an-stufe__falte")?.open, sichtbar: !s.hidden, })), /* Tragen alle Karten den Ton ihrer Kachel? Eine ohne waere grau. */ ohneTon: [...document.querySelectorAll(".an-karte")] .filter((k) => !k.dataset.ton) .map((k) => k.querySelector(".an-karte__name")?.textContent), schritte: document.querySelectorAll(".an-schritt").length, saetze: document.querySelectorAll(".an-saetze__zeile").length, nicht: document.querySelectorAll(".an-nicht__zeile").length, takt: document.querySelectorAll(".an-takt__wann").length, fragen: document.querySelectorAll(".an-frage").length, ohne: document.getElementById("ohne-fassung")?.hidden === false ? document.getElementById("ohne-fassung").textContent.slice(0, 70) : null, /* Etappe 2 */ haken: document.querySelectorAll(".an-haken").length, abgehakt: document.querySelectorAll(".an-haken__feld:checked").length, stand: document.getElementById("stand")?.hidden === false ? document.getElementById("stand-satz").textContent : "(kein Fortschritt)", }); async function bild(name, keks, breite, hoehe, vorher) { const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe }, hasTouch: breite <= 860, }); await kontext.addCookies(keks.split("; ").map((k) => { const [n, ...r] = k.split("="); return { name: n, value: r.join("="), domain: "127.0.0.1", path: "/" }; })); const seite = await kontext.newPage(); const konsole = []; seite.on("console", (e) => { if (e.type() === "error" && !/favicon|net::ERR/i.test(e.text())) konsole.push(e.text().slice(0, 160)); }); seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message)); await seite.goto(`http://127.0.0.1:${PORT}/workspace/anleitung.html`, { waitUntil: "networkidle" }); await seite.waitForTimeout(1000); if (vorher) await vorher(seite); await seite.waitForTimeout(400); const blick = await seite.evaluate(nachmessen); await seite.screenshot({ path: `bild-anleitung-${name}.png`, fullPage: true }); console.log(`\n--- ${name} (${breite}px) ---`); if (blick.ohne) console.log(` Ohne Fassung: ${blick.ohne}…`); console.log(` Karten: ${blick.karten} Schritte: ${blick.schritte} ` + `Saetze: ${blick.saetze} Nicht deins: ${blick.nicht} ` + `Takt: ${blick.takt} Fragen: ${blick.fragen}`); for (const s of blick.stufen) { console.log(` ${String(s.stufe).padEnd(13)} ${String(s.zahl).padEnd(3)} ` + `${s.offen ? "offen" : "zu "} ${s.sichtbar ? "" : "(ausgeblendet)"}`); } console.log(` Fortschritt: ${blick.stand} Haken: ${blick.haken} (${blick.abgehakt} gesetzt)`); console.log(` Karten ohne Farbe: ${blick.ohneTon.length ? blick.ohneTon.join(", ") : "keine"}`); console.log(` Quer schieben: ${blick.schiebtQuer ? "JA — Befund!" : "nein"}`); console.log(` Zu kleine Ziele: ${blick.zuKlein.length ? blick.zuKlein.join(", ") : "keine"}`); if (konsole.length) console.log(` KONSOLE: ${konsole.join(" | ")}`); await kontext.close(); return blick; } await bild("creator-handy", keksCreator, 412, 950); await bild("creator-rechner", keksCreator, 1440, 1000); /* Der Filter muss etwas TUN. Auf einem Bild sieht man nur den gedrueckten Knopf -- ob die anderen Kaesten verschwinden, nicht. */ await bild("creator-nur-muss", keksCreator, 412, 950, async (seite) => { await seite.click('.an-filter__knopf[data-wert="muss"]'); }); /* DogFather ohne gewaehlte Sicht: Er bekommt keine Fassung, sondern eine Auskunft. Das Bild zeigt, ob die wie eine Erklaerung aussieht und nicht wie ein Fehler. */ await bild("dogfather-ohne-sicht", keksDogi, 412, 950); /* ===================================================================== DIE BREITE KACHEL AUF DER STARTSEITE (Etappe 2) Sie entsteht nur, solange Erste Schritte offen sind. Beide Zustaende werden gemessen -- ein Bild von "sie ist da" allein beweist nicht, dass sie auch wieder verschwindet, und genau das ist ihr Versprechen. ===================================================================== */ async function startBild(name, keks, breite) { const kontext = await browser.newContext({ viewport: { width: breite, height: 1100 }, hasTouch: breite <= 860, }); await kontext.addCookies(keks.split("; ").map((k) => { const [n, ...r] = k.split("="); return { name: n, value: r.join("="), domain: "127.0.0.1", path: "/" }; })); const seite = await kontext.newPage(); const konsole = []; seite.on("console", (e) => { if (e.type() === "error" && !/favicon|net::ERR/i.test(e.text())) konsole.push(e.text().slice(0, 160)); }); await seite.goto(`http://127.0.0.1:${PORT}/workspace/start.html`, { waitUntil: "networkidle" }); await seite.waitForTimeout(1400); const blick = await seite.evaluate(() => { const gruppen = [...document.querySelectorAll(".gruppe")].map((g) => g.querySelector(".gruppe__name")?.textContent); const breiteKachel = [...document.querySelectorAll('.kachel[data-gross="ja"]')] .map((k) => k.querySelector(".kachel__name")?.textContent + " [" + (k.querySelector(".kachel__zahl")?.textContent || "ohne Zahl") + "]"); const dran = [...document.querySelectorAll(".dran__text")].map((e) => e.textContent); return { gruppen, breiteKachel, dran, anleitungKacheln: [...document.querySelectorAll('a[href="anleitung.html"]')].length }; }); /* Ganze Seite: Die breite Kachel sitzt unter der Zentrale und waere im Ausschnitt nicht zu sehen -- also waere das Bild kein Beleg. */ await seite.screenshot({ path: `bild-anleitung-${name}.png`, fullPage: true }); console.log(` --- ${name} (${breite}px, Startseite) ---`); console.log(` Gruppen: ${blick.gruppen.join(" | ")}`); console.log(` Breite Kacheln: ${blick.breiteKachel.length ? blick.breiteKachel.join(", ") : "keine"}`); console.log(` Wege zur Anleitung: ${blick.anleitungKacheln}`); console.log(` Was ist dran: ${blick.dran.join(" | ") || "nichts"}`); if (konsole.length) console.log(` KONSOLE: ${konsole.join(" | ")}`); await kontext.close(); return blick; } await startBild("start-mit-schritten", keksCreator, 412); /* Jetzt alle fuenf abhaken -- ueber die Schnittstelle, wie es die Seite auch tut -- und noch einmal hinsehen. */ const standVorher = await anfrage("/workspace/api/anleitung", "GET", null, keksCreator); if (standVorher.code !== 200) { console.log(`ABBRUCH: Anleitung nicht abrufbar (${standVorher.code})`); process.exit(2); } const schritte = JSON.parse(standVorher.text).schritte || []; for (const sch of schritte) { await new Promise((fertig, schief) => { const daten = JSON.stringify({ erledigt: true }); const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: `/workspace/api/anleitung/schritt/${sch.id}`, method: "PUT", headers: { Cookie: keksCreator, "Content-Type": "application/json", "Content-Length": Buffer.byteLength(daten), Origin: `http://127.0.0.1:${PORT}` }, }, (antwort) => { antwort.on("data", () => {}); antwort.on("end", fertig); }); a.on("error", schief); a.write(daten); a.end(); }); } console.log(` (${schritte.length} Schritte abgehakt)`); await startBild("start-ohne-schritte", keksCreator, 412); await bild("creator-fertig", keksCreator, 412, 950); await browser.close(); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } console.log("\nBilder geschrieben: bild-anleitung-*.png"); process.exit(0);