diff --git a/server/pruef-treff-seiten.mjs b/server/pruef-treff-seiten.mjs new file mode 100644 index 00000000..9a6843f6 --- /dev/null +++ b/server/pruef-treff-seiten.mjs @@ -0,0 +1,344 @@ +/* ===================================================================== + 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 +