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." DIE FUNKTION GAB ES SEIT DEM 09.09.2026 -- eine Linie „Ab hier neu" und einen Sprung darauf. Sie hat trotzdem nicht funktioniert, und zwar aus DREI Gruenden, die sich gegenseitig verdeckt haben. Gefunden hat sie keine Ueberlegung, sondern eine Messung in Pixeln: Wie weit ist die Linie vom oberen Rand entfernt? Ein negativer Wert heisst „darueber", also unsichtbar. 1. DER SPRUNG RECHNETE GEGEN DEN FALSCHEN PUNKT. `linie.offsetTop` ist der Abstand zum `offsetParent` -- und das ist nur dann der Verlauf, wenn dieser `position: relative` traegt. Tut er nicht. Gemessen auf 390 px landete die Linie 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. Jetzt: Oberkante der Linie minus Oberkante des Verlaufs plus dessen Bildlaufposition. Das gilt immer, egal wer wessen offsetParent ist. 2. JEDES NACHLADEN LOESCHTE DIE LINIE. `gelesenBeimOeffnen` wurde bei JEDEM Aufruf gesetzt, auch beim sanften Nachladen. Sanft laedt der Verlauf staendig nach -- vor allem, wenn der Ereignisstrom sich verbindet. Die Seite laedt, zeichnet, meldet „gelesen bis hier", der Strom verbindet sich, laedt sanft nach -- und jetzt steht in `gelesen_bis` schon die letzte Nachricht. Keine Linie mehr, Sprung ans Ende. DAS IST DER FEHLER, DEN FILIPE GESEHEN HAT. Und er wuerfelte: Kommt die Verbindung vor dem ersten Zeichnen, passiert nichts; danach ist die Linie weg. Ueber sechs Rollen gemessen waren mal drei rot, mal zwei, mal andere -- bei unveraendertem Code. 3. UND EIN EINZIGER SPRUNG REICHT NICHT. Zwischen Sprung und fertigem Bild waechst die Hoehe noch: Schriften kommen an und setzen den Text um, Bilder melden ihre Groesse. Jetzt wird nachgezogen -- nach den Schriften, nach jedem Bild, nach zwei Bildwiederholungen -- und die Stelle haelt, bis der Mensch selbst scrollt. „Ich habe dich an die neue Stelle gesetzt" ist eine Zusage; sie beim naechsten Nachladen zu brechen waere schlimmer, als sie nie gegeben zu haben. GEMESSEN -- server/pruef-chat-neu-stelle.mjs (neu), 63 Pruefungen, 0 Fehler, zweimal hintereinander mit demselben Ergebnis: Sechs Rollen in beiden Haeusern (DogFather, rechte Hand, linke Hand, Modi auf crew.; Manager und Creator auf workspace.), je auf Handy (390 px) und Rechner (1280 px). Je Blick: Steht die Linie im Verlauf? Ist sie zu SEHEN? Steht Zusammenhang darueber? Und ist der Verlauf NICHT am Ende? Ergebnis: 115-118 px unter dem oberen Rand am Handy, 150-153 px am Rechner -- darueber jeweils die letzte alte Nachricht. Dazu drei Gegenproben: Ohne Ungelesenes gibt es keine Linie und der Verlauf steht am Ende (sonst laendete man grundlos mitten im Verlauf); und die Messung erkennt „nicht sichtbar" auch wirklich (-1403 px an einem absichtlich nach unten gescrollten Verlauf) -- sonst waere jede gruene Zeile darueber wertlos. DIE PRUEFUNG SELBST HAT ZWEIMAL DAS FALSCHE GEMESSEN, bevor sie das Richtige maass, und beides steht als Begruendung darin: Sie schickte `/gelesen` ohne `bis` (die Route verlangt eine Nummer und lehnt sonst ab -- der Lesestand blieb null, die Linie entstand nie), und sie benutzte einen Raum je Rolle fuer zwei Blicke, was einen Wettlauf mit der „gelesen"-Meldung erzeugte. Jetzt bekommt jeder Blick seinen eigenen Raum: mehr Aufbau, dafuer immer dasselbe Ergebnis. pruef-chat 63/0, pruef-chat-optik 62/0, pruef-chat-kanaele 81/0, pruef-treffchat 110/0, pruef-chat-neu 36/0, pruef-pin-fuer-mich 37/0, pruef-erwaehnung 129/0, pruef-gifs 17/0. Co-Authored-By: Claude Opus 5 <[email protected]>
263 lines
12 KiB
JavaScript
263 lines
12 KiB
JavaScript
/* =====================================================================
|
|
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);
|