Files
dogfather-universe/server/pruef-treff-seiten.mjs
T
DogFatherGitandClaude Opus 5 1ebaf8a864 Die drei neuen Seiten angesehen — und die Bühne lag hinter der Rechtetafel
Sie waren geprüft und nie ANGESEHEN. pruef-treff-seiten.mjs (42
Prüfungen, 1440 px und 390 px) holt das nach: Steht Text da, sind die
Platzhalter ersetzt, liegt etwas übereinander, schiebt die Seite
seitwärts, ist alles lesbar.

DER BEFUND, DEN KEINE ZAHL GESEHEN HAT: Hinter der Rechtetafel lag die
Bühne — Spicy-Media-Motiv, Neonlicht, Chilischoten — und die Zeilen der
Tabelle standen mitten darin. Die Kontrastmessung war grün, weil sie
gegen #07060f rechnete: Sie hatte den Grund nicht gemessen, sondern
angenommen. Ein grüner Haken über einer falschen Voraussetzung.

Behoben an beiden Enden:
 · Die Abschnitte bekommen eine DECKENDE Fläche — kein Kartenauftritt
   (keine Fase, kein Kantenlicht), nur ein Blatt Papier unter dem Text.
   Der Einwand von vorher (Karten zerteilen einen langen Text) bleibt
   damit gewahrt, die Bühne bleibt ringsum sichtbar.
 · Die Messung bekommt einen DRITTEN AUSGANG: Findet sie über einem Text
   keine deckende Fläche, ist das jetzt ein Fehler ("konnte nicht
   nachsehen") und kein stilles Grün.

Zwei weitere Meldungen waren Messfehler und sind als solche behoben,
nicht weggeklickt:
 · "Meine Sicht" überlappt sich selbst — das ist das <select> für
   Vorleseprogramme, das UNTER dem eigenen Bedienelement liegen MUSS.
   Ausgelassen wird jetzt, was ausdrücklich fürs Auge weggenommen wurde.
 · Die Rechtetafel ragt auf 390 px über den Rand — sie steht in einem
   Schiebekasten, und genau so soll eine Tabelle mit neun Spalten sich
   auf einem Handy verhalten. Gemessen wird jetzt, ob die SEITE schiebt.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-11 18:04:33 +02:00

345 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
Die drei neuen Seiten — angesehen, nicht nur geprüft (11.09.2026)
pruef-treff und pruef-treff-werkzeuge messen, was der Server ANTWORTET.
Diese Datei sieht nach, was auf dem Bildschirm LANDET.
Der Unterschied ist nicht theoretisch. Am 06.09.2026 hat in diesem
Haus eine Kopfleiste zwölf Seiten lang den falschen Knopf getroffen,
und keine einzige Schnittstellenprüfung hat es bemerkt — sie waren
alle grün. Gefunden hat es ein Blick.
GEPRÜFT WIRD, WAS MAN NICHT ERRATEN KANN:
* Steht überhaupt Text da? Eine Seite, deren Skript stillschweigend
abbricht, sieht in der Antwort tadellos aus und ist leer.
* Sind die Platzhalter ersetzt? Die Zahlen im Regeltext kommen vom
Server; bleibt "…" stehen, hat der Abruf nicht funktioniert.
* Liegt etwas übereinander? Zwei Bedienelemente, die sich
überlappen, sind auf dem Handy die Sorte Fehler, bei der man auf
A tippt und B bekommt.
* Lässt sich der Text lesen? Kontrast auf ECHTEN Bildpunkten
gemessen, nicht aus den Farbwerten gerechnet — die sagen nichts
über das, was hinter einer halbdurchsichtigen Fläche liegt.
* Und auf 390 px Breite: Bricht etwas aus dem Bild heraus?
Aufrufen mit: node server/pruef-treff-seiten.mjs
===================================================================== */
import { mkdtempSync, rmSync, mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { request as httpAnfrage } from "node:http";
import { portMussFreiSein } from "./helfer-port.mjs";
const PORT = await portMussFreiSein(4403, "pruef-treff-seiten");
const ordner = mkdtempSync(join(tmpdir(), "ws-treffs-"));
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, "pruef-treff-seiten");
await new Promise((r) => setTimeout(r, 700));
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
let fehler = 0, geprueft = 0;
const melde = (t) => console.log(t);
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
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 schluessel = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", schluessel, jetzt);
const ALT = new Date(Date.now() - 40 * 86400000).toISOString();
function anlegen(name, rolle, code, erstellt = jetzt) {
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", schluessel).update(code).digest("hex"), erstellt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
const idKessi = anlegen("Kessi", "gast", "CODE-GAST-0001", ALT);
/* ETWAS ZUM ANSEHEN. Eine leere Seite sieht aufgeräumt aus und beweist
nichts -- die Fälle auf der Moderationsseite entstehen erst, wenn es
eine Meldung und eine Maßnahme gibt. */
d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von)"
+ " VALUES ('treff','hallo','Hallo, ich bin die Kessi','Freue mich, hier zu sein!',?,?,?)")
.run(jetzt.slice(0, 10), jetzt, idKessi);
const beitrag = d.prepare("SELECT last_insert_rowid() AS id").get().id;
d.close();
function roh(pfad, kopf = {}, koerper = null, methode = null) {
if (methode === "DELETE") koerper = null;
return new Promise((fertig, schief) => {
const a = httpAnfrage({
host: "127.0.0.1", port: PORT, path: pfad,
method: methode || (koerper ? "POST" : "GET"),
headers: { ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf },
}, (antwort) => {
let text = "";
antwort.on("data", (s) => { text += s; });
antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers }));
});
a.on("error", schief);
if (koerper) a.write(JSON.stringify(koerper));
a.end();
});
}
/* Eine Meldung und eine Maßnahme anlegen -- über die echten Wege. */
const anm = await roh("/workspace/api/anmelden", {}, { rolle: "gast", code: "CODE-GAST-0001" });
const kGast = [].concat(anm.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; ");
const anm2 = await roh("/workspace/api/anmelden", {}, { rolle: "admin", code: "CODE-DOGI-0001" });
const kDogi = [].concat(anm2.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; ");
await roh(`/workspace/api/treff/eintrag/${beitrag}/melden`, { cookie: kGast },
{ grund: "Da steht eine Handynummer drin, die da nicht hingehört." });
await roh("/workspace/api/treff/massnahme", { cookie: kDogi },
{ person_id: idKessi, art: "hinweis", grund: "Bitte keine Nummern posten – einmalig, kein Drama." });
const { chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await chromium.launch();
const BILDER = join(ordner, "bilder");
mkdirSync(BILDER, { recursive: true });
/* Kontrast auf ECHTEN Bildpunkten: Die Schrift liegt teils über
halbdurchsichtigen Flächen und über der Bühne; aus den Farbwerten im
CSS lässt sich der tatsächliche Kontrast deshalb nicht ausrechnen. */
const KONTRAST = `(() => {
const leuchte = (r, g, b) => {
const f = (c) => { c /= 255; return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); };
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]));
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];
}
n = n.parentElement;
}
return null;
};
const schlecht = [];
const geraten = [];
let gemessen = 0;
for (const e of document.querySelectorAll("p, li, h1, h2, h3, td, th, span, a, button")) {
const t = (e.textContent || "").trim();
if (!t || e.children.length) continue;
const k = e.getBoundingClientRect();
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);
/* DRITTER AUSGANG: konnte nicht nachsehen.
Findet sich über diesem Text keine deckende Fläche, dann liegt er
auf der Bühne — einem Foto. Was dahinter liegt, lässt sich aus
den Farbwerten NICHT ausrechnen, und eine angenommene
Hintergrundfarbe wäre genau der grüne Haken über einer falschen
Voraussetzung. Genau so ist am 11.09.2026 die Rechtetafel durch
die Messung gekommen, während im Bildschirmfoto Chilischoten
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 l1 = leuchte(...vorn), l2 = leuchte(...hinten);
const k2 = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
gemessen++;
const gross = parseFloat(s.fontSize) >= 24
|| (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 };
})()`;
/* WAS ABSICHTLICH ÜBEREINANDERLIEGT, IST KEIN FEHLER (11.09.2026).
Der erste Entwurf meldete auf allen drei Seiten dasselbe:
"Meine SichtKessi / Meine Sicht". Das ist der Umschalter in der
Kopfleiste — ein selbst gebautes Bedienelement, unter dem ein echtes
<select> für Vorleseprogramme liegt. Es MUSS dort liegen, sonst
findet es niemand mit einem Bildschirmleser.
Es ist für das Auge unsichtbar gemacht, aber nicht über
`visibility` oder `opacity` — sonst würden manche Vorleseprogramme
es ebenfalls überspringen. Übliche Technik ist ein 1x1-Ausschnitt
(`clip-path`) oder eine Verschiebung aus dem Bild.
Eine Prüfung, die so etwas meldet, ist schlimmer als keine: Sie ist
auf JEDER Seite rot, man gewöhnt sich daran, und die eine echte
Überlappung geht darin unter. Ausgelassen wird deshalb, was
ausdrücklich für das Auge weggenommen wurde — und NICHT einfach
alles, was "Meine Sicht" heißt. */
const UEBERLAPP = `(() => {
const versteckt = (e) => {
const s = getComputedStyle(e);
if (s.clipPath && s.clipPath !== "none") return true;
if (s.clip && s.clip !== "auto") return true;
if (Number(s.opacity) < 0.2) return true;
if (e.getAttribute("aria-hidden") === "true") return true;
const k = e.getBoundingClientRect();
return k.right < 1 || k.bottom < 1;
};
const teile = [...document.querySelectorAll("button, a.schritt, input, select")]
.filter((e) => { const k = e.getBoundingClientRect(); return k.width > 4 && k.height > 4; })
.filter((e) => !versteckt(e));
const treffer = [];
for (let i = 0; i < teile.length; i++) {
for (let j = i + 1; j < teile.length; j++) {
if (teile[i].contains(teile[j]) || teile[j].contains(teile[i])) continue;
const a = teile[i].getBoundingClientRect(), b = teile[j].getBoundingClientRect();
const x = Math.min(a.right, b.right) - Math.max(a.left, b.left);
const y = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top);
if (x > 2 && y > 2) {
treffer.push((teile[i].textContent || "?").trim().slice(0, 18)
+ " / " + (teile[j].textContent || "?").trim().slice(0, 18));
}
}
}
return treffer;
})()`;
/* ÜBERBREITE: DIE FRAGE IST, OB DIE SEITE SCHIEBT (11.09.2026).
Der erste Entwurf zählte jedes Element, dessen rechte Kante über den
Rand ragt. Er meldete die Rechtetafel auf 390 px — zu Recht in dem
Sinne, dass die Tabelle breiter ist als das Bild, und zu Unrecht in
jedem anderen: Sie steht in einem Kasten, den man seitwärts schieben
kann (`overflow-x: auto`), und genau so soll eine Tabelle mit neun
Spalten sich auf einem Handy verhalten. Alles andere hieße, Spalten
wegzulassen oder Schrift unlesbar zu verkleinern.
Schlimm ist nur, wenn die GANZE SEITE seitwärts schiebt — dann
verrutscht beim Lesen der Text unter dem Daumen. Das ist eine
einzige Zahl, und sie lügt nicht: scrollWidth gegen clientWidth am
<html>. Was in einem Schiebekasten steht, zählt dort nicht mit.
Die Liste der Ausreißer bleibt trotzdem — aber nur als Auskunft
DANEBEN, und Elemente in einem Schiebekasten sind ausgenommen. */
const UEBERBREIT = `(() => {
const wurzel = document.documentElement;
const schiebt = wurzel.scrollWidth - wurzel.clientWidth;
const imKasten = (e) => {
let n = e.parentElement;
while (n && n !== document.body) {
const s = getComputedStyle(n);
if (s.overflowX === "auto" || s.overflowX === "scroll") return true;
n = n.parentElement;
}
return false;
};
const w = wurzel.clientWidth;
const raus = [...document.querySelectorAll("main *")]
.filter((e) => { const k = e.getBoundingClientRect(); return k.right > w + 2 && k.width > 8; })
.filter((e) => !imKasten(e))
.map((e) => e.tagName.toLowerCase() + "." + (e.className || "").toString().split(" ")[0])
.slice(0, 5);
return { schiebt, raus };
})()`;
async function ansehen(name, pfad, code, breite, hoehe) {
const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe } });
const seite = await kontext.newPage();
const konsole = [];
seite.on("console", (m) => {
if (m.type() === "error" && !/favicon|ERR_/i.test(m.text())) konsole.push(m.text());
});
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message));
await seite.goto(`${BASIS}/workspace/`, { waitUntil: "domcontentloaded" });
await seite.click(`.rolle[data-rolle="admin"]`);
await seite.fill("#code", code);
await seite.click("button[type=submit]");
await seite.waitForURL("**/start.html", { timeout: 15000 });
await seite.goto(`${BASIS}/workspace/${pfad}`, { waitUntil: "networkidle" });
await seite.waitForTimeout(900);
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);
const kontrast = await seite.evaluate(KONTRAST);
const ueberlapp = await seite.evaluate(UEBERLAPP);
const breit = await seite.evaluate(UEBERBREIT);
await seite.screenshot({ path: join(BILDER, `${name}-${breite}.png`), fullPage: true });
await kontext.close();
return { text, punkte, kontrast, ueberlapp, breit, konsole };
}
for (const [name, pfad] of [
["regeln", "treff-regeln.html"],
["rechte", "rechte.html"],
["moderation", "treff-moderation.html"],
]) {
melde("");
melde(`=== ${pfad} ===`);
for (const [breite, hoehe] of [[1440, 1000], [390, 844]]) {
const r = await ansehen(name, pfad, "CODE-DOGI-0001", breite, hoehe);
const wo = `${breite} px`;
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
das nur hier. */
ok(r.punkte === 0, `${wo}: keine Platzhalter mehr übrig (${r.punkte})`);
ok(r.konsole.length === 0,
`${wo}: keine Fehler im Browser${r.konsole.length ? " -- " + r.konsole[0].slice(0, 80) : ""}`);
ok(r.ueberlapp.length === 0,
`${wo}: nichts liegt übereinander${r.ueberlapp.length ? " -- " + r.ueberlapp.join(" | ") : ""}`);
ok(r.breit.schiebt <= 1 && r.breit.raus.length === 0,
`${wo}: die Seite schiebt nicht seitwärts (${r.breit.schiebt} px)`
+ (r.breit.raus.length ? " -- DRAUSSEN: " + r.breit.raus.join(", ") : ""));
/* DIE ZAHL GEHÖRT IN DIE BEDINGUNG: Fände die Messung kein einziges
Textstück, wäre "nichts zu dunkel" trivial wahr. */
ok(r.kontrast.gemessen > 15 && 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(" | ") : ""));
/* 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. */
ok(r.kontrast.geraten.length === 0,
`${wo}: jeder Text steht auf einer deckenden Fläche`
+ (r.kontrast.geraten.length
? ` -- AUF DER BÜHNE (${r.kontrast.geraten.length}): `
+ r.kontrast.geraten.slice(0, 3).join(" | ") : ""));
}
}
await browser.close();
melde("");
melde(`Bilder liegen in ${BILDER}`);
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
/* Der Ordner bleibt liegen, solange der Dienst die Datenbank offen hat
-- Aufräumrest, kein Befund. Die Bilder darin sind ohnehin das
Ergebnis; sie werden bewusst NICHT mit weggeräumt. */
process.exit(fehler ? 1 : 0);