/* ===================================================================== DIE KACHELFARBEN MIT ECHTEN AUGEN ANSEHEN (22.09.2026) --------------------------------------------------------------------- `pruef-kachelfarben.mjs` rechnet Abstände in OKLab. Das sagt, dass keine zwei Farben zu nah beieinander liegen — es sagt NICHT, ob die Wand gut aussieht. Am 22.09. hat der Blick mit echten Augen schon drei Dinge gefunden, die keine Messung gemeldet hätte. Dieses Werkzeug nimmt die Startseite auf, als DogFather und als Zuschauer, am Rechner und am Handy — und misst dabei das eine, was sich sonst nur im Bild zeigt: ob zwei NEBENEINANDER liegende Kacheln ähnlich aussehen. Zwei ähnliche Farben an entgegengesetzten Enden der Wand fallen niemandem auf; zwei nebeneinander schon. AUFRUF node tools/farben-blick.mjs BREITE=390 node tools/farben-blick.mjs ===================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { spawn } 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 { execFileSync } from "node:child_process"; import { chromium } from "playwright-core"; const HIER = join(dirname(fileURLToPath(import.meta.url)), "..", "server"); const PORT = Number(process.env.PORT_BLICK || 4366); const BASIS = `http://127.0.0.1:${PORT}`; const BREITE = Number(process.env.BREITE || 1500); const ordner = mkdtempSync(join(tmpdir(), "ws-farben-")); const DB = join(ordner, "workspace.db"); let kind = null; async function start() { 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 aus = ""; kind.stdout.on("data", (d) => { aus += d; }); kind.stderr.on("data", (d) => { aus += d; }); for (let i = 0; i < 80; i++) { await new Promise((r) => setTimeout(r, 250)); if (kind.exitCode !== null) break; try { const a = await fetch(BASIS + "/workspace/", { redirect: "manual" }); if (a.status < 500) return; } catch { /* noch nicht oben */ } } throw new Error("Server startete nicht:\n" + aus.slice(-1500)); } function personAnlegen(name, rolle, code) { const d = new DatabaseSync(DB); const salz = randomBytes(16).toString("hex"); const hash = scryptSync(code, 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(name, rolle, hash, salz, 32768, new Date().toISOString()); d.close(); } function rohAnfrage(pfad, host, koerper) { return new Promise((fertig, schief) => { const daten = JSON.stringify(koerper); const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: pfad, method: "POST", headers: { "content-type": "application/json", Host: host, "content-length": Buffer.byteLength(daten) }, }, (antwort) => { let text = ""; antwort.on("data", (d) => { text += d; }); antwort.on("end", () => fertig({ code: antwort.statusCode, text, keks: (antwort.headers["set-cookie"] || []).join("; ").split(";")[0], })); }); a.on("error", schief); a.end(daten); }); } async function anmelden(rolle, code, host) { let a = await rohAnfrage("/workspace/api/anmelden", host, { rolle, code }); if (/alter/i.test(a.text)) { a = await rohAnfrage("/workspace/api/anmelden", host, { rolle, code, alter_ok: true }); } if (a.code !== 200) throw new Error(`Anmeldung ${rolle} -> ${a.code} ${a.text.slice(0, 160)}`); return a.keks; } /* Chromium zwingt crew.dogfather-universe.com per eingebauter HSTS-Liste auf https. Deshalb derselbe selbst unterschriebene Vorbau wie in pruef-handy-teamdogi.mjs -- eine Nachbildung waere die zweite Fassung, die anders altert. */ function schluesselUndZeugnis() { const k = join(ordner, "k.pem"), z = join(ordner, "z.pem"); execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", k, "-out", z, "-days", "2", "-subj", "/CN=crew.dogfather-universe.com", "-addext", "subjectAltName=DNS:crew.dogfather-universe.com"], { stdio: "ignore" }); return { k, z }; } await start(); personAnlegen("Filipe", "admin", "CODE-DOGI-0001"); personAnlegen("Jonas", "gast", "CODE-GAST-0001"); const { k, z } = schluesselUndZeugnis(); const { readFileSync } = await import("node:fs"); const vorbau = createServer({ key: readFileSync(k), cert: readFileSync(z) }, (an, ab) => { const weiter = httpAnfrage({ host: "127.0.0.1", port: PORT, path: an.url, method: an.method, headers: { ...an.headers, host: "crew.dogfather-universe.com" }, }, (antwort) => { ab.writeHead(antwort.statusCode, antwort.headers); antwort.pipe(ab); }); /* Beim Aufraeumen bricht der Browser laufende Anfragen ab. Das ist kein Befund -- ohne diesen Zuhoerer beendet Node den Lauf mit einem Fehler, NACHDEM die Bilder schon fertig sind. */ weiter.on("error", () => { try { ab.destroy(); } catch {} }); an.on("error", () => {}); an.pipe(weiter); }); const HTTPS_PORT = PORT + 1; await new Promise((r) => vorbau.listen(HTTPS_PORT, r)); const browser = await chromium.launch({ args: [`--host-resolver-rules=MAP crew.dogfather-universe.com 127.0.0.1:${HTTPS_PORT}`], ignoreHTTPSErrors: true, }); const rollen = [ { rolle: "admin", code: "CODE-DOGI-0001", wer: "DogFather" }, { rolle: "gast", code: "CODE-GAST-0001", wer: "Community" }, ]; for (const r of rollen) { const keks = await anmelden(r.rolle, r.code, "crew.dogfather-universe.com"); const ctx = await browser.newContext({ viewport: { width: BREITE, height: 1000 }, ignoreHTTPSErrors: true, isMobile: BREITE < 700, hasTouch: BREITE < 700, }); const [name, wert] = keks.split("="); await ctx.addCookies([{ name, value: wert, domain: "crew.dogfather-universe.com", path: "/" }]); const s = await ctx.newPage(); await s.goto("https://crew.dogfather-universe.com/workspace/start.html", { waitUntil: "networkidle" }); await s.waitForTimeout(600); /* WAS LIEGT NEBENEINANDER? Das ist die Frage, die eine Tabelle von Abständen nicht beantwortet. */ const bild = await s.evaluate(() => { const linear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4); const oklab = (rgb) => { const [r, g, b] = rgb.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 kacheln = [...document.querySelectorAll(".kachel")].map((k) => { const r = k.getBoundingClientRect(); const roh = getComputedStyle(k).getPropertyValue("--ton").trim(); let rgb = null; if (/^#[0-9a-f]{6}$/i.test(roh)) { rgb = [1, 3, 5].map((i) => parseInt(roh.slice(i, i + 2), 16) / 255); } return { name: (k.querySelector(".kachel__name") || k).textContent.trim().split("\n")[0], ton: roh, lab: rgb ? oklab(rgb) : null, x: Math.round(r.x), y: Math.round(r.y), b: Math.round(r.width), h: Math.round(r.height), }; }); /* Nachbarn: alles, was sich horizontal oder vertikal beruehrt. */ const nah = []; const d = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]); for (let i = 0; i < kacheln.length; i++) { for (let j = i + 1; j < kacheln.length; j++) { const A = kacheln[i], B = kacheln[j]; if (!A.lab || !B.lab) continue; const waagerecht = Math.abs(A.y - B.y) < 8 && Math.abs(A.x - B.x) < A.b + B.b + 40; const senkrecht = Math.abs(A.x - B.x) < 8 && Math.abs(A.y - B.y) < A.h + B.h + 40; if (!waagerecht && !senkrecht) continue; nah.push({ a: A.name, b: B.name, d: d(A.lab, B.lab) }); } } nah.sort((p, q) => p.d - q.d); return { anzahl: kacheln.length, ohneTon: kacheln.filter((k) => !k.lab).length, nah: nah.slice(0, 5) }; }); console.log(`\n${r.wer} bei ${BREITE} px: ${bild.anzahl} Kacheln` + (bild.ohneTon ? `, ${bild.ohneTon} OHNE Farbe` : "")); for (const n of bild.nah) { console.log(` ${n.d.toFixed(4)} ${n.a} neben ${n.b}` + (n.d < 0.09 ? " <<< sehen sich aehnlich" : "")); } const pfad = join("C:/Users/qciga/Documents/Obelix/DogFather", `farben-${r.rolle}-${BREITE}.png`); await s.screenshot({ path: pfad, fullPage: true }); console.log(` Bild: ${pfad}`); await ctx.close(); } await browser.close(); await new Promise((r) => vorbau.close(r)); if (kind) kind.kill(); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Zugabe */ }