Files
dogfather-universe/server/mess-kachelreihen.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

246 lines
11 KiB
JavaScript

/* =====================================================================
WIE STEHEN DIE KACHELN — je Rolle, Gruppe für Gruppe (24.09.2026)
---------------------------------------------------------------------
WARUM ES DIESE DATEI GIBT
Filipe schickt seine Wünsche zur Startseite fast immer als
Bildschirmfoto: „die kachel auf screen 2 soll zwischen den kacheln
auf screen3 sein", „wechsel bitte die reihenfolge, links soll
unterstützen sein". Beides sind Fragen nach einer REIHE, nicht nach
einer Liste.
Und genau da liegt die Falle: Die Reihenfolge im Quelltext ist NICHT
die Reihenfolge auf dem Bildschirm.
* Das Raster hat drei Spalten, „Willkommen" belegt zwei — alles
danach verschiebt sich um eins.
* Gruppiert wird nach dem ERSTEN Auftreten einer Gruppe im Feld;
eine Kachel am Ende der Liste kann dadurch mitten auf der Seite
landen.
* Und eine Kachel ohne `gruppe` bekommt einen eigenen Abschnitt
ohne Überschrift — genau so stand der Support einen halben Tag
lang allein in einem namenlosen Kasten, und beim Lesen des
Quelltextes sieht man das nicht.
Wer das im Kopf ausrechnet, rechnet früher oder später falsch.
Deshalb wird es hier GEMESSEN: angemeldet, im Browser, je Rolle.
Sie prüft NICHTS und meldet keinen Fehlschlag. Sie beantwortet eine
Frage, die man vor jeder Umsortierung stellen muss.
Sie heißt `mess-` und nicht `pruef-`: Die Portvergabe leitet sich
aus den `pruef-`-Namen ab, und eine Messdatei darf die Nummern der
Prüfungen nicht verschieben.
Aufrufen mit: node server/mess-kachelreihen.mjs
Nur eine Rolle: ROLLE=modi node server/mess-kachelreihen.mjs
Bild: SCHIRM=1 node server/mess-kachelreihen.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 = 5387, HP = 5388;
const CREW = "crew.dogfather-universe.com";
const ordner = mkdtempSync(join(tmpdir(), "ws-kachelreihen-"));
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(240_000, "mess-kachelreihen");
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 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");
d.prepare("INSERT INTO personen"
+ " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
+ " VALUES (?,?,?,?,?,?,1,?)")
.run(name, rolle, h, s, 32768,
createHmac("sha256", sch).update(code).digest("hex"), jetzt);
};
/* Die fünf Rollen, die auf der Crew-Adresse eine Startseite haben. */
const ROLLEN = [
["Filipe", "admin", "CODE-DOGI-0001"],
["VanVan", "hand", "CODE-HAND-0001"],
["Kessi", "linke", "CODE-LINKE-0001"],
["Diene", "modi", "CODE-MODI-0001"],
["Zuschauer", "gast", "CODE-GAST-0001"],
];
for (const [name, rolle, code] of ROLLEN) anlegen(name, rolle, code);
d.close();
/* ÜBER DIE CREW-ADRESSE, nicht über 127.0.0.1: Welches Haus gemeint
ist, leitet der Server aus dem `Host`-Kopf ab. Über die Zahladresse
landet man im Agenturhaus, und dort gibt es weder `gast` noch
`modi` — man misst dann eine Seite, die niemand so sieht. */
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 kekse = {};
for (const [, rolle, code] of ROLLEN) {
const an = await anfrage("/workspace/api/anmelden", "POST",
rolle === "gast" ? { rolle, code, alter_ok: true } : { rolle, code });
if (an.code !== 200) {
console.log(` ABBRUCH: ${rolle} ließ sich nicht anmelden (HTTP ${an.code}). `
+ "Ohne Anmeldung gibt es keine Startseite zu messen.");
process.exit(2);
}
kekse[rolle] = (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"],
});
try {
const nur = process.env.ROLLE;
for (const [, rolle] of ROLLEN) {
if (nur && nur !== rolle) continue;
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1800 } });
await kontext.addCookies(kekse[rolle].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/i.test(e.text())) {
konsole.push(e.text().slice(0, 110));
}
});
await seite.goto(`https://${CREW}:${HP}/workspace/start.html`, { waitUntil: "networkidle" });
await seite.waitForTimeout(800);
const gruppen = await seite.evaluate(() =>
[...document.querySelectorAll("section")].map((s) => {
const titel = s.querySelector("h2, .gruppe__titel")?.textContent?.trim() || "";
const k = [...s.querySelectorAll(".kachel")].map((x) => {
const r = x.getBoundingClientRect();
return {
n: x.querySelector(".kachel__name")?.textContent,
x: Math.round(r.x), y: Math.round(r.y), b: Math.round(r.width),
};
});
return k.length ? { titel, k } : null;
}).filter(Boolean));
console.log("");
console.log(`=== ${rolle} ===`);
for (const g of gruppen) {
/* NACH ZEILEN SORTIEREN, nicht nach Feldreihenfolge -- das ist ja
gerade die Frage. Gerundet auf 20 px, damit zwei Kacheln
derselben Zeile mit leicht verschiedener Oberkante
zusammenbleiben. */
const zeilen = new Map();
for (const k of g.k) {
const y = Math.round(k.y / 20) * 20;
if (!zeilen.has(y)) zeilen.set(y, []);
zeilen.get(y).push(k);
}
const sortiert = [...zeilen.entries()].sort((a, b) => a[0] - b[0]);
const name = g.titel || "!! OHNE ÜBERSCHRIFT !!";
console.log(` [${name}]`);
for (const [, ks] of sortiert) {
console.log(" " + ks.sort((a, b) => a.x - b.x)
.map((k) => `${k.n} (${k.b}px)`).join(" | "));
}
}
/* EIN ABSCHNITT OHNE ÜBERSCHRIFT IST EIN BEFUND, kein Zufall:
Er entsteht, wenn eine Kachel keine `gruppe` hat. */
const ohne = gruppen.filter((g) => !g.titel);
if (ohne.length) {
console.log(` !! ${ohne.length} Abschnitt(e) OHNE Überschrift: `
+ ohne.map((g) => g.k.map((k) => k.n).join(", ")).join(" / "));
}
if (konsole.length) console.log(" !! Konsole: " + konsole[0]);
if (process.env.SCHIRM) {
/* MIT `GRUPPE="Für dich"` nur diesen einen Abschnitt -- ein Bild
der ganzen Seite ist 1280 mal viertausend Pixel, und darauf
sucht man die drei Kacheln, um die es gerade geht. */
const nurGruppe = process.env.GRUPPE;
const window0 = await seite.evaluate(() => window.scrollY);
const feld = nurGruppe ? await seite.evaluate((titel) => {
const s2 = [...document.querySelectorAll("section")]
.find((x) => (x.querySelector("h2, .gruppe__titel")?.textContent || "").trim() === titel);
return s2 ? s2.getBoundingClientRect().toJSON() : null;
}, nurGruppe) : null;
const datei = `server/mess-kachelreihen-${rolle}.png`;
try {
if (feld) {
/* MIT `fullPage`, auch beim Ausschnitt: Die Gruppe liegt
meist unterhalb des Fensters, und ohne fullPage kennt
Playwright dort keine Koordinaten -- der Aufruf scheitert.
Gemessen am 24.09.2026, als genau das passierte. */
await seite.screenshot({ path: datei, fullPage: true, clip: {
x: Math.max(0, feld.x - 10), y: Math.max(0, feld.y + window0 - 10),
width: Math.min(1280, feld.width + 20), height: feld.height + 20 } });
} else {
await seite.screenshot({ path: datei, fullPage: true });
}
console.log(` -- Bild: ${datei}`);
} catch (f) {
/* NICHT STILL SCHEITERN. Ein Lauf, der erfolgreich aussieht und
kein Bild hinterlaesst, kostet drei Wiederholungen. */
console.log(` !! Bild ging nicht: ${String(f?.message || f).slice(0, 120)}`);
}
}
await kontext.close();
}
} finally {
await browser.close();
try { vorbau.close(); } catch { /* egal */ }
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
process.exit(0);
}