Files
dogfather-universe/server/mess-quer.mjs
T
DogFatherGitandClaude Opus 5 996231af6e Reaction: Material statt Kaesten, Spende in der Mitte, Gaeste werden gefragt
Nach Filipes sieben Bildschirmfotos vom 07.10.2026. Sieben Wuensche,
und einer davon ("alles viel hochwertiger") betrifft alle anderen --
deshalb steht das Fundament zuerst und nicht am Ende.

DAS AUSSEHEN HAT EINEN WERKSTOFF. Jede Flaeche sagt jetzt, ob sie oben
oder unten liegt: Was obenauf liegt, bekommt oben eine helle Kante und
darunter einen Schatten; was vertieft ist, genau umgekehrt. Dazwischen
gibt es nichts mehr. Vorher stand im ganzen Pult zweimal eine
Lichtkante und sonst nirgends -- genau deshalb sahen diese zwei Stellen
gut aus und alles daneben danebengestellt. Dazu: jeder Knopf senkt sich
beim Druecken, jedes Feld ist ein Loch und kein Kasten, und wer mit der
Tastatur bedient, wird zum ersten Mal ueberhaupt gesehen.

UND EIN FUND DABEI: Die drei Kacheln des Regieplatzes hiessen in der
HTML anders als im Stilblatt (kachel--laeuft gegen regiekachel--laeuft,
regieregiekachel__kopf gegen regiekachel__kopf). Die Aufteilung "links
was laeuft, rechts was man eintraegt" hat damit NIE gegriffen -- und
mess-reaktion hat den falschen Zustand gemessen und gruen gemeldet,
weil sie gegen die HTML geschrieben war.

DIE SPENDENKARTE STEHT IN DER MITTE und kommt aus der Tiefe nach vorn.
Das dreht eine Entscheidung vom 28.09. um ("sie deckt nie die Mitte
zu"); die Begruendung war richtig und ist es noch -- sie schuetzt das
Video, und Filipe sieht die Spende als das, was in dem Augenblick
zaehlt. Der alte Platz bleibt einen Knopf weit weg und gilt fuer Saal
UND OBS-Tafel. Die Karte besteht dafuer aus zwei Teilen: `opacity`
unter 1 zwingt ein Element auf `transform-style: flat`, eine Karte mit
Deckkraft-Uebergang waere also genau waehrend des Hereinkommens flach
und spraenge am Ende in einem Bild in die Tiefe.

FERTIGE SAETZE GEHEN SOFORT INS BILD. Der Browser schickt nur die
Kennung, den Text holt der Server -- deshalb darf hier die Bestaetigung
entfallen und beim freien Text nicht. Zwei Schranken bleiben: sofort
nur mit einem der vier Knopfbetraege (Filipes Ansage galt dem SATZ,
nicht der Summe), und GEZEIGT IST NICHT GEBUCHT -- die Dogen gibt es
erst nach dem Tipp der Leitung, sonst stellt sich jeder seine eigene
Waehrung aus.

WER GESPENDET HAT, GLAENZT IM CHAT -- je nach Hoehe. Die Dauer haengt
an der Stufe und nicht an einer zweiten Leiter; 0 heisst "gar nicht".

GAESTE WERDEN GEFRAGT, NICHT GEHOLT. Vorher sprang die Kamera eines
Zuschauers an, sobald jemand "Dazuholen" drueckte -- er hat es daran
gemerkt, dass er sich selbst sah. Jetzt gibt es einen Vorraum: eigenes
Bild, Pegel, Kamera und Mikro vorwaehlen, Geraete waehlen, dann "Dabei
sein" oder "Lieber nicht". Zwei Minuten Frist, und sie verfaellt
SICHTBAR -- eine verschwundene Zeile sieht fuer den Host aus wie ein
Verklicker.

DAS MISCHPULT. Hier stand fest `echoCancellation: true,
noiseSuppression: true`. Fuer ein Laptopmikrofon richtig, ueber einem
RODECaster falsch: Das Pult hat Gate, Kompressor und EQ schon gemacht,
und die Echo-Unterdrueckung legt sich danach als zweite Automatik
darueber und duckt, sobald von der Gegenseite Ton kommt -- genau das
Pumpen vom 14.09. Jetzt: Geraetewahl fuer Mikro, Kamera und Ausgabe
(auch fuer Gaeste), ein Pultmodus, und darunter GEMESSEN, was wirklich
ankommt. Mit und ohne Pult.

DAS ZWEITE VIDEO war nie gesperrt -- es war nur unsichtbar. Jetzt
stehen Vorschaubild, Titel und die gemerkte Stelle da, "Jetzt wechseln"
auch von hier aus, und aus der Warteschlange legt ein Knopf eines
bereit.

UND EIN FUND AUF DEM BILDSCHIRMFOTO: Das Band "Ton einschalten" lag auf
derselben Hoehe wie die Senderleiste. Bei vier Knoepfen reichte die
Leiste bis zur Mitte -- vom Mikro-Knopf war nur noch ein "t" zu sehen.
Das war schon vorher so; der fuenfte Knopf hat es sichtbar gemacht.
Dieselbe Lehre wie am 06.09.: keine neue Zahl suchen, sondern die
Begegnung unmoeglich machen. mess-reaktion misst das jetzt als
UEBERLAPPUNG.

GEPRUEFT: pruef-reaktion 423 -> 478 (0 Fehler), pruef-spenden 172 -> 215
(0), pruef-reaktion-schmal 44 (0), pruef-tippziele 13 (0), dazu gruen:
pruef-css-klassen, pruef-alle-wege, pruef-haus-trennung, pruef-agentur,
pruef-deutsche-texte. Keine Zahl ist gefallen.

Drei Doppelungen hat erst das Pruefen gefunden: `satz_unbekannt` gab es
zweimal (Muenzsatz), `saetze` im Spendenstand war belegt (ebenfalls
Muenzsaetze), und `glanz_minuten` waere in einem frischen Haus immer 0
geblieben -- die Spalte kam per ALTER TABLE, und das laeuft VOR dem
Anlegen der Werksstufen.

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

615 lines
28 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 },
});
/* ZUSAGEN GEHOERT SEIT DEM 08.10.2026 DAZU. Ein Eingeladener steht
noch NICHT auf der Buehne -- ohne diesen zweiten Aufruf maesse
dieses Werkzeug einen Saal mit einem Kamerafenster statt zweien,
und zwar ohne einen einzigen Fehler zu melden. */
await roh("/workspace/api/reaktion/gast/antwort", {
method: "POST", keks: k.gast2, rumpf: { ja: true, kamera: true, mikro: true },
});
/* 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" });
/* AUF DIE LEISTE WARTEN, NICHT AUF DIE REGISTER (01.10.2026).
Seit die Register am Fingergeraet hinter einem Knopf liegen, ist
`#reiter` dort unsichtbar -- `waitForSelector` wartet aber auf
SICHTBARKEIT und lief in die Zeitsperre ("33 x locator resolved
to hidden"). Gewartet wird jetzt auf die Leiste selbst; die
steht in jedem Fall. */
await host.seite.waitForSelector("#regieleiste", { timeout: 15000 });
/* UND DANN AUFKLAPPEN WIE EIN MENSCH. Diese Messung sucht
seitlichen Ueberlauf IN den Registern -- sie muss sie also
sehen. Der Knopf gibt es nur am Fingergeraet; am Rechner steht
die Leiste ohnehin offen, und `?.` faengt das ab. */
await host.seite.evaluate(() => {
const l = document.getElementById("regieleiste");
if (l && l.dataset.auf !== "auf") document.getElementById("reiter-auf")?.click();
});
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) => {
/* ERST AUFKLAPPEN, DANN WAEHLEN. Ein Klick auf ein Register
klappt die Leiste am Handy wieder zu -- das naechste waere
sonst nicht anklickbar. */
const leiste = document.getElementById("regieleiste");
if (leiste && leiste.dataset.auf !== "auf") {
document.getElementById("reiter-auf")?.click();
}
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);