DER GRUND JEDER KACHEL IM HAUS VON TEAM DOGI
Filipe: "ich will dass die hintergrunde von den kacheln immer unviersum
artig ist, es muss richtig geil sein aber immer so dass man alles noch
gut erkkent. und das IN DER GANZEN WEBSITE VON TEAM DOGI. nur die
kacheln. [...] wichtig ist die form der kacheln soll gleich bleiben."
module.css hat eine kanonische KACHELLISTE -- 48 Klassen, siebenmal in
der Datei, von pruef-css-klassen gegeneinander gehalten. Eine achte
Abschrift in crew-haus.css waere die Sorte Fehler, die nicht auffaellt:
heute vollstaendig, bei der naechsten neuen Kachel lautlos nicht mehr.
Deshalb faerbt crew-haus.css keine einzige Kachel. module.css baut den
Grund jetzt aus vier Werten (--sternenfeld, -mass, -lage,
--modul-schleier), und das zweite Haus setzt nur diese vier um. Damit
hat jede Kachel der ganzen Adresse den Himmel -- auch die, die es noch
nicht gibt. Im Agenturhaus steht `none`: kein Pixel aendert sich.
ZWEI DINGE HAT ERST DIE MESSUNG GEFUNDEN, NICHT DAS NACHDENKEN:
1. Die Nebel standen zuerst oben links. Dort ist aber JEDE Kachel dieses
Hauses schon von sich aus am hellsten -- ihr eigener Lichtverlauf
laeuft bei allen aus derselben Richtung (155/150/158 Grad) --, und
genau dort stehen ueberall die Ueberschriften. Hinter der leisesten
Textzeile lagen dadurch 2,09 % der Bildpunkte unter 4,5:1; im
Agenturhaus sind es an derselben Stelle 0,115 %. Nach dem Umzug in
die beiden gegenueberliegenden Ecken: 0,22 % -- und der Nebel durfte
dabei KRAEFTIGER werden (.80 statt .62), weil er nicht mehr auf dem
hellsten Punkt liegt. Besser lesbar und deutlicher zu sehen; das ist
selten und war hier umsonst zu haben.
2. Kleinere, dafuer hellere Sternkerne waren der falsche Weg: Der
hellste Punkt blieb fast gleich, der Stern wurde nur unschaerfer.
Entschieden hat die Deckkraft, nicht die Groesse.
Form unangetastet: Fase, Silhouette und die drei Eckwinkel werden vor
und nach dem Hauswechsel Zeichen fuer Zeichen verglichen.
pruef-kachel-universum.mjs (NEU, 37 Pruefungen, Port 4391) misst an
echten Bildpunkten und fragt nicht nach Durchschnitt allein, sondern
nach dem ANTEIL der Punkte unter 4,5:1 -- das unterscheidet einen Punkt
von einer Flaeche. Zwei Gegenproben: ein zu dunkler Text UND ein zu
heller Nebel muessen durchfallen.
MEINE SICHT -- "GENAU SO WIE SIE ES SEHEN"
Filipe: "oben bei meine sicht soll ich auch die sicht von allen jeden
moment sehen koennen und das genau genau so wie sie es sehen alles.
ausser die kalender daten oder chat daten wo ich nicht mit drin bin.."
Gemessen wurde nicht "mit Umschalter gegen ohne" -- das ist bei duenner
Datenlage ueberall gleich und beweist nichts. Gemessen wurde die
Antwort mit Umschalter gegen die Antwort, die die Person SELBST bekommt.
Das hat sechs Stellen gefunden:
* workspace-zentrale.js las `req.person.sicht` -- ein Feld, das es nicht
gibt. Der Ausdruck war immer `undefined || req.person`, daneben ein
ausfuehrlicher Kommentar, der genau das Richtige beschrieb. Die grosse
Kachel zeigte verlaesslich die eigene Lage, waehrend die Zahlen
darunter der fremden folgten -- zwei Wahrheiten in einer Kachel. Ein
Tippfehler in einem Variablennamen macht nichts kaputt; er tut nur
nichts, und genau deshalb faellt so etwas nie von selbst auf. Die
Route hatte ausserdem ZWEI Personenvariablen; jetzt hat sie eine.
* sichtPerson() gab die angesehene Person ohne Feld `haus` zurueck --
und nurHaus()/hausBedingung() fangen beide mit `haus !== "crew"` an.
Jede fremde Sicht war damit eine Agentursicht: In der Sicht auf einen
Modi kamen die Dateien, Personen und Berichte des anderen Hauses.
Das Haus haengt jetzt an der ROLLE, nicht an der Adresse.
* leistung, profil, schulung, fruehwarnung, report und teamlage lasen
weiterhin den Angemeldeten. Nur LESEN ist umgestellt, nie ein Recht --
und weil DogFather ohnehin alles sehen darf, kann das nichts oeffnen,
nur weniger zeigen.
Der Sicht-Umschalter zeichnete ausserdem nur die fuenf Rollen aus
bereiche.js; wer eine sechste hat, stand nicht darin. Dieselbe Luecke
wie in der Chat-Auswahl und der Personenliste, zum dritten Mal. Die
Ueberschrift kommt jetzt vom Server (`gruppe`), der Browser zeichnet,
was ankommt -- auch eine Rolle, deren Namen er nicht kennen darf.
DREI STELLEN FOLGEN BEWUSST NICHT: die Personenliste (aus ihr wird der
Umschalter gebaut -- folgte sie der Sicht, kaeme man aus einer fremden
nicht mehr heraus), die Auswahllisten beim Anlegen (Kategorien,
Empfaenger) und steckbrief/mein (ein Formular, das fremd liest und
eigen speichert, zerstoert Daten).
KALENDER UND CHAT BLEIBEN PRIVAT, auch mit Umschalter -- Termine,
Calls und Wiederholungen lesen ab jetzt immer die eigene Person. Eine
Pruefung musste dafuer umgedreht werden: pruef-sicht verlangte bis
heute das Gegenteil ("dafuer gibt es den Umschalter"). Die Gegenprobe
bleibt dieselbe Frage, nur andersherum -- Patrick selbst MUSS seine
Termine sehen, sonst hiesse "DogFather sieht sie nicht" nur, dass sie
niemand sieht. Dabei fiel auf, dass die Managerin gar keinen Call
hatte: Die Pruefung "bleibt privat" war nicht bestanden, sondern nicht
durchfuehrbar. Antwort darauf sind Daten, keine weichere Bedingung.
Gruen: pruef-sicht 84 (vorher 53), pruef-kachel-universum 37 (neu),
pruef-rollen 282, pruef-crew-adresse 129, pruef-start-ansicht 143,
pruef-kalender 104, pruef-haus-trennung 62, pruef-team-ampel 32,
pruef-team-stufen 26, pruef-css-klassen. Stempel 202609110209.
Co-Authored-By: Claude Opus 5 <[email protected]>
603 lines
28 KiB
JavaScript
603 lines
28 KiB
JavaScript
/* =====================================================================
|
|
DER GRUND JEDER KACHEL IM HAUS VON TEAM DOGI (11.09.2026)
|
|
|
|
Filipe: "veraender die kacheln und den hintergrund, ich will dass
|
|
die hintergrunde von den kacheln immer unviersum artig ist, es muss
|
|
richtig geil sein aber immer so dass man alles noch gut erkkent.
|
|
und das IN DER GANZEN WEBSITE VON TEAM DOGI. nur die kacheln. [...]
|
|
wichtig ist die form der kacheln soll gleich bleiben."
|
|
|
|
Daraus werden vier pruefbare Saetze, und jeder einzelne kann
|
|
scheitern:
|
|
|
|
1. UEBERALL. Nicht "auf der Startseite", sondern auf jeder Kachel
|
|
des Hauses -- auch auf denen, die es heute noch nicht gibt.
|
|
Deshalb wird geprueft, dass crew-haus.css KEINE Kachelklasse
|
|
nennt: Sie setzt vier Werte um, aus denen module.css den Grund
|
|
jeder Kachel baut. Eine Datei, die stattdessen Kachel fuer
|
|
Kachel aufzaehlt, waere bei der naechsten neuen unvollstaendig
|
|
-- lautlos.
|
|
2. NUR DIE KACHELN. Der Grund der SEITE bleibt, wie er war.
|
|
3. DIE FORM BLEIBT GLEICH. Fase, Silhouette und die drei
|
|
Eckwinkel werden vor und nach dem Hauswechsel verglichen,
|
|
Zeichen fuer Zeichen.
|
|
4. MAN ERKENNT ALLES NOCH GUT. Und das ist der Teil, der wirklich
|
|
Arbeit macht.
|
|
|
|
---------------------------------------------------------------------
|
|
WIE LESBARKEIT AUF EINEM STERNENHIMMEL GEMESSEN WIRD
|
|
|
|
Ein Sternenfeld ist kein Hintergrund, sondern viele. Hinter einem
|
|
Buchstaben liegt mal fast schwarzer Grund, mal Nebel, mal -- selten
|
|
-- der Kern eines Sterns. Eine einzige Durchschnittszahl wuerde
|
|
genau den Fall verschlucken, der stoert; eine Regel auf den
|
|
einzelnen hellsten Punkt dagegen waere eine Regel GEGEN Sterne, denn
|
|
ein Stern ist per Definition der hellste Punkt seiner Umgebung.
|
|
|
|
Deshalb drei Zahlen je Textstelle, alle aus denselben echten
|
|
Bildpunkten eines Bildschirmfotos:
|
|
|
|
* MITTEL -- die Flaeche, die ein Buchstabe wirklich sieht.
|
|
Muss 4,5:1 halten. Das ist die Regel.
|
|
* ANTEIL -- wie viel Prozent der Punkte hinter dem Text unter
|
|
4,5:1 liegen. Das ist die eigentliche Messung: Sie
|
|
unterscheidet einen PUNKT von einer FLAECHE. Ein
|
|
Nebel, der zu hell wird, faellt hier sofort auf,
|
|
auch wenn der Mittelwert noch durchgeht. Grenze:
|
|
1,5 Prozent. Gemessen wurden 0,25 Prozent, im
|
|
Agenturhaus an derselben Stelle 0,115 Prozent.
|
|
* HELLSTER -- der einzelne hellste Bildpunkt, also ein Sternkern.
|
|
Grenze 3,4:1 -- bewusst unter der Regel, denn dieser
|
|
Punkt ist kleiner als ein Buchstabenstrich breit.
|
|
Ohne diese Zahl koennte jemand die Sterne beliebig
|
|
hell machen, solange nur wenige davon da sind.
|
|
|
|
Gemessen wird an ECHTEN BILDPUNKTEN, nicht an Farbangaben. Das ist
|
|
hier keine Vorsicht, sondern Notwendigkeit: Was hinter dem Text
|
|
liegt, entsteht erst beim Zeichnen aus Grund, Nebel, Raster und
|
|
Schleier. Keine Farbangabe kennt das Ergebnis.
|
|
|
|
UND DER INHALT DER KACHEL WIRD DABEI UNSICHTBAR GESCHALTET. Der
|
|
erste Anlauf hat das nicht getan und Kontraste von 1,95:1 gemeldet
|
|
-- gegen das grosse Zeichen der Kachel, das gar keinen Text
|
|
verdeckt. Gefragt ist, was HINTER dem Text liegt, und das gehoert
|
|
der Kachel, nicht ihren Kindern.
|
|
|
|
---------------------------------------------------------------------
|
|
WARUM DIE ZWEITE ADRESSE HIER NICHT IM BROWSER AUFGERUFEN WIRD
|
|
|
|
Das Haus liefert `upgrade-insecure-requests` aus. Ruft man
|
|
crew.dogfather-universe.com im Testbrowser ueber eine umgebogene
|
|
Namensaufloesung auf, macht der Browser daraus https und findet
|
|
nichts. Diese Grenze ist bekannt und wird wie bei den anderen
|
|
Haus-Pruefungen umgangen, indem die Frage in zwei geteilt wird:
|
|
|
|
a) KOMMT DIE DATEI DORT AN? Das beantwortet eine echte Anfrage mit
|
|
gesetztem Host-Kopf ueber node:http -- ohne Browser, ohne CSP.
|
|
b) WIE SIEHT SIE AUS? Das beantwortet der Browser, indem genau
|
|
dieselbe Datei nachgeladen wird, an derselben Stelle wie im
|
|
Betrieb: als letzte Stilvorlage. pruef-css-klassen sichert ab,
|
|
dass sie im Betrieb auch wirklich zuletzt steht.
|
|
|
|
Nachgeladen wird ueber ihre ADRESSE, nicht ueber ihren Dateipfad.
|
|
Das ist kein Detail: Bei einer eingefuegten Datei rechnet der
|
|
Browser relative Verweise gegen die SEITE, und aus `../img/…` wird
|
|
ein 404, den es im Betrieb nicht gibt. Zwei Minuten Fehlersuche an
|
|
einem Fehler, den die Pruefung selbst erzeugt hatte.
|
|
===================================================================== */
|
|
import { mkdtempSync, rmSync, readFileSync } 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(4391, "pruef-kachel-universum");
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-universum-"));
|
|
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-kachel-universum");
|
|
await new Promise((r) => setTimeout(r, 700));
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
/* EIGENE ZUHOERER. index.js haengt fuer den Betrieb Auffangnetze an
|
|
`uncaughtException` -- eine Pruefung, die dieselbe Datei in denselben
|
|
Prozess laedt, erbt sie und wuerde nach einem Absturz einfach
|
|
weiterhaengen, statt rot zu werden. */
|
|
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 { 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);
|
|
|
|
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", schluessel).update(code).digest("hex"), jetzt);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
|
|
anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
|
anlegen("Rieke", "hand", "CODE-HAND-0001");
|
|
anlegen("Frida", "modi", "CODE-MODI-0001");
|
|
anlegen("Tomma", "modi", "CODE-MODI-0002");
|
|
/* Die Teamseite zeichnet ihre Personenkarten aus dem, was das jeweilige
|
|
Haus zu sehen bekommt. Auf DIESER Adresse sind das Manager und
|
|
Scouts -- ohne sie bliebe `.tperson` leer, und die Messung der
|
|
Personenkarte waere ein uebersprungener Test, der nichts beweist.
|
|
Die Kachel ist in beiden Haeusern dieselbe; gemessen wird ihr Grund,
|
|
nicht wessen Name darin steht. */
|
|
anlegen("Mara", "manager", "CODE-MANA-0001");
|
|
anlegen("Sten", "scout", "CODE-SCOU-0001");
|
|
d.close();
|
|
|
|
const CREW_CSS_ADRESSE = "/workspace/assets/css/crew-haus.css";
|
|
const CREW_CSS = readFileSync(
|
|
new URL("../workspace/assets/css/crew-haus.css", import.meta.url).pathname
|
|
.replace(/^\/([A-Za-z]:)/, "$1"), "utf8");
|
|
|
|
/* =======================================================================
|
|
1. DIE DATEI KOMMT AUF DER ZWEITEN ADRESSE AN -- UND NUR DORT
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 1. Ausgeliefert wird sie da, wo sie hingehoert ===");
|
|
|
|
function holen(host) {
|
|
return new Promise((fertig, schief) => {
|
|
const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: "/workspace/assets/css/haus.css",
|
|
method: "GET", headers: { Host: host } }, (antwort) => {
|
|
let text = "";
|
|
antwort.on("data", (s) => { text += s; });
|
|
antwort.on("end", () => fertig({ code: antwort.statusCode, text }));
|
|
});
|
|
a.on("error", schief);
|
|
a.end();
|
|
});
|
|
}
|
|
{
|
|
const crew = await holen("crew.dogfather-universe.com");
|
|
const agentur = await holen("workspace.dogfather-universe.com");
|
|
ok(crew.code === 200 && crew.text.includes("--sternenfeld:"),
|
|
`die zweite Wand bekommt das Sternenfeld (${crew.code}, ${crew.text.length} Zeichen)`);
|
|
ok(crew.text.includes("--modul-schleier: .30"),
|
|
"und den Schleier, der dazugehoert");
|
|
ok(agentur.code === 200 && !agentur.text.includes("--sternenfeld"),
|
|
`die Agenturadresse bekommt es NICHT (${agentur.text.length} Zeichen)`);
|
|
ok(crew.text.length > agentur.text.length + 500,
|
|
"die beiden Haeuser liefern wirklich verschiedene Dateien aus");
|
|
}
|
|
|
|
/* KEINE ACHTE ABSCHRIFT DER KACHELLISTE.
|
|
Der eigentliche Gedanke hinter dem ganzen Umbau: crew-haus.css darf
|
|
keine Kachel beim Namen nennen. Sonst waere sie beim naechsten neuen
|
|
Bauteil unvollstaendig, ohne dass es auffiele. */
|
|
{
|
|
const kachelklassen = [".eintrag-karte", ".t-kachel", ".tperson", ".liste-kachel",
|
|
".steckbrief", ".zahlkarte", ".k-anlasskachel", ".leer-hinweis"];
|
|
const gefunden = kachelklassen.filter((k) => CREW_CSS.includes(k));
|
|
ok(gefunden.length === 0,
|
|
`crew-haus.css zaehlt keine Kachelklassen auf (${gefunden.join(" ") || "keine"})`);
|
|
ok(CREW_CSS.includes("--sternenfeld:") && CREW_CSS.includes("--sternenfeld-mass:")
|
|
&& CREW_CSS.includes("--sternenfeld-lage:"),
|
|
"sie setzt stattdessen die Werte, aus denen module.css den Grund baut");
|
|
|
|
const modul = readFileSync(
|
|
new URL("../workspace/assets/css/module.css", import.meta.url).pathname
|
|
.replace(/^\/([A-Za-z]:)/, "$1"), "utf8");
|
|
ok((modul.match(/var\(--sternenfeld,/g) || []).length === 1,
|
|
"und module.css benutzt das Bild an genau EINER Stelle");
|
|
ok(modul.includes("--sternenfeld: none;"),
|
|
"im Agenturhaus steht dort `none` -- eine Ebene ohne Bild zeichnet nichts");
|
|
}
|
|
|
|
/* KEIN KARO. Wiederholen sich zwei Sternebenen im selben Abstand, sieht
|
|
man sofort ein Muster statt eines Feldes. Die Abstaende duerfen
|
|
deshalb keinen gemeinsamen Teiler haben -- das ist keine Kosmetik,
|
|
sondern der Unterschied zwischen Himmel und Tapete. */
|
|
{
|
|
const zeile = CREW_CSS.split("--sternenfeld-mass:")[1].split(";")[0];
|
|
const zahlen = (zeile.match(/(\d+)px/g) || []).map((z) => parseInt(z, 10));
|
|
const ggt = (a, b) => (b === 0 ? a : ggt(b, a % b));
|
|
let groesster = 0;
|
|
for (let i = 0; i < zahlen.length; i++)
|
|
for (let j = i + 1; j < zahlen.length; j++)
|
|
groesster = Math.max(groesster, ggt(zahlen[i], zahlen[j]));
|
|
ok(zahlen.length >= 6, `sechs Abstaende fuer drei Sternebenen (${zahlen.join(" ")})`);
|
|
ok(groesster === 1, `kein gemeinsamer Teiler, also kein Karo (groesster: ${groesster})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
2. DER BROWSER: erst das Agenturhaus, dann dasselbe mit dem Himmel
|
|
======================================================================= */
|
|
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await pw.chromium.launch();
|
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 1100 } });
|
|
const seite = await ctx.newPage();
|
|
const konsole = [];
|
|
seite.on("pageerror", (f) => konsole.push("Absturz: " + f.message));
|
|
seite.on("console", (m) => { if (m.type() === "error") konsole.push("Konsole: " + m.text()); });
|
|
seite.on("response", (a) => { if (a.status() >= 400) konsole.push(`${a.status()} ${a.url()}`); });
|
|
|
|
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: 25000 });
|
|
await seite.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" });
|
|
await seite.waitForSelector(".kachel", { timeout: 10000 });
|
|
await seite.waitForTimeout(600);
|
|
|
|
/* Alles, was sich beim Hauswechsel NICHT aendern darf, wird vorher
|
|
abgeholt -- Form, Silhouette, Eckwinkel, Seitengrund. */
|
|
async function formAufnehmen() {
|
|
return seite.evaluate(() => {
|
|
const k = document.querySelector(".kachel");
|
|
const s = getComputedStyle(k);
|
|
const n = getComputedStyle(k, "::after");
|
|
const v = getComputedStyle(document.documentElement);
|
|
/* Ein Hintergrund ist eine Liste, und in jeder Ebene stehen selbst
|
|
Kommas. Mit einem Muster kommt man da nicht weit -- Chromium
|
|
schreibt die Verlaeufe voll aus, Klammern in Klammern. Gezaehlt
|
|
wird deshalb, wie tief man gerade steht. */
|
|
let tiefe = 0, ebenen = 1;
|
|
for (const z of n.backgroundImage) {
|
|
if (z === "(") tiefe++;
|
|
else if (z === ")") tiefe--;
|
|
else if (z === "," && tiefe === 0) ebenen++;
|
|
}
|
|
return {
|
|
fase: v.getPropertyValue("--fase").trim(),
|
|
silhouette: s.clipPath,
|
|
/* Die sechs Eckwinkel sind die ersten sechs Ebenen. Ihre Groessen
|
|
und Lagen SIND die Form. */
|
|
winkelMass: n.backgroundSize.split(",").slice(0, 6).join(",").trim(),
|
|
winkelLage: n.backgroundPosition.split(",").slice(0, 6).join(",").trim(),
|
|
ebenen,
|
|
seitengrund: getComputedStyle(document.body).backgroundColor,
|
|
sternenfeld: v.getPropertyValue("--sternenfeld").trim(),
|
|
schleier: v.getPropertyValue("--modul-schleier").trim(),
|
|
hausname: v.getPropertyValue("--haus-name").trim(),
|
|
};
|
|
});
|
|
}
|
|
const vorher = await formAufnehmen();
|
|
|
|
melde("");
|
|
melde("=== 2. Das Agenturhaus bleibt unberuehrt ===");
|
|
ok(vorher.sternenfeld === "none",
|
|
`dort steht kein Sternenfeld (${vorher.sternenfeld})`);
|
|
ok(vorher.schleier === ".26",
|
|
`und der Schleier steht unveraendert auf .26 (${vorher.schleier})`);
|
|
ok(vorher.ebenen === 9,
|
|
`neun Ebenen: sechs Eckwinkel, zwei Rasterlinien, eine leere (${vorher.ebenen})`);
|
|
|
|
/* Bildpunkte aus einem Bildschirmfoto -- derselbe Weg wie in
|
|
pruef-lesbarkeit und pruef-personen-kachel. Zurueck kommen der
|
|
Mittelwert und die SORTIERTE Helligkeitsliste; wer daraus einen
|
|
Anteil rechnen will, braucht die ganze Liste, nicht eine Kennzahl. */
|
|
async function punkte(clip) {
|
|
const bild = await seite.screenshot({ clip });
|
|
return seite.evaluate(async (b64) => {
|
|
const bild = new Image();
|
|
await new Promise((r) => { bild.onload = r; bild.src = "data:image/png;base64," + b64; });
|
|
const c = document.createElement("canvas");
|
|
c.width = bild.width; c.height = bild.height;
|
|
const g = c.getContext("2d", { willReadFrequently: true });
|
|
g.drawImage(bild, 0, 0);
|
|
const dd = g.getImageData(0, 0, c.width, c.height).data;
|
|
const hell = (p) => {
|
|
const f = p.map((x) => { const s = x / 255; return s <= .03928 ? s / 12.92 : Math.pow((s + .055) / 1.055, 2.4); });
|
|
return .2126 * f[0] + .7152 * f[1] + .0722 * f[2];
|
|
};
|
|
const liste = [];
|
|
let sr = 0, sg = 0, sb = 0, hr = 0, hg = 0, hb = 0, best = -1;
|
|
for (let i = 0; i < dd.length; i += 4) {
|
|
sr += dd[i]; sg += dd[i + 1]; sb += dd[i + 2];
|
|
const h = hell([dd[i], dd[i + 1], dd[i + 2]]);
|
|
liste.push(h);
|
|
if (h > best) { best = h; hr = dd[i]; hg = dd[i + 1]; hb = dd[i + 2]; }
|
|
}
|
|
const n = liste.length;
|
|
liste.sort((a, b) => a - b);
|
|
return {
|
|
mittel: [Math.round(sr / n), Math.round(sg / n), Math.round(sb / n)],
|
|
hellsterPunkt: [hr, hg, hb],
|
|
hell: liste, anzahl: n,
|
|
};
|
|
}, bild.toString("base64"));
|
|
}
|
|
|
|
/* WAS HINTER DEM TEXT LIEGT, IST NICHT DER TEXT -- UND AUCH NICHT SEIN
|
|
NACHBAR. Fuer die Dauer der Messung wird der INHALT der Kachel
|
|
unsichtbar, die Kachel selbst nicht: Hintergrund, Kantenlicht,
|
|
Raster, Eckwinkel und Sternenfeld gehoeren ihr, nicht ihren Kindern. */
|
|
const KARTEN = ".kachel, .t-kachel, .tperson, .tl, .eintrag-karte, .person, .liste-kachel";
|
|
async function inhaltAus(waehler) {
|
|
await seite.evaluate(([w, karten]) => {
|
|
const e = document.querySelector(w);
|
|
const karte = e && (e.closest(karten) || e.parentElement);
|
|
if (!karte) return;
|
|
karte.dataset.grundmessung = "ja";
|
|
karte.querySelectorAll("*").forEach((x) => {
|
|
x.dataset.warSichtbar = x.style.visibility || "";
|
|
x.style.visibility = "hidden";
|
|
});
|
|
}, [waehler, KARTEN]);
|
|
}
|
|
async function inhaltAn() {
|
|
await seite.evaluate(() => {
|
|
const karte = document.querySelector('[data-grundmessung="ja"]');
|
|
if (!karte) return;
|
|
karte.querySelectorAll("*").forEach((x) => {
|
|
x.style.visibility = x.dataset.warSichtbar || "";
|
|
delete x.dataset.warSichtbar;
|
|
});
|
|
delete karte.dataset.grundmessung;
|
|
});
|
|
}
|
|
|
|
/* DER AUSSCHNITT LIEGT IN DER MITTE DER KACHEL. Am Rand sitzt das
|
|
Kantenlicht -- eine 1,6 px schmale Linie in fast vollem Kachelton,
|
|
und damit der hellste Punkt der ganzen Kachel. Wer den mitmisst,
|
|
misst die Form statt des Himmels: Der erste Anlauf meldete deshalb
|
|
"keine neuen hellen Punkte", obwohl das Sternenfeld laengst da war. */
|
|
const kachelRahmen = await seite.evaluate(() => {
|
|
const k = document.querySelector(".kachel");
|
|
k.scrollIntoView({ block: "center" });
|
|
const r = k.getBoundingClientRect();
|
|
return { x: Math.round(r.x + r.width * .22), y: Math.round(r.y + r.height * .22),
|
|
width: Math.round(r.width * .56), height: Math.round(r.height * .56) };
|
|
});
|
|
await inhaltAus(".kachel");
|
|
const grundAgentur = await punkte(kachelRahmen);
|
|
await inhaltAn();
|
|
|
|
/* ---- Jetzt dasselbe Fenster, aber im Haus von Team Dogi ---- */
|
|
await seite.addStyleTag({ url: CREW_CSS_ADRESSE });
|
|
await seite.waitForTimeout(400);
|
|
const nachher = await formAufnehmen();
|
|
|
|
melde("");
|
|
melde("=== 3. Die Form bleibt gleich, der Grund nicht ===");
|
|
ok(nachher.hausname === '"crew"',
|
|
`das Haus von Team Dogi ist angekommen (${nachher.hausname})`);
|
|
ok(nachher.fase === vorher.fase, `die Fase ist unveraendert (${nachher.fase})`);
|
|
ok(nachher.silhouette === vorher.silhouette, "die Silhouette der Kachel ist Zeichen fuer Zeichen dieselbe");
|
|
ok(nachher.winkelMass === vorher.winkelMass, "die drei Eckwinkel haben dieselbe Groesse");
|
|
ok(nachher.winkelLage === vorher.winkelLage, "und dieselbe Lage");
|
|
ok(nachher.ebenen === 13,
|
|
`aus der leeren Ebene sind fuenf geworden: drei Sternebenen, zwei Nebel (${nachher.ebenen})`);
|
|
ok(nachher.schleier === ".30", `der Schleier laesst etwas mehr durch (${nachher.schleier})`);
|
|
ok(nachher.sternenfeld.includes("radial-gradient"),
|
|
"und das Sternenfeld steht als Bild da, nicht als none");
|
|
|
|
await inhaltAus(".kachel");
|
|
const grundCrew = await punkte(kachelRahmen);
|
|
await inhaltAn();
|
|
{
|
|
const abstand = Math.max(...grundCrew.mittel.map((x, i) => Math.abs(x - grundAgentur.mittel[i])));
|
|
ok(abstand >= 4,
|
|
`der Kachelgrund ist messbar ein anderer geworden (${grundAgentur.mittel} -> ${grundCrew.mittel})`);
|
|
ok(grundCrew.hell[grundCrew.anzahl - 1] > grundAgentur.hell[grundAgentur.anzahl - 1] * 1.5,
|
|
`und es gibt jetzt helle Punkte, die es vorher nicht gab (${grundAgentur.hellsterPunkt} -> ${grundCrew.hellsterPunkt})`);
|
|
}
|
|
|
|
/* GEGENPROBE ZUM MESSGERAET. Nimmt man das Sternenfeld wieder weg, muss
|
|
der Grund zurueckspringen. Ohne diese Probe koennte die Messung oben
|
|
auch etwas ganz anderes gesehen haben -- die Farben haben sich beim
|
|
Hauswechsel ja ohnehin geaendert. */
|
|
{
|
|
await seite.addStyleTag({ content: ":root { --sternenfeld: none; }" });
|
|
await seite.waitForTimeout(250);
|
|
await inhaltAus(".kachel");
|
|
const ohne = await punkte(kachelRahmen);
|
|
await inhaltAn();
|
|
ok(ohne.hell[ohne.anzahl - 1] < grundCrew.hell[grundCrew.anzahl - 1] * .8,
|
|
`ohne Sternenfeld verschwinden die hellen Punkte wieder (${grundCrew.hellsterPunkt} -> ${ohne.hellsterPunkt})`);
|
|
await seite.evaluate(() => {
|
|
const letzte = [...document.querySelectorAll("style")].pop();
|
|
if (letzte && letzte.textContent.includes("--sternenfeld: none")) letzte.remove();
|
|
});
|
|
await seite.waitForTimeout(250);
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. NUR DIE KACHELN -- der Grund der Seite bleibt
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 4. Nur die Kacheln ===");
|
|
{
|
|
const zwischenraum = await seite.evaluate(() => {
|
|
/* Ein Fleck ZWISCHEN zwei Kacheln. Er gehoert der Seite, nicht
|
|
einer Kachel -- dort darf kein Stern stehen. */
|
|
const kacheln = [...document.querySelectorAll(".kachel")];
|
|
if (kacheln.length < 2) return null;
|
|
const a = kacheln[0].getBoundingClientRect();
|
|
const b = kacheln[1].getBoundingClientRect();
|
|
if (b.x - (a.x + a.width) < 6) return null;
|
|
return { x: Math.round(a.x + a.width) + 1, y: Math.round(a.y) + 20,
|
|
width: Math.max(2, Math.round(b.x - (a.x + a.width)) - 2), height: 40 };
|
|
});
|
|
ok(zwischenraum !== null, "zwischen zwei Kacheln ist Platz zum Messen");
|
|
if (zwischenraum) {
|
|
const p = await punkte(zwischenraum);
|
|
const spanne = Math.max(...p.hellsterPunkt.map((x, i) => x - p.mittel[i]));
|
|
ok(spanne <= 12,
|
|
`der Grund zwischen den Kacheln ist ruhig, dort steht kein Stern (Spanne ${spanne})`);
|
|
}
|
|
}
|
|
|
|
/* =======================================================================
|
|
5. MAN ERKENNT ALLES NOCH GUT
|
|
======================================================================= */
|
|
function kontrast(a, lb) {
|
|
const l = (p) => {
|
|
const f = p.map((x) => { const s = x / 255; return s <= .03928 ? s / 12.92 : Math.pow((s + .055) / 1.055, 2.4); });
|
|
return .2126 * f[0] + .7152 * f[1] + .0722 * f[2];
|
|
};
|
|
const x = typeof a === "number" ? a : l(a);
|
|
return (Math.max(x, lb) + .05) / (Math.min(x, lb) + .05);
|
|
}
|
|
function leuchtkraft(p) {
|
|
const f = p.map((x) => { const s = x / 255; return s <= .03928 ? s / 12.92 : Math.pow((s + .055) / 1.055, 2.4); });
|
|
return .2126 * f[0] + .7152 * f[1] + .0722 * f[2];
|
|
}
|
|
|
|
async function messen(waehler) {
|
|
const stelle = await seite.evaluate((w) => {
|
|
const e = document.querySelector(w);
|
|
if (!e) return null;
|
|
e.scrollIntoView({ block: "center" });
|
|
const r = e.getBoundingClientRect();
|
|
if (r.width < 8 || r.height < 6) return null;
|
|
/* Die Schriftfarbe wird GEZEICHNET, nicht gelesen: color-mix()
|
|
liefert getComputedStyle als "color(srgb …)", und daraus Ziffern
|
|
zu klauben ergibt Unsinn. Ein Canvas versteht jede Schreibweise. */
|
|
const c = document.createElement("canvas");
|
|
c.width = 1; c.height = 1;
|
|
const g = c.getContext("2d", { willReadFrequently: true });
|
|
g.fillStyle = "#000000";
|
|
g.fillStyle = getComputedStyle(e).color;
|
|
g.fillRect(0, 0, 1, 1);
|
|
const px = g.getImageData(0, 0, 1, 1).data;
|
|
e.dataset.messen = "ja";
|
|
e.style.color = "transparent";
|
|
return { x: Math.round(r.x), y: Math.round(r.y),
|
|
w: Math.max(8, Math.round(r.width)), h: Math.round(Math.min(r.height, 40)),
|
|
farbe: [px[0], px[1], px[2]] };
|
|
}, waehler);
|
|
if (!stelle) return null;
|
|
await inhaltAus(waehler);
|
|
const hinten = await punkte({ x: stelle.x, y: stelle.y, width: stelle.w, height: stelle.h });
|
|
await inhaltAn();
|
|
await seite.evaluate(() => {
|
|
const e = document.querySelector('[data-messen="ja"]');
|
|
if (e) { e.style.color = ""; delete e.dataset.messen; }
|
|
});
|
|
const lt = leuchtkraft(stelle.farbe);
|
|
const unter = hinten.hell.filter((h) => kontrast(lt, h) < 4.5).length;
|
|
return {
|
|
farbe: stelle.farbe,
|
|
mittel: kontrast(lt, leuchtkraft(hinten.mittel)),
|
|
hellster: kontrast(lt, hinten.hell[hinten.anzahl - 1]),
|
|
anteil: 100 * unter / hinten.anzahl,
|
|
punkte: hinten.anzahl,
|
|
};
|
|
}
|
|
|
|
async function lesbar(name, waehler) {
|
|
const m = await messen(waehler);
|
|
if (m === null) { ok(false, `${name}: nicht gefunden (${waehler})`); return; }
|
|
ok(m.punkte > 200 && m.mittel >= 4.5 && m.anteil <= 1.5 && m.hellster >= 3.4,
|
|
`${name}: Mittel ${m.mittel.toFixed(2)} · unter 4,5 liegen ${m.anteil.toFixed(3)} % · hellster Punkt ${m.hellster.toFixed(2)} (${m.punkte} Bildpunkte)`);
|
|
}
|
|
|
|
melde("");
|
|
melde("=== 5. Lesbarkeit auf der Startseite ===");
|
|
for (const [name, w] of [
|
|
["Kachelname", ".kachel .kachel__name"],
|
|
["Kachelzeile", ".kachel .kachel__unter"],
|
|
["kleine Kachel, Name", '.kachel:not([data-gross="ja"]) .kachel__name'],
|
|
["kleine Kachel, Zeile", '.kachel:not([data-gross="ja"]) .kachel__unter'],
|
|
]) await lesbar(name, w);
|
|
|
|
/* GEGENPROBE: ein absichtlich zu dunkler Text MUSS durchfallen. Ohne
|
|
sie waere nicht bewiesen, dass diese Messung ueberhaupt "nicht in
|
|
Ordnung" sagen kann. */
|
|
{
|
|
await seite.evaluate(() => {
|
|
document.querySelector(".kachel .kachel__name").style.color = "#171326";
|
|
});
|
|
const schlecht = await messen(".kachel .kachel__name");
|
|
ok(schlecht !== null && schlecht.mittel < 4.5 && schlecht.anteil > 50,
|
|
`absichtlich zu dunkler Text faellt durch (${schlecht?.mittel.toFixed(2)}:1, ${schlecht?.anteil.toFixed(1)} % unter der Grenze)`);
|
|
await seite.evaluate(() => {
|
|
document.querySelector(".kachel .kachel__name").style.color = "";
|
|
});
|
|
}
|
|
|
|
/* GEGENPROBE ZUM ANTEIL: ein zu heller Nebel MUSS auffallen, auch wenn
|
|
der Mittelwert noch durchgeht. Das ist der Fall, fuer den diese Zahl
|
|
ueberhaupt gebaut wurde. */
|
|
{
|
|
await seite.addStyleTag({ content: ":root { --sternenfeld:"
|
|
+ " radial-gradient(ellipse 90% 90% at 50% 50%, rgba(255,255,255,.95) 0%, transparent 85%);"
|
|
+ " --sternenfeld-mass: 100% 100%; --sternenfeld-lage: 0 0; }" });
|
|
await seite.waitForTimeout(300);
|
|
const grell = await messen(".kachel .kachel__unter");
|
|
ok(grell !== null && grell.anteil > 1.5,
|
|
`ein zu heller Nebel faellt durch (${grell?.anteil.toFixed(1)} % unter der Grenze)`);
|
|
await seite.evaluate(() => {
|
|
const letzte = [...document.querySelectorAll("style")].pop();
|
|
if (letzte && letzte.textContent.includes("ellipse 90% 90%")) letzte.remove();
|
|
});
|
|
await seite.waitForTimeout(300);
|
|
}
|
|
|
|
melde("");
|
|
melde("=== 6. Lesbarkeit auf der Teamseite ===");
|
|
await seite.goto(BASIS + "/workspace/team.html", { waitUntil: "networkidle" });
|
|
await seite.addStyleTag({ url: CREW_CSS_ADRESSE });
|
|
await seite.waitForSelector(".t-kachel", { timeout: 15000 });
|
|
await seite.waitForTimeout(900);
|
|
/* Zugeklappte Abschnitte messen nichts -- erst aufklappen, dann messen. */
|
|
await seite.evaluate(() => {
|
|
document.querySelectorAll("details:not([open])").forEach((x) => { x.open = true; });
|
|
});
|
|
await seite.waitForTimeout(500);
|
|
{
|
|
const da = await seite.evaluate(() => ({
|
|
kachel: !!document.querySelector(".t-kachel"),
|
|
person: !!document.querySelector(".tperson"),
|
|
titel: !!document.querySelector(".t-kopf__titel"),
|
|
haus: getComputedStyle(document.documentElement).getPropertyValue("--haus-name").trim(),
|
|
}));
|
|
ok(da.kachel && da.titel && da.person && da.haus === '"crew"',
|
|
`die Teamseite ist gezeichnet und traegt das richtige Haus (${JSON.stringify(da)})`);
|
|
}
|
|
for (const [name, w] of [
|
|
["Abschnittstitel", ".t-kachel .t-kopf__titel"],
|
|
["Personenname", ".tperson .tperson__name"],
|
|
["Personenzeile (leisester Text im Haus)", ".tperson .tperson__unter"],
|
|
]) await lesbar(name, w);
|
|
|
|
melde("");
|
|
melde("=== 7. Nichts abgestuerzt ===");
|
|
ok(konsole.length === 0, `keine Fehler im Browser (${konsole.slice(0, 3).join(" | ") || "keine"})`);
|
|
|
|
await ctx.close();
|
|
await browser.close();
|
|
/* Der Server haelt die Datenbankdatei offen -- unter Windows laesst sie
|
|
sich dann nicht loeschen. Das ist kein Befund, sondern ein
|
|
Aufraeumrest im Temp-Ordner, und darf die Pruefung nicht rot machen. */
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
|
|
console.log("");
|
|
console.log(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
console.log(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG");
|
|
process.exit(fehler === 0 ? 0 : 1);
|