Files
dogfather-universe/server/mess-chat-neu-stelle.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

262 lines
12 KiB
JavaScript

/* =====================================================================
WO STEHT DER VERLAUF, WENN MAN IHN AUFMACHT? (25.09.2026)
=====================================================================
Filipe: „wenn ich in den chat rein gehe und es neue kommentare gibt,
will ich dass mein chat sich da öffnet wo die neuen nachrichten
anfangen die ich noch nicht gesehen hab bitte und nicht immer ganz
unten. sonst muss man immer hoch scrollen um die neuen zu lesen und
das ist scheisse."
IM QUELLTEXT STEHT DAS SEIT DEM 09.09.2026 -- eine Linie „Ab hier
neu" und ein Sprung darauf. Filipe sagt, es passiert nicht. Also
wird gemessen statt gelesen: Zwei Menschen, echte Nachrichten, und
danach die Frage, wo der Verlauf wirklich steht.
GEMESSEN WIRD IN PIXELN, nicht im Gefuehl: Wie weit ist die Linie
vom oberen Rand des sichtbaren Bereichs entfernt? Steht sie im Bild?
Und wie viel Verlauf liegt darunter -- also: wie weit haette man
scrollen muessen?
Aufrufen mit: node server/mess-chat-neu-stelle.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 = 5491, HP = 5492;
const CREW = "crew.dogfather-universe.com";
const ordner = mkdtempSync(join(tmpdir(), "ws-neustelle-"));
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(300_000, "mess-chat-neu-stelle");
await new Promise((r) => setTimeout(r, 900));
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", 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 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("Filipe", "admin", "CODE-DOGI-0001");
const idA = anlegen("Kessi", "modi", "CODE-MODI-0001");
d.close();
const roh = (keks, pfad, methode = "GET", koerper = null) =>
new Promise((fertig, schief) => {
const daten = koerper === null ? null : JSON.stringify(koerper);
const a = httpAnfrage({
host: "127.0.0.1", port: PORT, path: pfad, method: methode,
headers: {
Host: CREW, ...(keks ? { Cookie: keks } : {}),
Origin: `https://${CREW}`, Referer: `https://${CREW}/workspace/chat.html`,
...(daten ? { "Content-Type": "application/json",
"Content-Length": Buffer.byteLength(daten) } : {}),
},
}, (r) => {
let t = ""; r.on("data", (x) => { t += x; });
r.on("end", () => fertig({ status: r.statusCode, kopf: r.headers,
json: () => { try { return JSON.parse(t); } catch { return {}; } } }));
});
a.on("error", schief);
if (daten) a.write(daten);
a.end();
});
const anmelden = async (rolle, code) => {
const a = await roh(null, "/workspace/api/anmelden", "POST", { rolle, code });
return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
};
const kDogi = await anmelden("admin", "CODE-DOGI-0001");
const kA = await anmelden("modi", "CODE-MODI-0001");
const gr = await roh(kDogi, "/workspace/api/chat/direkt", "POST", { mit: idA });
const raumId = gr.json().raum_id;
console.log(`Raum #${raumId}`);
const schreiben = (keks, text) =>
roh(keks, `/workspace/api/chat/raeume/${raumId}/nachrichten`, "POST", { text });
/* ---- 20 Nachrichten, die DogFather schon gelesen hat -------------- */
let letzteAlte = 0;
for (let i = 1; i <= 20; i++) {
const a = await schreiben(kA, `Alte Nachricht ${i} von Kessi.`);
letzteAlte = a.json().nachricht?.id || a.json().id || letzteAlte;
}
/* DogFather macht auf und liest. `bis` IST PFLICHT -- die Route
verlangt eine Nummer und lehnt sonst mit 400 ab. Im ersten Anlauf
habe ich ein leeres Objekt geschickt; `gelesen_bis` blieb damit auf
null, die Linie „Ab hier neu" entstand nie, und die Messung meldete
einen Fehler, den es nicht gab. Eine Messung, die falsch aufsetzt,
findet zuverlaessig Befunde -- nur nicht die echten. */
await roh(kDogi, `/workspace/api/chat/raeume/${raumId}/nachrichten`);
const gel = await roh(kDogi, `/workspace/api/chat/raeume/${raumId}/gelesen`,
"PUT", { bis: letzteAlte });
console.log(`gelesen bis #${letzteAlte}: HTTP ${gel.status}`);
await new Promise((r) => setTimeout(r, 300));
/* ---- Und jetzt zwoelf neue, die er NICHT gesehen hat -------------- */
for (let i = 1; i <= 12; i++) await schreiben(kA, `NEUE Nachricht ${i} von Kessi.`);
const stand = await roh(kDogi, `/workspace/api/chat/raeume/${raumId}/nachrichten`);
console.log(`gelesen_bis beim Oeffnen: ${stand.json().gelesen_bis}`
+ `, Nachrichten: ${(stand.json().nachrichten || []).length}`);
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"],
});
for (const [wie, breite, hoehe, mobil] of [
["handy", 390, 844, true], ["rechner", 1280, 900, false],
]) {
/* ==== VOR JEDEM DURCHLAUF DEN LESESTAND ZURUECKSETZEN ===========
Beim Oeffnen meldet die Seite „gelesen bis hier" -- das ist
richtig und hat im ersten Anlauf den zweiten Durchlauf ruiniert:
Nach dem Handy war nichts mehr neu, die Linie entstand nicht, und
die Messung meldete fuer den Rechner „Linie FEHLT". Ein Befund,
den die Messung selbst erzeugt hatte.
Zurueckgesetzt wird direkt in der Datenbank und nicht ueber die
Route: Die laesst `bis` absichtlich nicht kleiner werden. */
const kontext = await browser.newContext({
viewport: { width: breite, height: hoehe },
deviceScaleFactor: mobil ? 2 : 1, isMobile: mobil, hasTouch: mobil,
});
await kontext.addCookies(kDogi.split("; ").map((k) => {
const [x, ...r] = k.split("=");
return { name: x, 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/i.test(e.text())) {
konsole.push(e.text().slice(0, 140));
}
});
/* DER LESESTAND WIRD UNMITTELBAR VOR DEM AUFRUF ZURUECKGESETZT.
Beim Oeffnen meldet die Seite „gelesen bis hier" -- richtig, und
im ersten Anlauf hat es den zweiten Durchlauf ruiniert: Nach dem
Handy war nichts mehr neu, die Linie entstand nicht, und die
Messung meldete fuer den Rechner „Linie FEHLT". Ein Befund, den
die Messung selbst erzeugt hatte.
Der Reset stand zuerst vor `newContext` -- ein paar Millisekunden
zu frueh: Die verzoegerte Meldung des vorigen Durchlaufs kam
danach. Jetzt steht er unmittelbar vor dem Aufruf, mit einer
kurzen Pause davor, damit der vorige Browser wirklich fertig ist.
Zurueckgesetzt wird in der Datenbank und nicht ueber die Route:
Die laesst `bis` absichtlich nicht kleiner werden. */
await new Promise((r) => setTimeout(r, 600));
{
const dd = new DatabaseSync(process.env.WORKSPACE_DB);
dd.prepare("UPDATE chat_teilnehmer SET gelesen_bis = ? WHERE raum_id = ? AND person_id = ?")
.run(letzteAlte, raumId, idDogi);
dd.close();
}
await seite.goto(`https://${CREW}:${HP}/workspace/chat.html?raum=${raumId}`,
{ waitUntil: "networkidle" });
await seite.waitForTimeout(2200);
const lage = await seite.evaluate(() => {
const v = document.getElementById("verlauf");
if (!v) return { da: false };
const linie = v.querySelector(".chat-neu");
const alle = [...v.querySelectorAll(".chat-nachricht")];
const sicht = v.getBoundingClientRect();
const ersteSichtbare = alle.find((z) => {
const r = z.getBoundingClientRect();
return r.bottom > sicht.top + 4;
});
return {
da: true,
hoehe: Math.round(v.clientHeight),
gesamt: Math.round(v.scrollHeight),
oben: Math.round(v.scrollTop),
amEnde: v.scrollHeight - v.scrollTop - v.clientHeight < 40,
linieDa: !!linie,
linieOben: linie ? Math.round(linie.offsetTop) : null,
/* Steht die Linie IM BILD? Das ist die eigentliche Frage. */
linieImBild: linie
? (linie.getBoundingClientRect().top >= sicht.top - 2
&& linie.getBoundingClientRect().bottom <= sicht.bottom + 2)
: false,
linieAbstandVonOben: linie
? Math.round(linie.getBoundingClientRect().top - sicht.top) : null,
ersteSichtbar: ersteSichtbare
? (ersteSichtbare.textContent || "").trim().slice(0, 34) : "-",
nachrichten: alle.length,
/* Wie weit muesste man hochscrollen, um die Linie zu sehen? */
muessteHoch: linie
? Math.max(0, Math.round(v.scrollTop - linie.offsetTop)) : null,
};
});
console.log(`\n=== ${wie} (${breite}x${hoehe}) ===`);
if (!lage.da) { console.log(" KEIN VERLAUF"); }
else {
console.log(` Verlauf ${lage.hoehe} px hoch, Inhalt ${lage.gesamt} px, `
+ `steht bei ${lage.oben}${lage.amEnde ? " (GANZ UNTEN)" : ""}`);
console.log(` Linie „Ab hier neu": ${lage.linieDa ? "da" : "FEHLT"}`
+ (lage.linieDa ? `, bei ${lage.linieOben} px` : ""));
if (lage.linieDa) {
console.log(` Im Bild: ${lage.linieImBild ? "JA" : "NEIN"}`
+ `, ${lage.linieAbstandVonOben} px unter dem oberen Rand`
+ (lage.muessteHoch ? ` -- man muesste ${lage.muessteHoch} px HOCHSCROLLEN` : ""));
}
console.log(` Oben im Bild steht: "${lage.ersteSichtbar}"`);
console.log(` ${lage.nachrichten} Nachrichten im Verlauf`);
}
if (konsole.length) console.log(` KONSOLE: ${konsole.join(" | ")}`);
await seite.screenshot({ path: `server/mess-neustelle-${wie}.png` }).catch(() => {});
await kontext.close();
}
await browser.close();
vorbau.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
process.exit(0);