From e12392a2890ec1434e146ef1b351fa8084e39a60 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Tue, 22 Sep 2026 10:26:49 +0200 Subject: [PATCH] Die Kachelfarben kommen jetzt WIRKLICH auf dem Bildschirm an Filipe: "ich raste aus im ernst, was verstehst du nicht darunter wenn ich sage das alle kacheln eine andere farbe haben sollen ich seh immer wieder sehr viele die einfach komplett die gleiche farbe haben." ER HATTE RECHT, UND MEINE PRUEFUNG WAR TROTZDEM GRUEN. Der Grund war ein Messfehler, und zwar meiner: pruef-kachelfarben 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. GEMESSEN an echten Bildpunkten (neu: tools/kachel-echtfarbe.mjs): Rohfarben: 0,0978 auseinander Auf dem Bildschirm: 0,0141 Es kamen an: 14,4 Prozent Fuer ein Auge gleich: ACHT Paare Drei Stellen haben die Farbe geschluckt, alle in .kachel: 1. Eine Wolke mit 16 Prozent -- in immer DEMSELBEN Blau, auf jeder Kachel. Der staerkste Gleichmacher der ganzen Wand. Sie traegt jetzt den Ton der Kachel. 2. Die Vignette legte bis zu 55 Prozent Dunkelblau darueber. 3. Der Grund trug den Ton zu 24 Prozent und ging ab 58 Prozent in fast reines Schwarz. Zwei Drittel jeder Kachel waren damit dieselbe Farbe, egal welcher Ton darueber stand. Danach: 51,1 Prozent kamen an, NULL ununterscheidbare Paare. UND DANN HAETTE ICH EINEN FEHLER GEGEN EINEN ANDEREN GETAUSCHT. Gemessen an jedem einzelnen Kacheltext: VORHER war 1 von 31 unter 4,5:1, DANACH 27 von 31. Also genau das, was Filipe schon zweimal gemeldet hat ("man bekommt fast nichts gelesen"). Kein Messwert hat es angezeigt -- pruef-buehne nimmt Stichproben und traf die Kacheltexte nicht. Behoben mit einem Lesesaum: ein dunkler Verlauf VON UNTEN, der genau den Streifen abdunkelt, auf dem gelesen wird, und die oberen zwei Drittel in Ruhe laesst. Dasselbe Mittel, mit dem jede Bildunterschrift auf einem Foto lesbar gemacht wird. Texte unter 4,5:1: 1 -> 27 -> 0 Farbe kommt an: 14,4 % -> 51,1 % -> 36,5 % VERTRAULICH MELDEN IST JETZT KNALLROT. Filipe: "nicht die kachel draussen soll rot sein sondern die kachel vertraulich melden nur die soll knall rot sein!!!" Ich hatte screen5 und screen6 vertauscht. Ringtausch ohne neue Farbe: Vertraulich melden 28 -> 38 (#ff1a1a, knallrot) Draussen 38 -> 39 (die Farbe, die Regeln & Hilfe hatte) Regeln & Hilfe 39 -> 28 (das frei gewordene Gruen) NEU, UND DER EIGENTLICHE LERNEFFEKT: server/helfer-kachel-echtfarbe.mjs misst beides in EINER Messung -- Farbunterschied UND Lesbarkeit. Wer sie trennt, verbessert das eine und verliert das andere; genau das ist heute passiert. Benutzt von der Pruefung und vom Werkzeug, damit es nur eine Fassung gibt. pruef-kachelfarben 18/0 (war 13), davon fuenf am echten Bildschirm: - kein Paar sieht auf dem Bildschirm gleich aus (0,0357) - mindestens ein Drittel der Rohfarbe kommt an (36,5 %) - jeder Kacheltext erreicht 4,5:1 (schlechtester: 4,80:1) Mit drittem Ausgang: kein Browser heisst "konnte nicht nachsehen", nicht "in Ordnung". Co-Authored-By: Claude Opus 5 --- server/helfer-kachel-echtfarbe.mjs | 300 +++++++++++++++++++++++++++++ server/pruef-kachelfarben.mjs | 87 ++++++++- server/workspace.js | 40 ++-- tools/kachel-echtfarbe.mjs | 193 +++++++++++++++++++ workspace/anruf-probe.html | 8 +- workspace/app.webmanifest | 8 +- workspace/assets/css/start.css | 72 ++++++- workspace/aufgaben.html | 34 ++-- workspace/automation.html | 30 +-- workspace/befinden.html | 28 +-- workspace/bereich.html | 40 ++-- workspace/bewerben.html | 34 ++-- workspace/bewerbungen.html | 32 +-- workspace/calls.html | 32 +-- workspace/chat.html | 36 ++-- workspace/content.html | 32 +-- workspace/crew-index.html | 10 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 32 +-- workspace/entwicklung.html | 28 +-- workspace/hilfe.html | 26 +-- workspace/index.html | 8 +- workspace/kalender.html | 32 +-- workspace/leistung.html | 30 +-- workspace/material.html | 26 +-- workspace/personen.html | 30 +-- workspace/profil.html | 34 ++-- workspace/rechte.html | 28 +-- workspace/report.html | 32 +-- workspace/scouting.html | 30 +-- workspace/start.html | 30 +-- workspace/startcheck.html | 32 +-- workspace/steckbrief.html | 34 ++-- workspace/talente.html | 30 +-- workspace/team.html | 28 +-- workspace/teamlage.html | 28 +-- workspace/teilen.html | 26 +-- workspace/treff-moderation.html | 28 +-- workspace/treff-regeln.html | 28 +-- workspace/uebersicht.html | 30 +-- workspace/unsere-seiten.html | 26 +-- workspace/werdegang.html | 28 +-- workspace/willkommen.html | 24 +-- workspace/wissen.html | 32 +-- 44 files changed, 1205 insertions(+), 559 deletions(-) create mode 100644 server/helfer-kachel-echtfarbe.mjs create mode 100644 tools/kachel-echtfarbe.mjs diff --git a/server/helfer-kachel-echtfarbe.mjs b/server/helfer-kachel-echtfarbe.mjs new file mode 100644 index 00000000..7e7c9f58 --- /dev/null +++ b/server/helfer-kachel-echtfarbe.mjs @@ -0,0 +1,300 @@ +/* ===================================================================== + 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"; + +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"); + try { + execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", + "-keyout", zKey, "-out", zCrt, "-days", "2", "-subj", `/CN=${CREW}`, + "-addext", `subjectAltName=DNS:${CREW}`], { stdio: "ignore" }); + } catch { + return { moeglich: false, grund: "openssl ist nicht da (fuer den https-Vorbau)" }; + } + + 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 */ } + } +} diff --git a/server/pruef-kachelfarben.mjs b/server/pruef-kachelfarben.mjs index 435a31fb..4b67e4f5 100644 --- a/server/pruef-kachelfarben.mjs +++ b/server/pruef-kachelfarben.mjs @@ -57,10 +57,14 @@ const MINDEST_BUNTHEIT = 0.12; /* Was festgelegt ist — mit dem Grund, aus dem es festgelegt wurde. */ const FESTGELEGT = [ - { ton: 28, kachel: "Vertraulich melden", - warum: "dieselbe Farbe wie fuer Regeln & Hilfe -- wer Hilfe sucht, landet bei einem von beiden" }, - { ton: 38, kachel: "Draußen", hex: "#ff1a1a", - warum: "knalliges Rot, traegt ab 20 Uhr den LIVE-Punkt" }, + /* BERICHTIGT AM 22.09.2026. Ich hatte screen5 und screen6 vertauscht + und das Rot auf "Draussen" gelegt. Filipe: "nicht die kachel + draussen soll rot sein sondern die kachel vertraulich melden nur + die soll knall rot sein!!!" */ + { ton: 38, kachel: "Vertraulich melden", hex: "#ff1a1a", + warum: "knalliges Rot -- wer den vertraulichen Weg sucht, soll ihn finden, ohne zu lesen" }, + { ton: 39, kachel: "Draußen", + warum: "die Farbe, die Regeln & Hilfe hatte -- ausdruecklicher Wunsch (screen5)" }, ]; let ok = 0, fehl = 0; @@ -225,5 +229,80 @@ console.log("\n=== Gegenprobe ==="); pruef("ein blasser Ton wuerde auffallen (#ffe5ae)", buntheit("#ffe5ae") < MINDEST_BUNTHEIT); } +/* ===================================================================== + 5. WAS AUF DEM BILDSCHIRM ANKOMMT -- der Teil, der gefehlt hat + ===================================================================== + Alles bisher misst `--ton`: die ROHFARBE aus start.css. Und genau + damit war diese Pruefung gruen, waehrend Filipe am 22.09.2026 sagte: + "ich seh immer wieder sehr viele die einfach komplett die gleiche + farbe haben." + + Er hatte recht. Die Kachel zeichnet ihren Ton nicht als Flaeche, + sondern als Verlauf unter einem Sternenfeld, einer Vignette und + (bis heute) einer fest blauen Wolke. Gemessen an echten + Bildpunkten: Von 0,0978 Abstand kamen 0,0141 an -- 14,4 Prozent. + ACHT Paare waren fuer ein Auge nicht mehr zu unterscheiden. + + Eine Pruefung, die die Rohfarbe misst, beantwortet die Frage + "sehen die Kacheln verschieden aus" NICHT. Sie beantwortet + "stehen verschiedene Zahlen in der Datei". Das ist derselbe Fehler + wie am 01.09.2026 beim `every()` auf einem leeren Feld: Sie lief, + sie war gruen, und sie war wertlos. + + Deshalb hier der Browser. Er kostet zwei Minuten -- und er ist der + einzige, der die Frage beantworten kann. + ===================================================================== */ +if (process.env.OHNE_BROWSER !== "ja") { + console.log("\n=== Und was davon auf dem Bildschirm ankommt ==="); + const { pruefeEchteFarben } = await import("./helfer-kachel-echtfarbe.mjs"); + const e = await pruefeEchteFarben(); + + if (!e.moeglich) { + /* DER DRITTE AUSGANG. Kein Browser, kein openssl, kein Port -- + das ist "konnte nicht nachsehen", nicht "in Ordnung". */ + console.log(` KONNTE NICHT NACHSEHEN: ${e.grund}`); + console.log(" (mit OHNE_BROWSER=ja laeuft die Pruefung bewusst ohne diesen Teil)"); + fehl++; + } else { + pruef(`${e.anzahl} Kacheln am echten Bildschirm gemessen`, e.anzahl >= 20, + `nur ${e.anzahl} gefunden`); + + /* DIE ZAHL GEHOERT IN DIE BEDINGUNG -- sonst waere dieser ganze + Abschnitt bei null Kacheln trivial gruen. */ + pruef("kein Paar sieht auf dem Bildschirm gleich aus " + + `(engstes: ${e.engstes.d.toFixed(4)}, ${e.engstes.a} <-> ${e.engstes.b})`, + e.anzahl >= 20 && e.ununterscheidbar === 0, + `${e.ununterscheidbar} von ${e.paare} Paaren unter 0,02`); + + /* WIE VIEL VON DER ROHFARBE UEBERHAUPT ANKOMMT. Faellt das wieder + unter ein Drittel, ist die Gestaltung der Kachel der Grund -- + und dann nuetzt die schoenste Rohfarbe nichts. */ + pruef(`mindestens ein Drittel der Rohfarbe kommt an (${e.anteil.toFixed(1)} %)`, + e.anteil >= 33, + `nur ${e.anteil.toFixed(1)} % -- die Kachelgestaltung schluckt die Farbe`); + + /* UND DER TEXT MUSS DARAUF LESBAR BLEIBEN. + ================================================================ + Diese zwei Zeilen sind die wichtigsten des ganzen Abschnitts. + Am 22.09.2026 wurde die Farbe kraeftiger gemacht -- richtig, und + gemessen. Dabei fielen 27 von 31 Kacheltexten unter 4,5:1; + vorher war es EINER. Kein Messwert hat das gemeldet, weil keine + Pruefung danach gefragt hat: `pruef-buehne` nimmt Stichproben + und traf die Kacheltexte nicht. + + Farbe und Lesbarkeit gehoeren in DIESELBE Messung. Wer sie + trennt, verbessert das eine und verliert das andere -- und + merkt es erst, wenn jemand sagt, er koenne nichts mehr lesen. */ + pruef(`${e.textGemessen} Kacheltexte auf Lesbarkeit gemessen`, + e.textGemessen >= 20, `nur ${e.textGemessen}`); + pruef("jeder Kacheltext erreicht 4,5:1 auf seiner Kachel" + + (e.textSchlechteste + ? ` (schlechtester: ${e.textSchlechteste.name} ${e.textSchlechteste.kontrast.toFixed(2)}:1)` + : ""), + e.textGemessen >= 20 && e.textZuWenig === 0, + `${e.textZuWenig} zu dunkel: ${e.textNamen.join(", ")}`); + } +} + console.log(`\n ${ok} bestanden, ${fehl} fehlgeschlagen\n`); process.exit(fehl ? 1 : 0); diff --git a/server/workspace.js b/server/workspace.js index a3822f75..a98f7278 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -1299,8 +1299,14 @@ const TREFF_BEREICHE = [ besser als der erste Vorschlag), Kontrast 6,08:1. */ { ...TREFF_GRUPPE, name: "Material", unter: "Bilder und Videos zum Posten – jedes nur einmal", zeichen: "galerie", ton: 42, ziel: "material.html", szene: "studio" }, + /* DIE FARBE, DIE "REGELN & HILFE" HATTE (22.09.2026). + Filipes screen5: "die farbe dieser kachel soll die farbe von der + kachel: regeln & hilfe werden". Regeln & Hilfe bekommt dafuer + eine eigene -- zwei Kacheln mit derselben Farbe in derselben + Ansicht waeren genau das, was screen4 abschafft. + Ton 39 ist GESETZT. */ { ...TREFF_GRUPPE, name: "Draußen", unter: "Ist er live? Kanäle, Shop & dein Rabatt", - zeichen: "hinaus", ton: 38, ziel: "unsere-seiten.html", szene: "portal" }, + zeichen: "hinaus", ton: 39, ziel: "unsere-seiten.html", szene: "portal" }, { ...TREFF_GRUPPE, name: "Mitmachen", unter: "Wie du dazugeh\u00f6ren kannst", zeichen: "personen", ton: 32, ziel: "bereich.html?b=mitmachen", szene: "garage" }, /* UNSERE SEITEN (19.09.2026) -- die einzige Kachel, die HINAUSFUEHRT. @@ -1343,11 +1349,11 @@ const TREFF_BEREICHE = [ Das Brett gibt es weiter; treff-regeln.html verlinkt darauf. Ein Ort fuer die Frage, nicht zwei. */ - /* EIGENER TON SEIT 22.09.2026. Ton 28 traegt jetzt "Vertraulich - melden" -- ausdruecklicher Wunsch, dort gesetzt. Zwei Kacheln - mit derselben Farbe in derselben Ansicht waeren genau das, was - screen4 abschaffen soll. */ - zeichen: "schutz", ton: 39, ziel: "treff-regeln.html", szene: "studio" }, + /* EIGENER TON (22.09.2026). "Draussen" traegt jetzt die Farbe, + die hier stand -- ausdruecklicher Wunsch. Zwei Kacheln mit + derselben Farbe in derselben Ansicht waeren genau das, was + screen4 abschafft. */ + zeichen: "schutz", ton: 28, ziel: "treff-regeln.html", szene: "studio" }, ]; /* MELDUNGEN & MASSNAHMEN -- FUER DAS TEAM, NICHT FUER DIE COMMUNITY @@ -1466,15 +1472,21 @@ export const TREFF_ROLLEN = new Set(["gast", "modi", "hand", "linke", "admin"]); const HILFE_KACHEL = { gruppe: "Für dich", gruppeUnter: "Deine eigene Seite", name: "Vertraulich melden", unter: "Nur DogFather und die rechte Hand", - /* DIESELBE FARBE WIE "REGELN & HILFE" -- AUSDRUECKLICH SO GEWOLLT - (22.09.2026, Filipe: "die farbe dieser kachel soll die farbe von - der kachel: regeln & hilfe werden ... und diese zwei kacheln - sollen egal wie dan auch so bleiben wie hier erwartet"). + /* KNALLROT -- AUSDRUECKLICH DIESE KACHEL (berichtigt 22.09.2026). + ================================================================== + Filipe: "nicht die kachel draussen soll rot sein sondern die + kachel vertraulich melden nur die soll knall rot sein!!!" - Beide gehoeren zusammen: Wer Hilfe sucht, landet bei einem von - beiden. Ton 28 ist damit GESETZT -- die Farbverteilung von - screen4 fasst ihn nicht an, sie weicht ihm aus. */ - zeichen: "schutz", ton: 28, ziel: "hilfe.html", szene: "studio", + Ich hatte screen5 und screen6 vertauscht und das Rot auf + "Draussen" gelegt. Hier sitzt es richtig: Ein vertraulicher + Meldeweg ist das, was jemand im schlimmsten Moment sucht -- und + dann zaehlt, dass man ihn auf der Wand sofort findet, ohne zu + lesen. Rot ist hier die Aussage, nicht die Dekoration. + + #ff1a1a, voll gesaettigt, Kontrast 4,84:1 gegen den Grund. + Ton 38 ist GESETZT: pruef-kachelfarben wird rot, wenn ein + Farblauf ihn anfasst. */ + zeichen: "schutz", ton: 38, ziel: "hilfe.html", szene: "studio", }; /* DAS TEAM SIEHT DEN TREFF MIT -- als eigene Gruppe unter seinen diff --git a/tools/kachel-echtfarbe.mjs b/tools/kachel-echtfarbe.mjs new file mode 100644 index 00000000..a1963540 --- /dev/null +++ b/tools/kachel-echtfarbe.mjs @@ -0,0 +1,193 @@ +/* ===================================================================== + WELCHE FARBE HAT EINE KACHEL WIRKLICH? (22.09.2026) + --------------------------------------------------------------------- + Filipe: „ich seh immer wieder sehr viele die einfach komplett die + gleiche farbe haben ich will dass sie wirklich alle alle alle + komplett unterschiedlich sind." + + Und er hat recht, obwohl `pruef-kachelfarben` grün war. Der Grund + ist ein Messfehler, und zwar meiner: + + `pruef-kachelfarben` misst `--ton` — die ROHFARBE aus start.css. + Die ist zwischen allen 31 Kacheln weit auseinander (0,0978 in + OKLab). Nur steht sie so nie auf dem Bildschirm: Die Kachel zeichnet + sie als Nebel mit 5 bis 9 Prozent Deckkraft über einen dunklen + Grund. Bei 5 Prozent sind 31 verschiedene Farben 31 mal fast + dasselbe Dunkelblau. + + Dieses Werkzeug fotografiert die Wand und liest die ECHTEN + Bildpunkte aus — genau das, was ein Auge sieht. Es nimmt bewusst + NICHT eine Stelle, sondern den Durchschnitt über die ganze Kachel: + Ein Sternenfeld hat helle Punkte, und eine Einzelmessung träfe + vielleicht einen davon. + + AUFRUF + node tools/kachel-echtfarbe.mjs + ===================================================================== */ + +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 { chromium } from "playwright-core"; + +const HIER = join(dirname(fileURLToPath(import.meta.url)), "..", "server"); +const CREW = "crew.dogfather-universe.com"; +const PORT = Number(process.env.PORT_ECHT || 4368); +const BASIS = `http://127.0.0.1:${PORT}`; +const ordner = mkdtempSync(join(tmpdir(), "ws-echt-")); +const DB = join(ordner, "workspace.db"); +let kind = null; + +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"] }); +for (let i = 0; i < 80; i++) { + await new Promise((r) => setTimeout(r, 250)); + try { const a = await fetch(BASIS + "/workspace/", { redirect: "manual" }); + if (a.status < 500) break; } catch { /* noch nicht oben */ } +} + +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: 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); +}); + +/* Chromium zwingt crew.… per eingebauter HSTS-Liste auf https. */ +const zKey = join(ordner, "k.pem"), zCrt = join(ordner, "z.pem"); +execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", + "-keyout", zKey, "-out", zCrt, "-days", "2", "-subj", `/CN=${CREW}`, + "-addext", `subjectAltName=DNS:${CREW}`], { stdio: "ignore" }); +const HTTPS_PORT = PORT + 1; +const vorbau = createServer({ key: readFileSync(zKey), cert: readFileSync(zCrt) }, + (an, ab) => { + const weiter = httpAnfrage({ + host: "127.0.0.1", port: 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)); + +const 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: 1500, height: 1000 }, ignoreHTTPSErrors: true }); +const [n, w] = keks.split("="); +await ctx.addCookies([{ name: n, value: w, domain: CREW, path: "/" }]); +const s = await ctx.newPage(); +await s.goto(`https://${CREW}/workspace/start.html`, { waitUntil: "networkidle" }); +await s.waitForTimeout(1200); + +/* ---- Jede Kachel einzeln fotografieren und mitteln ------------------ */ +const kacheln = await s.evaluate(() => [...document.querySelectorAll(".kachel")] + .map((k, i) => ({ + i, + name: (k.querySelector(".kachel__name") || k).textContent.trim().split("\n")[0], + ton: getComputedStyle(k).getPropertyValue("--ton").trim(), + }))); + +const zeiger = await s.$$(".kachel"); +const gemessen = []; +for (let i = 0; i < zeiger.length; i++) { + const bild = await zeiger[i].screenshot({ type: "png" }); + /* PNG von Hand lesen waere ein eigenes Werkzeug. Stattdessen laesst + der Browser selbst zaehlen: Bild in ein , Mittelwert. */ + const b64 = bild.toString("base64"); + const mittel = await s.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, n = 0; + for (let k = 0; k < p.length; k += 4) { r += p[k]; gr += p[k + 1]; bl += p[k + 2]; n++; } + return [Math.round(r / n), Math.round(gr / n), Math.round(bl / n)]; + }, b64); + gemessen.push({ ...kacheln[i], echt: mittel }); +} + +/* ---- Wie weit liegen die ECHTEN Farben auseinander? ----------------- */ +const linear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4); +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 s2 = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b); + return [0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s2, + 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s2, + 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s2]; +} +const roh = (hex) => (/^#[0-9a-f]{6}$/i.test(hex) + ? [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)) : null); +const abst = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]); + +const hex = (c) => "#" + c.map((x) => x.toString(16).padStart(2, "0")).join(""); +console.log("\nKachel Rohfarbe was man WIRKLICH sieht"); +for (const k of gemessen) { + console.log(" " + k.name.padEnd(24) + (k.ton || "—").padEnd(11) + hex(k.echt)); +} + +let engRoh = { d: Infinity }, engEcht = { d: Infinity }; +for (let i = 0; i < gemessen.length; i++) { + for (let j = i + 1; j < gemessen.length; j++) { + const a = gemessen[i], b = gemessen[j]; + const dE = abst(oklab(a.echt), oklab(b.echt)); + if (dE < engEcht.d) engEcht = { d: dE, a: a.name, b: b.name }; + const ra = roh(a.ton), rb = roh(b.ton); + if (ra && rb) { + const dR = abst(oklab(ra), oklab(rb)); + if (dR < engRoh.d) engRoh = { d: dR, a: a.name, b: b.name }; + } + } +} +console.log(`\n ${gemessen.length} Kacheln gemessen.`); +console.log(` Engstes Paar der ROHFARBEN: ${engRoh.d.toFixed(4)} (${engRoh.a} <-> ${engRoh.b})`); +console.log(` Engstes Paar auf dem SCHIRM: ${engEcht.d.toFixed(4)} (${engEcht.a} <-> ${engEcht.b})`); +console.log(` Davon kommt an: ${(engEcht.d / engRoh.d * 100).toFixed(1)} Prozent.`); + +/* Wie viele Paare sind auf dem Schirm praktisch gleich? Unter 0,02 in + OKLab sieht ein Auge keinen Unterschied mehr. */ +let gleich = 0, paare = 0; +for (let i = 0; i < gemessen.length; i++) { + for (let j = i + 1; j < gemessen.length; j++) { + paare++; + if (abst(oklab(gemessen[i].echt), oklab(gemessen[j].echt)) < 0.02) gleich++; + } +} +console.log(` Paare, die ein Auge NICHT unterscheiden kann: ${gleich} von ${paare}`); + +await browser.close(); +await new Promise((r) => vorbau.close(r)); +if (kind) kind.kill(); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Zugabe */ } diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index 707cb9d7..8d6f79c8 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,15 +16,15 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +