DER BEFUND
Im naechtlichen Lauf standen dreizehn Pruefungen mit derselben Zeile
rot:
[uncaughtException] Error: spawnSync openssl ENOENT
Kein Programmfehler. Die Pruefungen brauchen einen https-Vorbau (der
Sitzungskeks des Hauses ist `secure` und kommt ueber http nicht an),
und dafuer erzeugen sie ein Wegwerf-Zertifikat mit openssl.
GEMESSEN, NICHT VERMUTET
openssl liegt hier: C:\Program Files\Git\mingw64\bin\openssl.exe
C:\Program Files\Git\usr\bin\openssl.exe
im System-PATH steht: C:\Program Files\Git\cmd (nur git.exe)
im Benutzer-PATH: nichts mit Git
Wer aus Git Bash startet, erbt die mingw-Pfade und merkt nie etwas.
Die Aufgabenplanung startet `node.exe` DIREKT, ohne Shell -- und
bekommt sie nicht. Bei mir gruen, nachts rot, und der Grund steht
nicht im Code.
Das Schlimmste daran ist nicht der Ausfall: Dreizehn dauerhaft rote
Zeilen in einer Notiz, die Filipe morgens liest, gewoehnen einem das
Hinsehen ab -- und decken dabei die echten Befunde zu.
DIE REPARATUR
`server/helfer-openssl.mjs` sucht openssl erst im PATH (ohne `where`
oder `which` -- beides sind selbst Programme und koennen genauso
fehlen), danach an den Orten, an denen es auf einem Windows-Rechner
mit Git wirklich liegt.
Dazu `zertifikatBauen(schluessel, zertifikat, host)`. Die acht
Aufrufformen im Haus waren identisch bis auf die Variablennamen
(schl/zert, schluesselDatei/zertDatei, zKey/zCrt, sk/zt, key/crt) --
eine Stelle traegt alle neunundzwanzig. Zwei Tage zuvor war eine
davon um ein `-addext` aermer als die anderen; gemerkt hat es
niemand, weil der Browser den fehlenden Alternativnamen erst bei
einer Weiterleitung anmahnt. Eine Stelle kann nicht von sich selbst
abweichen.
KEIN EINTRAG IN DEN SYSTEM-PATH. `mingw64\bin` enthaelt rund hundert
Programme mit Unix-Namen (`find`, `sort`, `link`), die gleichnamige
Windows-Befehle verdecken. Das fuer eine Pruefung zu aendern haette
an ganz anderen Stellen Fehler verursacht, die niemand hierher
zurueckverfolgt.
GEGENPROBE IN BEIDE RICHTUNGEN, mit dem PATH des Nachtlaufs:
vorher (direkter Aufruf): ABSTURZ: spawnSync openssl ENOENT
nachher (ueber den Helfer): Zertifikat gebaut, 1236 Bytes
Und eine echte Pruefung unter denselben Bedingungen:
PATH ohne mingw64 -> pruef-chat-neu-stelle
63 Pruefungen, 0 Fehler, 0 Abstuerze
Genau diese Datei stand im Nachtlauf mit ENOENT rot.
EINE WACHE DAGEGEN
pruef-struktur prueft ab jetzt, dass niemand openssl wieder direkt
ruft -- der naechste merkt es dort und nicht erst in einem
Nachtlauf, den niemand liest. Drei Gegenproben.
Beim ersten Anlauf schlug sie auf ihre EIGENEN Probetexte an: Sie
liest alle Serverdateien, und dazu gehoert sie selbst. Die Texte
werden jetzt zusammengesetzt. Derselbe Selbsttreffer ist mir heute
schon zweimal passiert.
DER DRITTE AUSGANG BLEIBT, WO ER HINGEHOERT
`helfer-kachel-echtfarbe.mjs` faengt den Fehler weiterhin ab und
meldet `moeglich: false` mit Grund, statt abzubrechen. Eine
Sicherung abzuschaffen, weil ihr Anlass gerade behoben ist, ist der
Anfang des naechsten stillen Fehlschlags.
NEBENBEI: pruef-community-sicht
Sie war rot mit "ZU VIEL: reaktion.html" -- mein eigener Rueckstand
vom 28.09. Die Reaction steht als Kachel im Community-Bereich; die
Liste war nicht nachgezogen. Jetzt 10 / 0.
Die Liste bleibt bewusst von Hand gepflegt: Sie ist eine ABSICHT,
keine Ableitung. Aus rechte.js gelesen verglichen sich zwei Kopien
derselben Quelle -- immer gruen, nie ein Beweis.
GEPRUEFT
pruef-struktur ALLES IN ORDNUNG (mit der neuen Wache)
pruef-community-sicht 10 / 0
pruef-chat-neu-stelle 63 / 0 (mit dem PATH des Nachtlaufs)
pruef-notizen, -teamlage-karten, -werdegang: EXIT 0, keine Abstuerze
mess-quer EXIT 0
29 Dateien: node --check auf allen, kein direkter Aufruf mehr
311 lines
14 KiB
JavaScript
311 lines
14 KiB
JavaScript
/* =====================================================================
|
|
helfer-kachel-echtfarbe.mjs — welche Farbe eine Kachel WIRKLICH hat
|
|
---------------------------------------------------------------------
|
|
Filipe am 22.09.2026: „ich seh immer wieder sehr viele die einfach
|
|
komplett die gleiche farbe haben ich will dass sie wirklich alle
|
|
alle alle komplett unterschiedlich sind."
|
|
|
|
Er hatte recht, und `pruef-kachelfarben` war trotzdem grün. Der
|
|
Grund war ein Messfehler: Sie las `--ton` aus `start.css` — die
|
|
ROHFARBE. Die lag zwischen allen 31 Kacheln weit auseinander
|
|
(0,0978 in OKLab). Nur steht sie so nie auf dem Bildschirm: Die
|
|
Kachel zeichnet sie als Verlauf unter einem Sternenfeld, einer
|
|
Vignette und (bis zu diesem Tag) einer fest blauen Wolke.
|
|
|
|
Gemessen kamen 14,4 Prozent an. Acht Paare waren für ein Auge
|
|
nicht mehr zu unterscheiden.
|
|
|
|
WARUM DER DURCHSCHNITT ÜBER DIE GANZE KACHEL
|
|
Eine Einzelmessung an einem Punkt träfe vielleicht einen Stern,
|
|
einen Rand oder das Zeichen. Gemittelt wird über alle Bildpunkte —
|
|
das ist ungefähr das, was ein Auge aus zwei Metern Entfernung
|
|
sieht, und genau darum geht es.
|
|
|
|
WARUM DAS HIER STEHT UND NICHT ZWEIMAL
|
|
`tools/kachel-echtfarbe.mjs` zeigt die Liste zum Ansehen,
|
|
`server/pruef-kachelfarben.mjs` prüft sie. Zwei Fassungen derselben
|
|
Messung altern unterschiedlich — dann sagt die eine „gleich" und
|
|
die andere „verschieden", und man glaubt der bequemeren.
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { spawn, execFileSync } from "node:child_process";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
import { scryptSync, randomBytes } from "node:crypto";
|
|
import { createServer } from "node:https";
|
|
import { request as httpAnfrage } from "node:http";
|
|
import { zertifikatBauen } from "./helfer-openssl.mjs";
|
|
|
|
const HIER = dirname(fileURLToPath(import.meta.url));
|
|
const CREW = "crew.dogfather-universe.com";
|
|
|
|
/* ---- Farbrechnung (OKLab, wie in den Farbwerkzeugen) --------------- */
|
|
const linear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
|
export function oklab([R, G, B]) {
|
|
const [r, g, b] = [R / 255, G / 255, B / 255].map(linear);
|
|
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
return [
|
|
0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
|
|
1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
|
|
0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s,
|
|
];
|
|
}
|
|
const abstand = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
const ausHex = (hex) => (/^#[0-9a-f]{6}$/i.test(hex)
|
|
? [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)) : null);
|
|
export const alsHex = (c) => "#" + c.map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
|
|
/* UNTER 0,02 SIEHT EIN AUGE KEINEN UNTERSCHIED MEHR.
|
|
In OKLab entspricht 1 Einheit ungefähr dem ganzen Helligkeitsbereich;
|
|
0,02 ist damit die Grenze, ab der zwei Flächen nebeneinander noch
|
|
als „zwei Farben" gelesen werden. Darunter ist es eine. */
|
|
export const NICHT_UNTERSCHEIDBAR = 0.02;
|
|
|
|
/**
|
|
* Fotografiert die Kachelwand und liest die echten Bildpunkte aus.
|
|
*
|
|
* Gibt IMMER ein Ergebnis zurück — auch wenn es nicht messen konnte.
|
|
* `moeglich: false` mit Grund ist der dritte Ausgang: nicht „in
|
|
* Ordnung", nicht „kaputt", sondern „konnte nicht nachsehen".
|
|
*/
|
|
export async function pruefeEchteFarben({ port = 4369, breite = 1500 } = {}) {
|
|
let chromium;
|
|
try {
|
|
({ chromium } = await import("playwright-core"));
|
|
} catch {
|
|
return { moeglich: false, grund: "playwright-core ist nicht da" };
|
|
}
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-echtfarbe-"));
|
|
const DB = join(ordner, "workspace.db");
|
|
let kind = null, vorbau = null, browser = null;
|
|
|
|
try {
|
|
kind = spawn(process.execPath, [join(HIER, "index.js")], {
|
|
env: { ...process.env, WORKSPACE_DB: DB, PORT: String(port),
|
|
SITE_ACCESS_SECRET: "lokaler-test",
|
|
SITE_PUBLIC_LAUNCH_AT: "2020-01-01T00:00:00+01:00" },
|
|
stdio: ["ignore", "pipe", "pipe"] });
|
|
let oben = false;
|
|
for (let i = 0; i < 80; i++) {
|
|
await new Promise((r) => setTimeout(r, 250));
|
|
if (kind.exitCode !== null) break;
|
|
try {
|
|
const a = await fetch(`http://127.0.0.1:${port}/workspace/`, { redirect: "manual" });
|
|
if (a.status < 500) { oben = true; break; }
|
|
} catch { /* noch nicht oben */ }
|
|
}
|
|
if (!oben) return { moeglich: false, grund: `der Server kam auf Port ${port} nicht hoch` };
|
|
|
|
const d = new DatabaseSync(DB);
|
|
const salz = randomBytes(16).toString("hex");
|
|
const hash = scryptSync("CODE-DOGI-0001", salz, 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, salz, 32768, new Date().toISOString());
|
|
d.close();
|
|
|
|
const keks = await new Promise((fertig, schief) => {
|
|
const daten = JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" });
|
|
const a = httpAnfrage({ host: "127.0.0.1", port, path: "/workspace/api/anmelden",
|
|
method: "POST",
|
|
headers: { "content-type": "application/json", Host: CREW,
|
|
"content-length": Buffer.byteLength(daten) } }, (antwort) => {
|
|
let t = ""; antwort.on("data", (x) => { t += x; });
|
|
antwort.on("end", () => fertig((antwort.headers["set-cookie"] || [])
|
|
.join("; ").split(";")[0]));
|
|
});
|
|
a.on("error", schief); a.end(daten);
|
|
});
|
|
if (!keks) return { moeglich: false, grund: "die Anmeldung hat keinen Keks geliefert" };
|
|
|
|
/* Chromium zwingt crew.… per eingebauter HSTS-Liste auf https --
|
|
deshalb derselbe selbst unterschriebene Vorbau wie überall. */
|
|
const zKey = join(ordner, "k.pem"), zCrt = join(ordner, "z.pem");
|
|
/* HIER BLEIBT DAS try/catch -- anders als an den 28 anderen
|
|
Stellen. Diese Datei hat einen dritten Ausgang: Sie sagt
|
|
„moeglich: false" samt Grund, statt den ganzen Lauf
|
|
abzubrechen. Ein fehlender https-Vorbau ist hier kein Befund
|
|
ueber die Farben, sondern „konnte nicht nachsehen".
|
|
|
|
Seit dem 29.09.2026 sucht `zertifikatBauen` openssl auch
|
|
ausserhalb des PATH -- der Fall sollte damit nicht mehr
|
|
eintreten. Der Ausgang bleibt trotzdem stehen: Eine Sicherung
|
|
abzuschaffen, weil ihr Anlass gerade behoben ist, ist der
|
|
Anfang des naechsten stillen Fehlschlags. */
|
|
try {
|
|
zertifikatBauen(zKey, zCrt, CREW);
|
|
} catch (f) {
|
|
return { moeglich: false, grund: `openssl: ${f.message.split("\n")[0]}` };
|
|
}
|
|
|
|
const HTTPS_PORT = port + 1;
|
|
vorbau = createServer({ key: readFileSync(zKey), cert: readFileSync(zCrt) },
|
|
(an, ab) => {
|
|
const weiter = httpAnfrage({
|
|
host: "127.0.0.1", port, path: an.url, method: an.method,
|
|
headers: { ...an.headers, host: CREW },
|
|
}, (antwort) => { ab.writeHead(antwort.statusCode, antwort.headers); antwort.pipe(ab); });
|
|
weiter.on("error", () => { try { ab.destroy(); } catch { /* Zugabe */ } });
|
|
an.on("error", () => {});
|
|
an.pipe(weiter);
|
|
});
|
|
await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r));
|
|
|
|
browser = await chromium.launch({
|
|
args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1:${HTTPS_PORT}`],
|
|
ignoreHTTPSErrors: true,
|
|
});
|
|
const ctx = await browser.newContext({
|
|
viewport: { width: breite, height: 1000 }, ignoreHTTPSErrors: true });
|
|
const [n, w] = keks.split("=");
|
|
await ctx.addCookies([{ name: n, value: w, domain: CREW, path: "/" }]);
|
|
const seite = await ctx.newPage();
|
|
await seite.goto(`https://${CREW}/workspace/start.html`, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1200);
|
|
|
|
const beschriftung = await seite.evaluate(() => [...document.querySelectorAll(".kachel")]
|
|
.map((k) => ({
|
|
name: (k.querySelector(".kachel__name") || k).textContent.trim().split("\n")[0],
|
|
ton: getComputedStyle(k).getPropertyValue("--ton").trim(),
|
|
})));
|
|
|
|
/* WO DER TEXT STEHT -- fuer den Lesbarkeitsteil weiter unten. */
|
|
const textlagen = await seite.evaluate(() => [...document.querySelectorAll(".kachel")]
|
|
.map((k) => {
|
|
const t = k.querySelector(".kachel__unter") || k.querySelector(".kachel__name");
|
|
if (!t) return null;
|
|
const r = t.getBoundingClientRect(), kr = k.getBoundingClientRect();
|
|
return {
|
|
x: Math.round(r.x - kr.x), y: Math.round(r.y - kr.y),
|
|
b: Math.round(r.width), h: Math.round(r.height),
|
|
farbe: getComputedStyle(t).color,
|
|
};
|
|
}));
|
|
|
|
const zeiger = await seite.$$(".kachel");
|
|
const liste = [];
|
|
for (let i = 0; i < zeiger.length; i++) {
|
|
const png = (await zeiger[i].screenshot({ type: "png" })).toString("base64");
|
|
/* Der Browser zählt selbst -- ein PNG hier von Hand zu lesen
|
|
wäre ein eigenes Werkzeug, und eines mehr, das altert. */
|
|
const mittel = await seite.evaluate(async (daten) => {
|
|
const bild = new Image();
|
|
await new Promise((f) => { bild.onload = f; bild.src = "data:image/png;base64," + daten; });
|
|
const c = document.createElement("canvas");
|
|
c.width = bild.width; c.height = bild.height;
|
|
const g = c.getContext("2d");
|
|
g.drawImage(bild, 0, 0);
|
|
const p = g.getImageData(0, 0, c.width, c.height).data;
|
|
let r = 0, gr = 0, bl = 0, z = 0;
|
|
for (let k = 0; k < p.length; k += 4) { r += p[k]; gr += p[k + 1]; bl += p[k + 2]; z++; }
|
|
return [Math.round(r / z), Math.round(gr / z), Math.round(bl / z)];
|
|
}, png);
|
|
/* WIE LESBAR IST DER TEXT DARAUF?
|
|
============================================================
|
|
Das ist die Frage, die am 22.09.2026 beinahe untergegangen
|
|
waere. Die Farben kraeftiger zu machen war richtig -- gemessen
|
|
stieg der Anteil, der ankommt, von 14,4 auf 51,1 Prozent.
|
|
Nur waren damit 27 von 31 Kacheltexten unter 4,5:1; vorher
|
|
war es EINER. Ein Fehler gegen einen anderen getauscht, und
|
|
zwar genau der, den Filipe schon zweimal gemeldet hat ("man
|
|
bekommt fast nichts gelesen").
|
|
|
|
Gemessen wird der HAEUFIGSTE Farbwert hinter dem Text, nicht
|
|
der hellste: Der hellste Punkt IST der Text (ein erster Anlauf
|
|
meldete darum bei allen 31 einen Kontrast von 1,00 -- also
|
|
einen unmoeglichen Wert). Der Text deckt nur einen Bruchteil
|
|
der Flaeche; der haeufigste Wert ist der Grund. */
|
|
const l = textlagen[i];
|
|
let kontrast = null;
|
|
if (l && l.b > 4 && l.h > 4) {
|
|
const grund = await seite.evaluate(async ([daten, lage]) => {
|
|
const bild = new Image();
|
|
await new Promise((f) => { bild.onload = f; bild.src = "data:image/png;base64," + daten; });
|
|
const c = document.createElement("canvas");
|
|
c.width = bild.width; c.height = bild.height;
|
|
const g = c.getContext("2d");
|
|
g.drawImage(bild, 0, 0);
|
|
const x = Math.max(0, lage.x), y = Math.max(0, lage.y);
|
|
const bb = Math.min(lage.b, c.width - x), hh = Math.min(lage.h, c.height - y);
|
|
if (bb <= 0 || hh <= 0) return null;
|
|
const p = g.getImageData(x, y, bb, hh).data;
|
|
const zaehler = new Map();
|
|
for (let k = 0; k < p.length; k += 4) {
|
|
const sch = ((p[k] >> 3) << 10) | ((p[k + 1] >> 3) << 5) | (p[k + 2] >> 3);
|
|
zaehler.set(sch, (zaehler.get(sch) || 0) + 1);
|
|
}
|
|
let haeufig = 0, wie = -1;
|
|
for (const [sch, n2] of zaehler) if (n2 > wie) { wie = n2; haeufig = sch; }
|
|
return [((haeufig >> 10) & 31) << 3, ((haeufig >> 5) & 31) << 3, (haeufig & 31) << 3];
|
|
}, [png, l]);
|
|
if (grund) {
|
|
const tf = (l.farbe.match(/\d+/g) || [255, 255, 255]).slice(0, 3).map(Number);
|
|
const hell = (c) => {
|
|
const [r, g2, b] = c.map((x) => {
|
|
const v = x / 255;
|
|
return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
|
});
|
|
return 0.2126 * r + 0.7152 * g2 + 0.0722 * b;
|
|
};
|
|
const [a1, a2] = hell(tf) > hell(grund) ? [hell(tf), hell(grund)] : [hell(grund), hell(tf)];
|
|
kontrast = (a1 + 0.05) / (a2 + 0.05);
|
|
}
|
|
}
|
|
|
|
liste.push({ ...beschriftung[i], echt: mittel, kontrast });
|
|
}
|
|
|
|
/* ---- Auswerten ---------------------------------------------- */
|
|
let engstes = { d: Infinity, a: "", b: "" };
|
|
let engstesRoh = Infinity;
|
|
let ununterscheidbar = 0, paare = 0;
|
|
for (let i = 0; i < liste.length; i++) {
|
|
for (let j = i + 1; j < liste.length; j++) {
|
|
paare++;
|
|
const dE = abstand(oklab(liste[i].echt), oklab(liste[j].echt));
|
|
if (dE < engstes.d) engstes = { d: dE, a: liste[i].name, b: liste[j].name };
|
|
if (dE < NICHT_UNTERSCHEIDBAR) ununterscheidbar++;
|
|
const ra = ausHex(liste[i].ton), rb = ausHex(liste[j].ton);
|
|
if (ra && rb) engstesRoh = Math.min(engstesRoh, abstand(oklab(ra), oklab(rb)));
|
|
}
|
|
}
|
|
|
|
const mitKontrast = liste.filter((k) => k.kontrast !== null);
|
|
const zuDunkel = mitKontrast.filter((k) => k.kontrast < 4.5);
|
|
|
|
return {
|
|
moeglich: true,
|
|
anzahl: liste.length,
|
|
/* Wie viele Kacheltexte nicht lesbar sind -- und welche. */
|
|
textGemessen: mitKontrast.length,
|
|
textZuWenig: zuDunkel.length,
|
|
textSchlechteste: mitKontrast.length
|
|
? mitKontrast.reduce((a, b) => (a.kontrast < b.kontrast ? a : b))
|
|
: null,
|
|
textNamen: zuDunkel.slice(0, 5).map((k) => `${k.name} (${k.kontrast.toFixed(2)}:1)`),
|
|
liste,
|
|
engstes,
|
|
engstesRoh,
|
|
ununterscheidbar,
|
|
paare,
|
|
/* Wie viel von der Rohfarbe auf dem Bildschirm ankommt. Faellt
|
|
das unter ein Drittel, nuetzt die schoenste Rohfarbe nichts. */
|
|
anteil: engstesRoh > 0 ? (engstes.d / engstesRoh) * 100 : 0,
|
|
};
|
|
} catch (f) {
|
|
return { moeglich: false, grund: f?.message || String(f) };
|
|
} finally {
|
|
try { if (browser) await browser.close(); } catch { /* Zugabe */ }
|
|
try { if (vorbau) await new Promise((r) => vorbau.close(r)); } catch { /* Zugabe */ }
|
|
try { if (kind) kind.kill(); } catch { /* Zugabe */ }
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Zugabe */ }
|
|
}
|
|
}
|