Files
dogfather-universe/server/mess-chat-optik.mjs
T
DogFatherGit 077925ab25 Dreizehn rote Pruefungen, eine Ursache: openssl steht nicht im PATH
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
2026-09-29 16:14:13 +02:00

253 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
WIE DER CHAT AUSSIEHT (25.09.2026)
Filipe: „ich will dass du die komplette seite viel geiler und
moderner machst. die komplette kachel. die chat liste und die chats
selber. … es soll komplett aus der rolle fahren und was was wir noch
nie hatten."
SIE PRUEFT NICHTS und meldet keinen Fehlschlag -- sie macht Bilder,
auf denen man vergleichen kann. Deshalb `mess-` und nicht `pruef-`.
VIER BILDER, weil die Seite zwei Zustaende und zwei Breiten hat:
Liste ohne Gespraech / Liste mit offenem Gespraech, jeweils am
Rechner und am Handy. Wer nur eines ansieht, baut fuer eines.
EIGENE WEGWERF-DATENBANK: Es entstehen echte Gespraeche und
Nachrichten. In einem laufenden System waeren das Testdaten, und die
sind verboten. Hier liegt alles in einem `mktemp`-Ordner.
Aufrufen mit: node server/mess-chat-optik.mjs
===================================================================== */
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { execFileSync } from "node:child_process";
import { request as httpAnfrage } from "node:http";
import { createServer as httpsServer } from "node:https";
const PORT = 5403, HP = 5404;
const CREW = "crew.dogfather-universe.com";
const ordner = mkdtempSync(join(tmpdir(), "ws-chatoptik-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = String(PORT);
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
const express = (await import("express")).default;
const ec = express.response.cookie;
express.response.cookie = function (n, w, o) {
return ec.call(this, n, w, { ...(o || {}), secure: false });
};
import { notbremse } from "./helfer-notbremse.mjs";
import { zertifikatBauen } from "./helfer-openssl.mjs";
await import("./index.js");
notbremse(260_000, "mess-chat-optik");
await new Promise((r) => setTimeout(r, 900));
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
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 vorStunden = (h) => new Date(Date.now() - h * 3600_000).toISOString();
const sch = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", sch, jetzt);
const anlegen = (name, rolle, code) => {
const s = randomBytes(16).toString("hex");
const h = scryptSync(code, s, 64,
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
return d.prepare("INSERT INTO personen"
+ " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
+ " VALUES (?,?,?,?,?,?,1,?) RETURNING id")
.get(name, rolle, h, s, 32768,
createHmac("sha256", sch).update(code).digest("hex"), jetzt).id;
};
const idDogi = anlegen("Dogfather", "admin", "CODE-DOGI-0001");
const idVan = anlegen("VanVan", "hand", "CODE-HAND-0001");
const idFunny = anlegen("Funny", "linke", "CODE-LINK-0001");
const idDiene = anlegen("Diene", "modi", "CODE-MODI-0001");
const idMarina = anlegen("Marina", "modi", "CODE-MODI-0002");
const idGhost = anlegen("Ghost", "modi", "CODE-MODI-0003");
const idMiss = anlegen("Miss", "modi", "CODE-MODI-0004");
/* ---- Gespraeche und Nachrichten direkt in die Datenbank -------------
Ueber die Schnittstelle waeren es fuenfzehn Aufrufe fuer einen
Zustand, den es nur fuer das Bild braucht. */
const raum = (art, name, kategorie, haus = "crew") => d.prepare(
"INSERT INTO chat_raeume (art, name, kategorie, erstellt, erstellt_von, haus)"
+ " VALUES (?,?,?,?,?,?) RETURNING id").get(art, name, kategorie, jetzt, idDogi, haus).id;
const rein = (r, p) => d.prepare(
"INSERT INTO chat_teilnehmer (raum_id, person_id, seit, gelesen_bis) VALUES (?,?,?,0)")
.run(r, p, jetzt);
const sagen = (r, p, text, wann) => d.prepare(
"INSERT INTO chat_nachrichten (raum_id, person_id, text, erstellt) VALUES (?,?,?,?)")
.run(r, p, text, wann);
const rudel = raum("kanal", "Das Rudel", "treff");
for (const p of [idDogi, idVan, idFunny, idDiene, idMarina, idGhost, idMiss]) rein(rudel, p);
sagen(rudel, idFunny, "@rudel das mache ich heute Abend fertig, versprochen 🙌", vorStunden(20));
const modkanal = raum("kanal", "Chat-Moderation", "chat");
for (const p of [idDogi, idVan, idFunny, idDiene, idMarina, idGhost]) rein(modkanal, p);
sagen(modkanal, idMarina, "Ach man 😢😢", vorStunden(30));
const gruppe = raum("gruppe", "Abmeldungen 🏥", null);
for (const p of [idDogi, idVan, idFunny, idDiene, idMarina, idGhost, idMiss]) rein(gruppe, p);
sagen(gruppe, idDiene, "Guten Morgen ihr Lieben – ich bin heute leider raus, Hals tut weh.", vorStunden(31));
const mitVan = raum("direkt", null, null);
rein(mitVan, idDogi); rein(mitVan, idVan);
sagen(mitVan, idVan, "Da hat Miss was falsch eingegeben glaube ich", vorStunden(29));
sagen(mitVan, idDogi, "Hab's gesehen. Ich schreib ihr gleich, kein Ding.", vorStunden(28));
sagen(mitVan, idVan, "Top. Und die Abrechnung vom Wochenende liegt bei dir im Ordner.", vorStunden(27));
sagen(mitVan, idDogi, "Perfekt, danke dir ❤️", vorStunden(2));
const mitGhost = raum("direkt", null, null);
rein(mitGhost, idDogi); rein(mitGhost, idGhost);
sagen(mitGhost, idGhost, "Sorry hab's gefunden ist gelöscht 👍", vorStunden(29));
d.prepare("UPDATE chat_raeume SET letzte_am = ? WHERE id = ?").run(vorStunden(20), rudel);
d.prepare("UPDATE chat_raeume SET letzte_am = ? WHERE id = ?").run(vorStunden(2), mitVan);
d.prepare("UPDATE chat_raeume SET letzte_am = ? WHERE id = ?").run(vorStunden(29), mitGhost);
d.prepare("UPDATE chat_raeume SET letzte_am = ? WHERE id = ?").run(vorStunden(30), modkanal);
d.prepare("UPDATE chat_raeume SET letzte_am = ? WHERE id = ?").run(vorStunden(31), gruppe);
d.close();
const anfrage = (pfad, art, koerper) => new Promise((fertig, schief) => {
const a = httpAnfrage({
host: "127.0.0.1", port: PORT, path: pfad, method: art,
headers: { Host: CREW, "Content-Type": "application/json" },
}, (r) => {
let t = ""; r.on("data", (x) => { t += x; });
r.on("end", () => fertig({ code: r.statusCode, kopf: r.headers, text: t }));
});
a.on("error", schief);
if (koerper) a.write(JSON.stringify(koerper));
a.end();
});
const an = await anfrage("/workspace/api/anmelden", "POST",
{ rolle: "admin", code: "CODE-DOGI-0001" });
if (an.code !== 200) { console.log(`ABBRUCH: Anmeldung ${an.code}`); process.exit(2); }
const keks = (an.kopf["set-cookie"] || []).map((x) => x.split(";")[0]).join("; ");
const schl = join(ordner, "b.key"), zert = join(ordner, "b.crt");
zertifikatBauen(schl, zert, CREW);
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 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"],
});
const MARKE = process.env.MARKE || "jetzt";
try {
for (const [wie, breite, hoehe] of [["computer", 1440, 980], ["handy", 412, 915]]) {
/* MIT FINGER, WENN ES EIN HANDY IST.
Ohne `hasTouch` meldet der Browser `pointer: fine` -- und
damit greift KEINE Regel aus `@media (pointer: coarse)`.
Dort stehen im ganzen Haus die 44-Pixel-Beruehrziele und
seit dem 28.09. auch das Ausblenden der Tastenkuerzel. Eine
Handy-Messung mit Mauszeiger zeigt eine Seite, die auf
keinem Handy so aussieht. */
const kontext = await browser.newContext({
viewport: { width: breite, height: hoehe },
hasTouch: breite <= 860,
});
await kontext.addCookies(keks.split("; ").map((k) => {
const [n, ...r] = k.split("=");
return { name: n, value: r.join("="), domain: CREW, path: "/" };
}));
const seite = await kontext.newPage();
const konsole = [];
seite.on("console", (e) => {
if (e.type() === "error" && !/favicon|ERR_CERT|net::ERR|410/i.test(e.text())) {
konsole.push(e.text().slice(0, 140));
}
});
await seite.goto(`https://${CREW}:${HP}/workspace/chat.html`, { waitUntil: "networkidle" });
await seite.waitForTimeout(1600);
const liste = await seite.evaluate(() => ({
raeume: document.querySelectorAll("#raeume li").length,
namen: [...document.querySelectorAll(".chat-raum__name")].map((x) => x.textContent),
}));
console.log(`\n=== Chat, ${breite} x ${hoehe} (${wie}) ===`);
console.log(" Gespraeche:", liste.raeume, "|", liste.namen.join(" · "));
await seite.screenshot({ path: `server/mess-chat-${MARKE}-${wie}-liste.png` });
/* Und jetzt mit offenem Gespraech -- das ist die Haelfte der
Seite, die man am laengsten ansieht. */
await seite.evaluate(() => {
const zeilen = [...document.querySelectorAll("#raeume .chat-raum__knopf")];
(zeilen.find((z) => /VanVan/.test(z.textContent)) || zeilen[0])?.click();
});
await seite.waitForTimeout(1500);
const verlauf = await seite.evaluate(() => ({
nachrichten: document.querySelectorAll(".chat-nachricht").length,
titel: document.getElementById("verlauf-titel")?.textContent,
}));
console.log(" Verlauf:", verlauf.nachrichten, "Nachrichten in", verlauf.titel);
await seite.screenshot({ path: `server/mess-chat-${MARKE}-${wie}-verlauf.png` });
if (konsole.length) console.log(" FEHLER IN DER KONSOLE:", konsole);
console.log(` Bilder: server/mess-chat-${MARKE}-${wie}-{liste,verlauf}.png`);
if (breite <= 860) {
/* WIE VIELE BRECHEN UM -- nicht „die erste".
`querySelector` nimmt die erste Fusszeile, und wie breit
die ist, haengt vom Text darueber ab: Bei einer kurzen
Nachricht ist die Blase schmal, bei einer langen breit.
Gemessen an der ersten sah eine Aenderung deshalb mal
besser und mal schlechter aus, ohne dass sich etwas
geaendert hatte. Gefragt ist, wie viele Nachrichten
betroffen sind. */
const fuss = await seite.evaluate(() => {
const alle = [...document.querySelectorAll(".chat-nachricht__fuss")]
.map((f) => {
const kinder = [...f.children].map((e) => e.getBoundingClientRect());
/* DIE BREITESTE ZEILE, NICHT DIE SUMME. Seit die Uhrzeit
absichtlich in einer eigenen Zeile steht, waere eine
Summe ueber alle Kinder die Breite zweier Zeilen
uebereinander -- sie meldete 444 px in einer 234 px
breiten Fusszeile und sah wie ein Ueberlauf aus. */
const nach = new Map();
for (const q of kinder) {
const y = Math.round(q.top);
nach.set(y, (nach.get(y) || 0) + Math.round(q.width));
}
return { zeilen: nach.size, hoch: Math.round(f.getBoundingClientRect().height),
platz: Math.round(f.getBoundingClientRect().width),
inhalt: Math.max(...nach.values()) };
});
if (!alle.length) return null;
const um = alle.filter((x) => x.zeilen > 1);
return { gesamt: alle.length, um: um.length,
schlimmste: alle.reduce((a2, x) => (x.zeilen > a2.zeilen ? x : a2), alle[0]),
hoehe: Math.round(alle.reduce((a2, x) => a2 + x.hoch, 0) / alle.length) };
});
if (fuss) {
console.log(` Handgriffe: ${fuss.um} von ${fuss.gesamt} Fusszeilen brechen um, `
+ `im Schnitt ${fuss.hoehe} px hoch`);
console.log(` schlimmste: ${fuss.schlimmste.zeilen} Zeilen, `
+ `Platz ${fuss.schlimmste.platz} px, Inhalt ${fuss.schlimmste.inhalt} px`);
}
}
await kontext.close();
}
} finally {
await browser.close();
vorbau.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
process.exit(0);
}