diff --git a/server/mess-chat-optik.mjs b/server/mess-chat-optik.mjs new file mode 100644 index 00000000..7a655a71 --- /dev/null +++ b/server/mess-chat-optik.mjs @@ -0,0 +1,203 @@ +/* ===================================================================== + 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); +} diff --git a/server/pruef-chat-optik.mjs b/server/pruef-chat-optik.mjs index 95e4249f..717be9e7 100644 --- a/server/pruef-chat-optik.mjs +++ b/server/pruef-chat-optik.mjs @@ -769,8 +769,36 @@ melde("\n=== Die Schreibzeile auf dem Handy ==="); `das Schreibfeld ist ${zeile.feld?.b} px breit (mindestens 220)`); /* SENDEN STEHT BEIM FELD. Vorher lag es eine Zeile tiefer, unten links -- weiter vom Text entfernt als die Bueroklammer. */ - ok(Math.abs((zeile.senden?.y || 0) - (zeile.feld?.y || 0)) <= 6, - `und "Senden" steht daneben, nicht darunter (y ${zeile.senden?.y} zu ${zeile.feld?.y})`); + /* ==== SIE VERGLICH DIE OBERKANTEN (behoben 25.09.2026) ========= + + Hier stand `Math.abs(senden.y - feld.y) <= 6`. Die Absicht ist + richtig, die Messung war es nicht: Die Zeile ist `align-items: + flex-end` -- beide stehen UNTEN buendig. Das Schreibfeld ist + zwei Zeilen hoch (66 px), der Knopf 42. Die Oberkanten liegen + damit zwangslaeufig 24 px auseinander, OBWOHL beide in + derselben Zeile stehen. + + Die Pruefung war dadurch dauerhaft rot, ohne dass etwas kaputt + war -- und eine Warnung, die immer kommt, wird ueberlesen. + Gefunden habe ich es erst, als ich zweimal an der falschen + Stelle repariert hatte (Knopfbreite, Abstand): Beide Male + aenderte sich nichts an der Zahl. Eine Zahl, die sich durch die + Reparatur nicht bewegt, misst etwas anderes, als man denkt. + + GEFRAGT WIRD JETZT, WAS GEMEINT IST: Teilen sich die beiden + eine Zeile? Das heisst, ihre senkrechten Bereiche UEBERLAPPEN + sich -- und zwar deutlich, nicht nur um einen Pixel. Rutscht + "Senden" wirklich eine Zeile tiefer, ist die Ueberlappung null + und die Zeile wird rot. Sie prueft damit mehr als vorher, nicht + weniger: Die alte Fassung haette zwei gleich hohe Elemente auch + dann durchgelassen, wenn sie in verschiedenen Spalten stehen. */ + const fO = zeile.feld?.y || 0, fU = fO + (zeile.feld?.h || 0); + const sO = zeile.senden?.y || 0, sU = sO + (zeile.senden?.h || 0); + const gemeinsam = Math.min(fU, sU) - Math.max(fO, sO); + const kleinste = Math.min(zeile.feld?.h || 0, zeile.senden?.h || 0); + ok(kleinste > 0 && gemeinsam >= kleinste * 0.8, + `und "Senden" steht daneben, nicht darunter ` + + `(${gemeinsam} von ${kleinste} px gemeinsame Hoehe)`); ok(zeile.drueber.length === 0, `nichts liegt uebereinander (${zeile.drueber.join(", ") || "nichts"})`); ok(zeile.klein.length === 0, diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index f76d2f83..75b645a3 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,15 +16,15 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +