Files
dogfather-universe/server/mess-quer.mjs
T
DogFatherGitandClaude Opus 5 f6225d7341 Ports: auch die Messwerkzeuge leiten ihre Nummer ab
GEMESSEN, BEVOR ICH ETWAS ANGEFASST HABE:

  204 pruef-Dateien, abgeleitete Ports 5000-5409 (dicht belegt)
  24 mess-Dateien, Nummern von Hand: 4471 bis 5493
  davon IM Pruefbereich: 5387, 5397, 5397, 5399, 5399, 5401, 5403, 5405
  untereinander doppelt: 5397, 5399, 5461, 5483, 5491

Aufgefallen ist es, weil mess-buehne und mess-reaktion beide auf 5483
lagen und ein haengengebliebener Lauf gestern einen ganzen Messlauf
gekostet hat.

Beim Umbau kam das Groessere heraus: EINUNDZWANZIG der 24 Messdateien
hatten nicht nur eine Nummer von Hand, sondern ueberhaupt keinen
Waechter -- schlicht `const PORT = 5397;`. Liegt dort schon ein
Server, startet der eigene still nicht, und gemessen wird ab da ein
fremder Stand. Genau der Fehler, gegen den helfer-port.mjs gebaut
wurde; die Messdateien standen die ganze Zeit ausserhalb.

WAS JETZT GILT

Zwei Sorten, zwei Bereiche, beide abgeleitet aus der Stelle im
Alphabet -- jede Sorte unter ihresgleichen, sonst verschoebe eine
neue Pruefung die Nummern aller Messungen. Pruefungen ab 5000,
Messungen ab MESS_BASIS = 5900.

5900 und nicht 5500: dazwischen bleibt Platz fuer 245 weitere
Pruefdateien (bei 5500 waeren es 45). Nach oben 5948 + AUSWEICHEN
4000 = 9948, also unter 10080, der naechsten gesperrten Nummer.
Nachgerechnet, nicht geschaetzt -- eine geschaetzte 4500 hatte bei
BASIS schon einmal danebengelegen.

Eine Wache dazu: Waechst der Pruefbereich bis an MESS_BASIS heran,
bricht die Ableitung ab und sagt, was zu tun ist. Eine stille
Ueberschneidung waere genau der Fehler, den das hier beseitigt.

Die zweite Nummer kommt ueber nr=1, nie ueber `PORT + 1`:
portNummer ueberspringt gesperrte Nummern, deshalb kann die naechste
Zahl die Nummer der naechsten DATEI sein, sobald einmal eine Sperre
dazwischenliegt. Heute liegt dort keine -- das ist Glueck, kein
Entwurf.

NEBENBEI GEFUNDEN UND MIT REPARIERT

Sechs bild-*.mjs riefen den Waechter und warfen seine Antwort weg:

    await portMussFreiSein(4315, "das Bildwerkzeug");
    process.env.PORT = "4315";
    const BASIS = "http://127.0.0.1:4315";

Er lief, meldete nichts und wirkte nicht. Gibt das System den Port
dauerhaft nicht her, weicht er auf Port + 4000 aus und GIBT DIE NEUE
NUMMER ZURUECK -- diese Werkzeuge hoerten danach trotzdem auf der
alten und stuerzten mit `listen EACCES` ab, also mit genau dem
Fehler, gegen den er gebaut wurde. Dazu stand die Zahl dreimal je
Datei. Jetzt einmal, und die Antwort wird benutzt.

tiktok-videos.mjs hatte den Waechter ABGESCHRIEBEN -- eine kurze
eigene Fassung ohne den dritten Ausgang: Bei EACCES meldete sie
"belegt" und brach ab, statt auszuweichen. Auf diesem Rechner ist
genau das am 23.09. eingetreten (Port 5040, Windows-Dienst).
mess-fokus und mess-notizblock hatten dieselbe Abschrift. Eine
abgeschriebene Sicherung ist dieselbe Falle wie eine abgeschriebene
Liste.

GEPRUEFT

  pruef-portnummern  15 -> 41 Pruefungen, 0 Fehler
  pruef-ports         8 -> 10 Pruefungen, 456 statt 408 Ports geprobt
  node --check auf allen 33 geaenderten Dateien

Gegenproben, die wirklich rot werden:
  - eine Messdatei auf eine feste Nummer zurueckgesetzt -> 2 FEHL,
    danach wieder 41/0
  - die Wache: in einem Wegwerf-Ordner mit 452 pruef-Dateien bricht
    eigenerPort ab statt still zu ueberlappen; eine Datei knapp
    darunter bekommt weiter ihre Nummer (5846)
  - die Erkennungen fuer feste Nummern, PORT + 1 und weggeworfene
    Waechterantworten je gegen einen gebauten Rueckschritt

Am echten Verhalten gemessen:
  - mess-chat-liste und mess-alle-einzelsicht (beide vorher 5397)
    GLEICHZEITIG gestartet: 5906 und 5900, beide exit=0. Vorher war
    das unmoeglich.
  - die 48 neuen Nummern 5900-5947 auf diesem Rechner durchprobiert:
    keine belegt, keine vom System gesperrt
  - bild-chat.mjs durchgelaufen, drei Bilder, exit=0

Kein Eingriff am laufenden Dienst: helfer-port.mjs wird von index.js
und workspace.js nicht geladen (nachgesehen), nur von Pruef- und
Messwerkzeugen. Beide Haeuser unberuehrt -- es wird keine Zeile
angefasst, die eine Seite ausliefert.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-01 01:19:41 +02:00

584 lines
26 KiB
JavaScript

/* =====================================================================
WAS MUSS MAN SEITLICH SCHIEBEN? (28.09.2026)
Die Pruefung nebenan misst, was die Schnittstelle antwortet. Das
sagt nichts darueber, ob ein Mensch damit einen Abend verbringen
moechte. Dieses Werkzeug oeffnet die Seite in einem echten Browser,
schaltet sie durch alle drei Zustaende und legt Bilder ab.
Gemessen wird dabei auch, was man sonst uebersieht:
- laeuft etwas in die Konsole?
- ragt etwas seitlich hinaus?
- ist der Saal so hoch wie das Fenster, oder muss man scrollen?
NIE DIE ECHTE DATENBANK. Eigener Ordner, eigener Port, eigenes
Zertifikat -- und am Ende ist alles weg.
Aufrufen mit: node server/mess-reaktion.mjs
===================================================================== */
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { request } from "node:http";
import { execFileSync } from "node:child_process";
import { createServer as httpsServer } from "node:https";
import { zertifikatBauen } from "./helfer-openssl.mjs";
const HIER = dirname(fileURLToPath(import.meta.url));
const ordner = mkdtempSync(join(tmpdir(), "mess-reakt-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
import { eigenerPort } from "./helfer-port.mjs";
const PORT = await eigenerPort(import.meta);
const HTTPS_PORT = await eigenerPort(import.meta, "mess-quer (HTTPS)", 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";
for (const art of ["uncaughtException", "unhandledRejection"]) {
process.on(art, (f) => {
console.error(`\nABBRUCH (${art}): ${f?.stack || f}`);
process.exit(8);
});
}
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 }); };
await import("./index.js");
await new Promise((r) => setTimeout(r, 1000));
const CREW = "crew.dogfather-universe.com";
/* ---------- Leute ------------------------------------------------------- */
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const kenn = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", kenn, new Date().toISOString());
const ALT = new Date(Date.now() - 200 * 86400000).toISOString();
function anlegen(name, rolle, code) {
const salt = randomBytes(16).toString("hex");
const hash = scryptSync(code, salt, 64,
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n,"
+ " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768,
createHmac("sha256", kenn).update(code).digest("hex"), ALT);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
const CODES = { admin: "CODE-M00-000001", gast: "CODE-M01-000001", gast2: "CODE-M02-000001" };
const IDS = {
admin: anlegen("DogFather", "admin", CODES.admin),
gast: anlegen("Nachtfalter", "gast", CODES.gast),
gast2: anlegen("Lenny", "gast", CODES.gast2),
};
d.close();
function roh(pfad, { method = "GET", keks = "", rumpf = null } = {}) {
return new Promise((fertig, schief) => {
const daten = rumpf === null ? null : Buffer.from(JSON.stringify(rumpf));
const a = request({
host: "127.0.0.1", port: PORT, path: pfad, method,
headers: {
Host: CREW, Origin: `https://${CREW}`,
...(keks ? { Cookie: keks } : {}),
...(daten ? { "Content-Type": "application/json", "Content-Length": daten.length } : {}),
},
}, (antwort) => {
const teile = [];
antwort.on("data", (s) => teile.push(s));
antwort.on("end", () => {
const text = Buffer.concat(teile).toString("utf8");
let j = null; try { j = JSON.parse(text); } catch { /* kein JSON */ }
fertig({ code: antwort.statusCode, text, j, kopf: antwort.headers });
});
});
a.on("error", schief);
if (daten) a.write(daten);
a.end();
});
}
async function anmelden(rolle) {
const a = await roh("/workspace/api/anmelden", {
method: "POST",
rumpf: { rolle: rolle === "gast2" ? "gast" : rolle, code: CODES[rolle], alter_ok: true },
});
return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
}
const k = {
admin: await anmelden("admin"),
gast: await anmelden("gast"),
gast2: await anmelden("gast2"),
};
/* ---------- Die Buehne fuellen ------------------------------------------ */
await roh("/workspace/api/reaktion", {
method: "PUT", keks: k.admin,
rumpf: {
titel: "Wir schauen den neuen Trailer",
video: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
beginnt_am: new Date(Date.now() + 7 * 60_000).toISOString(),
},
});
/* ZWEI STUECKE IN DIE WARTESCHLANGE. Eine leere Liste sieht auf
einem Bildschirmfoto genauso aus wie eine kaputte -- beides ist
ein Kasten ohne Zeilen. Zwei echte Eintraege zeigen, ob Titel,
Vorschaubild und die vier Knoepfe je Zeile wirklich ankommen. */
for (const adresse of ["https://youtu.be/9bZkp7q19f0",
"https://www.youtube.com/watch?v=kJQP7kiw5Fk"]) {
await roh("/workspace/api/reaktion/liste", {
method: "POST", keks: k.admin, rumpf: { video: adresse },
});
}
const REDEN = [
["gast", "Bin schon daaa 🐺"],
["gast2", "Ich auch! Hab extra Popcorn geholt"],
["gast", "Wie lange noch?"],
["admin", "Zwei Minuten, ich richte noch die Kamera"],
["gast2", "Kein Stress, wir sitzen bequem"],
["gast", "Letztes Mal war so gut, ich hab geheult"],
];
/* ---------- Der Vorbau -------------------------------------------------- */
const schluesselDatei = join(ordner, "p.key");
const zertDatei = join(ordner, "p.crt");
zertifikatBauen(schluesselDatei, zertDatei, CREW);
const vorbau = httpsServer({
key: readFileSync(schluesselDatei), cert: readFileSync(zertDatei),
}, (von, nach) => {
const weiter = request({
host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
headers: { ...von.headers, host: `${CREW}:${HTTPS_PORT}` },
}, (antwort) => {
nach.writeHead(antwort.statusCode || 502, antwort.headers);
antwort.pipe(nach);
});
weiter.on("error", () => { try { nach.writeHead(502); nach.end(); } catch { /* egal */ } });
von.pipe(weiter);
});
await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r));
const BASIS = `https://${CREW}:${HTTPS_PORT}`;
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",
/* Kamera und Mikrofon ohne Rueckfrage -- und ein bewegtes
Testbild statt eines schwarzen Rechtecks. Ohne das sieht jede
Aufnahme aus, als waere die Kamera kaputt. */
"--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream"],
});
/* JEDE GEOEFFNETE SEITE HAELT ZWEI STEHENDE VERBINDUNGEN (der
Ereignisstrom der Kopfleiste und der dieser Seite). Ein Browser
laesst je Adresse nur wenige davon zu -- wer seine Seiten nicht
schliesst, nimmt den spaeteren den Platz weg.
GEMESSEN: Die Seiten aus den ersten beiden Abschnitten blieben
offen, und danach kam Lennys Strom gar nicht mehr zustande. Die
Signale gingen ins Leere, und die Kamerafenster blieben schwarz --
ein Messfehler, der wie ein Programmfehler aussah. */
async function alsWer(rolle, breite, hoehe) {
const kontext = await browser.newContext({
viewport: { width: breite, height: hoehe },
/* MIT FINGER, WENN ES EIN HANDY IST.
Ohne `hasTouch` meldet der Browser `pointer: fine` -- und
damit greift KEINE der Regeln aus `@media (pointer: coarse)`.
Dort stehen im ganzen Haus die 44-Pixel-Beruehrziele und seit
dem 28.09. auch das Ausblenden der Tastenkuerzel. Eine
Handy-Messung mit Mauszeiger misst eine Seite, die auf keinem
Handy so aussieht -- und meldet Hoehen, die dort andere sind. */
hasTouch: breite <= 860,
ignoreHTTPSErrors: true,
permissions: ["camera", "microphone"],
});
await kontext.addCookies((k[rolle] || "").split("; ").filter(Boolean).map((z) => {
const [n, ...r] = z.split("=");
return { name: n, value: r.join("="), domain: CREW, path: "/" };
}));
const seite = await kontext.newPage();
const konsole = [];
const anfragen = [];
seite.on("request", (r) => { if (/\/api\//.test(r.url())) anfragen.push(r.url().replace(/^https?:\/\/[^/]+/, "")); });
seite.on("pageerror", (f) => konsole.push(String(f?.message || f)));
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
seite.on("response", (a) => {
if (a.status() >= 400) konsole.push(`${a.status()} ${a.url().replace(/^https?:\/\/[^/]+/, "")}`);
});
return { kontext, seite, konsole, anfragen };
}
async function bild(seite, name) {
await seite.screenshot({ path: join(HIER, `mess-reaktion-${name}.png`) });
console.log(` Bild: server/mess-reaktion-${name}.png`);
}
async function vermessen(seite, wo) {
const m = await seite.evaluate(() => ({
ueber: Math.max(0, document.documentElement.scrollWidth - document.documentElement.clientWidth),
hoch: Math.max(0, document.documentElement.scrollHeight - document.documentElement.clientHeight),
stand: document.getElementById("saal")?.dataset.stand || "?",
schild: document.getElementById("schild-wort")?.textContent || "",
}));
console.log(` ${wo}: Stand „${m.stand}", Schild „${m.schild}", `
+ `seitlich ${m.ueber} px, senkrecht ${m.hoch} px`);
return m;
}
/* =====================================================================
WAS MUSS MAN SEITLICH SCHIEBEN?
Filipe, 28.09.2026: „ich will auch nicht dass man sachen nach links
und rechts schieben muss. perfektion das untereinander. ich will
niemals irgendwas nach links oder rechts swippen muessen."
NICHT GERATEN, SONDERN AUFGEZAEHLT. Ein `grep` nach `overflow-x`
findet die absichtlichen Roller. Es findet NICHT die Stelle, an der
ein Inhalt breiter ist als sein Kasten und der Browser von sich aus
eine Rollleiste anhaengt -- und genau das ist der Fall auf Filipes
Bildschirmfoto. Dieses Werkzeug geht jedes Element durch und nennt
jedes, dessen Inhalt breiter ist als sein Platz.
===================================================================== */
/* AUF SENDUNG. Nur dann steht die Seite vollstaendig da: ohne
laufende Sendung fehlen Chat, Kamerafenster und der halbe
Transport -- und dann misst man eine Seite, die es im Betrieb
nie gibt.
EIN EIGENER FEHLER, GEFUNDEN AM BILDSCHIRMFOTO: Die erste Fassung
schickte `PUT`; die Route nimmt aber `POST`. Der Aufruf lief ins
Leere, die Messung lief weiter, und das Schild sagte noch immer
„GESCHLOSSEN". Deshalb steht unten eine Nachfrage: Eine Messung,
die ihre eigene Voraussetzung nicht nachprueft, misst irgendetwas
und meldet es als Ergebnis. */
for (const stufe of ["vorbereitung", "live"]) {
await roh("/workspace/api/reaktion/stand", {
method: "POST", keks: k.admin, rumpf: { stand: stufe },
});
}
/* Ein Gast dazu -- sonst fehlt die halbe Gaesteliste, und im Saal
steht nur ein Kamerafenster statt zweier. */
await roh("/workspace/api/reaktion/gast", {
method: "POST", keks: k.admin, rumpf: { person_id: IDS.gast2 },
});
/* Und ein paar Worte im Chat -- eine leere Schiene kann nicht
ueberlaufen, und genau das waere ein gruener Haken ohne Inhalt. */
for (const [wer, text] of REDEN) {
await roh("/workspace/api/reaktion/chat", { method: "POST", keks: k[wer], rumpf: { text } });
}
/* EIN ZWEITES VIDEO -- SONST FEHLT DER UMSCHALTER.
Dieses Werkzeug meldete „nichts rollt seitlich", waehrend
`mess-reaktion` am selben Aufbau 76 Pixel fand. Der Unterschied:
Dort war ein zweites Video eingerichtet, also stand der
Umschalter in der Leiste -- mit einem ganzen Videotitel darin,
und genau der lief hinaus. Hier war er `hidden`.
Ein Knopf, den die Messung nie sieht, kann nicht ueberlaufen --
und ihr gruener Haken sagt dann nichts ueber ihn. */
await roh("/workspace/api/reaktion/zweitvideo", {
method: "POST", keks: k.admin,
rumpf: { video: "https://www.youtube.com/watch?v=kJQP7kiw5Fk" },
});
{
const nach = await roh("/workspace/api/reaktion", { keks: k.admin });
if (nach.j?.stand !== "live") {
console.error(`ABBRUCH: die Sendung steht auf „${nach.j?.stand}" statt „live" -- `
+ "ohne Sendung fehlen Chat und Kamerafenster, und dann misst dieses "
+ "Werkzeug eine Seite, die es im Betrieb nicht gibt.");
process.exit(9);
}
console.log(`Buehne steht: auf Sendung, ein Gast, Chat gefuellt, `
+ `zweites Video ${nach.j?.zweit?.video ? "bereit" : "FEHLT"}.`);
if (!nach.j?.zweit?.video) {
console.error("ABBRUCH: ohne zweites Video fehlt der Umschalter in der "
+ "Transportleiste -- und genau der lief am 28.09. seitlich hinaus.");
process.exit(9);
}
}
const GERAETE = [
["Rechner", 1440, 900],
["Tablet", 900, 1200],
["Handy", 412, 915],
["iPhone", 390, 844],
["kleines Handy", 360, 640],
["kleinstes Handy", 320, 568],
];
let befunde = 0;
for (const [gname, breite, hoehe] of GERAETE) {
console.log(`\n=== ${gname} (${breite}x${hoehe}) ===`);
const host = await alsWer("admin", breite, hoehe);
await host.seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "domcontentloaded" });
await host.seite.waitForSelector("#reiter", { timeout: 15000 });
/* ZUERST ZUGEKLAPPT -- DAS IST DER HAEUFIGSTE ZUSTAND.
Die erste Fassung mass nur mit offener Regie, weil dort die
gemeldete Stelle lag. `mess-reaktion` fand daraufhin 76 Pixel
seitlichen Ueberlauf auf einem 390er Handy, die hier nicht
auftauchten -- schlicht weil dort die Regie ZU war. Wer nur
den Zustand misst, in dem der Fehler gemeldet wurde, findet
den zweiten nie. */
{
/* AUSDRUECKLICH ZUKLAPPEN. Die Seite merkt sich den Zustand,
und beim Laden stand sie hier auf „offen" -- die Messung
haette den zugeklappten Fall also nie gesehen und trotzdem
„zugeklappt: 0 px" gemeldet. Ein gruener Haken ueber einen
Zustand, den man gar nicht hergestellt hat. */
await host.seite.evaluate(() => {
const p = document.getElementById("pult");
if (p && p.dataset.auf === "ja") document.getElementById("pult-griff")?.click();
});
await host.seite.waitForTimeout(400);
const zu = await host.seite.evaluate(() => ({
auf: document.getElementById("pult")?.dataset.auf,
ueber: Math.max(0, document.documentElement.scrollWidth
- document.documentElement.clientWidth),
breitestes: (() => {
let b = null, max = 0;
for (const e of document.querySelectorAll("*")) {
const r = e.getBoundingClientRect();
if (r.right > max) { max = r.right; b = e; }
}
return b ? `${b.tagName.toLowerCase()}${b.id ? "#" + b.id : ""}`
+ `${typeof b.className === "string" && b.className
? "." + b.className.trim().split(/\s+/)[0] : ""} bis ${Math.round(max)} px` : "-";
})(),
}));
console.log(` zugeklappt (auf=${zu.auf}): seitlich ${zu.ueber} px`);
if (zu.ueber > 0) {
console.log(` ACHTUNG: die Seite rollt bei ZUGEKLAPPTER Regie ${zu.ueber} px seitlich`);
console.log(` am weitesten rechts: ${zu.breitestes}`);
befunde++;
}
}
/* Die Regie aufklappen -- zugeklappt sieht man die Tafeln nicht,
und genau in ihnen steckt der gemeldete Ueberlauf. */
const auf = await host.seite.evaluate(() => {
const p = document.getElementById("pult");
if (p && p.dataset.auf !== "ja") document.getElementById("pult-griff")?.click();
return document.getElementById("pult")?.dataset.auf || "?";
});
await host.seite.waitForTimeout(400);
console.log(` Regie: ${auf}`);
const register = await host.seite.evaluate(() =>
[...document.querySelectorAll(".reiter__knopf")].map((b) => b.dataset.tafel));
console.log(` Register: ${register.join(" · ")}`);
{
const z = await host.seite.evaluate(() => {
const saal = document.getElementById("saal");
const teile = [...saal.children].filter((e) => e.getBoundingClientRect().height > 0)
.map((e) => { const q = e.getBoundingClientRect();
return `${e.id || e.className.split(" ")[0]} ${Math.round(q.top)}..${Math.round(q.bottom)}`; });
return `Zeilen ${getComputedStyle(saal).gridTemplateRows} | ${teile.join(" / ")}`;
});
console.log(` ${z}`);
}
/* Erst die Seite als Ganzes. */
const seitlich = await host.seite.evaluate(() =>
Math.max(0, document.documentElement.scrollWidth - document.documentElement.clientWidth));
if (seitlich > 0) { console.log(` ACHTUNG: die SEITE selbst rollt ${seitlich} px seitlich`); befunde++; }
for (const reg of register) {
await host.seite.evaluate((r) => {
document.querySelector(`.reiter__knopf[data-tafel="${r}"]`)?.click();
}, reg);
await host.seite.waitForTimeout(250);
const quer = await host.seite.evaluate(() => {
const raus = [];
for (const el of document.querySelectorAll("*")) {
const w = el.scrollWidth - el.clientWidth;
if (w <= 1) continue;
const r = el.getBoundingClientRect();
if (r.width === 0 || r.height === 0) continue; /* verborgen */
const st = getComputedStyle(el);
if (st.display === "none" || st.visibility === "hidden") continue;
/* NUR WAS MAN WIRKLICH SCHIEBEN KANN.
`overflow-x: hidden` ist KEIN Wischen -- dort ist der Text
abgeschnitten (meist mit „…"), und schieben laesst sich
nichts. Das ist ein anderes Thema und gehoert nicht in
diese Messung; wer es mitzaehlt, meldet 78 Stellen, von
denen 60 gar keine sind, und findet die echten nicht mehr.
`visible` zaehlt ebenfalls nicht als Stelle -- dort wandert
der Ueberlauf nach oben zum naechsten Roller, und DER ist
die Stelle. Ausnahme: Wurzel und Rumpf, denn dort rollt
das Fenster selbst. */
const wurzel = el === document.documentElement || el === document.body;
if (!wurzel && st.overflowX !== "auto" && st.overflowX !== "scroll") continue;
raus.push({
wer: el.tagName.toLowerCase()
+ (el.id ? "#" + el.id : "")
+ (el.className && typeof el.className === "string"
? "." + el.className.trim().split(/\s+/).join(".") : ""),
ueber: w,
innen: el.clientWidth,
inhalt: el.scrollWidth,
ox: st.overflowX,
/* Was ist das breiteste Kind? Das ist fast immer die
Ursache -- und ohne diese Angabe sucht man sie von Hand. */
/* DIE KETTE BIS ZUR URSACHE. Das breiteste Kind zu nennen
reicht nicht: Es ist meist selbst nur ein Kasten, der
sich nach SEINEM breitesten Kind richtet. Erst am Ende
der Kette steht das Ding, das wirklich zu breit ist --
und nur dort kann man etwas reparieren. */
kette: (() => {
const teile = [];
let jetzt = el;
for (let tief = 0; tief < 8; tief++) {
let b = null, max = 0;
for (const kind of jetzt.children) {
const kr = kind.getBoundingClientRect();
if (kr.width > max) { max = kr.width; b = kind; }
}
if (!b) break;
teile.push(`${b.tagName.toLowerCase()}${b.id ? "#" + b.id : ""}`
+ `${typeof b.className === "string" && b.className
? "." + b.className.trim().split(/\s+/)[0] : ""}:${Math.round(max)}`);
jetzt = b;
}
return teile.length ? teile.join(" > ") : "-";
})(),
});
}
return raus;
});
/* EIN BILD JE REGISTER AUF DEM RECHNER.
Zahlen sagen, dass nichts mehr rollt. Sie sagen nicht, ob es
danach gut aussieht -- und genau das war Filipes Frage. */
if (breite === 1440) {
await host.seite.locator("#pult-innen").screenshot({
path: join(HIER, `mess-quer-tafel-${reg}.png`) }).catch(() => {});
/* Die Stufenleiter steht weiter unten als das Fenster reicht --
sie braucht ein eigenes Bild. Ausgerechnet sie trug bis heute
den Stil der Chat-Leiste (Namensstreit), also ist sie die
Stelle, an der man nachsehen muss. */
if (reg === "gestaltung") {
await host.seite.locator("#stufen").screenshot({
path: join(HIER, "mess-quer-stufen.png") }).catch(() => {});
}
}
if (quer.length === 0) { console.log(` ${reg}: nichts rollt seitlich`); continue; }
console.log(` ${reg}: ${quer.length} Stelle(n) rollen seitlich`);
for (const q of quer) {
console.log(` ACHTUNG ${q.wer}`);
console.log(` Platz ${q.innen} px, Inhalt ${q.inhalt} px `
+ `(${q.ueber} px zu viel), overflow-x: ${q.ox}`);
console.log(` Kette: ${q.kette}`);
/* WER RAGT UEBER DIE RECHTE KANTE?
Die erste Fassung fragte nach der BREITE eines Kindes --
und fand nichts, obwohl 211 Pixel ueberliefen. Der Grund:
Ein Kind kann genau passen und sein ENKEL trotzdem rechts
hinausragen, weil dazwischen `overflow: visible` steht.
Gefragt ist also nicht „wer ist zu breit", sondern „wessen
rechte Kante liegt jenseits der Kante des Rollers" -- und
davon das oberste Glied, denn alles darunter folgt ihm. */
const schuld = await host.seite.evaluate((wer) => {
const nenne = (e) => e.tagName.toLowerCase()
+ (e.id ? "#" + e.id : "")
+ (e.className && typeof e.className === "string"
? "." + e.className.trim().split(/\s+/).join(".") : "");
const roller = [...document.querySelectorAll("*")].find((e) => nenne(e) === wer);
if (!roller) return ["(nicht gefunden)"];
const kante = roller.getBoundingClientRect().left + roller.clientWidth;
const zeilen = [];
/* IMMER ABSTEIGEN, AUCH DURCH UNAUFFAELLIGES.
Die erste Fassung stieg nur in Kinder hinab, die selbst
schon hinausragten -- und fand deshalb gar nichts: Die
sichtbare Tafel passte (327 px in 359), und erst IHR Kind
ragte 572 px weit hinaus. Eine Suche, die nur dort
weitersucht, wo sie schon fuendig ist, findet nie etwas,
das hinter etwas Unauffaelligem steckt. */
const gehe = (el, tief) => {
if (tief > 7) return;
for (const kind of el.children) {
const kr = kind.getBoundingClientRect();
if (kr.width === 0) continue;
if (kr.right <= kante + 1) { gehe(kind, tief); continue; }
const st = getComputedStyle(kind);
zeilen.push(`${" ".repeat(tief)}${nenne(kind).split(".").slice(0, 2).join(".")}`
+ ` ragt ${Math.round(kr.right - kante)} px hinaus`
+ ` (${Math.round(kr.width)} px breit)`
+ ` · ${st.display}`
+ (st.gridTemplateColumns !== "none" ? ` · spalten ${st.gridTemplateColumns}` : "")
+ (st.display.includes("flex") ? ` · wrap ${st.flexWrap} · basis ${st.flexBasis}` : "")
+ ` · min-width ${st.minWidth}`
+ (st.whiteSpace !== "normal" ? ` · ${st.whiteSpace}` : ""));
gehe(kind, tief + 1);
}
};
gehe(roller, 0);
return zeilen.length ? zeilen : ["(nichts ragt hinaus -- Rand oder Schatten?)"];
}, q.wer);
for (const z of schuld) console.log(` ${z}`);
befunde++;
}
}
/* ==== UND IN JEDER ANORDNUNG ====================================
`mess-reaktion` meldete auf einem 390er Handy 76 Pixel
seitlichen Ueberlauf, die hier nicht auftauchten. Der
Unterschied war die ANORDNUNG: Dort war vorher „nur Kamera"
eingestellt, hier stand die Vorgabe. Die Anordnung gehoert der
Sendung und gilt fuer alle -- wer sie nicht durchprobiert,
misst nur einen von fuenf Faellen. */
for (const anordnung of ["kino", "gleich", "kameras", "nur_video", "nur_kamera"]) {
await host.seite.evaluate((a) => {
document.querySelector(`#layout-reihe .layout-knopf[data-layout="${a}"]`)?.click();
}, anordnung);
await host.seite.waitForTimeout(500);
const m = await host.seite.evaluate(() => {
const ueber = Math.max(0, document.documentElement.scrollWidth
- document.documentElement.clientWidth);
if (ueber === 0) return { ueber };
let b = null, max = 0;
for (const e of document.querySelectorAll("*")) {
const r = e.getBoundingClientRect();
if (r.width === 0) continue;
if (r.right > max) { max = r.right; b = e; }
}
return {
ueber,
wer: b ? `${b.tagName.toLowerCase()}${b.id ? "#" + b.id : ""}`
+ `${typeof b.className === "string" && b.className
? "." + b.className.trim().split(/\s+/)[0] : ""}` : "-",
bis: Math.round(max),
};
});
if (m.ueber > 0) {
console.log(` ACHTUNG Anordnung „${anordnung}": ${m.ueber} px seitlich `
+ `-- am weitesten rechts ${m.wer} (bis ${m.bis} px)`);
befunde++;
} else {
console.log(` Anordnung „${anordnung}": seitlich 0 px`);
}
}
await host.seite.evaluate(() => {
document.querySelector('#layout-reihe .layout-knopf[data-layout="kino"]')?.click();
});
await host.seite.screenshot({ path: join(HIER, `mess-quer-${breite}.png`) });
await host.kontext.close();
}
console.log(`\n=== ${befunde === 0 ? "NICHTS rollt seitlich." : befunde + " Stelle(n) rollen seitlich."} ===`);
await browser.close();
await new Promise((r) => vorbau.close(r));
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Windows haelt die Datei noch */ }
process.exit(befunde === 0 ? 0 : 1);