Filipe: "eine community rolle und community kategorie, wo die community auch dann nur die sieht" · "wieso nur eine kachel — ich will mehrere, mit mehreren optionen" · "egal welche rolle hinzugefügt wird soll immer nur das sehen was ich erlaube". DIE DRITTE ADRESSE. treff.dogfather-universe.com bekommt eine eigene Weiche (treff-adresse.js), eine eigene Zugangswand, ein eigenes Manifest und acht eigene Bühnenbilder. Ohne sie stünde dort die Wand der Agentur: Spicy-Logo, "Creator Workspace", fünf Rollenkacheln mit den Namen Admin, Manager, Scout, Creator — die komplette Struktur des Unternehmens, vor der Anmeldung, für jeden mit der Adresse. Welche Wand zu welcher Tür gehört, steht jetzt in EINER Tafel (istFremdeWand) statt als Sonderfall im Code. Bei zwei Wänden war ein Sonderfall richtig; bei drei wären es drei geworden, bei vier sechs. SIEBEN BRETTER UND EIN ACHTES NUR FÜRS TEAM. Anschlagbrett, Was ansteht, Der Treff, Wunschliste, Highlights, Regeln & Hilfe, Mitmachen — dazu "Meldungen & Maßnahmen", das ausdrücklich NICHT in TREFF_BEREICHE steht: dort wäre es lautlos bei der Community gelandet und hätte ausgesehen wie die anderen sieben. WAS AUF DEN BRETTERN PASSIERT: "Will ich auch" (eine Stimme je Person, erzwungen durch den Schlüssel, nicht durch eine Prüfung) · Anheften, höchstens drei, gezählt IN der Transaktion · Freigabe vor Sichtbarkeit für Termine und Highlights, wobei Abwesenheit der Ruhezustand ist · Melden mit Pflicht-Grund (DSA Art. 16) · Entfernen mit Grund, der den Beitrag ÜBERLEBT (DSA Art. 17) · drei Stufen, gerechnet statt gespeichert · die Bannleiter: Modi bis 7 Tage, dauerhaft nur DogFather (seine Entscheidung) · "Mitmachen" wird zum Talent, genau einmal. Der Ausschluss wirkt in sitzungLesen() — also nicht nur beim Schreiben. Wer ausgeschlossen ist, ist weg, nicht stumm. ZWEI SEITEN MEHR. treff-regeln.html holt seine ZAHLEN vom Server (Mindestalter 18, Fristen, Anschläge) — ein Regeltext, der eine andere Zahl nennt als die Regel, ist schlimmer als keiner. rechte.html rechnet "Wer sieht was" aus derselben Tafel aus, aus der die Schranke ihre Entscheidung holt. Die Community steht dort bei 3 von 23. SECHS BEFUNDE, KEINEN HAT DAS LESEN GEFUNDEN: · Ein Modi kam über den verborgenen Zugang in den Treff — die Bedingung war durch Ausschluss formuliert und nahm die neue Wand automatisch mit · /api/personen gab einem Mitglied Namen und Rolle von DogFather · Ein DELETE mit Körper wird von Nodes HTTP-Parser mit einem leeren 400 abgewiesen, bevor Express ihn sieht (gemessen). Der Grund reist jetzt in der Adresse · Der Kommentar, der erklärt, warum die Zugangswand keine Rollennamen nennt, nannte selbst einen — in einer ausgelieferten Datei · Zwei Listen für "welche Seiten sind ohne Anmeldung offen". Sie stimmten, weil beide am selben Tag entstanden · Ein Portkonflikt, den ich selbst gebaut hatte (4391 gehört pruef-chat-aufloesen) Dazu zwei feste Zahlen abgeschafft (die 20 in pruef-rechtetafel, die Kachelzahl in pruef-treff) und zwei Werkzeuge, die jetzt SUCHEN statt aufzuzählen: Der Versionsstempel findet seine Manifeste selbst — das dritte hätte sonst keinen bekommen, und die neue App hätte bis zu vier Stunden lang das falsche Zeichen getragen. Das Bildwerkzeug baut beide Zugangswände aus einer Vorlage mit zwei Werten. Zwei neue Kacheltöne, gemessen statt gewählt: #2f2f7f (Abstand 31,4 in Lab) und #7c2771 (29,7). Das schwächste Paar der vorhandenen 34 liegt bei 24,9. Geprüft: treff 69 · treff-werkzeuge 60 · rollen 312 · start-ansicht 147 · crew-adresse 129 · sicht 84 · modi-verborgen 80 · haus-trennung 62 · bereiche-lesend 37 · css-klassen 30 · zwischenspeicher 21 · alle-wege 19 · rechtetafel 19 — alles grün. 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(4401, "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);
|