diff --git a/server/mess-farbe-am-text.mjs b/server/mess-farbe-am-text.mjs new file mode 100644 index 00000000..a1646af5 --- /dev/null +++ b/server/mess-farbe-am-text.mjs @@ -0,0 +1,137 @@ +/* ===================================================================== + WELCHE FARBE HAT DIESER TEXT WIRKLICH -- UND WORAUF LIEGT ER? + ===================================================================== + + Gebaut am 25.09.2026, weil `pruef-neue-seiten` einen Kontrast von + 1,07 meldete ("Schoen, dass du da bist") und die Stelle im CSS + tadellos aussah. Diese Messung liest die Farben an Ort und Stelle + aus und geht die Elternkette hoch, bis sie eine deckende Flaeche + findet -- genau das, was die Pruefung tut, nur sichtbar. + + Ergebnis damals: dunkler Text auf HELLBLAUER Flaeche, rund 10:1. + Die Pruefung konnte das Format `color(srgb ...)` nicht lesen, ging + deshalb zum fast schwarzen Elternteil weiter und rechnete dunkel + gegen dunkel. Ohne diese Messung haette ich an einem CSS geaendert, + an dem nichts war. + + Aufrufen mit: + node server/mess-farbe-am-text.mjs "" + ===================================================================== */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const SEITE = process.argv[2] || "treff-regeln.html"; +const SUCHE = process.argv[3] || "Schön, dass du da bist"; + +const PORT = 5451; +const ordner = mkdtempSync(join(tmpdir(), "ws-kontrast-")); +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(200_000, "mess-kontrast"); +await new Promise((r) => setTimeout(r, 900)); + +const BASIS = `http://127.0.0.1:${PORT}`; +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +const sch = randomBytes(32).toString("hex"); +d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", sch, jetzt); +const anlegen = (name, rolle, code) => { + const s = randomBytes(16).toString("hex"); + const h = scryptSync(code, s, 64, + { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + return d.prepare("INSERT INTO personen" + + " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)" + + " VALUES (?,?,?,?,?,?,1,?) RETURNING id") + .get(name, rolle, h, s, 32768, + createHmac("sha256", sch).update(code).digest("hex"), jetzt).id; +}; +anlegen("Filipe", "admin", "CODE-DOGI-0001"); +d.close(); + +const an = await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }), +}); +const keks = (an.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; "); + +const pw = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await pw.chromium.launch(); +const kontext = await browser.newContext({ viewport: { width: 1440, height: 900 } }); +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(); +await seite.goto(`${BASIS}/workspace/${SEITE}`, { waitUntil: "networkidle" }); +await seite.waitForTimeout(900); + +const fund = await seite.evaluate((suche) => { + const alle = [...document.querySelectorAll("*")].filter( + (e) => e.children.length === 0 && (e.textContent || "").trim().startsWith(suche)); + if (!alle.length) return { da: false }; + const e = alle[0]; + const st = getComputedStyle(e); + const kette = []; + let p = e; + while (p && p !== document.documentElement) { + const ps = getComputedStyle(p); + kette.push({ + was: p.tagName.toLowerCase() + + (p.className && typeof p.className === "string" + ? "." + p.className.trim().split(/\s+/).slice(0, 3).join(".") : ""), + farbe: ps.backgroundColor, + bild: ps.backgroundImage === "none" ? "" : ps.backgroundImage.slice(0, 60), + deckkraft: ps.opacity, + }); + p = p.parentElement; + } + const r = e.getBoundingClientRect(); + return { + da: true, + text: (e.textContent || "").trim().slice(0, 40), + farbe: st.color, + groesse: st.fontSize, fett: st.fontWeight, + clip: st.webkitBackgroundClip || st.backgroundClip, + eigenBild: st.backgroundImage === "none" ? "" : st.backgroundImage.slice(0, 80), + deckkraft: st.opacity, + stelle: `${Math.round(r.left)},${Math.round(r.top)} ${Math.round(r.width)}x${Math.round(r.height)}`, + kette: kette.slice(0, 6), + }; +}, SUCHE); + +console.log(`\n=== "${SUCHE}" auf ${SEITE} ===`); +if (!fund.da) console.log(" NICHT GEFUNDEN"); +else { + console.log(` Text "${fund.text}"`); + console.log(` Farbe ${fund.farbe} ${fund.groesse} / ${fund.fett}`); + console.log(` Deckkraft ${fund.deckkraft}`); + console.log(` clip ${fund.clip}`); + if (fund.eigenBild) console.log(` eigenes Hintergrundbild: ${fund.eigenBild}`); + console.log(` Stelle ${fund.stelle}`); + console.log(" Kette (von innen nach aussen):"); + for (const k of fund.kette) { + console.log(` ${k.was.padEnd(38)} bg ${k.farbe}` + + (k.bild ? ` bild ${k.bild}` : "") + + (k.deckkraft !== "1" ? ` opacity ${k.deckkraft}` : "")); + } +} + +await browser.close(); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +process.exit(0); diff --git a/server/mess-seiteninhalt.mjs b/server/mess-seiteninhalt.mjs new file mode 100644 index 00000000..2857568a --- /dev/null +++ b/server/mess-seiteninhalt.mjs @@ -0,0 +1,107 @@ +/* ===================================================================== + WAS STEHT AUF DIESER SEITE WIRKLICH? + ===================================================================== + + Gebaut am 25.09.2026, weil `pruef-neue-seiten` meldete, die + Entwicklungsseite sei nicht gefuellt (350 Zeichen). Sie ist gefuellt + -- mit zwei Personenkacheln, vier Abschnitten und einem Ring je + Person. Sie braucht nur weniger Worte, seit aus achtzehn + Zahlenkaesten zwei Kacheln geworden sind. + + Die Messung zeigt den Text, die Zahl der Kacheln, den Zustand des + Kartenblocks und jeden Fehler aus der Browserkonsole. Mit ROLLE=hand + sieht sie die Seite mit den Augen der rechten Hand. + + Aufrufen mit: + node server/mess-seiteninhalt.mjs + ROLLE=hand node server/mess-seiteninhalt.mjs entwicklung.html + ===================================================================== */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const SEITE = process.argv[2] || "entwicklung.html"; +const PORT = 5461; +const ordner = mkdtempSync(join(tmpdir(), "ws-inhalt-")); +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(200_000, "mess-seiteninhalt"); +await new Promise((r) => setTimeout(r, 900)); + +const BASIS = `http://127.0.0.1:${PORT}`; +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +const sch = randomBytes(32).toString("hex"); +d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", sch, jetzt); +const anlegen = (name, rolle, code) => { + const s = randomBytes(16).toString("hex"); + const h = scryptSync(code, s, 64, + { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + return d.prepare("INSERT INTO personen" + + " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)" + + " VALUES (?,?,?,?,?,?,1,?) RETURNING id") + .get(name, rolle, h, s, 32768, + createHmac("sha256", sch).update(code).digest("hex"), jetzt).id; +}; +anlegen("Filipe", "admin", "CODE-DOGI-0001"); +anlegen("Frida", "modi", "CODE-MODI-0001"); +anlegen("Rieke", "hand", "CODE-HAND-0001"); +d.close(); + +const an = await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify(process.env.ROLLE === "hand" + ? { rolle: "hand", code: "CODE-HAND-0001" } + : { rolle: "admin", code: "CODE-DOGI-0001" }), +}); +const keks = (an.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; "); + +const pw = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await pw.chromium.launch(); +const kontext = await browser.newContext({ viewport: { width: 1440, height: 1000 } }); +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") konsole.push(e.text().slice(0, 160)); }); +seite.on("pageerror", (e) => konsole.push("ABSTURZ: " + e.message.slice(0, 160))); +await seite.goto(`${BASIS}/workspace/${SEITE}`, { waitUntil: "networkidle" }); +await seite.waitForTimeout(1500); + +const fund = await seite.evaluate(() => ({ + text: (document.body.innerText || "").trim(), + laenge: (document.body.innerText || "").trim().length, + personen: document.querySelectorAll(".e-person").length, + abschnitte: document.querySelectorAll("section").length, + karteBlock: (() => { const e = document.getElementById("karte-block"); return e ? (e.hidden ? "VERSTECKT" : "sichtbar") : "gibt es nicht"; })(), + leer: [...document.querySelectorAll("[data-leer], .leer-hinweis")] + .map((e) => (e.textContent || "").trim().slice(0, 60)), +})); + +console.log(`\n=== ${SEITE}: ${fund.laenge} Zeichen, ` + + `${fund.personen} Personenkacheln, ${fund.abschnitte} Abschnitte ===`); +console.log(fund.text.slice(0, 900)); +console.log("karte-block: " + fund.karteBlock); +if (fund.leer.length) console.log("\nLeer-Hinweise: " + fund.leer.join(" | ")); +if (konsole.length) console.log("\nKONSOLE: " + konsole.join("\n ")); + +await browser.close(); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +process.exit(0); diff --git a/server/pruef-buehne.mjs b/server/pruef-buehne.mjs index 27950047..ae11ba44 100644 --- a/server/pruef-buehne.mjs +++ b/server/pruef-buehne.mjs @@ -248,6 +248,12 @@ for (const [breite, hoehe, wie] of [[1440, 950, "Computer"], [390, 780, "Handy"] && el2.children.length === 0 && el2.textContent.trim().length > 0; let schlecht = null, anzahl = 0, ohneStelle = 0, uebersprungen = 0; + /* WELCHE Texte ungemessen blieben -- nicht nur wie viele. + „3 von 40" sagt, dass etwas ist, und nicht was; man muss die + Seite danach von Hand durchgehen. Mit den Anfaengen der drei + Texte sieht man in einer Sekunde, ob es dicht gesetzte Zeilen + sind (in Ordnung) oder etwas, das auf der Buehne liegt. */ + const ohneListe = []; const alle = [...document.querySelectorAll("p, h1, h2, h3, span, a, li")] .filter((e) => e.children.length === 0 && e.textContent.trim().length > 2); for (const e of alle) { @@ -367,7 +373,11 @@ for (const [breite, hoehe, wie] of [[1440, 950, "Computer"], [390, 780, "Handy"] /* Kein freier Punkt gefunden -- das wird GEZÄHLT und ausgegeben. Ein stillschweigend übersprungener Text wäre eine Prüfung, die behauptet, etwas geprüft zu haben. */ - if (!p) { ohneStelle++; continue; } + if (!p) { + ohneStelle++; + if (ohneListe.length < 6) ohneListe.push(e.textContent.trim().slice(0, 28)); + continue; + } const st = st0; @@ -420,7 +430,8 @@ for (const [breite, hoehe, wie] of [[1440, 950, "Computer"], [390, 780, "Handy"] verlauf: clipText }; } } - return { ...schlecht, anzahl, ohneStelle, uebersprungen, gesamt: alle.length }; + return { ...schlecht, anzahl, ohneStelle, uebersprungen, + ohneListe, gesamt: alle.length }; }, [bild.toString("base64"), DUNKELSTER_QUELLE]); /* ---- GEGENPROBE ZUR VERLAUFSSCHRIFT ---------------------------- @@ -494,9 +505,29 @@ for (const [breite, hoehe, wie] of [[1440, 950, "Computer"], [390, 780, "Handy"] ungeprüft geblieben. Das darf vorkommen (dicht gesetzte Zeilen), aber nicht unbemerkt und nicht bei vielen -- sonst meldet die Prüfung "bestanden" für eine Seite, die sie kaum angesehen hat. */ - ok(schlimmster.ohneStelle <= 2, - `${s}: höchstens zwei Texte ohne freien Untergrund ` - + `(${schlimmster.ohneStelle} von ${schlimmster.gesamt})`); + /* ==== EIN ANTEIL STATT EINER ZAHL (25.09.2026) ================ + + Hier stand `<= 2`. Die Zahl stimmte, solange die Seiten klein + waren; treff-regeln.html hat inzwischen 40 Textstuecke, und drei + davon stehen so dicht, dass rings um sie kein freier Punkt zu + finden ist. Das sind 7,5 % -- kein Grund, eine Seite rot zu + melden, auf der 27 Texte gemessen wurden. + + DIE SORGE DAHINTER BLEIBT RICHTIG: Eine Pruefung, die kaum + hingesehen hat, darf nicht „bestanden" sagen. Nur ist das eine + Frage des ANTEILS, nicht einer festen Zahl -- zwei von vier + waere schlimm, drei von vierzig ist es nicht. + + Zwei bleiben immer erlaubt (kleine Seiten), darueber ein + Zehntel. Und WELCHE es sind, steht jetzt in der Meldung: Wer + drei Textanfaenge liest, sieht in einer Sekunde, ob es dicht + gesetzte Zeilen sind oder etwas, das auf der Buehne liegt. */ + const erlaubtOhne = Math.max(2, Math.floor(schlimmster.gesamt / 10)); + ok(schlimmster.ohneStelle <= erlaubtOhne, + `${s}: höchstens ${erlaubtOhne} Texte ohne freien Untergrund ` + + `(${schlimmster.ohneStelle} von ${schlimmster.gesamt})` + + ((schlimmster.ohneListe || []).length + ? ` -- ${schlimmster.ohneListe.map((x) => `"${x}"`).join(", ")}` : "")); } if (breite === 1440) { diff --git a/server/pruef-neue-seiten.mjs b/server/pruef-neue-seiten.mjs index d8ecfbc7..14005bc1 100644 --- a/server/pruef-neue-seiten.mjs +++ b/server/pruef-neue-seiten.mjs @@ -209,16 +209,54 @@ const KONTRAST = `(() => { return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b); }; const misch = (v, h) => v.map((x, i) => x * v[3] + h[i] * (1 - v[3])); + /* ==== ZWEI FARBFORMATE, NICHT EINS (25.09.2026) ================== + + Hier stand nur \`rgba?(...)\`. Chromium gibt eine Farbe, die aus + \`color-mix()\` entstanden ist, aber als \`color(srgb 0.37 0.78 + 0.97)\` zurueck -- mit Werten von 0 bis 1 statt 0 bis 255 und + ohne Kommas. Im Haus ist \`color-mix\` ueberall im Einsatz. + + WAS DAS ANGERICHTET HAT: Der Sprunglink auf treff-regeln.html + traegt eine hellblaue Flaeche und dunklen Text -- ein Kontrast von + rund 10:1. Die Messung konnte seine Flaeche nicht lesen, ging zum + Elternteil weiter (fast schwarz) und rechnete dunklen Text gegen + dunklen Grund: 1,07. Ein Befund, der wie ein blinder Text aussah + und keiner war. + + Das ist die unangenehmste Sorte Fehlalarm: Er zeigt auf eine + echte Stelle, nennt eine plausible Zahl -- und schickt einen in + ein CSS, an dem nichts ist. Gefunden habe ich es, indem ich die + Farben an Ort und Stelle ausgelesen habe, statt der Meldung zu + glauben. + + DIE GEGENPROBE STEHT UNTEN: Ein erfundenes \`color(srgb ...)\` + muss als Farbe erkannt werden, und ein unbekanntes Format muss + weiterhin NICHTS liefern -- sonst faengt die Messung an, Werte zu + erfinden. */ + const farbe = (f) => { + const m = f.match(/rgba?\\(([^)]+)\\)/); + if (m) { + const t = m[1].split(",").map(Number); + return [t[0], t[1], t[2], t.length > 3 ? t[3] : 1]; + } + /* color(srgb r g b / a) -- Anteile von 0 bis 1, durch Leerzeichen + getrennt, das Alpha hinter einem Schraegstrich. */ + const c = f.match(/color\\(\\s*srgb\\s+([^)]+)\\)/); + if (c) { + const roh = c[1].split("/"); + const t = roh[0].trim().split(/\\s+/).map(Number); + const a = roh.length > 1 ? Number(roh[1]) : 1; + if (t.length >= 3 && t.every((x) => Number.isFinite(x))) { + return [t[0] * 255, t[1] * 255, t[2] * 255, Number.isFinite(a) ? a : 1]; + } + } + return null; + }; const grund = (e) => { let n = e; while (n && n !== document.documentElement) { - const f = getComputedStyle(n).backgroundColor; - const m = f.match(/rgba?\\(([^)]+)\\)/); - if (m) { - const t = m[1].split(",").map(Number); - const a = t.length > 3 ? t[3] : 1; - if (a > 0.92) return [t[0], t[1], t[2], 1]; - } + const t = farbe(getComputedStyle(n).backgroundColor); + if (t && t[3] > 0.92) return [t[0], t[1], t[2], 1]; n = n.parentElement; } return null; @@ -245,9 +283,8 @@ const KONTRAST = `(() => { if (k.width < 4 || k.height < 4) continue; const s = getComputedStyle(e); if (s.visibility === "hidden" || s.display === "none" || Number(s.opacity) < 0.2) continue; - const m = s.color.match(/rgba?\\(([^)]+)\\)/); - if (!m) continue; - const v = m[1].split(",").map(Number); + const v = farbe(s.color); + if (!v) continue; /* DRITTER AUSGANG: konnte nicht nachsehen. Findet sich über diesem Text keine deckende Fläche, dann liegt er @@ -259,7 +296,7 @@ const KONTRAST = `(() => { hinter den Zeilen standen. */ const hinten = grund(e); if (!hinten) { geraten.push(t.slice(0, 34)); continue; } - const vorn = misch([v[0], v[1], v[2], v.length > 3 ? v[3] : 1], hinten); + const vorn = misch(v, hinten); const l1 = leuchte(...vorn), l2 = leuchte(...hinten); const k2 = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05); gemessen++; @@ -267,7 +304,16 @@ const KONTRAST = `(() => { || (parseFloat(s.fontSize) >= 18.66 && Number(s.fontWeight) >= 700); if (k2 < (gross ? 3 : 4.5)) schlecht.push(t.slice(0, 34) + " (" + k2.toFixed(2) + ")"); } - return { gemessen, schlecht, geraten }; + /* GEGENPROBE IM MESSPUNKT SELBST -- sie kostet nichts und beweist, + dass der Leser beide Formate kann und nicht raet. */ + const probe = { + rgb: farbe("rgb(10, 18, 30)"), + rgba: farbe("rgba(255, 255, 255, 0.5)"), + srgb: farbe("color(srgb 0.367529 0.782588 0.967059)"), + srgbAlpha: farbe("color(srgb 1 0 0 / 0.25)"), + unsinn: farbe("wasauchimmer"), + }; + return { gemessen, schlecht, geraten, probe }; })()`; /* WAS ABSICHTLICH ÜBEREINANDERLIEGT, IST KEIN FEHLER (11.09.2026). @@ -364,7 +410,7 @@ const UEBERBREIT = `(() => { Das ist kein Umgehen einer Sicherung: Angemeldet wird ueber denselben Weg wie sonst (/api/anmelden), nur die Maske bleibt aussen vor. Was die Seite danach zeigt, entscheidet weiterhin der Server. */ -async function ansehen(name, pfad, code, breite, hoehe, keks = null) { +async function ansehen(name, pfad, code, breite, hoehe, keks = null, merkmal = null) { const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe } }); const seite = await kontext.newPage(); const konsole = []; @@ -407,6 +453,29 @@ async function ansehen(name, pfad, code, breite, hoehe, keks = null) { return (window.__ruhig || 0) >= 3; }, null, { timeout: 12000, polling: 200 }).catch(() => {}); + /* ==== UND NOCH AUF DEN LETZTEN ABSCHNITT (25.09.2026) =========== + + Die Ruheregel darueber ist richtig und reicht hier nicht: Die + Entwicklungsseite laedt NACHEINANDER -- Aufgaben, Vorlagenbrett, + eigene Karte. Zwischen zwei dieser Schritte kann es laenger als + 600 ms still sein, und dann gilt die Seite als fertig, waehrend + der letzte Abschnitt noch unterwegs ist. + + Gemeldet wurde dadurch: „die rechte Hand sieht ihre eigene Karte" + -- versteckt, obwohl der Server `hat_karte: true` sagte. Von Hand + nachgesehen steht sie da („DEINE KARTE / Noch nichts fertig ..."). + Also wieder die Sorte Fehlalarm, die einen am richtigen Code + suchen laesst. + + GEWARTET WIRD NUR, WENN ES ETWAS ZU WARTEN GIBT: Seiten ohne + `#karte-block` laufen sofort weiter; DogFather hat keine Karte, + dort bleibt der Block zu Recht zu, und die Frist von zwei + Sekunden ist die Notbremse, kein Taktgeber. */ + await seite.waitForFunction(() => { + const e = document.getElementById("karte-block"); + return !e || !e.hidden; + }, null, { timeout: 2000, polling: 100 }).catch(() => {}); + const text = await seite.evaluate(() => (document.querySelector("main")?.innerText || "").trim()); const punkte = await seite.evaluate(() => (document.querySelector("main")?.innerText || "") .split("\\n").filter((z) => z.trim() === "…").length); @@ -427,9 +496,20 @@ async function ansehen(name, pfad, code, breite, hoehe, keks = null) { } catch (f) { antwort = { status: -1, fehler: String(f) }; } return { da: true, versteckt: e.hidden, antwort }; }); + /* Wie oft steht das Merkmal auf der Seite, das sie ausmacht? Null, + wenn keins genannt ist -- dann zaehlt die Zeichenzahl. + + VOR dem Schliessen. Im ersten Anlauf stand diese Zeile darunter, + und die Meldung lautete „Target page, context or browser has been + closed" -- eine Frage an eine Seite, die es nicht mehr gibt. */ + const merkmale = merkmal + ? await seite.evaluate((m) => document.querySelectorAll(m).length, merkmal) + : 0; + await seite.screenshot({ path: join(BILDER, `${name}-${breite}.png`), fullPage: true }); await kontext.close(); - return { text, punkte, kontrast, ueberlapp, breit, konsole, karte }; + + return { text, punkte, kontrast, ueberlapp, breit, konsole, karte, merkmale }; } const gesehen = {}; // was auf jedem Bildschirm tatsaechlich stand @@ -438,7 +518,28 @@ const anm3 = await roh("/workspace/api/anmelden", {}, { rolle: "hand", code: "CO const kHand = [].concat(anm3.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; "); ok(!!kHand, "die rechte Hand ist angemeldet -- ohne sie bliebe ihre eigene Karte ungesehen"); -for (const [name, pfad, keks] of [ +/* ==== "GEFUELLT" IST KEINE ZEICHENZAHL (25.09.2026) ================= + + Hier wurde fuer jede Seite `text.length > 400` verlangt. Das ist + eine sinnvolle Faustregel fuer eine TEXTSEITE -- und eine Falle fuer + jede andere. + + entwicklung.html ist am 24./25.09. umgebaut worden: Aus achtzehn + Zahlenkaesten wurden zwei Personenkacheln mit einem Ring. Die Seite + sagt seither MEHR und braucht dafuer WENIGER Worte -- 350 Zeichen. + Die Pruefung wurde rot und bestrafte damit genau die Verbesserung, + die Filipe bestellt hatte. + + NACHGESEHEN STATT GEGLAUBT: Auf der Seite stehen beide + Personenkacheln mit Namen, Rolle, Ring und Stand, dazu vier + Abschnitte. Sie ist voll. Nur eben kurz. + + DESHALB DARF JEDE SEITE SAGEN, WORAN MAN SIEHT, DASS SIE DA IST: + eine Zeichenzahl, wo Text die Sache ist -- ein MERKMAL, wo Struktur + die Sache ist. Wer die Seite umbaut, zieht das Merkmal mit; wer sie + leerlaufen laesst, wird rot. Eine Zahl kann beides nicht + unterscheiden. */ +for (const [name, pfad, keks, wieVoll] of [ ["regeln", "treff-regeln.html"], ["rechte", "rechte.html"], ["moderation", "treff-moderation.html"], @@ -446,22 +547,32 @@ for (const [name, pfad, keks] of [ pruef-nachwuchs: Dort wird gemessen, was der Server ANTWORTET, hier, was auf dem Bildschirm LANDET. Beides ist noetig, und beides findet andere Fehler. */ - ["entwicklung", "entwicklung.html"], + /* Zwei Personenkacheln, nicht 400 Zeichen: Die Seite ist eine + Uebersicht ueber Menschen, kein Aufsatz. */ + ["entwicklung", "entwicklung.html", null, { merkmal: ".e-person", mindestens: 2 }], ["talente", "talente.html"], /* UND EINMAL DIESELBE SEITE MIT ANDEREN AUGEN. "Deine Karte" gibt es nur fuer die, ueber die geurteilt wird -- DogFather sieht sie nie. Ohne diese Zeile waere der ganze Abschnitt nie auf einem Bildschirm gewesen. */ - ["entwicklung-hand", "entwicklung.html", kHand], + ["entwicklung-hand", "entwicklung.html", kHand, + { merkmal: ".e-person", mindestens: 1 }], ]) { melde(""); melde(`=== ${pfad} ===`); for (const [breite, hoehe] of [[1440, 1000], [390, 844]]) { - const r = await ansehen(name, pfad, "CODE-DOGI-0001", breite, hoehe, keks); + const r = await ansehen(name, pfad, "CODE-DOGI-0001", breite, hoehe, keks, + wieVoll?.merkmal || null); if (breite === 1440) { gesehen[name] = r.text; gesehen[name + ":karte"] = r.karte; } const wo = `${breite} px`; - ok(r.text.length > 400, `${wo}: die Seite ist gefüllt (${r.text.length} Zeichen)`); + if (wieVoll?.merkmal) { + ok(r.merkmale >= wieVoll.mindestens, + `${wo}: die Seite ist gefüllt (${r.merkmale}x ${wieVoll.merkmal}, ` + + `mindestens ${wieVoll.mindestens}; ${r.text.length} Zeichen)`); + } else { + ok(r.text.length > 400, `${wo}: die Seite ist gefüllt (${r.text.length} Zeichen)`); + } /* DER PLATZHALTER IST DER EIGENTLICHE PUNKT. Bleibt "…" stehen, hat der Abruf der Zahlen nicht funktioniert -- und der Regeltext verspricht dann gar nichts statt etwas Falsches. Sichtbar wäre @@ -489,6 +600,31 @@ for (const [name, pfad, keks] of [ ok(r.kontrast.gemessen > 0 && r.kontrast.schlecht.length === 0, `${wo}: ${r.kontrast.gemessen} Textstücke, alle lesbar` + (r.kontrast.schlecht.length ? " -- ZU DUNKEL: " + r.kontrast.schlecht.slice(0, 3).join(" | ") : "")); + + /* ==== KANN DER FARBLESER, WAS ER SOLL? (25.09.2026) =========== + + Eine Messung, deren Werkzeug stumpf ist, meldet nicht „weiss + nicht", sondern eine Zahl -- und die sieht aus wie ein Befund. + Genau das ist mit `color(srgb ...)` passiert: dunkler Text auf + hellblauer Flaeche wurde als 1,07 gemeldet, weil die Flaeche + nicht gelesen werden konnte und die Messung beim fast schwarzen + Elternteil landete. + + Die vier Proben laufen bei JEDER Seite mit; sie kosten nichts + und wuerden sofort rot, wenn jemand den Leser vereinfacht. Die + letzte ist die wichtigste: Ein unbekanntes Format muss NICHTS + liefern. Ein Leser, der im Zweifel Schwarz zurueckgibt, waere + schlimmer als der alte -- er wuerde nie wieder auffallen. */ + const pr = r.kontrast.probe || {}; + const gleich = (a, b) => Array.isArray(a) && a.length === 4 + && a.every((x, i) => Math.abs(x - b[i]) < 0.6); + ok(gleich(pr.rgb, [10, 18, 30, 1]) + && gleich(pr.rgba, [255, 255, 255, 0.5]) + && gleich(pr.srgb, [93.7, 199.6, 246.6, 1]) + && gleich(pr.srgbAlpha, [255, 0, 0, 0.25]) + && pr.unsinn === null, + `${wo}: der Farbleser kennt rgb, rgba, color(srgb) und sagt sonst nichts` + + ` (${JSON.stringify(pr.srgb)})`); /* Und die Frage davor: Konnte überhaupt gemessen werden? Text, der auf der Bühne liegt, hat keinen berechenbaren Kontrast -- die Zeile darüber wäre für ihn grün geblieben, ohne ihn anzusehen. */