diff --git a/server/mess-chat-neu-stelle.mjs b/server/mess-chat-neu-stelle.mjs new file mode 100644 index 00000000..6fd48254 --- /dev/null +++ b/server/mess-chat-neu-stelle.mjs @@ -0,0 +1,262 @@ +/* ===================================================================== + WO STEHT DER VERLAUF, WENN MAN IHN AUFMACHT? (25.09.2026) + ===================================================================== + + Filipe: „wenn ich in den chat rein gehe und es neue kommentare gibt, + will ich dass mein chat sich da öffnet wo die neuen nachrichten + anfangen die ich noch nicht gesehen hab bitte und nicht immer ganz + unten. sonst muss man immer hoch scrollen um die neuen zu lesen und + das ist scheisse." + + IM QUELLTEXT STEHT DAS SEIT DEM 09.09.2026 -- eine Linie „Ab hier + neu" und ein Sprung darauf. Filipe sagt, es passiert nicht. Also + wird gemessen statt gelesen: Zwei Menschen, echte Nachrichten, und + danach die Frage, wo der Verlauf wirklich steht. + + GEMESSEN WIRD IN PIXELN, nicht im Gefuehl: Wie weit ist die Linie + vom oberen Rand des sichtbaren Bereichs entfernt? Steht sie im Bild? + Und wie viel Verlauf liegt darunter -- also: wie weit haette man + scrollen muessen? + + Aufrufen mit: node server/mess-chat-neu-stelle.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 = 5491, HP = 5492; +const CREW = "crew.dogfather-universe.com"; +const ordner = mkdtempSync(join(tmpdir(), "ws-neustelle-")); +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(300_000, "mess-chat-neu-stelle"); +await new Promise((r) => setTimeout(r, 900)); +process.on("uncaughtException", (f) => { console.error("ABSTURZ:", 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; +}; +const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); +const idA = anlegen("Kessi", "modi", "CODE-MODI-0001"); +d.close(); + +const roh = (keks, pfad, methode = "GET", koerper = null) => + new Promise((fertig, schief) => { + const daten = koerper === null ? null : JSON.stringify(koerper); + const a = httpAnfrage({ + host: "127.0.0.1", port: PORT, path: pfad, method: methode, + headers: { + Host: CREW, ...(keks ? { Cookie: keks } : {}), + Origin: `https://${CREW}`, Referer: `https://${CREW}/workspace/chat.html`, + ...(daten ? { "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(daten) } : {}), + }, + }, (r) => { + let t = ""; r.on("data", (x) => { t += x; }); + r.on("end", () => fertig({ status: r.statusCode, kopf: r.headers, + json: () => { try { return JSON.parse(t); } catch { return {}; } } })); + }); + a.on("error", schief); + if (daten) a.write(daten); + a.end(); + }); +const anmelden = async (rolle, code) => { + const a = await roh(null, "/workspace/api/anmelden", "POST", { rolle, code }); + return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; "); +}; + +const kDogi = await anmelden("admin", "CODE-DOGI-0001"); +const kA = await anmelden("modi", "CODE-MODI-0001"); + +const gr = await roh(kDogi, "/workspace/api/chat/direkt", "POST", { mit: idA }); +const raumId = gr.json().raum_id; +console.log(`Raum #${raumId}`); + +const schreiben = (keks, text) => + roh(keks, `/workspace/api/chat/raeume/${raumId}/nachrichten`, "POST", { text }); + +/* ---- 20 Nachrichten, die DogFather schon gelesen hat -------------- */ +let letzteAlte = 0; +for (let i = 1; i <= 20; i++) { + const a = await schreiben(kA, `Alte Nachricht ${i} von Kessi.`); + letzteAlte = a.json().nachricht?.id || a.json().id || letzteAlte; +} +/* DogFather macht auf und liest. `bis` IST PFLICHT -- die Route + verlangt eine Nummer und lehnt sonst mit 400 ab. Im ersten Anlauf + habe ich ein leeres Objekt geschickt; `gelesen_bis` blieb damit auf + null, die Linie „Ab hier neu" entstand nie, und die Messung meldete + einen Fehler, den es nicht gab. Eine Messung, die falsch aufsetzt, + findet zuverlaessig Befunde -- nur nicht die echten. */ +await roh(kDogi, `/workspace/api/chat/raeume/${raumId}/nachrichten`); +const gel = await roh(kDogi, `/workspace/api/chat/raeume/${raumId}/gelesen`, + "PUT", { bis: letzteAlte }); +console.log(`gelesen bis #${letzteAlte}: HTTP ${gel.status}`); +await new Promise((r) => setTimeout(r, 300)); + +/* ---- Und jetzt zwoelf neue, die er NICHT gesehen hat -------------- */ +for (let i = 1; i <= 12; i++) await schreiben(kA, `NEUE Nachricht ${i} von Kessi.`); + +const stand = await roh(kDogi, `/workspace/api/chat/raeume/${raumId}/nachrichten`); +console.log(`gelesen_bis beim Oeffnen: ${stand.json().gelesen_bis}` + + `, Nachrichten: ${(stand.json().nachrichten || []).length}`); + +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"], +}); + +for (const [wie, breite, hoehe, mobil] of [ + ["handy", 390, 844, true], ["rechner", 1280, 900, false], +]) { + /* ==== VOR JEDEM DURCHLAUF DEN LESESTAND ZURUECKSETZEN =========== + + Beim Oeffnen meldet die Seite „gelesen bis hier" -- das ist + richtig und hat im ersten Anlauf den zweiten Durchlauf ruiniert: + Nach dem Handy war nichts mehr neu, die Linie entstand nicht, und + die Messung meldete fuer den Rechner „Linie FEHLT". Ein Befund, + den die Messung selbst erzeugt hatte. + + Zurueckgesetzt wird direkt in der Datenbank und nicht ueber die + Route: Die laesst `bis` absichtlich nicht kleiner werden. */ + const kontext = await browser.newContext({ + viewport: { width: breite, height: hoehe }, + deviceScaleFactor: mobil ? 2 : 1, isMobile: mobil, hasTouch: mobil, + }); + await kontext.addCookies(kDogi.split("; ").map((k) => { + const [x, ...r] = k.split("="); + return { name: x, 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)); + } + }); + /* DER LESESTAND WIRD UNMITTELBAR VOR DEM AUFRUF ZURUECKGESETZT. + + Beim Oeffnen meldet die Seite „gelesen bis hier" -- richtig, und + im ersten Anlauf hat es den zweiten Durchlauf ruiniert: Nach dem + Handy war nichts mehr neu, die Linie entstand nicht, und die + Messung meldete fuer den Rechner „Linie FEHLT". Ein Befund, den + die Messung selbst erzeugt hatte. + + Der Reset stand zuerst vor `newContext` -- ein paar Millisekunden + zu frueh: Die verzoegerte Meldung des vorigen Durchlaufs kam + danach. Jetzt steht er unmittelbar vor dem Aufruf, mit einer + kurzen Pause davor, damit der vorige Browser wirklich fertig ist. + + Zurueckgesetzt wird in der Datenbank und nicht ueber die Route: + Die laesst `bis` absichtlich nicht kleiner werden. */ + await new Promise((r) => setTimeout(r, 600)); + { + const dd = new DatabaseSync(process.env.WORKSPACE_DB); + dd.prepare("UPDATE chat_teilnehmer SET gelesen_bis = ? WHERE raum_id = ? AND person_id = ?") + .run(letzteAlte, raumId, idDogi); + dd.close(); + } + await seite.goto(`https://${CREW}:${HP}/workspace/chat.html?raum=${raumId}`, + { waitUntil: "networkidle" }); + await seite.waitForTimeout(2200); + + const lage = await seite.evaluate(() => { + const v = document.getElementById("verlauf"); + if (!v) return { da: false }; + const linie = v.querySelector(".chat-neu"); + const alle = [...v.querySelectorAll(".chat-nachricht")]; + const sicht = v.getBoundingClientRect(); + const ersteSichtbare = alle.find((z) => { + const r = z.getBoundingClientRect(); + return r.bottom > sicht.top + 4; + }); + return { + da: true, + hoehe: Math.round(v.clientHeight), + gesamt: Math.round(v.scrollHeight), + oben: Math.round(v.scrollTop), + amEnde: v.scrollHeight - v.scrollTop - v.clientHeight < 40, + linieDa: !!linie, + linieOben: linie ? Math.round(linie.offsetTop) : null, + /* Steht die Linie IM BILD? Das ist die eigentliche Frage. */ + linieImBild: linie + ? (linie.getBoundingClientRect().top >= sicht.top - 2 + && linie.getBoundingClientRect().bottom <= sicht.bottom + 2) + : false, + linieAbstandVonOben: linie + ? Math.round(linie.getBoundingClientRect().top - sicht.top) : null, + ersteSichtbar: ersteSichtbare + ? (ersteSichtbare.textContent || "").trim().slice(0, 34) : "-", + nachrichten: alle.length, + /* Wie weit muesste man hochscrollen, um die Linie zu sehen? */ + muessteHoch: linie + ? Math.max(0, Math.round(v.scrollTop - linie.offsetTop)) : null, + }; + }); + + console.log(`\n=== ${wie} (${breite}x${hoehe}) ===`); + if (!lage.da) { console.log(" KEIN VERLAUF"); } + else { + console.log(` Verlauf ${lage.hoehe} px hoch, Inhalt ${lage.gesamt} px, ` + + `steht bei ${lage.oben}${lage.amEnde ? " (GANZ UNTEN)" : ""}`); + console.log(` Linie „Ab hier neu": ${lage.linieDa ? "da" : "FEHLT"}` + + (lage.linieDa ? `, bei ${lage.linieOben} px` : "")); + if (lage.linieDa) { + console.log(` Im Bild: ${lage.linieImBild ? "JA" : "NEIN"}` + + `, ${lage.linieAbstandVonOben} px unter dem oberen Rand` + + (lage.muessteHoch ? ` -- man muesste ${lage.muessteHoch} px HOCHSCROLLEN` : "")); + } + console.log(` Oben im Bild steht: "${lage.ersteSichtbar}"`); + console.log(` ${lage.nachrichten} Nachrichten im Verlauf`); + } + if (konsole.length) console.log(` KONSOLE: ${konsole.join(" | ")}`); + await seite.screenshot({ path: `server/mess-neustelle-${wie}.png` }).catch(() => {}); + await kontext.close(); +} + +await browser.close(); +vorbau.close(); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +process.exit(0); diff --git a/server/pruef-chat-neu-stelle.mjs b/server/pruef-chat-neu-stelle.mjs new file mode 100644 index 00000000..67095a2d --- /dev/null +++ b/server/pruef-chat-neu-stelle.mjs @@ -0,0 +1,379 @@ +/* ===================================================================== + DER CHAT OEFFNET DORT, WO DAS NEUE ANFAENGT (25.09.2026) + ===================================================================== + + Filipe: „wenn ich in den chat rein gehe und es neue kommentare gibt, + will ich dass mein chat sich da öffnet wo die neuen nachrichten + anfangen die ich noch nicht gesehen hab bitte und nicht immer ganz + unten. sonst muss man immer hoch scrollen um die neuen zu lesen und + das ist scheisse. perfektionier das bei allen rollen." + + --------------------------------------------------------------------- + WARUM ES DIESE PRUEFUNG GIBT, OBWOHL DIE FUNKTION SEIT DEM 09.09. DA IST + + Sie war da und sass falsch. Der Sprung rechnete mit `offsetTop` -- + dem Abstand zum `offsetParent`, und das ist nur dann der Verlauf + selbst, wenn dieser `position: relative` traegt. Tut er nicht. + Gemessen auf 390 px landete die Linie „Ab hier neu" 23 px OBERHALB + des sichtbaren Bereichs: Man musste genau das tun, was Filipe nicht + mehr tun wollte. Am Rechner stimmte es zufaellig, weil dort weniger + dazwischenliegt -- deshalb ist es nie aufgefallen. + + EIN FEHLER, DEN NUR EINE MESSUNG IN PIXELN FINDET. „Die Linie ist + da" war die ganze Zeit wahr. Gemessen wird hier deshalb, ob sie IM + BILD steht und wie weit sie vom oberen Rand entfernt ist. + + --------------------------------------------------------------------- + BEI ALLEN ROLLEN, UND ZWAR IN BEIDEN HAEUSERN + + Die Mechanik ist dieselbe Datei fuer alle -- aber „dieselbe Datei" + war schon oefter ein Trugschluss: Die Kopfleiste ist je nach Rolle + verschieden hoch, der Treff hat eigene Regeln, und die Agentur laedt + ihre eigene Wand. Ein Verlauf, der bei DogFather richtig sitzt und + beim Modi 40 px daneben, ist fuer den Modi kaputt. + + Aufrufen mit: node server/pruef-chat-neu-stelle.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 ordner = mkdtempSync(join(tmpdir(), "ws-neustelle-p-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); + +const { eigenerPort } = await import("./helfer-port.mjs"); +const PORT = await eigenerPort(import.meta, "die Pruefung der Oeffnungsstelle"); +const HP = PORT + 1; + +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 }); +}; +import { notbremse } from "./helfer-notbremse.mjs"; +await import("./index.js"); +notbremse(300_000, "pruef-chat-neu-stelle"); +await new Promise((r) => setTimeout(r, 1000)); +process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); +process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); }); + +const CREW = "crew.dogfather-universe.com"; +const AGENTUR = "workspace.dogfather-universe.com"; + +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 { 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; +}; +const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); +const idHand = anlegen("Rieke", "hand", "CODE-HAND-0001"); +const idLinke = anlegen("Mara", "linke", "CODE-LINK-0001"); +const idModi = anlegen("Kessi", "modi", "CODE-MODI-0001"); +const idManager = anlegen("Cigdem", "manager", "CODE-MANA-0001"); +const idCreator = anlegen("Luna", "creator", "CODE-CREA-0001"); +/* DIE BETREUUNG GEHOERT DAZU. Im Agenturhaus darf ein Manager nur mit + SEINEN Creators schreiben -- ohne diese Zeile antwortet die Route + mit 403, und die Pruefung meldet einen Fehler, den es nicht gibt. + Im ersten Anlauf genau so passiert: „Manager: ein Gespraech entsteht + (403)". Der Server hatte recht; die Lage war unvollstaendig. */ +d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit, gesetzt_von)" + + " VALUES (?,?,?,?) ON CONFLICT(creator_id) DO UPDATE SET betreuer_id = excluded.betreuer_id") + .run(idCreator, idManager, jetzt, idDogi); +d.close(); + +const roh = (host, keks, pfad, methode = "GET", koerper = null) => + new Promise((fertig, schief) => { + const daten = koerper === null ? null : JSON.stringify(koerper); + const a = httpAnfrage({ + host: "127.0.0.1", port: PORT, path: pfad, method: methode, + headers: { + Host: host, ...(keks ? { Cookie: keks } : {}), + Origin: `https://${host}`, Referer: `https://${host}/workspace/chat.html`, + ...(daten ? { "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(daten) } : {}), + }, + }, (r) => { + let t = ""; r.on("data", (x) => { t += x; }); + r.on("end", () => fertig({ status: r.statusCode, kopf: r.headers, + json: () => { try { return JSON.parse(t); } catch { return {}; } } })); + }); + a.on("error", schief); + if (daten) a.write(daten); + a.end(); + }); +const anmelden = async (host, rolle, code) => { + const a = await roh(host, null, "/workspace/api/anmelden", "POST", { rolle, code }); + if (a.status !== 200) { console.log(`ABBRUCH: ${rolle} ${a.status}`); process.exit(2); } + return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; "); +}; + +/* ======================================================================= + DIE LAGE: je Rolle ein Gespraech mit 20 alten und 12 neuen Zeilen + ======================================================================= */ +const LEUTE = [ + { wer: "DogFather", host: CREW, rolle: "admin", code: "CODE-DOGI-0001", id: idDogi, + partner: idModi, partnerCode: "CODE-MODI-0001", partnerRolle: "modi", partnerHost: CREW }, + { wer: "rechte Hand", host: CREW, rolle: "hand", code: "CODE-HAND-0001", id: idHand, + partner: idModi, partnerCode: "CODE-MODI-0001", partnerRolle: "modi", partnerHost: CREW }, + { wer: "linke Hand", host: CREW, rolle: "linke", code: "CODE-LINK-0001", id: idLinke, + partner: idModi, partnerCode: "CODE-MODI-0001", partnerRolle: "modi", partnerHost: CREW }, + { wer: "Modi", host: CREW, rolle: "modi", code: "CODE-MODI-0001", id: idModi, + partner: idHand, partnerCode: "CODE-HAND-0001", partnerRolle: "hand", partnerHost: CREW }, + { wer: "Manager", host: AGENTUR, rolle: "manager", code: "CODE-MANA-0001", id: idManager, + partner: idCreator, partnerCode: "CODE-CREA-0001", partnerRolle: "creator", + partnerHost: AGENTUR }, + { wer: "Creator", host: AGENTUR, rolle: "creator", code: "CODE-CREA-0001", id: idCreator, + partner: idManager, partnerCode: "CODE-MANA-0001", partnerRolle: "manager", + partnerHost: AGENTUR }, +]; + +/* ---- Zertifikat und Vorbau (eine Adresse je Haus) ------------------- */ +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},DNS:${AGENTUR}`], { 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 }, + }, (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,MAP ${AGENTUR} 127.0.0.1`, + "--ignore-certificate-errors"], +}); + +/** Wo steht der Verlauf, nachdem die Seite geoeffnet wurde? */ +async function blick(person, breite, hoehe, keks, raumId) { + const kontext = await browser.newContext({ + viewport: { width: breite, height: hoehe }, + deviceScaleFactor: breite < 700 ? 2 : 1, + isMobile: breite < 700, hasTouch: breite < 700, + }); + await kontext.addCookies(keks.split("; ").map((k) => { + const [x, ...r] = k.split("="); + return { name: x, value: r.join("="), domain: person.host, path: "/" }; + })); + const seite = await kontext.newPage(); + const konsole = []; + seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message.slice(0, 120))); + await seite.goto(`https://${person.host}:${HP}/workspace/chat.html?raum=${raumId}`, + { waitUntil: "networkidle" }); + await seite.waitForTimeout(2200); + + const lage = await seite.evaluate(() => { + const v = document.getElementById("verlauf"); + if (!v) return { da: false }; + const linie = v.querySelector(".chat-neu"); + const sicht = v.getBoundingClientRect(); + return { + da: true, + hoehe: Math.round(v.clientHeight), + amEnde: v.scrollHeight - v.scrollTop - v.clientHeight < 40, + linieDa: !!linie, + /* DIE EIGENTLICHE FRAGE: Steht sie im Bild, und wie weit unter + dem oberen Rand? Ein negativer Wert heisst: darueber, also + unsichtbar -- genau der Fehler, um den es hier geht. */ + abstand: linie + ? Math.round(linie.getBoundingClientRect().top - sicht.top) : null, + sichtbar: linie + ? (linie.getBoundingClientRect().top >= sicht.top - 2 + && linie.getBoundingClientRect().bottom <= sicht.bottom + 2) + : false, + /* Was steht ganz oben im Bild? Ueber der Linie soll Zusammenhang + stehen, nicht schon das Neue. */ + obenImBild: (() => { + const z = [...v.querySelectorAll(".chat-nachricht")] + .find((x) => x.getBoundingClientRect().bottom > sicht.top + 4); + return z ? (z.textContent || "").trim().slice(0, 24) : "-"; + })(), + }; + }); + await kontext.close(); + return { ...lage, konsole }; +} + +melde(""); +melde("=== 1. Bei jeder Rolle oeffnet der Chat an der neuen Stelle ==="); + +/* ==== JEDER BLICK BEKOMMT SEINEN EIGENEN RAUM ====================== + + Der erste Anlauf benutzte einen Raum je Rolle und setzte den + Lesestand zwischen Handy und Rechner zurueck. Das Ergebnis + FLATTERTE: Mal war DogFather rot, mal die rechte Hand, mal der Modi + -- bei unveraendertem Code. + + Der Grund ist ein Wettlauf: Die Seite meldet „gelesen bis hier" beim + Oeffnen, und zwar nicht sofort. Wird der Stand danach + zurueckgesetzt, gewinnt mal das eine, mal das andere. Eine Pause + davor macht es wahrscheinlicher und nicht sicher -- und eine + Pruefung, die von Wahrscheinlichkeit lebt, ist keine. + + MIT EINEM EIGENEN RAUM JE BLICK gibt es nichts zurueckzusetzen. Das + ist mehr Aufbau und dafuer eine Messung, die immer dasselbe sagt. */ +for (const person of LEUTE) { + const keks = await anmelden(person.host, person.rolle, person.code); + const kPartner = await anmelden(person.partnerHost, person.partnerRolle, person.partnerCode); + + for (const [wie, breite, hoehe] of [["Handy", 390, 844], ["Rechner", 1280, 900]]) { + const gr = await roh(person.host, keks, "/workspace/api/chat/gruppe", "POST", + { name: `${person.wer} ${wie}`, mit: [person.partner] }); + const raumId = gr.json().raum_id; + if (!raumId) { + ok(false, `${person.wer} / ${wie}: ein Raum entsteht (${gr.status})`); + continue; + } + + let letzteAlte = 0; + for (let i = 1; i <= 20; i++) { + const a = await roh(person.partnerHost, kPartner, + `/workspace/api/chat/raeume/${raumId}/nachrichten`, "POST", + { text: `Alte Nachricht ${i} -- lange genug, damit sie auf einem Handy umbricht.` }); + letzteAlte = a.json().nachricht?.id || a.json().id || letzteAlte; + } + await roh(person.host, keks, `/workspace/api/chat/raeume/${raumId}/gelesen`, + "PUT", { bis: letzteAlte }); + for (let i = 1; i <= 12; i++) { + await roh(person.partnerHost, kPartner, + `/workspace/api/chat/raeume/${raumId}/nachrichten`, "POST", + { text: `NEUE Nachricht ${i} -- auch diese ist lang genug zum Umbrechen.` }); + } + + /* Was sagt der Server unmittelbar vor dem Blick? Ohne diese Zeile + steht bei einem Fehlschlag nur „die Linie fehlt" da -- und man + weiss nicht, ob die Oberflaeche sie nicht zeichnet oder der + Server schon alles als gelesen fuehrt. */ + const vorher = await roh(person.host, keks, + `/workspace/api/chat/raeume/${raumId}/nachrichten`); + const standVor = vorher.json().gelesen_bis; + + const l = await blick(person, breite, hoehe, keks, raumId); + const wo = `${person.wer} / ${wie}`; + ok(l.da && l.linieDa, `${wo}: die Linie „Ab hier neu" steht im Verlauf ` + + `(gelesen_bis ${standVor}, letzte alte #${letzteAlte})`); + ok(l.sichtbar === true, + `${wo}: und sie ist zu SEHEN (${l.abstand} px unter dem oberen Rand)`); + /* Nicht am Rand kleben: Ueber der Linie soll Zusammenhang stehen. + Ein Viertel der Hoehe ist die Vorgabe; gemessen wird grosszuegig + (mindestens 24 px), damit eine andere Kopfhoehe nicht sofort rot + wird -- die Aussage ist „es steht etwas darueber", nicht „genau + ein Viertel". */ + ok((l.abstand || 0) >= 24, + `${wo}: mit Zusammenhang darueber ("${l.obenImBild}")`); + ok(l.amEnde === false, `${wo}: und NICHT ganz unten`); + ok(l.konsole.length === 0, `${wo}: keine Abstuerze (${l.konsole.join(" | ") || "keine"})`); + } +} + +/* ======================================================================= + 2. GEGENPROBEN + ======================================================================= */ +melde(""); +melde("=== 2. Gegenproben (jede MUSS anschlagen) ==="); +{ + const person = LEUTE[0]; + const keks = await anmelden(person.host, person.rolle, person.code); + const kPartner = await anmelden(CREW, "modi", "CODE-MODI-0001"); + + const gr = await roh(person.host, keks, "/workspace/api/chat/direkt", "POST", + { mit: idModi }); + const raumId = gr.json().raum_id; + + /* (a) NICHTS NEUES -> ans Ende. Ohne diese Zeile koennte der Sprung + immer stattfinden, und wer nichts verpasst hat, laendete + grundlos mitten im Verlauf. */ + let letzte = 0; + for (let i = 1; i <= 8; i++) { + const a = await roh(CREW, kPartner, `/workspace/api/chat/raeume/${raumId}/nachrichten`, + "POST", { text: `Alles gelesen ${i}.` }); + letzte = a.json().nachricht?.id || a.json().id || letzte; + } + await roh(person.host, keks, `/workspace/api/chat/raeume/${raumId}/gelesen`, + "PUT", { bis: letzte }); + await new Promise((r) => setTimeout(r, 400)); + + const ohneNeues = await blick(person, 390, 844, keks, raumId); + ok(ohneNeues.linieDa === false, + `ohne Ungelesenes gibt es keine Linie (${ohneNeues.linieDa})`); + ok(ohneNeues.amEnde === true, + "und der Verlauf steht am Ende -- da, wo man weiterschreibt"); + + /* (b) Kann die Messung „nicht sichtbar" ueberhaupt erkennen? Sonst + waere jede gruene Zeile oben wertlos. Gemessen wird an einem + Verlauf, der absichtlich nach unten gescrollt wird. */ + for (let i = 1; i <= 12; i++) { + await roh(CREW, kPartner, `/workspace/api/chat/raeume/${raumId}/nachrichten`, + "POST", { text: `Danach noch etwas ${i}.` }); + } + { + const dd = new DatabaseSync(process.env.WORKSPACE_DB); + dd.prepare("UPDATE chat_teilnehmer SET gelesen_bis = ? WHERE raum_id = ? AND person_id = ?") + .run(letzte, raumId, person.id); + dd.close(); + } + const kontext = await browser.newContext({ viewport: { width: 390, height: 844 }, + deviceScaleFactor: 2, isMobile: true, hasTouch: true }); + await kontext.addCookies(keks.split("; ").map((k) => { + const [x, ...r] = k.split("="); + return { name: x, value: r.join("="), domain: person.host, path: "/" }; + })); + const seite = await kontext.newPage(); + await seite.goto(`https://${person.host}:${HP}/workspace/chat.html?raum=${raumId}`, + { waitUntil: "networkidle" }); + await seite.waitForTimeout(2000); + const kuenstlich = await seite.evaluate(() => { + const v = document.getElementById("verlauf"); + v.scrollTop = v.scrollHeight; /* mit Absicht ans Ende */ + const linie = v.querySelector(".chat-neu"); + const sicht = v.getBoundingClientRect(); + if (!linie) return null; + return { + abstand: Math.round(linie.getBoundingClientRect().top - sicht.top), + sichtbar: linie.getBoundingClientRect().top >= sicht.top - 2 + && linie.getBoundingClientRect().bottom <= sicht.bottom + 2, + }; + }); + await kontext.close(); + ok(kuenstlich && kuenstlich.sichtbar === false && kuenstlich.abstand < 0, + `am Ende ist die Linie NICHT sichtbar (${kuenstlich?.abstand} px) ` + + "-- die Messung kann also auch nein sagen"); +} + +await browser.close(); +vorbau.close(); + +melde(""); +melde(`${geprueft} Pruefungen, ${fehler} Fehler`); +melde(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG"); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +process.exit(fehler === 0 ? 0 : 1); diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index 5d130db6..93cf4345 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,8 +16,8 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +