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

226 lines
10 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.
/* =====================================================================
KOMMT MAN AUS EINEM FENSTER WIEDER HERAUS? (25.09.2026)
---------------------------------------------------------------------
DER ANLASS
Miss, über den Support-Weg: „Ich konnte da wieder nicht zurück gehen,
musste die App schließen damit ich wieder auf die Hauptseite kam."
Dazu ein Bildschirmfoto von „Aufgabe bearbeiten" auf dem Handy.
„WIEDER" IST DAS WICHTIGE WORT: Es ist nicht das erste Mal. Ein
Fenster, aus dem man nur herauskommt, indem man die App beendet, ist
die schlimmste Sorte Sackgasse -- man verliert dabei auch alles, was
man gerade eingetippt hat.
---------------------------------------------------------------------
WAS GEMESSEN WIRD
Für JEDES Fenster auf JEDER Seite, auf der es eines gibt:
1. Ist ein Ausgang (Abbrechen/Schließen) überhaupt im Baum?
2. Liegt er INNERHALB des sichtbaren Bereichs -- oder unterhalb?
3. Und wenn er unterhalb liegt: Lässt sich dorthin SCROLLEN?
Punkt 3 ist der eigentliche: Ein Knopf weiter unten ist harmlos,
solange man hinkommt. `.dialog` hat aber `overflow: hidden`, und nur
`.dialog--breit .neu` bekommt eine Scroll-Höhe. Bei allen anderen
wird der Rest schlicht abgeschnitten.
GEMESSEN WIRD AUF 412 x 915 -- der häufigsten Android-Größe. Auf dem
Rechner fällt das nie auf: Dort ist der Bildschirm hoch genug.
Aufrufen mit: node server/mess-dialog-ausgang.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 = 5399, HP = 5400;
const CREW = "crew.dogfather-universe.com";
const ordner = mkdtempSync(join(tmpdir(), "ws-dialog-"));
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-dialog-ausgang");
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;
};
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
const idModi = anlegen("Miss", "modi", "CODE-MODI-0001");
/* Eine Aufgabe, die dem Modi gehoert -- sonst gibt es nichts zu
bearbeiten, und die Messung waere gruen ueber einer Null. */
d.prepare(`INSERT INTO aufgaben (titel, beschreibung, status, prioritaet,
verantwortlich_id, frist, erstellt, erstellt_von, haus)
VALUES (?,?, 'offen', 'hoch', ?, ?, ?, ?, 'crew')`)
.run("Die Regeln des Rudels einmal ganz lesen",
"Nicht überfliegen. Wer sie kennt, kann sie erklären – und Erklären ist in "
+ "neun von zehn Fällen wirksamer als Löschen.",
idModi, "2026-09-26", jetzt, idDogi);
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: "modi", code: "CODE-MODI-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"],
});
/* Die Hoehen, die es wirklich gibt -- nicht die aus dem Datenblatt.
`app` ist die installierte App (nur die Systemleiste), `browser`
zusaetzlich mit Adresszeile, `klein` ein aelteres Geraet, `tastatur`
dasselbe Geraet mit offener Bildschirmtastatur. Die letzte ist die
wichtigste: Man hat ja gerade etwas eingetippt. */
const GROESSEN = [
["app", 412, 915],
["browser", 412, 780],
["klein", 360, 640],
["tastatur", 412, 430],
];
let schlimm = 0;
try {
/* MEHRERE GROESSEN UND NICHT EINE (25.09.2026). Der erste Anlauf
mass nur 412 x 915 -- die Angabe aus dem Geraetedatenblatt. Dort
passte es KNAPP: 782 px Inhalt in 784 px Fenster, Ausgang gerade
noch sichtbar, und die Messung meldete "JA".
Das ist der BESTFALL und nicht der Alltag: Im Browser steht die
Adresszeile darueber, in der App die Systemleiste, und beim Tippen
kommt die Tastatur. Jede dieser Hoehen ist kleiner. Eine Messung,
die nur den guenstigsten Fall kennt, sagt genau dann "in Ordnung",
wenn der Nutzer schon feststeckt. */
for (const [wie, breite, hoehe] of GROESSEN) {
const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe } });
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();
await seite.goto(`https://${CREW}:${HP}/workspace/aufgaben.html`, { waitUntil: "networkidle" });
await seite.waitForTimeout(1200);
/* Das Bearbeiten-Fenster oeffnen -- ueber den Stift an der Aufgabe,
genau so wie Miss es getan hat. */
const stift = seite.locator('.aufgabe [title*="earbeit"], .aufgabe button').first();
if (await stift.count()) { await stift.click().catch(() => {}); }
await seite.waitForTimeout(400);
const offen = await seite.evaluate(() => {
const dlg = document.getElementById("bearbeiten-dialog");
if (dlg && !dlg.open) { try { dlg.showModal(); } catch { /* egal */ } }
return !!(dlg && dlg.open);
});
await seite.waitForTimeout(500);
const mass = await seite.evaluate(() => {
const dlg = document.getElementById("bearbeiten-dialog");
if (!dlg || !dlg.open) return null;
const raus = dlg.querySelector("#b-abbrechen");
const rd = raus ? raus.getBoundingClientRect() : null;
const dd = dlg.getBoundingClientRect();
/* WELCHES ELEMENT KANN ROLLEN? Gefragt wird der gemessene Zustand
(scrollHeight > clientHeight UND overflow erlaubt es), nicht die
Absicht im Stilblatt. */
const rollt = (el) => {
if (!el) return false;
const s = getComputedStyle(el);
const geht = /auto|scroll/.test(s.overflowY);
return geht && el.scrollHeight - el.clientHeight > 4;
};
const form = dlg.querySelector("form");
return {
fensterHoehe: window.innerHeight,
dialogOben: Math.round(dd.top), dialogUnten: Math.round(dd.bottom),
dialogHoehe: Math.round(dd.height),
inhaltHoehe: form ? form.scrollHeight : null,
dialogOverflow: getComputedStyle(dlg).overflowY,
formOverflow: form ? getComputedStyle(form).overflowY : null,
ausgangDa: !!raus,
ausgangUnten: rd ? Math.round(rd.bottom) : null,
ausgangSichtbar: !!(rd && rd.bottom <= window.innerHeight + 1 && rd.top >= -1
&& rd.width > 0 && rd.height > 0),
dialogRollt: rollt(dlg),
formRollt: rollt(form),
};
});
console.log(`\n=== Aufgabe bearbeiten, ${breite} x ${hoehe} (${wie}) ===\n`);
if (!offen || !mass) {
console.log(" ABBRUCH: Das Fenster liess sich nicht oeffnen -- gemessen wurde nichts.");
schlimm = 1;
} else {
for (const [k, v] of Object.entries(mass)) console.log(` ${k.padEnd(16)} ${v}`);
const rauskommen = mass.ausgangSichtbar || mass.dialogRollt || mass.formRollt;
console.log(`\n ==> Kommt man heraus? ${rauskommen ? "JA" : "NEIN -- SACKGASSE"}`);
if (!rauskommen) schlimm = 1;
await seite.locator("#bearbeiten-dialog")
.screenshot({ path: `server/mess-dialog-${wie}.png` }).catch(() => {});
console.log(` Bild: server/mess-dialog-${wie}.png`);
}
await kontext.close();
}
} finally {
await browser.close();
vorbau.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
process.exit(schlimm);
}