/* ===================================================================== WAS SEHEN DIE VIER CREW-ROLLEN HEUTE AUF willkommen.html? Ausgangsmessung vom 06.10.2026, bevor der Bauplan auf der Crew-Seite umgesetzt wird. Ohne sie waere hinterher nicht zu sagen, was die Arbeit bewirkt hat -- und ob die Seite vorher wirklich leer war oder nur anders. Gemessen wird am HOSTNAMEN, nicht an der Rolle: Erst der Hostname macht `person.haus` zu "crew", und genau daran haengt die Schranke im Servermodul. ===================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { request, request as httpAnfrage } from "node:http"; import { createServer as httpsServer } from "node:https"; import { readFileSync } from "node:fs"; const ordner = mkdtempSync(join(tmpdir(), "ws-anlcrew-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); import { notbremse } from "./helfer-notbremse.mjs"; const { eigenerPort } = await import("./helfer-port.mjs"); const PORT = await eigenerPort(import.meta, "mess-anleitung-crew"); process.env.PORT = `${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 }); }; await import("./index.js"); notbremse(120_000, "mess-anleitung-crew"); await new Promise((r) => setTimeout(r, 900)); const CREW = "crew.dogfather-universe.com"; /** Ein Aufruf mit frei waehlbarem Host-Kopf -- `fetch` laesst `Host` * nicht setzen, deshalb node:http. */ function ruf(pfad, { keks = "", methode = "GET", rumpf = null } = {}) { return new Promise((fertig) => { const kopf = { Host: CREW }; if (keks) kopf.Cookie = keks; if (rumpf) { kopf["Content-Type"] = "application/json"; } const a = request({ host: "127.0.0.1", port: PORT, path: pfad, method: methode, headers: kopf }, (r) => { let t = ""; r.on("data", (c) => { t += c; }); r.on("end", () => fertig({ status: r.statusCode, keks: (r.headers["set-cookie"] || []).map((k) => k.split(";")[0]).join("; "), text: t, json: (() => { try { return JSON.parse(t); } catch { return null; } })(), })); }); a.on("error", () => fertig({ status: 0, text: "", json: null, keks: "" })); if (rumpf) a.write(rumpf); a.end(); }); } const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes } = await import("node:crypto"); await ruf("/workspace/api/anmelden", { methode: "POST", rumpf: JSON.stringify({ rolle: "admin", code: "x" }) }); for (let i = 0; i < 60; i++) { try { const p = new DatabaseSync(process.env.WORKSPACE_DB); try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); } } catch { await new Promise((r) => setTimeout(r, 250)); } } const ROLLEN = [["gast", "Community"], ["modi", "Modi"], ["linke", "Linke Hand"], ["hand", "Rechte Hand"]]; { const d = new DatabaseSync(process.env.WORKSPACE_DB); for (const [rolle] of ROLLEN) { const salt = randomBytes(16).toString("hex"); const code = `CODE-${rolle.toUpperCase()}-0001`; const hash = scryptSync(code, salt, 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(rolle, rolle, hash, salt, 32768, new Date().toISOString()); } d.close(); } console.log("\n=== Was die Crew-Rollen heute auf willkommen.html bekommen ==="); for (const [rolle, wie] of ROLLEN) { /* DIE ALTERSBESTAETIGUNG IST EIN EIGENER SCHRITT -- nur fuer den Gast. Ohne sie antwortet der Server mit 400, und die Messung haette "Community kommt nicht herein" gemeldet, ohne dass etwas kaputt waere. */ const an = await ruf("/workspace/api/anmelden", { methode: "POST", rumpf: JSON.stringify({ rolle, code: `CODE-${rolle.toUpperCase()}-0001`, alter_ok: true }), }); if (an.status !== 200) { console.log(` ${wie.padEnd(12)} kommt nicht herein (${an.status})`); continue; } const seite = await ruf("/workspace/willkommen.html", { keks: an.keks }); const api = await ruf("/workspace/api/anleitung", { keks: an.keks }); const karten = api.json?.karten?.length ?? "-"; console.log(` ${wie.padEnd(12)} Seite ${seite.status} · Schnittstelle ${api.status}` + ` · Karten ${karten}` + (api.json?.fehler ? ` · "${api.json.fehler}"` : "")); } /* ==== FUEHREN DIE KACHELN DER LINKEN HAND INS LEERE? ================ Der Bauplan spricht der Linken Hand vier Kacheln ab (Talente, Personen & Zugaenge, Wer sieht was, Vertraulich melden). Die Kachelliste gibt sie ihr trotzdem. Entweder irrt der Plan -- oder sie sieht vier Kacheln, die auf Seiten fuehren, die sie nicht oeffnen darf. Gefragt wird der laufende Server, nicht die Tabelle. */ { console.log("\n=== Linke Hand: fuehren ihre Kacheln irgendwohin? ==="); const an = await ruf("/workspace/api/anmelden", { methode: "POST", rumpf: JSON.stringify({ rolle: "linke", code: "CODE-LINKE-0001" }), }); for (const pfad of ["/workspace/talente.html", "/workspace/personen.html", "/workspace/rechte.html", "/workspace/hilfe.html", "/workspace/aufgaben.html"]) { const a2 = await ruf(pfad, { keks: an.keks }); const wohin = a2.status >= 300 && a2.status < 400 ? " -> umgeleitet" : ""; console.log(` ${pfad.replace("/workspace/", "").padEnd(18)} ${a2.status}${wohin}`); } } /* Und was steht in der Datenbank -- je Haus. */ { const d = new DatabaseSync(process.env.WORKSPACE_DB); console.log("\n=== Inhalte in der Datenbank ==="); for (const r of d.prepare( "SELECT haus, rolle, COUNT(*) n FROM anl_karte GROUP BY haus, rolle ORDER BY haus, rolle").all()) { console.log(` ${r.haus.padEnd(8)} ${r.rolle.padEnd(10)} ${r.n} Karten`); } const crew = d.prepare("SELECT COUNT(*) n FROM anl_karte WHERE haus = 'crew'").get().n; console.log(` -> fuer die Crew: ${crew} Karten`); d.close(); } /* ===================================================================== UND WIE SIEHT ES AUS? Zahlen sagen, dass die Karten da sind -- nicht, ob die Seite lesbar ist. Der Bauplan verlangt "Handy zuerst: Karten untereinander, grosse Tippflaechen, kein Querscrollen". Das misst man nur im Browser. ===================================================================== */ { /* ==== WARUM EIN HTTPS-VORBAU (06.10.2026) ===================== Ueber `http://crew.dogfather-universe.com:PORT` laedt das Dokument, aber KEINE einzige Unterressource -- gemessen: alle CSS- und JS-Dateien melden "requestfailed", und die Seite steht nackt da. Der Grund ist HSTS: Der Name steht in der Vorladeliste von Chromium, und der Browser erzwingt deshalb https:// fuer alles, was die Seite nachlaedt. Auf einem Klartext-Port findet er dort nichts. Deshalb derselbe Weg wie in pruef-chat-neu-stelle: ein kleiner HTTPS-Vorbau mit eigenem Zertifikat, der an den echten Server weiterreicht. Der Hostname bleibt damit `crew.` -- und nur daran erkennt der Server das Haus. */ const { zertifikatBauen } = await import("./helfer-openssl.mjs"); const HP = PORT + 1; 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 }, }, (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 { chromium } = await import("playwright"); const browser = await chromium.launch({ args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors"], ignoreHTTPSErrors: true, }); for (const [rolle, wie, datei] of [ ["gast", "Community", "bild-anleitung-crew-community.png"], ["hand", "Rechte Hand", "bild-anleitung-crew-hand.png"], ]) { /* ANGEMELDET WIRD UEBER DIE SCHNITTSTELLE, nicht ueber die Wand. Auf der Crew-Wand liegt bei 390 px das Buehnenbild ueber den Knoepfen und faengt jeden Klick ab. Gemessen werden soll hier die ANLEITUNG, nicht die Anmeldung -- die hat ihre eigenen Pruefungen. Ein Keks aus der Schnittstelle ist derselbe Keks. */ const an = await ruf("/workspace/api/anmelden", { methode: "POST", rumpf: JSON.stringify({ rolle, code: `CODE-${rolle.toUpperCase()}-0001`, alter_ok: true }), }); const kontext = await browser.newContext({ viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true, }); await kontext.addCookies(an.keks.split("; ").filter(Boolean).map((k) => { const [x, ...r] = k.split("="); return { name: x, value: r.join("="), domain: CREW, path: "/" }; })); const seite = await kontext.newPage(); const klagen = []; seite.on("pageerror", (f) => klagen.push(String(f?.message || f))); const schlecht = []; seite.on("response", (r) => { if (r.status() >= 300) schlecht.push(`${r.status()} ${r.url().split("/workspace/")[1] || r.url()}`); }); seite.on("requestfailed", (r) => schlecht.push(`weg: ${r.url().split("/workspace/")[1]}`)); await seite.goto(`https://${CREW}:${HP}/workspace/anleitung.html`, { waitUntil: "networkidle" }); await seite.waitForTimeout(1500); const lage = await seite.evaluate(() => { const z = (id) => document.getElementById(id); const karten = document.querySelectorAll(".an-karte").length; const breit = document.documentElement.scrollWidth > document.documentElement.clientWidth + 1; const klein = [...document.querySelectorAll("button, a")] .filter((e) => { const r = e.getBoundingClientRect(); return r.width > 0 && r.height > 0 && r.height < 44; }).length; return { karten, quer: breit, klein, vorweg: z("vorweg-block") && !z("vorweg-block").hidden, stufe: z("stufe-block") && !z("stufe-block").hidden, stufeText: z("stufe-jetzt")?.textContent || "", titel: z("seiten-titel")?.textContent || "", }; }); console.log(` === ${wie} im Browser (390 px) ===`); console.log(` Titel : "${lage.titel}"`); console.log(` Karten : ${lage.karten}`); console.log(` Drei Dinge vorweg : ${lage.vorweg}`); console.log(` Deine Stufe : ${lage.stufe} ${lage.stufeText}`); console.log(` Querscrollen : ${lage.quer ? "JA -- Befund" : "nein"}`); console.log(` Tippziele unter 44 : ${lage.klein}`); console.log(` Browsermeldungen : ${klagen.length ? klagen.join(" | ") : "keine"}`); console.log(` Fehlgeschlagen : ${schlecht.length ? schlecht.slice(0, 6).join(" | ") : "nichts"}`); await seite.screenshot({ path: new URL("./" + datei, import.meta.url).pathname .replace(/^\/([A-Za-z]:)/, "$1"), fullPage: true }); await kontext.close(); } await browser.close(); vorbau.close(); } try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } process.exit(0);