/* ===================================================================== WAS SIEHT DIE RECHTE HAND AUF DER PERSONENSEITE? (25.09.2026) --------------------------------------------------------------------- WARUM ES DIESE DATEI GIBT Filipe, zum wiederholten Mal: „die rechte hand sieht das immer noch nicht obwohl ich will dass die rechte hand das auch sieht." Dazu ein Bildschirmfoto der Personenseite -- Rollenkarten oben, „Letzte Ereignisse" unten. ICH KOENNTE RATEN, welches „das" gemeint ist. Am 24.09.2026 habe ich genau das getan, auf ein Bildschirmfoto hin an der falschen Seite gebaut und es im Commit selbst notiert („ZUERST EIN IRRTUM VON MIR"). Diesmal wird nachgesehen, bevor gebaut wird: Zwei Anmeldungen, dieselbe Seite, und die Unterschiede werden AUFGEZAEHLT. SIE PRUEFT NICHTS und meldet keinen Fehlschlag. Sie macht eine Liste und zwei Bilder, auf denen man nachsehen kann. Deshalb `mess-` und nicht `pruef-`: Die Portvergabe leitet sich aus den `pruef-`-Namen ab, und eine Messdatei darf die Nummern der Pruefungen nicht verschieben. EIGENE WEGWERF-DATENBANK, NIE DIE ECHTE. Es entstehen echte Personen und echte Protokollzeilen; in einem laufenden System waeren das Testdaten, und die sind verboten. Aufrufen mit: node server/mess-hand-personen.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 = 5405, HP = 5406; const CREW = "crew.dogfather-universe.com"; const ordner = mkdtempSync(join(tmpdir(), "ws-hand-pers-")); 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(240_000, "mess-hand-personen"); 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; }; /* Dieselbe Besetzung wie auf Filipes Bildschirmfoto: beide Haende, mehrere Modis, mehrere aus der Community. */ anlegen("Filipe", "admin", "CODE-DOGI-0001"); anlegen("VanVan", "hand", "CODE-HAND-0001"); anlegen("Kessi", "linke", "CODE-LINKE-0001"); anlegen("Diene", "modi", "CODE-MODI-0001"); anlegen("Marina", "modi", "CODE-MODI-0002"); anlegen("Ghost", "modi", "CODE-MODI-0003"); anlegen("Anni", "gast", "CODE-GAST-0001"); anlegen("Cleo", "gast", "CODE-GAST-0002"); /* Ein paar Protokollzeilen, damit „Letzte Ereignisse" nicht leer ist -- sonst sieht „sieht nichts" bei beiden Rollen gleich aus. */ for (let i = 0; i < 6; i++) { d.prepare("INSERT INTO protokoll (zeitpunkt, person_id, rolle, aktion, detail, ip)" + " VALUES (?,1,'admin','einstellung_geaendert',?,'127.0.0.1')") .run(new Date(Date.now() - i * 60000).toISOString(), `probe_${i}`); } d.close(); const anfrage = (pfad, art, koerper, keks) => 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", ...(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(JSON.stringify(koerper)); a.end(); }); const anmelden = async (rolle, code) => { const a = await anfrage("/workspace/api/anmelden", "POST", { rolle, code }); if (a.code !== 200) { console.log(`ABBRUCH: ${rolle} ließ sich nicht anmelden (HTTP ${a.code}) ${a.text.slice(0, 120)}`); process.exit(2); } return (a.kopf["set-cookie"] || []).map((x) => x.split(";")[0]).join("; "); }; const kekse = { DogFather: await anmelden("admin", "CODE-DOGI-0001"), "rechte Hand": await anmelden("hand", "CODE-HAND-0001"), }; /* ---- Erst die ANTWORTEN vergleichen, dann den Bildschirm ------------ Wenn der Server schon verschieden antwortet, muss man im Browser gar nicht mehr suchen. */ console.log("\n=== Was der Server den beiden gibt ==="); for (const [wer, keks] of Object.entries(kekse)) { const p = await anfrage("/workspace/api/verwaltung/personen", "GET", null, keks); const prot = await anfrage("/workspace/api/verwaltung/protokoll?anzahl=25", "GET", null, keks); let leute = 0, rollen = ""; try { const j = JSON.parse(p.text); leute = (j.personen || []).length; rollen = [...new Set((j.personen || []).map((x) => x.rolle))].join(", "); } catch { /* zeigt sich am Code */ } let zeilen = -1; try { zeilen = (JSON.parse(prot.text).eintraege || []).length; } catch { /* egal */ } console.log(` ${wer.padEnd(12)} Personen HTTP ${p.code} → ${leute} Leute (${rollen})`); console.log(` ${"".padEnd(12)} Protokoll HTTP ${prot.code} → ${zeilen < 0 ? prot.text.slice(0, 60) : zeilen + " Zeilen"}`); } const schl = join(ordner, "b.key"), zert = join(ordner, "b.crt"); execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", schl, "-out", zert, "-days", "2", "-subj", `/CN=${CREW}`, "-addext", `subjectAltName=DNS:${CREW}`], { stdio: "ignore" }); 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"], }); const MARKE = process.env.MARKE || "hand"; const befund = {}; try { for (const [wer, keks] of Object.entries(kekse)) { const kontext = await browser.newContext({ viewport: { width: 1440, height: 1500 } }); 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/personen.html`, { waitUntil: "networkidle" }); await seite.waitForTimeout(1800); const lage = await seite.evaluate(() => { const sicht = (e) => !!(e && e.offsetParent !== null); const text = (s) => (document.querySelector(s)?.textContent || "").trim(); return { wo: location.pathname, gruppen: [...document.querySelectorAll(".gruppe__titel, .rollen-block__titel, h2")] .map((e) => e.textContent.trim()).filter(Boolean).slice(0, 12), karten: [...document.querySelectorAll("[data-rolle]")] .map((e) => e.dataset.rolle).filter((x, i, a) => x && a.indexOf(x) === i), knoepfe: [...document.querySelectorAll("button")] .filter(sicht).map((e) => e.textContent.trim()) .filter(Boolean).filter((x, i, a) => a.indexOf(x) === i).slice(0, 25), protokollBlock: !!document.querySelector(".protokoll-block"), protokollSichtbar: sicht(document.querySelector(".protokoll-block")), protokollZeilen: document.querySelectorAll(".protokoll-zeile").length, protokollText: text("#protokoll").slice(0, 60), schalter: sicht(document.getElementById("protokoll-schalter")), }; }); befund[wer] = lage; await seite.screenshot({ path: `server/mess-${MARKE}-personen-${wer === "DogFather" ? "dogi" : "hand"}.png`, fullPage: true, }); if (konsole.length) console.log(` [${wer}] Browser meldet: ${konsole[0]}`); await kontext.close(); } } finally { await browser.close(); await new Promise((r) => vorbau.close(r)); } console.log("\n=== Was auf dem Bildschirm steht ==="); for (const [wer, l] of Object.entries(befund)) { console.log(`\n --- ${wer} (${l.wo}) ---`); console.log(` Rollen-Karten: ${l.karten.join(", ") || "(keine)"}`); console.log(` Protokoll-Block: ${l.protokollBlock ? "im Dokument" : "FEHLT"}` + ` / sichtbar: ${l.protokollSichtbar}` + ` / Zeilen: ${l.protokollZeilen}` + ` / Schalter: ${l.schalter}`); console.log(` Protokoll-Text: ${JSON.stringify(l.protokollText)}`); console.log(` Knoepfe: ${l.knoepfe.join(" · ")}`); } /* DER UNTERSCHIED IST DIE ANTWORT -- alles andere ist Beiwerk. */ const a = befund.DogFather, b = befund["rechte Hand"]; if (a && b) { console.log("\n=== Der Unterschied ==="); const fehltKarte = a.karten.filter((x) => !b.karten.includes(x)); const fehltKnopf = a.knoepfe.filter((x) => !b.knoepfe.includes(x)); console.log(` Karten, die ihr fehlen: ${fehltKarte.join(", ") || "(keine)"}`); console.log(` Knoepfe, die ihr fehlen: ${fehltKnopf.join(" · ") || "(keine)"}`); console.log(` Protokollzeilen: DogFather ${a.protokollZeilen}, rechte Hand ${b.protokollZeilen}`); } console.log(`\n Bilder: server/mess-${MARKE}-personen-{dogi,hand}.png\n`); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } process.exit(0);