Filipe hat neun Bilder geschickt, damit es VERSCHIEDENE Hintergruende gibt. Benutzt wurde davon genau eines (das Studio), die uebrigen acht lagen unbenutzt im Ordner -- lounge.png und skyline.png sogar schon fertig kopiert. Zu Recht beanstandet. Jetzt wird aus jeder Szene eine Buehne, breit und schmal, und jede Seite bekommt die, die zu ihr passt: studio Startseite der neutrale Ort, alles beginnt hier showbuehne Dashboard, Review die grosse Buehne, alles im Blick garage Aufgaben, Technik Werkstatt, hier wird gearbeitet skyline Kalender Nacht ueber der Stadt, Zeit lounge Calls, Community Sitzecke, hier wird geredet arena Dateien, Wissen Archiv hinter dem Portal halle Profil, Personen die Halle, in der jemand steht wald Start-Check, Scouting der Weg, den man erst sucht portal Content, LIVE Durchgang, hier entsteht etwas Die Zuordnung steht in bereiche.js -- derselben Liste, aus der schon Zeichen und Farbton kommen. Eine Seite traegt damit dreierlei aus einer einzigen Quelle: Farbe, Zeichen und Buehne. Sie koennen nicht auseinanderlaufen. Alle neun bekommen dieselbe Behandlung (abgedunkelt auf 0.42, Vignette in der Mitte, wo der Text steht, oben ausgeblendet, wo die Kopfleiste sitzt). Sie sehen verschieden aus und verhalten sich gleich -- eine Buehne, die je Seite anders hell waere, waere Unruhe statt Vielfalt. Der schmale Ausschnitt ist je Szene ein anderer, weil ein auf 9:16 gequetschtes Breitbild nur noch Wand zeigt; genommen wird der Bereich, in dem die Figuren stehen. 18 Dateien, zusammen 580 KB -- pro Seite werden davon zwei geladen (breit ODER schmal), also 16 bis 48 KB. Die Rohbilder waren 2,4 MB je Stueck. Kontrast auf allen sechs geprueften Seiten an echten Bildpunkten nachgemessen: haelt (schlechtester Wert 4,59:1). FUND: Die Buehnenpruefung suchte nach "buehne-breit" und fand die neuen Namen nicht mehr -- sie meldete "die Buehne liegt im Hintergrund: FEHL", obwohl sie da war. Die Seitenpruefung sieht jetzt nach, dass jede Seite ihre Szene hat UND dass das Bild wirklich ankommt: Ein data-buehne ohne passende CSS-Regel waere still wirkungslos. Co-Authored-By: Claude Opus 5 <[email protected]>
211 lines
10 KiB
JavaScript
211 lines
10 KiB
JavaScript
/* ===================================================================
|
|
Pruefung der Buehne (Casper und HasiDog im Hintergrund).
|
|
|
|
Ein Bild hinter einem Arbeitswerkzeug ist der schnellste Weg zu einer
|
|
Oberflaeche, die gut aussieht und unbenutzbar ist. Deshalb wird hier
|
|
NICHT geprueft, ob es huebsch ist, sondern ob der Text noch lesbar
|
|
ist -- und zwar an echten BILDPUNKTEN aus dem fertigen Bild, nicht an
|
|
der Farbangabe im Stil. Die sagt naemlich nichts darueber, was
|
|
dahinterliegt.
|
|
|
|
Massstab WCAG: 4,5:1 fuer normalen Text, 3:1 fuer grossen.
|
|
=================================================================== */
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-buehne-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
process.env.PORT = "4193";
|
|
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:4193";
|
|
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 220_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");
|
|
|
|
/* Warten, bis der Server die Datenbank wirklich angelegt hat. Der
|
|
Anmeldeversuch oben STOESST das nur an -- Schema und Umstellungen
|
|
laufen danach. Wer sofort weiterschreibt, trifft auf "no such table". */
|
|
for (let i = 0; i < 60; i++) {
|
|
try {
|
|
const p = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
|
|
} catch (f) {
|
|
if (i === 0 || i === 5) console.log(" warte auf die Datenbank:", f.message);
|
|
await new Promise((r) => setTimeout(r, 250));
|
|
}
|
|
}
|
|
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const jetzt = new Date().toISOString();
|
|
const salt = randomBytes(16).toString("hex");
|
|
const hash = scryptSync("CODE-DOGI-0001", 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("Filipe", "admin", hash, salt, 32768, jetzt);
|
|
d.close();
|
|
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
|
|
/* Der Kalender kam am 01.09.2026 dazu -- er bringt die meisten neuen
|
|
Farbflächen mit (vier Arten, Feiertagsfärbung, Anlassmarken), und
|
|
genau dort entscheidet sich, ob der hellere Hintergrund noch trägt. */
|
|
const SEITEN = ["start.html", "uebersicht.html", "personen.html", "content.html",
|
|
"aufgaben.html", "kalender.html"];
|
|
|
|
for (const [breite, hoehe, wie] of [[1440, 950, "Computer"], [390, 780, "Handy"]]) {
|
|
console.log(`\n=== ${wie} (${breite}x${hoehe})`);
|
|
const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe } });
|
|
const seite = await kontext.newPage();
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
|
await seite.click('.rolle[data-rolle="admin"]');
|
|
await seite.fill("#code", "CODE-DOGI-0001");
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
|
|
|
for (const s of SEITEN) {
|
|
await seite.goto(BASIS + "/workspace/" + s, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(800);
|
|
|
|
const da = await seite.evaluate(() => ({
|
|
/* Seit dem 01.09.2026 gibt es neun Bühnen statt einer, benannt
|
|
nach ihrer Szene (buehne-garage-breit.webp). Der alte Ausdruck
|
|
suchte nach "buehne-breit" und fand nichts mehr -- gemeldet als
|
|
"die Bühne liegt im Hintergrund: FEHL", obwohl sie da war. */
|
|
buehne: /buehne-[a-z]+-(breit|schmal)\.webp/
|
|
.test(getComputedStyle(document.body, "::before").backgroundImage),
|
|
schleier: !!document.querySelector(".schleier"),
|
|
durchlaessig: getComputedStyle(document.querySelector(".schleier") || document.body).pointerEvents,
|
|
}));
|
|
if (s === "start.html") {
|
|
ok(da.buehne, "die Bühne liegt im Hintergrund");
|
|
}
|
|
ok(da.schleier && da.durchlaessig === "none",
|
|
`${s}: Schleier da und durchlässig für Klicks (${da.durchlaessig})`);
|
|
|
|
/* Der eigentliche Punkt: Kontrast an ECHTEN Bildpunkten aus dem
|
|
fertigen Bild. Gemessen wird der Untergrund direkt NEBEN jedem
|
|
sichtbaren Text -- nicht die Farbangabe im Stil, die weiß nichts
|
|
davon, was dahinterliegt. */
|
|
const bild = await seite.screenshot({ type: "png" });
|
|
const schlimmster = await seite.evaluate(async (b64) => {
|
|
const kanal = (v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; };
|
|
const hell = (c) => 0.2126 * kanal(c[0]) + 0.7152 * kanal(c[1]) + 0.0722 * kanal(c[2]);
|
|
const kontrast = (a, b) => {
|
|
const w = [hell(a), hell(b)].sort((x, y) => y - x);
|
|
return (w[0] + 0.05) / (w[1] + 0.05);
|
|
};
|
|
|
|
const bild = new Image();
|
|
bild.src = "data:image/png;base64," + b64;
|
|
await bild.decode();
|
|
const c = document.createElement("canvas");
|
|
c.width = bild.width; c.height = bild.height;
|
|
const g = c.getContext("2d");
|
|
g.drawImage(bild, 0, 0);
|
|
const skala = bild.width / window.innerWidth;
|
|
|
|
/* Wo darf man den Untergrund abtasten?
|
|
|
|
Vorher wurde stur 4 px rechts neben dem Text gemessen. Das ging
|
|
gut, solange dort nichts stand -- sobald aber zwei Textstücke
|
|
nebeneinanderliegen (Grußwort und Name, Marke und Ortsangabe),
|
|
traf der Punkt DEN NACHBARTEXT und die Prüfung verglich Schrift
|
|
mit Schrift. Ergebnis: 1,1:1 und ein Alarm, der nichts mit der
|
|
Lesbarkeit zu tun hatte.
|
|
|
|
Jetzt werden mehrere Stellen rings um den Text angeboten und
|
|
jede zuerst gefragt: Liegt dort wirklich Untergrund? Ein Punkt
|
|
gilt nur, wenn an seiner Stelle KEIN fremdes Textelement liegt.
|
|
Das macht die Prüfung strenger, nicht lockerer -- sie misst
|
|
jetzt tatsächlich das, was sie behauptet zu messen. */
|
|
const istFremderText = (el2, e) =>
|
|
el2 && el2 !== e && !el2.contains(e) && !e.contains(el2)
|
|
&& el2.children.length === 0 && el2.textContent.trim().length > 0;
|
|
|
|
let schlecht = null, anzahl = 0, ohneStelle = 0;
|
|
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) {
|
|
const r = e.getBoundingClientRect();
|
|
if (r.width < 8 || r.height < 8) continue;
|
|
if (r.top < 0 || r.bottom > window.innerHeight) continue;
|
|
|
|
const mitte = r.top + r.height / 2;
|
|
const kandidaten = [
|
|
[r.right + 4, mitte], [r.right + 14, mitte],
|
|
[r.left - 4, mitte], [r.left - 14, mitte],
|
|
[r.left + r.width / 2, r.top - 3], [r.left + r.width / 2, r.bottom + 3],
|
|
];
|
|
let p = null;
|
|
for (const [kx, ky] of kandidaten) {
|
|
if (kx < 0 || ky < 0 || kx >= window.innerWidth || ky >= window.innerHeight) continue;
|
|
if (istFremderText(document.elementFromPoint(kx, ky), e)) continue;
|
|
const bx = Math.round(kx * skala), by = Math.round(ky * skala);
|
|
if (bx < 0 || bx >= c.width || by < 0 || by >= c.height) continue;
|
|
p = g.getImageData(bx, by, 1, 1).data;
|
|
break;
|
|
}
|
|
/* 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; }
|
|
|
|
const st = getComputedStyle(e);
|
|
const farbe = st.color.match(/\d+/g).slice(0, 3).map(Number);
|
|
const gr = parseFloat(st.fontSize);
|
|
const fett = Number(st.fontWeight) >= 700;
|
|
const grenze = (gr >= 24 || (gr >= 18.66 && fett)) ? 3 : 4.5;
|
|
const v = kontrast(farbe, [p[0], p[1], p[2]]);
|
|
anzahl++;
|
|
if (!schlecht || v / grenze < schlecht.v / schlecht.grenze) {
|
|
schlecht = { v, grenze, text: e.textContent.trim().slice(0, 26), grund: [p[0], p[1], p[2]] };
|
|
}
|
|
}
|
|
return { ...schlecht, anzahl, ohneStelle, gesamt: alle.length };
|
|
}, bild.toString("base64"));
|
|
|
|
ok(schlimmster.v >= schlimmster.grenze,
|
|
`${s}: schlechtester Kontrast ${schlimmster.v.toFixed(2)}:1 (nötig ${schlimmster.grenze}:1) `
|
|
+ `bei "${schlimmster.text}" auf rgb(${schlimmster.grund.join(",")}) · `
|
|
+ `${schlimmster.anzahl} Stellen gemessen`);
|
|
/* Wenn rings um einen Text nirgends freier Untergrund liegt, ist er
|
|
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})`);
|
|
}
|
|
|
|
if (breite === 1440) {
|
|
await seite.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(800);
|
|
await seite.screenshot({ path: "pruef-buehne.png" });
|
|
}
|
|
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);
|