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

307 lines
14 KiB
JavaScript

/* =====================================================================
DIE GIF-KISTE AUF DEM HANDY -- UND WAS PASSIERT, WENN MAN DRAUFTIPPT
=====================================================================
Filipe, 25.09.2026: „mach auch das mit den gifs fertig. eine
perfektion von perfektion bitte."
`pruef-gifs` misst den WEG (17 Pruefungen, alle gruen): hineinlegen,
anzeigen, verschicken, herausnehmen, und dass ein Creator nicht
herankommt. Was sie NICHT misst, ist das, was man sieht:
1. Wie die Kiste auf 390 px aussieht -- der Breite, auf der die
Modis tatsaechlich tippen. Eine Tafel, die am Rechner schoen
ist und auf dem Handy in einer Spalte klebt, ist auf dem
Handy kaputt, und keine Route merkt es.
2. Ob ein verschicktes GIF sich in der BILDSCHAU oeffnet (seit
gestern) -- und ob es dort noch laeuft. Ein GIF, das beim
Vergroessern zum Standbild wird, ist kein GIF mehr.
SIE PRUEFT NICHTS und meldet keinen Fehlschlag -- sie macht Bilder
und Zahlen, auf denen man nachsehen kann. Deshalb `mess-`.
EIGENE WEGWERF-DATENBANK in einem mktemp-Ordner: Es entstehen echte
GIFs und echte Nachrichten. In einem laufenden System waeren das
Testdaten, und die sind verboten.
Aufrufen mit: node server/mess-gifs-handy.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 = 5431, HP = 5432;
const CREW = "crew.dogfather-universe.com";
const ordner = mkdtempSync(join(tmpdir(), "ws-gifs-"));
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-gifs-handy");
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");
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;
};
anlegen("Filipe", "admin", "CODE-DOGI-0001");
const idHand = anlegen("Rieke", "hand", "CODE-HAND-0001");
d.close();
const anfrage = (pfad, art, koerper, keks, typ) => new Promise((fertig, schief) => {
const a = httpAnfrage({
host: "127.0.0.1", port: PORT, path: pfad, method: art,
headers: {
Host: CREW,
...(koerper ? { "Content-Type": typ || "application/json" } : {}),
...(keks ? { Cookie: keks } : {}),
},
}, (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(Buffer.isBuffer(koerper) ? koerper : 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("; ");
/* ---- Ein gueltiges GIF -------------------------------------------
Dasselbe wie in `pruef-gifs`: ein 1x1-Bildpunkt, der den strengen
Kopfcheck des Servers besteht ("GIF89a"). Mein erster Versuch war
ein selbstgebautes Zweibild-GIF und wurde mit 415 abgelehnt -- die
Bytefolge war naeher am Wunsch als an der Norm. Wo es eine
funktionierende Vorlage im Haus gibt, wird sie benutzt und nicht
nachgebaut.
Fuer diese Messung reicht ein Standbild: Gefragt sind die GROESSEN
der Kiste, ihrer Kacheln und Knoepfe -- und ob die Bildschau
aufgeht. Ob die Einzelbilder danach weiterlaufen, entscheidet der
Browser und nicht diese Seite. */
const GIF = Buffer.from(
"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7", "base64");
/* Die Route nimmt den nackten Rumpf mit `x-dateiname` im Kopf --
nachgesehen in workspace-chat.js, nicht geraten (multipart wurde
mit 415 abgelehnt). */
const rein = await new Promise((fertig, schief) => {
const req = httpAnfrage({
host: "127.0.0.1", port: PORT, path: "/workspace/api/chat/gifs", method: "POST",
headers: {
Host: CREW, Cookie: keks,
"x-dateiname": encodeURIComponent("Winken.gif"),
"Content-Type": "application/octet-stream",
"Content-Length": GIF.length,
Origin: `https://${CREW}`, Referer: `https://${CREW}/workspace/chat.html`,
},
}, (r) => {
let x = ""; r.on("data", (c) => { x += c; });
r.on("end", () => fertig({ code: r.statusCode, text: x }));
});
req.on("error", schief);
req.write(GIF);
req.end();
});
console.log(`GIF in die Kiste: HTTP ${rein.code} ${rein.text.slice(0, 90)}`);
/* Ein Gespraech, in dem das GIF landen kann. */
const raum = await anfrage("/workspace/api/chat/direkt", "POST", { mit: idHand }, keks);
const raumId = JSON.parse(raum.text || "{}").raum_id;
console.log(`Gespraech: HTTP ${raum.code}, Raum ${raumId}`);
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"],
});
async function blicken(wie, breite, hoehe, mobil) {
const kontext = await browser.newContext({
viewport: { width: breite, height: hoehe },
deviceScaleFactor: mobil ? 2 : 1, isMobile: mobil, hasTouch: mobil,
});
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/i.test(e.text())) {
konsole.push(e.text().slice(0, 140));
}
});
await seite.goto(`https://${CREW}:${HP}/workspace/chat.html?raum=${raumId}`,
{ waitUntil: "networkidle" });
await seite.waitForTimeout(1500);
console.log(`\n=== ${wie} (${breite} x ${hoehe}) ===`);
/* ---- 1. Die Kiste aufmachen ----------------------------------- */
const knopf = seite.locator("#gif-auf").first();
const knopfDa = await knopf.count();
if (knopfDa) await knopf.click().catch(() => {});
await seite.waitForTimeout(700);
const kiste = await seite.evaluate(() => {
const tafel = document.getElementById("gif-tafel");
if (tafel && tafel.hidden) return { da: true, zu: true };
if (!tafel) return { da: false };
const r = tafel.getBoundingClientRect();
const st = getComputedStyle(tafel);
const kacheln = [...tafel.querySelectorAll("img")].map((b) => {
const k = b.getBoundingClientRect();
return { b: Math.round(k.width), h: Math.round(k.height), src: b.currentSrc || b.src };
});
const knoepfe = [...tafel.querySelectorAll("button")].map((b) => {
const k = b.getBoundingClientRect();
return { t: (b.textContent || b.getAttribute("aria-label") || "").trim().slice(0, 18),
b: Math.round(k.width), h: Math.round(k.height) };
});
return {
da: true,
links: Math.round(r.left), rechts: Math.round(r.right),
breite: Math.round(r.width), hoehe: Math.round(r.height),
ausDemBild: r.left < 0 || r.right > window.innerWidth,
spalten: st.gridTemplateColumns,
kacheln, knoepfe,
/* NUR AM FINGER STRENG. Die 44 px sind die Hausgroesse fuer
Beruehrziele -- am Rechner waere derselbe Massstab falsch:
Dort ist ein 28-px-Kreis in der Ecke einer Kachel richtig, und
ein Knopf von 44 px wuerde die halbe Kachel verdecken. Der
erste Anlauf dieser Messung hat am Rechner „ZU KLEIN" gemeldet
und damit zwei Stellen als Befund ausgegeben, die Absicht
sind. Eine Messung, die den Zusammenhang nicht kennt, erzeugt
Arbeit statt Erkenntnis. */
zuKlein: (matchMedia("(pointer: coarse)").matches
? knoepfe.filter((k) => k.h > 0 && k.h < 44).map((k) => `${k.t} ${k.h}px`)
: []),
};
});
console.log(` Knopf da: ${knopfDa ? "ja" : "NEIN"}`);
if (!kiste.da) console.log(" Tafel: NICHT GEFUNDEN");
else {
console.log(` Tafel ${kiste.breite}x${kiste.hoehe} px, links ${kiste.links}, `
+ `rechts ${kiste.rechts}${kiste.ausDemBild ? " STEHT AUS DEM BILD" : ""}`);
console.log(` Spalten ${kiste.spalten}`);
console.log(` Kacheln ${kiste.kacheln.map((k) => `${k.b}x${k.h}`).join(", ") || "keine"}`);
console.log(` Knoepfe ${kiste.knoepfe.map((k) => `"${k.t}" ${k.b}x${k.h}`).join(", ")}`);
if (kiste.zuKlein.length) console.log(` ZU KLEIN zum Tippen: ${kiste.zuKlein.join(", ")}`);
}
await seite.screenshot({ path: `server/mess-gifs-${wie}-kiste.png` }).catch(() => {});
/* ---- 2. Verschicken und in der Bildschau oeffnen --------------- */
const kachel = seite.locator("#gif-tafel .gif-kachel__knopf").first();
if (await kachel.count()) {
await kachel.click().catch(() => {});
await seite.waitForTimeout(1200);
}
const imVerlauf = await seite.evaluate(() => {
const bilder = [...document.querySelectorAll(".chat-nachricht img, .chat-anhang img")];
const g = bilder.find((b) => /\.gif|gifs\/|anhang/.test(b.currentSrc || b.src));
if (!g) return { da: false, wieviele: bilder.length };
const r = g.getBoundingClientRect();
return { da: true, b: Math.round(r.width), h: Math.round(r.height),
src: (g.currentSrc || g.src).slice(-40) };
});
console.log(` Im Verlauf: ${imVerlauf.da
? `${imVerlauf.b}x${imVerlauf.h} px (${imVerlauf.src})`
: `NICHT GEFUNDEN (${imVerlauf.wieviele} Bilder im Verlauf)`}`);
if (imVerlauf.da) {
await seite.locator(".chat-nachricht img, .chat-anhang img").first().click()
.catch(() => {});
await seite.waitForTimeout(900);
const schau = await seite.evaluate(() => {
const dlg = document.querySelector("dialog.bildschau");
if (!dlg) return { da: false };
/* und NICHT das erste <img>: Der Dialog traegt
oben ein Husky-Zeichen und an den Seiten zwei Figuren. Mein
erster Anlauf nahm und maass damit das
Logo -- die Meldung lautete "Mitte 174 von 640", und die
Bildschau sah kaputt aus, obwohl sie es nicht war. Ein
Messfehler, der wie ein Befund aussieht, kostet mehr Zeit als
gar keine Messung. */
/* `.bildschau__bild` und NICHT das erste <img>: Der Dialog
traegt oben ein Husky-Zeichen und an den Seiten zwei Figuren.
Der erste Anlauf nahm `querySelector("img")` und maass damit
das Logo -- die Meldung lautete „Mitte 174 von 640", und die
Bildschau sah kaputt aus, obwohl sie mittig stand. Ein
Messfehler, der wie ein Befund aussieht, kostet mehr Zeit als
gar keine Messung. */
const b = dlg.querySelector(".bildschau__bild");
const r = b?.getBoundingClientRect();
return {
da: true, offen: dlg.open === true,
bildSrc: (b?.currentSrc || b?.src || "").slice(-40),
bild: r ? `${Math.round(r.width)}x${Math.round(r.height)}` : "kein Bild",
istGif: /\.gif|gifs\/|anhang/.test(b?.currentSrc || b?.src || ""),
mitte: r ? Math.round(r.left + r.width / 2) : 0,
fensterMitte: Math.round(window.innerWidth / 2),
};
});
if (!schau.da) console.log(" Bildschau: GEHT NICHT AUF");
else {
console.log(` Bildschau offen=${schau.offen}, Bild ${schau.bild}, `
+ `GIF=${schau.istGif}, Mitte ${schau.mitte} (Fenster ${schau.fensterMitte})`);
await seite.screenshot({ path: `server/mess-gifs-${wie}-schau.png` }).catch(() => {});
}
}
if (konsole.length) console.log(` FEHLER IN DER KONSOLE: ${konsole.join(" | ")}`);
await kontext.close();
}
try {
await blicken("handy", 390, 844, true);
await blicken("rechner", 1280, 900, false);
} finally {
await browser.close();
vorbau.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
process.exit(0);
}