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
635 lines
31 KiB
JavaScript
635 lines
31 KiB
JavaScript
/* =====================================================================
|
|
pruef-chat-neu.mjs — Sprachnachricht, GIF-Kiste und der Ruf ans Rudel
|
|
|
|
Alle drei sind in der Nacht zum 23.09.2026 entstanden:
|
|
|
|
„dan will ich auch dass man im chat auch sprachnachrichten und
|
|
gifts reinschicken kann. also nur die modis rechte linke hand
|
|
und dogfather."
|
|
„dan will ich auch dass nur die modis, rechte hand, linke hand
|
|
und dogfather, alle auch auf einmal markieren koennen im chat
|
|
mit einem, @rudel ,dann sollen alle eine benarichtigung
|
|
bekommen."
|
|
|
|
---------------------------------------------------------------------
|
|
WARUM ES DIESE DATEI GIBT, obwohl jedes Stueck schon geprueft ist
|
|
|
|
Geprueft war jedes einzeln -- und jedes in einer anderen Lage als
|
|
der echten:
|
|
|
|
pruef-chat-anhaenge Sprachnachricht und GIF, aber als DogFather
|
|
auf der AGENTURadresse, mit der Maus
|
|
pruef-erwaehnung der Rudel-Ruf, aber gegen den Server
|
|
|
|
Der Fall, den Filipe wirklich hat -- ein MODI auf crew., mit dem
|
|
FINGER, und ein zweiter Mensch, der es bekommen soll -- war nie am
|
|
Stueck gemessen. Genau in solchen Luecken sitzen die Fehler, die
|
|
niemand sieht: Jeder Einzeltest ist gruen, und zusammen geht es
|
|
trotzdem nicht.
|
|
|
|
ZWEI BROWSER, ZWEI MENSCHEN. Eine Sprachnachricht, die nur beim
|
|
Absender im Verlauf steht, ist keine. Deshalb wartet jede der drei
|
|
Proben darauf, dass die ZWEITE Person sie von selbst bekommt --
|
|
ohne Neuladen.
|
|
|
|
---------------------------------------------------------------------
|
|
ZWEI EIGENE FEHLER BEIM BAUEN, beide aufgeschrieben, weil sie sich
|
|
wiederholen werden:
|
|
|
|
(1) OHNE NOTBREMSE. Der erste Lauf blieb stehen und sah von aussen
|
|
aus wie „laeuft noch". Ein Werkzeug, das haengt, ist schlimmer
|
|
als eines, das scheitert.
|
|
|
|
(2) MIT ENTER ABGESCHICKT. Auf einem Beruehrgeraet schickt Enter
|
|
ABSICHTLICH nicht ab -- sonst kaeme man nie zu einer zweiten
|
|
Zeile. Die Messung meldete daraufhin „kommt nicht an"; in
|
|
Wahrheit war nie etwas abgeschickt worden. Dasselbe Muster wie
|
|
beim `pointer: coarse` am selben Tag: Wer im falschen
|
|
Geraeteprofil misst, misst ein anderes Programm.
|
|
|
|
AUFRUF: node server/pruef-chat-neu.mjs
|
|
===================================================================== */
|
|
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 { execFileSync } from "node:child_process";
|
|
import { createServer as httpsServer } from "node:https";
|
|
|
|
import { eigenerPort } from "./helfer-port.mjs";
|
|
import { zertifikatBauen } from "./helfer-openssl.mjs";
|
|
const PORT = await eigenerPort(import.meta, "pruef-chat-neu");
|
|
const CREW = "crew.dogfather-universe.com";
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-drei-"));
|
|
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";
|
|
process.env.TREFF_NACHT_AB = "0";
|
|
process.env.TREFF_NACHT_BIS = "0";
|
|
|
|
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 }); };
|
|
await import("./index.js");
|
|
/* NOTBREMSE. Ein Messwerkzeug, das HAENGT, ist schlimmer als eines,
|
|
das scheitert: Von aussen sieht Stillstand aus wie "laeuft noch".
|
|
Genau das ist mir hier gerade passiert. */
|
|
const { notbremse } = await import("./helfer-notbremse.mjs");
|
|
notbremse(240_000, "pruef-chat-neu");
|
|
await new Promise((r) => setTimeout(r, 1300));
|
|
|
|
|
|
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 k = randomBytes(32).toString("hex");
|
|
d.prepare("INSERT INTO einstellungen (schluessel,wert,geaendert,von) VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
|
.run("code_kennung_schluessel", k, jetzt);
|
|
const mach = (n, r, c) => {
|
|
const s = randomBytes(16).toString("hex");
|
|
const h = scryptSync(c, s, 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(n, r, h, s, 32768, createHmac("sha256", k).update(c).digest("hex"), jetzt);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
};
|
|
const idDogi = mach("Filipe", "admin", "CODE-DOGI-0001");
|
|
mach("Rieke", "hand", "CODE-HAND-0001");
|
|
const idA = mach("Frida", "modi", "CODE-MODI-0001");
|
|
const idB = mach("Nele", "modi", "CODE-MODI-0002");
|
|
d.close();
|
|
|
|
const schl = join(ordner, "b.key"), zert = join(ordner, "b.crt");
|
|
zertifikatBauen(schl, zert, CREW);
|
|
const HP = PORT + 1;
|
|
const vorbau = httpsServer({ key: readFileSync(schl), cert: readFileSync(zert) }, (von, nach) => {
|
|
const w = httpAnfrage({ host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
|
|
headers: { ...von.headers, host: `${CREW}:${HP}` } },
|
|
(a) => { nach.writeHead(a.statusCode || 502, a.headers); a.pipe(nach); });
|
|
von.pipe(w);
|
|
});
|
|
await new Promise((r) => vorbau.listen(HP, "127.0.0.1", r));
|
|
const SEITE = `https://${CREW}:${HP}`;
|
|
|
|
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await pw.chromium.launch({
|
|
args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors",
|
|
"--use-fake-device-for-media-stream", "--use-fake-ui-for-media-stream"] });
|
|
|
|
async function alsPerson(rolle, code) {
|
|
const ctx = await browser.newContext({
|
|
viewport: { width: 390, height: 844 }, deviceScaleFactor: 2,
|
|
hasTouch: true, isMobile: true, reducedMotion: "reduce",
|
|
permissions: ["microphone"] });
|
|
const seite = await ctx.newPage();
|
|
const meckern = [];
|
|
seite.on("console", (m) => { if (m.type() === "error" && !/favicon|ERR_CERT|net::ERR/i.test(m.text())) meckern.push(m.text().slice(0, 110)); });
|
|
seite.on("pageerror", (e) => meckern.push("Absturz: " + String(e).slice(0, 110)));
|
|
await seite.goto(`${SEITE}/workspace/`, { waitUntil: "networkidle" });
|
|
await seite.click(`.rolle[data-rolle="${rolle}"]`);
|
|
await seite.fill("#code", code);
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForTimeout(1300);
|
|
return { ctx, seite, meckern };
|
|
}
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const ok = (b, t) => {
|
|
geprueft++;
|
|
console.log((b ? " ok " : " FEHL ") + t);
|
|
if (!b) fehler++;
|
|
};
|
|
|
|
const frida = await alsPerson("modi", "CODE-MODI-0001");
|
|
const nele = await alsPerson("modi", "CODE-MODI-0002");
|
|
|
|
/* Beide in den Treff -- dort sitzen alle. */
|
|
for (const p of [frida, nele]) {
|
|
await p.seite.goto(`${SEITE}/workspace/chat.html?raum=treff`, { waitUntil: "networkidle" });
|
|
await p.seite.waitForTimeout(1800);
|
|
}
|
|
|
|
console.log("\n=== 1. Sprachnachricht ===");
|
|
{
|
|
const vorher = await nele.seite.locator(".chat-nachricht").count();
|
|
const knopfDa = await frida.seite.locator("#ton-auf").isVisible();
|
|
ok(knopfDa, "der Modi sieht den Mikrofonknopf auf crew.");
|
|
await frida.seite.click("#ton-auf");
|
|
await frida.seite.waitForSelector("#tonleiste:not([hidden])", { timeout: 8000 });
|
|
await frida.seite.waitForTimeout(2400);
|
|
const zeit = (await frida.seite.locator("#ton-zeit").innerText()).trim();
|
|
ok(/^0:0[2-9]$/.test(zeit), `die Zeit laeuft (${zeit})`);
|
|
await frida.seite.click("#ton-senden");
|
|
await frida.seite.waitForTimeout(3200);
|
|
const blase = await frida.seite.evaluate(() => {
|
|
const t = document.querySelector(".chat-ton");
|
|
return t ? { marke: t.querySelector(".chat-ton__marke")?.textContent.trim(),
|
|
dauer: t.querySelector(".chat-ton__dauer")?.textContent.trim(),
|
|
breit: Math.round(t.getBoundingClientRect().width) } : null;
|
|
});
|
|
ok(!!blase, `sie steht in seinem Verlauf (${blase?.marke} ${blase?.dauer})`);
|
|
ok((blase?.breit || 0) >= 200 && (blase?.breit || 0) <= 390,
|
|
`und passt auf den Bildschirm (${blase?.breit} px)`);
|
|
/* UND KOMMT BEIM ANDEREN AN, ohne dass der etwas tut. */
|
|
const beiNele = await nele.seite.waitForFunction(
|
|
(v) => document.querySelectorAll(".chat-nachricht").length > v && document.querySelector(".chat-ton"),
|
|
vorher, { timeout: 12000 }).then(() => true).catch(() => false);
|
|
ok(beiNele, "die zweite Modi bekommt sie von selbst -- ohne Neuladen");
|
|
}
|
|
|
|
console.log("\n=== 2. GIF aus der Kiste ===");
|
|
{
|
|
const GIF = "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
|
ok(await frida.seite.locator("#gif-auf").isVisible(), "der Modi sieht den GIF-Knopf");
|
|
await frida.seite.click("#gif-auf");
|
|
await frida.seite.waitForSelector("#gif-tafel:not([hidden])", { timeout: 8000 });
|
|
const tafel = await frida.seite.evaluate(() => {
|
|
const t = document.getElementById("gif-tafel");
|
|
const r = t.getBoundingClientRect();
|
|
return { b: Math.round(r.width), h: Math.round(r.height),
|
|
imBild: r.left >= -1 && r.right <= innerWidth + 1 && r.top >= -1,
|
|
leer: (document.getElementById("gif-leer")?.textContent || "").trim().slice(0, 40) };
|
|
});
|
|
ok(tafel.imBild, `die Kiste passt auf den Bildschirm (${tafel.b}x${tafel.h})`);
|
|
ok(/noch leer/.test(tafel.leer), `und sagt, was zu tun ist ("${tafel.leer}…")`);
|
|
await frida.seite.setInputFiles("#gif-feld", {
|
|
name: "daumen.gif", mimeType: "image/gif", buffer: Buffer.from(GIF, "base64") });
|
|
await frida.seite.waitForTimeout(2400);
|
|
const kacheln = await frida.seite.locator(".gif-kachel").count();
|
|
ok(kacheln === 1, `ein GIF liegt in der Kiste (${kacheln})`);
|
|
const vorher2 = await nele.seite.locator(".chat-nachricht").count();
|
|
await frida.seite.click(".gif-kachel__knopf");
|
|
await frida.seite.waitForTimeout(2600);
|
|
const beiNele = await nele.seite.waitForFunction(
|
|
(v) => document.querySelectorAll(".chat-nachricht").length > v,
|
|
vorher2, { timeout: 12000 }).then(() => true).catch(() => false);
|
|
ok(beiNele, "das GIF kommt bei der zweiten Modi an");
|
|
/* Und die Kiste ist fuer BEIDE da -- sie gehoert dem Rudel. */
|
|
await nele.seite.click("#gif-auf");
|
|
await nele.seite.waitForTimeout(1600);
|
|
const beiIhr = await nele.seite.locator(".gif-kachel").count();
|
|
ok(beiIhr === 1, `die zweite Modi sieht dasselbe GIF in ihrer Kiste (${beiIhr})`);
|
|
await nele.seite.keyboard.press("Escape").catch(() => {});
|
|
}
|
|
|
|
console.log("\n=== 3. Der Ruf ans Rudel ===");
|
|
{
|
|
const vorher = await nele.seite.locator(".chat-nachricht").count();
|
|
await frida.seite.click("#text");
|
|
await frida.seite.type("#text", "@", { delay: 60 });
|
|
await frida.seite.waitForTimeout(700);
|
|
const liste = await frida.seite.evaluate(() => {
|
|
const l = document.getElementById("erwaehn-liste");
|
|
if (!l || l.hidden) return null;
|
|
const erste = l.querySelector(".erwaehn-liste__eintrag");
|
|
const r = erste?.getBoundingClientRect();
|
|
return { erster: l.querySelector(".erwaehn-liste__name")?.textContent.trim() || "",
|
|
zusatz: l.querySelector(".erwaehn-liste__rolle")?.textContent.trim() || "",
|
|
hoch: Math.round(r?.height || 0),
|
|
imBild: r ? (r.left >= -1 && r.right <= innerWidth + 1) : false };
|
|
});
|
|
ok(!!liste, "ein @ oeffnet die Liste");
|
|
ok(liste?.erster === "rudel", `der Ruf steht oben (${liste?.erster})`);
|
|
ok(/alle in diesem Chat/i.test(liste?.zusatz || ""), `mit der Erklaerung daneben (${liste?.zusatz})`);
|
|
ok((liste?.hoch || 0) >= 44 && liste?.imBild,
|
|
`am Daumen treffbar und im Bild (${liste?.hoch} px)`);
|
|
await frida.seite.keyboard.press("Enter");
|
|
await frida.seite.waitForTimeout(400);
|
|
await frida.seite.type("#text", "bitte alle kurz schauen", { delay: 10 });
|
|
/* NICHT MIT ENTER. Auf einem Beruehrgeraet schickt Enter
|
|
ABSICHTLICH nicht ab -- sonst kaeme man nie zu einer zweiten
|
|
Zeile (steht so in chat.html). Der erste Anlauf dieser Messung
|
|
hat genau das uebersehen und einen Fehler gemeldet, den es nicht
|
|
gibt: Die Nachricht war nie abgeschickt worden. */
|
|
await frida.seite.click("#senden");
|
|
await frida.seite.waitForTimeout(2800);
|
|
const beiNele = await nele.seite.waitForFunction(
|
|
(v) => document.querySelectorAll(".chat-nachricht").length > v,
|
|
vorher, { timeout: 12000 }).then(() => true).catch(() => false);
|
|
ok(beiNele, "die Nachricht kommt bei der zweiten Modi an");
|
|
const marke = await nele.seite.evaluate(() => {
|
|
const m = document.querySelector('.chat-erwaehnung[data-rudel="ja"]');
|
|
return m ? { wort: m.textContent.trim(),
|
|
gewicht: getComputedStyle(m).fontWeight } : null;
|
|
});
|
|
ok(!!marke, `und der Ruf leuchtet bei ihr (${marke?.wort}, Gewicht ${marke?.gewicht})`);
|
|
/* Und in der Datenbank steht, dass sie gemeint war. */
|
|
const db2 = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true });
|
|
const gemeint = db2.prepare(`SELECT e.person_id AS wer FROM chat_erwaehnungen e
|
|
JOIN chat_nachrichten n ON n.id = e.nachricht_id WHERE n.rudel = 1`).all().map((x) => x.wer);
|
|
db2.close();
|
|
ok(gemeint.includes(idB) && gemeint.includes(idDogi) && !gemeint.includes(idA),
|
|
`gerufen wurden ${gemeint.length} -- die andere Modi und DogFather, nicht sie selbst`);
|
|
}
|
|
|
|
/* =====================================================================
|
|
4. DER FARBRING (23.09.2026)
|
|
|
|
Filipe: "anstatt sowas um die farben auszusuchen, kannst du nicht
|
|
so eine art diagramm machen wo man mit einem kreis herum gehen kann
|
|
und sie die farbe ganz genau selber auswaehlen kann?"
|
|
|
|
WAS HIER WIRKLICH AUF DEM SPIEL STEHT, ist nicht die Optik. Die
|
|
zwoelf Kacheln waren gerechnet, damit niemand sich unlesbar machen
|
|
kann -- alle auf derselben Leuchtdichte. Ein Ring mit freier Wahl
|
|
ist genau die Gelegenheit, dieses Versprechen zu verlieren, und
|
|
zwar unbemerkt: Die Farbe sieht huebsch aus, und erst die anderen
|
|
koennen den Text nicht mehr lesen.
|
|
|
|
Deshalb misst diese Probe zuerst den KONTRAST und dann die
|
|
Bedienung.
|
|
===================================================================== */
|
|
{
|
|
console.log("");
|
|
console.log("=== 4. Der Farbring ===");
|
|
|
|
const { KREIS } = await import("./chat-kreis.js");
|
|
|
|
/* ---- 4a. Das Versprechen, an allen 360 Toenen ------------------ */
|
|
const schriftFarben = await frida.seite.evaluate(() => {
|
|
/* AM ECHTEN ELEMENT MESSEN, nicht an einem selbstgebauten.
|
|
Erster Anlauf legte ein <div class="chat-nachricht__blase"> an
|
|
und las dort -- und bekam zweimal nichts zurueck. Die beiden
|
|
Werte stehen naemlich eine Ebene hoeher, auf .chat-nachricht,
|
|
und werden von dort vererbt. Ergebnis war ein roter Haken an
|
|
einer Stelle, an der nichts kaputt war: die Messung war falsch,
|
|
nicht das Programm.
|
|
|
|
Erst die Blase im Verlauf; gibt es keine, ein Ersatzstueck mit
|
|
der RICHTIGEN Klasse. */
|
|
const echt = document.querySelector(".chat-nachricht");
|
|
const ziel = echt || document.body.appendChild(
|
|
Object.assign(document.createElement("div"), { className: "chat-nachricht" }));
|
|
const s2 = getComputedStyle(ziel);
|
|
const raus = { text: s2.getPropertyValue("--blase-text").trim(),
|
|
leise: s2.getPropertyValue("--blase-leise").trim(),
|
|
woher: echt ? "aus dem Verlauf" : "aus einem Ersatzstueck" };
|
|
if (!echt) ziel.remove();
|
|
return raus;
|
|
});
|
|
|
|
const zuZahl = (f) => {
|
|
const t = /^#?([0-9a-f]{6})$/i.exec(String(f).trim());
|
|
if (t) return [0, 2, 4].map((i) => parseInt(t[1].slice(i, i + 2), 16));
|
|
/* ==== DIE DRITTE SCHREIBWEISE (25.09.2026) ====================
|
|
|
|
Seit die Probe ihre Farbe aus `color-mix(in srgb, …)` bekommt,
|
|
gibt Chromium sie als `color(srgb 0.676 0.645 0.504)` zurueck --
|
|
Anteile von 0 bis 1, keine Ganzzahlen, kein Komma. Die zwei
|
|
Muster darueber trafen das nicht, `zuZahl` lieferte `null`, und
|
|
die Zeile wurde rot, obwohl die Farbe auf den Bildpunkt genau
|
|
stimmte (172,5 / 164,5 / 128,5 gegen erwartete 173/165/129).
|
|
|
|
Das ist die Sorte Fehlalarm, die teurer ist als ein echter
|
|
Befund: Man sucht am Aussehen, und der Fehler liegt im
|
|
Lesegeraet. */
|
|
const s = /color\(srgb\s+([^)]+)\)/i.exec(String(f));
|
|
if (s) {
|
|
return s[1].trim().split(/[\s/]+/).slice(0, 3)
|
|
.map((x) => Math.round(parseFloat(x) * 255));
|
|
}
|
|
const r = /rgba?\(([^)]+)\)/i.exec(String(f));
|
|
return r ? r[1].split(",").slice(0, 3).map((x) => Math.round(parseFloat(x))) : null;
|
|
};
|
|
const linear = (v) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
|
|
const leucht = (rgb) => 0.2126 * linear(rgb[0] / 255) + 0.7152 * linear(rgb[1] / 255)
|
|
+ 0.0722 * linear(rgb[2] / 255);
|
|
const kontrast = (a, b) => {
|
|
const x = leucht(a), y = leucht(b);
|
|
return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
|
|
};
|
|
|
|
const textRgb = zuZahl(schriftFarben.text);
|
|
const leiseRgb = zuZahl(schriftFarben.leise);
|
|
ok(!!textRgb && !!leiseRgb,
|
|
`die Schriftfarben der Blase sind lesbar, ${schriftFarben.woher}`
|
|
+ ` (${schriftFarben.text || "nichts"} / ${schriftFarben.leise || "nichts"})`);
|
|
|
|
/* ==== DIESE MESSUNG WURDE NACHGEZOGEN (25.09.2026) ==============
|
|
|
|
Sie rechnete: „Jeder der 360 Ringtoene traegt als FLAECHE die
|
|
Schrift der Blase." Das war richtig, solange die Blase in der
|
|
gewaehlten Farbe stand.
|
|
|
|
Seit dem 25.09.2026 tut sie das nicht mehr -- sie ist dunkles
|
|
Glas fuer alle, und der gewaehlte Ton ist Kante und NAME. Die
|
|
alte Rechnung haette also eine Beziehung geprueft, die es nicht
|
|
mehr gibt; sie wurde beim Umbau sofort rot (Winkel 299), und das
|
|
war die richtige Reaktion einer Pruefung auf eine geaenderte
|
|
Sache.
|
|
|
|
WAS SIE JETZT MISST, und es ist mehr wert als vorher: Jeder der
|
|
360 Toene muss als NAME auf dem Blasengrund lesbar sein. Das ist
|
|
die Stelle, an der ein Ton heute durchfallen kann -- ein dunkles
|
|
Blau etwa waere als Name auf dunklem Glas nicht zu lesen. Die
|
|
Oberflaeche hellt jeden Ton dafuer mit Weiss auf; wie stark, steht
|
|
als `--namen-anteil` in chat.css und wird hier GELESEN, nicht
|
|
abgeschrieben.
|
|
|
|
4,5 UND NICHT 7: Ein Name ist ein kurzes Wort in fetter Schrift.
|
|
Dieselbe Schwelle wie in pruef-chatkachel, aus demselben Grund.
|
|
================================================================= */
|
|
{
|
|
const { readFileSync: lies } = await import("node:fs");
|
|
const { fileURLToPath: alsPfad } = await import("node:url");
|
|
const { dirname: ordnerVon, join: fuege } = await import("node:path");
|
|
const cssTxt = lies(fuege(ordnerVon(alsPfad(import.meta.url)), "..",
|
|
"workspace", "assets", "css", "chat.css"), "utf8");
|
|
const grundTreffer = /--blasengrund:\s*(#[0-9a-fA-F]{3,6})\s*;/.exec(cssTxt);
|
|
const anteilTreffer = /--namen-anteil:\s*([0-9.]+)%\s*;/.exec(cssTxt);
|
|
if (!grundTreffer || !anteilTreffer) {
|
|
/* DER DRITTE AUSGANG: nicht gruen, nicht rot -- "kann nicht
|
|
nachsehen", mit Grund. Ein Rueckfall auf feste Werte waere
|
|
genau die zweite Wahrheit, gegen die hier gebaut wird. */
|
|
ok(false, "ABBRUCH: --blasengrund oder --namen-anteil fehlt in chat.css — "
|
|
+ "ohne sie laesst sich die Namensfarbe nicht nachrechnen");
|
|
} else {
|
|
const grund = zuZahl(grundTreffer[1]);
|
|
/* Die Blase ist zu 92 % deckend; dahinter liegt ein Foto. Weiss
|
|
ist der hellste denkbare Grund -- damit wird gerechnet. */
|
|
const schlimmerGrund = grund.map((v) => Math.round(v * 0.92 + 255 * 0.08));
|
|
const anteil = Number(anteilTreffer[1]) / 100;
|
|
const hell = zuZahl("#f2f8ff");
|
|
/* ==== JEDE STUFE AN JEDEM WINKEL (25.09.2026) =================
|
|
|
|
Filipe: „ich will dass die leute auch heller und dunkler
|
|
aussuchen koennen."
|
|
|
|
Seit heute gibt es zu jedem der 360 Winkel sieben
|
|
Helligkeiten -- 2520 Farben statt 360. Nur den Grundton zu
|
|
pruefen hiesse, sechs Siebtel des Angebots ungeprueft
|
|
auszuliefern; und die dunkelste Stufe ist genau die, bei der
|
|
es kippen wuerde (gemessen 4,62 gegen noetige 4,5 -- die
|
|
naechste Stufe darunter faellt durch).
|
|
|
|
DIE MISCHANTEILE WERDEN GELESEN, nicht abgeschrieben: Sie
|
|
stehen in chat-kreis-stufen.js, und wer sie dort aendert,
|
|
aendert damit diese Pruefung mit. Eine zweite Liste hier waere
|
|
die, die beim naechsten Umbau nicht mitzieht. */
|
|
const { KREIS_STUFEN } = await import("./chat-kreis-stufen.js");
|
|
const mischen2 = (rgb, misch) => {
|
|
if (!misch) return rgb;
|
|
const ziel = misch < 0 ? 0 : 255;
|
|
const t2 = Math.abs(misch);
|
|
return rgb.map((v) => Math.round(v * (1 - t2) + ziel * t2));
|
|
};
|
|
let schlimmster = Infinity, schlimmsterWinkel = -1, gemessen = 0;
|
|
let schlimmsteStufe = "";
|
|
for (let w = 0; w < KREIS.length; w++) {
|
|
const f = zuZahl(KREIS[w]);
|
|
if (!f) continue;
|
|
for (const st of KREIS_STUFEN) {
|
|
gemessen++;
|
|
const ton = mischen2(f, st.misch);
|
|
const name = ton.map((v, i) => Math.round(v * anteil + hell[i] * (1 - anteil)));
|
|
const v2 = kontrast(name, schlimmerGrund) / 4.5;
|
|
if (v2 < schlimmster) {
|
|
schlimmster = v2; schlimmsterWinkel = w; schlimmsteStufe = st.wert;
|
|
}
|
|
}
|
|
}
|
|
/* DIE ANZAHL STEHT IN DER BEDINGUNG. Waere die Tabelle leer,
|
|
bliebe schlimmster auf Infinity und die Zeile waere gruen,
|
|
ohne eine einzige Farbe angesehen zu haben. */
|
|
/* DIE ANZAHL STEHT IN DER BEDINGUNG -- und sie ist seit heute
|
|
360 mal die Zahl der Stufen. Waere die Stufenliste leer,
|
|
bliebe `gemessen` bei 0 und die Zeile waere gruen, ohne eine
|
|
einzige Farbe angesehen zu haben. */
|
|
ok(gemessen === 360 * KREIS_STUFEN.length && schlimmster >= 1,
|
|
`alle ${gemessen} Farben des Rings (360 Winkel x ${KREIS_STUFEN.length} `
|
|
+ `Helligkeiten) sind als Name auf der Blase lesbar (knappste: Winkel `
|
|
+ `${schlimmsterWinkel}, Stufe ${schlimmsteStufe}, Faktor ${schlimmster.toFixed(3)})`);
|
|
}
|
|
}
|
|
if (textRgb && leiseRgb) {
|
|
/* Und die Schrift der Blase selbst -- sie ist seit dem Umbau fest
|
|
und steht auf einem festen Grund. Zwei Zahlen statt 360. */
|
|
void textRgb; void leiseRgb;
|
|
}
|
|
|
|
/* ---- 4b. Steht der Ring da, und ist er zu treffen? ------------- */
|
|
/* ERST ZURUECK ZUR LISTE -- und das ist keine Umstaendlichkeit,
|
|
sondern der Weg, den ein Mensch am Handy geht. Der Farbknopf
|
|
steht in der Spalte mit den Gespraechen, und die weicht auf
|
|
einem schmalen Bildschirm zur Seite, sobald ein Gespraech offen
|
|
ist. Der erste Anlauf dieser Probe hat genau hier 30 Sekunden
|
|
lang auf einen Knopf gewartet, den es zwar gab, den aber niemand
|
|
sehen konnte -- ein gutes Beispiel dafuer, dass "ist im HTML"
|
|
und "ist erreichbar" zwei verschiedene Aussagen sind. */
|
|
const zurueck = frida.seite.locator("#zur-liste");
|
|
if (await zurueck.isVisible().catch(() => false)) {
|
|
await zurueck.click();
|
|
await frida.seite.waitForTimeout(600);
|
|
}
|
|
const knopfDa = await frida.seite.locator("#kachel-oeffnen").isVisible().catch(() => false);
|
|
ok(knopfDa, "der Farbknopf ist am Handy erreichbar (ueber die Gespraechsliste)");
|
|
if (!knopfDa) throw new Error("ohne den Knopf ist der Rest nicht messbar");
|
|
await frida.seite.click("#kachel-oeffnen");
|
|
await frida.seite.waitForTimeout(600);
|
|
|
|
const rad = await frida.seite.evaluate(() => {
|
|
const r = document.getElementById("kachelrad-ring");
|
|
if (!r) return null;
|
|
const k = r.getBoundingClientRect();
|
|
const g = getComputedStyle(r);
|
|
return {
|
|
breit: Math.round(k.width), hoch: Math.round(k.height),
|
|
links: Math.round(k.left), oben: Math.round(k.top),
|
|
imBild: k.left >= -1 && k.right <= innerWidth + 1
|
|
&& k.top >= -1 && k.bottom <= innerHeight + 1,
|
|
farben: (g.backgroundImage.match(/rgb\(/g) || []).length,
|
|
fassen: g.touchAction,
|
|
rolle: r.getAttribute("role"),
|
|
};
|
|
});
|
|
ok(!!rad, "der Ring ist da");
|
|
/* 180 px sind nicht geraten: Ein Ring, den man mit dem Daumen im
|
|
Kreis fuehren soll, braucht sichtbar mehr als die 44 px einer
|
|
Schaltflaeche -- sonst rutscht man beim Drehen heraus. */
|
|
ok((rad?.breit || 0) >= 180 && rad?.imBild,
|
|
`er passt auf den Bildschirm und ist gross genug (${rad?.breit}x${rad?.hoch} px)`);
|
|
/* Der Verlauf muss aus VIELEN Farben bestehen. Waere die Liste
|
|
nicht angekommen, stuende dort ein einzelner Ton -- und der Ring
|
|
saehe aus wie eine Scheibe. */
|
|
ok((rad?.farben || 0) >= 300, `er ist aus ${rad?.farben} Farben gemalt`);
|
|
/* OHNE DAS SCHIEBT DAS HANDY DIE SEITE WEG, statt den Ring zu
|
|
drehen. Genau diese Zeile ist es, die auf einem Beruehrgeraet
|
|
ueber brauchbar und unbrauchbar entscheidet. */
|
|
ok(rad?.fassen === "none", `Wischen dreht den Ring, statt zu scrollen (${rad?.fassen})`);
|
|
ok(rad?.rolle === "slider", `und eine Sprachausgabe versteht ihn (${rad?.rolle})`);
|
|
|
|
/* ---- 4c. Drehen -- und kommt die richtige Farbe heraus? -------- */
|
|
if (rad) {
|
|
/* Winkel 90 heisst DREI UHR: der Verlauf beginnt oben und laeuft
|
|
im Uhrzeigersinn. Waere die Rechnung im Browser um 90 Grad
|
|
verdreht (der uebliche atan2-Fehler), kaeme hier eine andere
|
|
Farbe heraus als die, die unter dem Finger liegt. */
|
|
const mx = rad.links + rad.breit / 2;
|
|
const my = rad.oben + rad.hoch / 2;
|
|
const r80 = (rad.breit / 2) * 0.8;
|
|
await frida.seite.mouse.move(mx, my - r80);
|
|
await frida.seite.mouse.down();
|
|
await frida.seite.mouse.move(mx + r80, my, { steps: 12 });
|
|
await frida.seite.mouse.up();
|
|
await frida.seite.waitForTimeout(900);
|
|
|
|
const stand = await frida.seite.evaluate(() => {
|
|
const r = document.getElementById("kachelrad-ring");
|
|
const p2 = document.querySelector(".kachelrad__probe");
|
|
const leiter = document.getElementById("kachelhell-leiter");
|
|
return { winkel: Number(r?.getAttribute("aria-valuenow")),
|
|
wort: r?.getAttribute("aria-valuetext") || "",
|
|
/* NICHT MEHR DER GRUND, SONDERN DIE SCHRIFT. Siehe unten. */
|
|
probe: p2 ? getComputedStyle(p2).color : "",
|
|
stufen: leiter ? leiter.children.length : 0,
|
|
stufeJetzt: leiter?.getAttribute("aria-valuenow") || "" };
|
|
});
|
|
ok(Math.abs((stand.winkel || 0) - 90) <= 2,
|
|
`nach dem Drehen auf drei Uhr steht der Ring bei ${stand.winkel} Grad (erwartet 90)`);
|
|
|
|
/* ==== DIE PROBE MISST SEIT HEUTE ETWAS ANDERES (25.09.2026) =====
|
|
|
|
Hier stand: „die Mitte zeigt genau die Farbe dieses Winkels" --
|
|
gemessen am HINTERGRUND der Mitte. Das war richtig, solange die
|
|
Blase in der gewaehlten Farbe stand.
|
|
|
|
Seit dem 25.09.2026 ist die Blase fuer alle dunkles Glas, und
|
|
der gewaehlte Ton ist der NAME darauf. Die Probe zeigte damit
|
|
eine Darstellung, die es im Chat nicht mehr gibt -- und die
|
|
Pruefung haette sie festgehalten.
|
|
|
|
GEMESSEN WIRD JETZT DIE SCHRIFT der Probe: Sie muss genau die
|
|
Farbe tragen, die ein Name in der Blase traegt -- der Ton, mit
|
|
`--namen-anteil` nach #f2f8ff aufgehellt. Das ist dieselbe
|
|
Rechnung wie in chat.css, und sie wird hier NACHGERECHNET statt
|
|
abgeschrieben. */
|
|
const grundTon = zuZahl(KREIS[stand.winkel] || "");
|
|
const hell = zuZahl("#f2f8ff");
|
|
const anteil = 0.5; /* --namen-anteil; weiter unten aus dem CSS geprueft */
|
|
const erwartet = grundTon
|
|
? grundTon.map((v, i) => Math.round(v * anteil + hell[i] * (1 - anteil)))
|
|
: null;
|
|
const gezeigt = zuZahl(stand.probe);
|
|
ok(!!erwartet && !!gezeigt
|
|
&& Math.abs(erwartet[0] - gezeigt[0]) <= 2
|
|
&& Math.abs(erwartet[1] - gezeigt[1]) <= 2
|
|
&& Math.abs(erwartet[2] - gezeigt[2]) <= 2,
|
|
`die Probe zeigt den Namen in genau dieser Farbe (${stand.probe} gegen `
|
|
+ `${erwartet ? "rgb(" + erwartet.join(", ") + ")" : "?"})`);
|
|
|
|
/* ==== UND DIE SIEBEN HELLIGKEITEN (25.09.2026) =================
|
|
|
|
Filipe: „ich will dass die leute auch heller und dunkler
|
|
aussuchen koennen … so dass die leute viel krassere
|
|
moeglichkeiten haben." */
|
|
ok(stand.stufen === 7,
|
|
`die Helligkeitsleiter hat sieben Stufen (${stand.stufen})`);
|
|
ok(stand.stufeJetzt === "4",
|
|
`und steht von sich aus in der Mitte (${stand.stufeJetzt})`);
|
|
|
|
/* EINE STUFE WAEHLEN UND NACHSEHEN, OB SIE ANKOMMT. Ohne das
|
|
waere „es gibt sieben Felder" ein Haken ueber Zierde: Sieben
|
|
Kaestchen, die nichts tun, sehen genauso aus. */
|
|
await frida.seite.click(".kachelhell__feld[data-stufe='s7']");
|
|
await frida.seite.waitForTimeout(700);
|
|
const hellStand = await frida.seite.evaluate(() => {
|
|
const p2 = document.querySelector(".kachelrad__probe");
|
|
const leiter = document.getElementById("kachelhell-leiter");
|
|
const an = leiter?.querySelector('[data-an="ja"]');
|
|
return { probe: p2 ? getComputedStyle(p2).color : "",
|
|
stufe: leiter?.getAttribute("aria-valuenow") || "",
|
|
markiert: an?.dataset.stufe || "" };
|
|
});
|
|
ok(hellStand.stufe === "7" && hellStand.markiert === "s7",
|
|
`eine hellere Stufe laesst sich waehlen (${hellStand.stufe}, ${hellStand.markiert})`);
|
|
const vorher2 = zuZahl(stand.probe), nachher2 = zuZahl(hellStand.probe);
|
|
ok(!!vorher2 && !!nachher2 && leucht(nachher2) > leucht(vorher2),
|
|
`und die Probe wird dabei wirklich heller `
|
|
+ `(${leucht(vorher2).toFixed(3)} auf ${leucht(nachher2).toFixed(3)})`);
|
|
|
|
await frida.seite.screenshot({ path: "server/pruef-farbkreis.png" });
|
|
|
|
/* Und zurueck in die Mitte, damit die Pruefungen danach mit dem
|
|
gewohnten Stand weiterarbeiten. */
|
|
await frida.seite.click(".kachelhell__feld[data-stufe='s4']");
|
|
await frida.seite.waitForTimeout(500);
|
|
ok(/\d+ Grad/.test(stand.wort) && stand.wort.length > 8,
|
|
`und sie wird vorgelesen als "${stand.wort}"`);
|
|
|
|
/* ---- 4d. Wird es gespeichert? ------------------------------- */
|
|
const db3 = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true });
|
|
const gespeichert = db3.prepare("SELECT chat_kachel FROM personen WHERE id = ?")
|
|
.get(idA)?.chat_kachel;
|
|
db3.close();
|
|
ok(gespeichert === "ton-" + stand.winkel,
|
|
`und in der Datenbank steht die Wahl (${gespeichert})`);
|
|
|
|
/* ---- 4e. Und mit der Tastatur? ------------------------------ */
|
|
await frida.seite.focus("#kachelrad-ring");
|
|
await frida.seite.keyboard.press("ArrowRight");
|
|
await frida.seite.waitForTimeout(200);
|
|
const nachher = await frida.seite.evaluate(() =>
|
|
Number(document.getElementById("kachelrad-ring")?.getAttribute("aria-valuenow")));
|
|
ok(nachher === (stand.winkel + 1) % 360,
|
|
`eine Pfeiltaste dreht um genau ein Grad weiter (${stand.winkel} -> ${nachher})`);
|
|
}
|
|
}
|
|
|
|
const alleMeldungen = [...frida.meckern, ...nele.meckern];
|
|
console.log("");
|
|
ok(alleMeldungen.length === 0,
|
|
alleMeldungen.length
|
|
? `ein Browser meldet etwas: ${alleMeldungen[0]}`
|
|
: "kein Browser meldet etwas");
|
|
|
|
await frida.ctx.close(); await nele.ctx.close();
|
|
await browser.close();
|
|
await new Promise((r) => vorbau.close(r));
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Rest */ }
|
|
|
|
console.log("");
|
|
console.log(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
console.log(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
|
process.exit(fehler ? 1 : 0);
|