/* ===================================================================== WIE DER CHAT AUSSIEHT (25.09.2026) Filipe: „ich will dass du die komplette seite viel geiler und moderner machst. die komplette kachel. die chat liste und die chats selber. … es soll komplett aus der rolle fahren und was was wir noch nie hatten." SIE PRUEFT NICHTS und meldet keinen Fehlschlag -- sie macht Bilder, auf denen man vergleichen kann. Deshalb `mess-` und nicht `pruef-`. VIER BILDER, weil die Seite zwei Zustaende und zwei Breiten hat: Liste ohne Gespraech / Liste mit offenem Gespraech, jeweils am Rechner und am Handy. Wer nur eines ansieht, baut fuer eines. EIGENE WEGWERF-DATENBANK: Es entstehen echte Gespraeche und Nachrichten. In einem laufenden System waeren das Testdaten, und die sind verboten. Hier liegt alles in einem `mktemp`-Ordner. Aufrufen mit: node server/mess-chat-optik.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 = 5403, HP = 5404; const CREW = "crew.dogfather-universe.com"; const ordner = mkdtempSync(join(tmpdir(), "ws-chatoptik-")); 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(260_000, "mess-chat-optik"); 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 vorStunden = (h) => new Date(Date.now() - h * 3600_000).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("Dogfather", "admin", "CODE-DOGI-0001"); const idVan = anlegen("VanVan", "hand", "CODE-HAND-0001"); const idFunny = anlegen("Funny", "linke", "CODE-LINK-0001"); const idDiene = anlegen("Diene", "modi", "CODE-MODI-0001"); const idMarina = anlegen("Marina", "modi", "CODE-MODI-0002"); const idGhost = anlegen("Ghost", "modi", "CODE-MODI-0003"); const idMiss = anlegen("Miss", "modi", "CODE-MODI-0004"); /* ---- Gespraeche und Nachrichten direkt in die Datenbank ------------- Ueber die Schnittstelle waeren es fuenfzehn Aufrufe fuer einen Zustand, den es nur fuer das Bild braucht. */ const raum = (art, name, kategorie, haus = "crew") => d.prepare( "INSERT INTO chat_raeume (art, name, kategorie, erstellt, erstellt_von, haus)" + " VALUES (?,?,?,?,?,?) RETURNING id").get(art, name, kategorie, jetzt, idDogi, haus).id; const rein = (r, p) => d.prepare( "INSERT INTO chat_teilnehmer (raum_id, person_id, seit, gelesen_bis) VALUES (?,?,?,0)") .run(r, p, jetzt); const sagen = (r, p, text, wann) => d.prepare( "INSERT INTO chat_nachrichten (raum_id, person_id, text, erstellt) VALUES (?,?,?,?)") .run(r, p, text, wann); const rudel = raum("kanal", "Das Rudel", "treff"); for (const p of [idDogi, idVan, idFunny, idDiene, idMarina, idGhost, idMiss]) rein(rudel, p); sagen(rudel, idFunny, "@rudel das mache ich heute Abend fertig, versprochen 🙌", vorStunden(20)); const modkanal = raum("kanal", "Chat-Moderation", "chat"); for (const p of [idDogi, idVan, idFunny, idDiene, idMarina, idGhost]) rein(modkanal, p); sagen(modkanal, idMarina, "Ach man 😢😢", vorStunden(30)); const gruppe = raum("gruppe", "Abmeldungen 🏥", null); for (const p of [idDogi, idVan, idFunny, idDiene, idMarina, idGhost, idMiss]) rein(gruppe, p); sagen(gruppe, idDiene, "Guten Morgen ihr Lieben – ich bin heute leider raus, Hals tut weh.", vorStunden(31)); const mitVan = raum("direkt", null, null); rein(mitVan, idDogi); rein(mitVan, idVan); sagen(mitVan, idVan, "Da hat Miss was falsch eingegeben glaube ich", vorStunden(29)); sagen(mitVan, idDogi, "Hab's gesehen. Ich schreib ihr gleich, kein Ding.", vorStunden(28)); sagen(mitVan, idVan, "Top. Und die Abrechnung vom Wochenende liegt bei dir im Ordner.", vorStunden(27)); sagen(mitVan, idDogi, "Perfekt, danke dir ❤️", vorStunden(2)); const mitGhost = raum("direkt", null, null); rein(mitGhost, idDogi); rein(mitGhost, idGhost); sagen(mitGhost, idGhost, "Sorry hab's gefunden ist gelöscht 👍", vorStunden(29)); d.prepare("UPDATE chat_raeume SET letzte_am = ? WHERE id = ?").run(vorStunden(20), rudel); d.prepare("UPDATE chat_raeume SET letzte_am = ? WHERE id = ?").run(vorStunden(2), mitVan); d.prepare("UPDATE chat_raeume SET letzte_am = ? WHERE id = ?").run(vorStunden(29), mitGhost); d.prepare("UPDATE chat_raeume SET letzte_am = ? WHERE id = ?").run(vorStunden(30), modkanal); d.prepare("UPDATE chat_raeume SET letzte_am = ? WHERE id = ?").run(vorStunden(31), gruppe); 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: "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("; "); 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 || "jetzt"; try { for (const [wie, breite, hoehe] of [["computer", 1440, 980], ["handy", 412, 915]]) { 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(); const konsole = []; seite.on("console", (e) => { if (e.type() === "error" && !/favicon|ERR_CERT|net::ERR|410/i.test(e.text())) { konsole.push(e.text().slice(0, 140)); } }); await seite.goto(`https://${CREW}:${HP}/workspace/chat.html`, { waitUntil: "networkidle" }); await seite.waitForTimeout(1600); const liste = await seite.evaluate(() => ({ raeume: document.querySelectorAll("#raeume li").length, namen: [...document.querySelectorAll(".chat-raum__name")].map((x) => x.textContent), })); console.log(`\n=== Chat, ${breite} x ${hoehe} (${wie}) ===`); console.log(" Gespraeche:", liste.raeume, "|", liste.namen.join(" · ")); await seite.screenshot({ path: `server/mess-chat-${MARKE}-${wie}-liste.png` }); /* Und jetzt mit offenem Gespraech -- das ist die Haelfte der Seite, die man am laengsten ansieht. */ await seite.evaluate(() => { const zeilen = [...document.querySelectorAll("#raeume .chat-raum__knopf")]; (zeilen.find((z) => /VanVan/.test(z.textContent)) || zeilen[0])?.click(); }); await seite.waitForTimeout(1500); const verlauf = await seite.evaluate(() => ({ nachrichten: document.querySelectorAll(".chat-nachricht").length, titel: document.getElementById("verlauf-titel")?.textContent, })); console.log(" Verlauf:", verlauf.nachrichten, "Nachrichten in", verlauf.titel); await seite.screenshot({ path: `server/mess-chat-${MARKE}-${wie}-verlauf.png` }); if (konsole.length) console.log(" FEHLER IN DER KONSOLE:", konsole); console.log(` Bilder: server/mess-chat-${MARKE}-${wie}-{liste,verlauf}.png`); await kontext.close(); } } finally { await browser.close(); vorbau.close(); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } process.exit(0); }