/* ===================================================================== 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"; import { eigenerPort } from "./helfer-port.mjs"; const PORT = await eigenerPort(import.meta); const HP = await eigenerPort(import.meta, "mess-chat-neu-stelle (HTTPS)", 1); 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"; import { zertifikatBauen } from "./helfer-openssl.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"); 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, 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);