Files
dogfather-universe/server/pruef-community-sicht.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

346 lines
15 KiB
JavaScript

/* =====================================================================
WAS EIN MITGLIED DER COMMUNITY SIEHT -- SYSTEMATISCH (18.09.2026)
Filipe: „perfektioniere jede einzelne kategorie der community."
---------------------------------------------------------------------
WARUM ES DIESE PRÜFUNG GIBT
An einem Tag habe ich in der Community elf Dinge gefunden, die dort
nicht hingehörten: Aufgabenzähler, „gönn dir was", „0 betreut",
sieben Links auf eine Seite, die ein Mitglied gar nicht öffnen darf.
Jeder einzelne Fund kam daher, dass ich zufällig hingesehen habe.
Das ist keine Methode. Der Workspace ist für die Agentur gebaut
worden, und die Community hat ihn geerbt -- überall dort, wo
niemand hingesehen hat, steht noch die Sprache und die Logik eines
Arbeitsplatzes. Solche Reste findet man nicht durch Nachdenken,
sondern indem man ALLE Seiten durchgeht, die ein Mitglied erreichen
kann.
---------------------------------------------------------------------
ZWEI FRAGEN, BEIDE MESSBAR
1. TOTE WEGE: Führt irgendein Link, Knopf oder Formular auf eine
Seite, die diese Person nicht öffnen darf? Sie landet dann wieder
auf der Startseite -- ohne Meldung, ohne Grund. Man drückt, es
passiert nichts, und beim nächsten Knopf zweifelt man an sich.
2. FREMDE SPRACHE: Stehen dort Wörter, die es hier nicht gibt --
„Review", „betreut", „Pipeline", „Creator anlegen"? Sie sind
nicht falsch, sie gehören nur jemand anderem.
Aufrufen mit: node server/pruef-community-sicht.mjs
===================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { request as httpAnfrage } from "node:http";
import { eigenerPort } from "./helfer-port.mjs";
const PORT = await eigenerPort(import.meta, "pruef-community-sicht");
const ordner = mkdtempSync(join(tmpdir(), "ws-cs-"));
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";
await import("./index.js");
notbremse(180_000, "pruef-community-sicht");
await new Promise((r) => setTimeout(r, 700));
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
let fehler = 0, geprueft = 0;
const melde = (t) => console.log(t);
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
const CREW = "crew.dogfather-universe.com";
/* ---------------------------------------------------------------------
WELCHE SEITEN DARF EIN MITGLIED? -- aus der Rechtetabelle abgeleitet.
NICHT VON HAND AUFGEZÄHLT. Eine Liste hier müsste bei jeder neuen
Seite nachgezogen werden, und genau das vergisst man. Die Tabelle
in rechte.js ist ohnehin die Wahrheit -- also wird sie gefragt.
--------------------------------------------------------------------- */
const { darfSeite } = await import("./rechte.js");
const { readFileSync, readdirSync } = await import("node:fs");
const alleSeiten = readdirSync(join(import.meta.dirname, "..", "workspace"))
.filter((d) => d.endsWith(".html"));
const GAST = { rolle: "gast", haus: "crew" };
const erlaubt = alleSeiten.filter((d) => darfSeite(GAST, `/workspace/${d}`));
const verboten = alleSeiten.filter((d) => !darfSeite(GAST, `/workspace/${d}`));
melde("");
melde("=== 1. Was ueberhaupt offensteht ===");
/* ==== EINE LISTE STATT EINER SPANNE (25.09.2026) ===================
Hier stand `>= 3 && <= 8`. Eine Spanne sagt nichts darueber, WELCHE
Seiten offen sind -- und genau das ist die Frage. Sie war ausserdem
seit dem 24.09. rot: Mit dem Support sind es zwoelf geworden, und
jede einzelne davon gehoert dorthin (nachgesehen, nicht vermutet --
dieselben zwoelf stehen auf der Kachelwand eines Mitglieds).
AB JETZT WIRD DIE LISTE FESTGEHALTEN. Kommt eine Seite dazu, wird
diese Zeile rot und nennt sie beim Namen; wer sie hinzugefuegt hat,
traegt sie hier nach und hat damit BESTAETIGT, dass die Community
sie sehen soll. Verschwindet eine, faellt es genauso auf. Eine
Spanne kann beides nicht: Sie haette elf statt zwoelf stillschweigend
durchgehen lassen -- und zwar in beide Richtungen.
DAS IST DIESELBE BAUART wie die festgelegten Kachelfarben: nicht
„ungefaehr so viele", sondern „genau diese, und hier steht warum". */
const COMMUNITY_SEITEN = [
"bereich.html", // die Bretter (Highlights, Anschlag, Wunschliste ...)
"bewerben.html", // Mitmachen
"chat.html", // der Treff
"hilfe.html", // Regeln & Hilfe
"material.html", // Dogi-Media
/* DIE REACTION kam am 28.09.2026 dazu und steht im
Community-Bereich als eigene Kachel hinter Dogi-Media. Wer
zusieht, braucht die Seite -- sonst gaebe es niemanden im Saal.
WARUM DIESE LISTE TROTZDEM VON HAND GEPFLEGT WIRD: Sie ist eine
ABSICHT, keine Ableitung. Wuerde sie aus `rechte.js` gelesen,
verglichen zwei Kopien derselben Quelle miteinander -- dann
waere sie immer gruen und wuerde nichts beweisen. Der Preis ist
dieser Eintrag hier; die Pruefung sagt beim Anschlagen selbst,
welche Seite fehlt oder zu viel ist. */
"reaktion.html", // zusammen schauen (seit 28.09.2026)
"start.html", // die Wand
"steckbrief.html", // der eigene Steckbrief
"support.html", // etwas ist kaputt (seit 24.09. fuer alle)
"treff-regeln.html", // die Regeln des Rudels
"unsere-seiten.html", // Draussen
"unterstuetzen.html", // Unterstuetzen
"willkommen.html", // die Begruessung
];
const zuViel = erlaubt.filter((s) => !COMMUNITY_SEITEN.includes(s));
const fehltDa = COMMUNITY_SEITEN.filter((s) => !erlaubt.includes(s));
ok(zuViel.length === 0 && fehltDa.length === 0,
`ein Mitglied darf genau die ${COMMUNITY_SEITEN.length} vorgesehenen Seiten`
+ (zuViel.length ? ` -- ZU VIEL: ${zuViel.join(", ")}` : "")
+ (fehltDa.length ? ` -- ES FEHLT: ${fehltDa.join(", ")}` : ""));
ok(verboten.length >= 15,
`und ${verboten.length} nicht -- so viele Wege gibt es, ins Leere zu zeigen`);
/* ---------------------------------------------------------------------
DIE MESSUNG IM BROWSER
--------------------------------------------------------------------- */
function roh(pfad, host, kopf = {}, koerper = null) {
return new Promise((f, s) => {
const a = httpAnfrage({
host: "127.0.0.1", port: PORT, path: pfad,
method: koerper ? "POST" : "GET",
headers: { Host: host, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf },
}, (an) => {
let t = "";
an.on("data", (c) => { t += c; });
an.on("end", () => f({ code: an.statusCode, text: t, kopf: an.headers }));
});
a.on("error", s);
if (koerper) a.write(JSON.stringify(koerper));
a.end();
});
}
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes } = await import("node:crypto");
{
const d = new DatabaseSync(process.env.WORKSPACE_DB);
for (const [name, rolle, code] of [
["Filipe", "admin", "CODE-DOGI-0001"],
["Mara", "gast", "CODE-GAST-0001"],
]) {
const salz = randomBytes(16).toString("hex");
const hash = scryptSync(code, salz, 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, aktiv, erstellt)"
+ " VALUES (?,?,?,?,?,1,?)")
.run(name, rolle, hash, salz, 32768, new Date().toISOString());
}
d.close();
}
async function anmelden(rolle, code) {
let a = await roh("/workspace/api/anmelden", CREW, {}, { rolle, code });
if (a.code === 400 && a.text.includes("alter_offen")) {
a = await roh("/workspace/api/anmelden", CREW, {}, { rolle, code, alter_ok: true });
}
return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
}
const kGast = await anmelden("gast", "CODE-GAST-0001");
const kDogi = await anmelden("admin", "CODE-DOGI-0001");
/* Damit die Bretter nicht leer sind -- auf einer leeren Seite findet
man keine Wege, die ins Leere fuehren. */
for (const b of ["treff", "anschlag", "ansteht", "wunsch", "highlight", "regeln", "mitmachen"]) {
await roh(`/workspace/api/bereich/${b}/start`, CREW,
{ cookie: kDogi, origin: `http://${CREW}:${PORT}` }, {});
}
let pw = null;
try {
pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
} catch { /* dritter Ausgang unten */ }
if (!pw) {
melde("");
melde("KANN NICHT NACHSEHEN: Playwright fehlt -- die Seiten wurden nicht geoeffnet.");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
process.exit(3);
}
/* WÖRTER, DIE JEMAND ANDEREM GEHÖREN.
Keine Rollennamen -- die stehen bewusst nirgends in ausgelieferten
Dateien. Gesucht wird nach der SPRACHE eines Arbeitsplatzes. */
const FREMDE_WOERTER = [
"Vorarbeiten", "gönn dir was", "betreut", "Review",
"Pipeline", "überfällig", "In Arbeit", "Abgebrochen",
];
/* ZWEI BREITEN, NICHT EINE.
Die Community ist ueberwiegend mit dem Telefon unterwegs. Eine
Messung auf 1400 px beantwortet fuer sie die falsche Frage -- und
genau auf dem Handy sind heute die haesslichsten Sachen aufgefallen
(drei verschiedene Kartenanordnungen, ein Hinweis quer ueber der
naechsten Beschriftung). Was auf einer Breite in Ordnung ist, muss
es auf der anderen nicht sein. */
const BREITEN = [
{ name: "Rechner", breite: 1400, hoehe: 1000 },
{ name: "Handy", breite: 412, hoehe: 915 },
];
const browser = await pw.chromium.launch();
try {
for (const geraet of BREITEN) {
const ctx = await browser.newContext({
viewport: { width: geraet.breite, height: geraet.hoehe },
isMobile: geraet.breite < 700, hasTouch: geraet.breite < 700 });
await ctx.addCookies(kGast.split("; ").filter(Boolean).map((z) => {
const [name, ...rest] = z.split("=");
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
}));
const seite = await ctx.newPage();
const skriptfehler = [];
seite.on("pageerror", (e) => skriptfehler.push(String(e).slice(0, 100)));
/* Jede erlaubte Seite -- und bei bereich.html jedes der sieben
Bretter, denn sie sehen völlig verschieden aus. */
const ziele = [];
for (const d of erlaubt) {
if (d === "bereich.html") {
for (const b of ["treff", "anschlag", "ansteht", "wunsch", "highlight", "regeln", "mitmachen"]) {
ziele.push(`${d}?b=${b}`);
}
} else ziele.push(d);
}
melde("");
melde(`=== 2. Wege, die ins Leere fuehren (${geraet.name}) ===`);
let toteWege = 0, geprueftLinks = 0;
const gefunden = [];
for (const ziel of ziele) {
await seite.goto(`http://127.0.0.1:${PORT}/workspace/${ziel}`, { waitUntil: "networkidle" });
await seite.waitForTimeout(900);
const wege = await seite.evaluate(() =>
[...document.querySelectorAll("a[href]")]
.filter((a) => a.offsetParent !== null)
.map((a) => ({ ziel: a.getAttribute("href"), text: a.textContent.trim().slice(0, 30) })));
for (const w of wege) {
const datei = String(w.ziel).split("?")[0].split("#")[0].replace(/^\.?\//, "");
if (!datei.endsWith(".html")) continue;
geprueftLinks++;
if (verboten.includes(datei)) {
toteWege++;
gefunden.push(`${ziel}: „${w.text}" -> ${datei}`);
}
}
}
/* DIE ANZAHL GEHÖRT IN DIE BEDINGUNG. Ohne sie wäre „0 tote Wege"
auch dann wahr, wenn gar keine Seite geladen hätte. */
/* DIE UNTERGRENZE WIRD ABGELEITET, NICHT GESETZT.
Hier stand `>= 20`. Auf dem Rechner sind es 27 Wege, auf dem Handy
18 -- dort klappt die Kopfleiste um, und ein Teil der Links ist
nicht sichtbar. Die feste Zahl machte die Pruefung auf der
zweiten Breite rot, ohne dass irgendetwas falsch war.
Sie einfach auf 15 zu senken waere derselbe Fehler mit einer
anderen Zahl. Verlangt wird jetzt, was die Sache selbst hergibt:
mindestens ein sichtbarer Weg je Seite. Findet sich weniger, hat
etwas nicht geladen -- und dann ist „0 tote Wege" keine Auskunft,
sondern ein Messfehler. */
ok(geprueftLinks >= ziele.length && toteWege === 0,
`${geprueftLinks} sichtbare Wege auf ${ziele.length} Seiten, davon ${toteWege} ins Leere`);
for (const g of gefunden.slice(0, 10)) console.log(` -> ${g}`);
melde("");
melde(`=== 3. Sprache, die jemand anderem gehoert (${geraet.name}) ===`);
let fremd = 0;
const wo = [];
for (const ziel of ziele) {
await seite.goto(`http://127.0.0.1:${PORT}/workspace/${ziel}`, { waitUntil: "networkidle" });
await seite.waitForTimeout(700);
const text = await seite.evaluate(() => document.body.innerText);
for (const w of FREMDE_WOERTER) {
if (text.includes(w)) { fremd++; wo.push(`${ziel}: „${w}"`); }
}
}
ok(fremd === 0, `keine fremde Arbeitssprache auf ${ziele.length} Seiten (${fremd} Stellen)`);
for (const g of wo.slice(0, 10)) console.log(` -> ${g}`);
/* =====================================================================
4. DIE GEGENPROBE: BEHAELT DAS TEAM SEINE WEGE?
Was hier weggenommen wird, weil es fuer ein Mitglied ins Leere
fuehrt, muss fuer die anderen stehen bleiben. Ohne diesen
Abschnitt saehe eine abgeschaffte Funktion genauso aus wie eine,
die richtig entscheidet -- und zwar in beiden Faellen gruen.
===================================================================== */
melde("");
melde("=== 4. Und behaelt das Team seine Wege? ===");
if (geraet.breite > 700) {
const ctx2 = await browser.newContext({ viewport: { width: 1400, height: 1000 } });
await ctx2.addCookies(kDogi.split("; ").filter(Boolean).map((z) => {
const [name, ...rest] = z.split("=");
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
}));
const seite2 = await ctx2.newPage();
await seite2.goto(`http://127.0.0.1:${PORT}/workspace/start.html`, { waitUntil: "networkidle" });
await seite2.waitForTimeout(1200);
const z = await seite2.evaluate(() => ({
chat: !!document.querySelector(".chat-knopf"),
wege: [...document.querySelectorAll("a[href$='.html']")].length,
}));
ok(z.chat, "DogFather hat seinen Chat-Knopf weiterhin");
ok(z.wege >= 5, `und weiterhin Wege auf andere Seiten (${z.wege})`);
await ctx2.close();
}
melde("");
melde(`=== 5. Und laeuft ueberhaupt alles? (${geraet.name}) ===`);
ok(skriptfehler.length === 0,
`kein Skriptfehler auf ${ziele.length} Seiten (${geraet.name})`
+ (skriptfehler.length ? ` -- ${skriptfehler[0]}` : ""));
await ctx.close();
}
} finally {
await browser.close();
}
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Rest */ }
process.exit(fehler ? 1 : 0);