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]>
618 lines
27 KiB
JavaScript
618 lines
27 KiB
JavaScript
/* =====================================================================
|
|
DIE ZWEI QUELLEN FUER OBS (28.09.2026)
|
|
|
|
Gemessen wird die Behauptung, auf der alles steht: Beide Seiten
|
|
arbeiten OHNE ANMELDUNG, nur mit einem Schluessel in der Adresse.
|
|
Dafuer wird ein Browserfenster ohne jeden Keks geoeffnet.
|
|
|
|
Dazu: Der falsche Schluessel fuehrt zu nichts, der Grund dafuer
|
|
steht im Bild, und eine bestaetigte Spende laeuft als Karte auf
|
|
der durchsichtigen Tafel.
|
|
|
|
Aufrufen mit: node server/mess-buehne.mjs
|
|
===================================================================== */
|
|
/* =====================================================================
|
|
WIE DIE REACTION AUSSIEHT (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-buehne-"));
|
|
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-buehne (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 },
|
|
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;
|
|
}
|
|
|
|
|
|
/* ---------- Die Buehne vorbereiten -------------------------------------- */
|
|
await roh("/workspace/api/reaktion", {
|
|
method: "PUT", keks: k.admin,
|
|
rumpf: { titel: "Probe fuer OBS", video: "https://www.youtube.com/watch?v=dQw4w9WgXcQ" },
|
|
});
|
|
await roh("/workspace/api/reaktion/stand", {
|
|
method: "POST", keks: k.admin, rumpf: { stand: "live" },
|
|
});
|
|
await roh("/workspace/api/reaktion/dabei", { method: "POST", keks: k.admin });
|
|
|
|
const adr = await roh("/workspace/api/buehne/adressen", { keks: k.admin });
|
|
console.log(` Adressen: ${adr.code}`);
|
|
let ausgang = 0;
|
|
if (adr.code !== 200) {
|
|
console.log(" ACHTUNG: Die Adressen kommen nicht.");
|
|
process.exit(1);
|
|
}
|
|
const schluessel = new URL(adr.j.video).searchParams.get("s");
|
|
console.log(` Schluessel: ${schluessel.length} Zeichen`);
|
|
if (schluessel.length !== 64) {
|
|
console.log(" ACHTUNG: Der Schluessel ist zu kurz.");
|
|
ausgang = 1;
|
|
}
|
|
|
|
/* NUR DIE LEITUNG SIEHT DIE ADRESSEN. Wer sie hat, kann mitsehen --
|
|
das ist vertretbar fuer den, der sendet, und nicht fuer jeden im
|
|
Haus. Diese Messung legt nur DogFather und zwei Zuschauer an; die
|
|
uebrigen Rollen prueft `pruef-buehne`. */
|
|
const alsGast = await roh("/workspace/api/buehne/adressen", { keks: k.gast });
|
|
console.log(` Ein Zuschauer bekommt die Adressen: ${alsGast.code}`);
|
|
if (alsGast.code !== 403) {
|
|
console.log(" ACHTUNG: Ein Zuschauer darf die Adressen sehen.");
|
|
ausgang = 1;
|
|
}
|
|
|
|
/* ---------- Ein Fenster OHNE Anmeldung ---------------------------------- */
|
|
const fremd = await browser.newContext({
|
|
viewport: { width: 1280, height: 720 },
|
|
ignoreHTTPSErrors: true,
|
|
});
|
|
const konsole = [];
|
|
const tafelSeite = await fremd.newPage();
|
|
tafelSeite.on("pageerror", (f) => konsole.push(String(f?.message || f)));
|
|
/* DIE ADRESSE GEHOERT IN DIE MELDUNG. Der Browser sagt nur
|
|
„Failed to load resource" -- das schickt einen auf die Suche,
|
|
ohne zu sagen wonach. Die uebrigen Seiten schreiben den Pfad
|
|
seit jeher mit; diese zwei nicht, und genau sie sind die
|
|
Quellen, die in den Stream gehen. */
|
|
tafelSeite.on("response", (a) => {
|
|
if (a.status() >= 400) konsole.push(`${a.status()} ${a.url().replace(/^https?:\/\/[^/]+/, "")}`);
|
|
});
|
|
tafelSeite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
|
|
|
|
/* Erst der FALSCHE Schluessel. Eine Pruefung, die nur den richtigen
|
|
kennt, beweist nicht, dass der falsche abgewiesen wird. */
|
|
await tafelSeite.goto(`${BASIS}/workspace/tafel.html?s=falschfalschfalsch`,
|
|
{ waitUntil: "domcontentloaded" });
|
|
await tafelSeite.waitForTimeout(1200);
|
|
const beiFalsch = await tafelSeite.evaluate(() => ({
|
|
fehler: document.querySelector(".obs-fehler")?.textContent?.trim() || "",
|
|
karten: document.querySelectorAll(".spendenkarte").length,
|
|
}));
|
|
console.log(` Falscher Schluessel: „${beiFalsch.fehler.slice(0, 48)}…", ${beiFalsch.karten} Karten`);
|
|
if (!beiFalsch.fehler.includes("stimmt nicht") || beiFalsch.karten !== 0) {
|
|
console.log(" ACHTUNG: Ein falscher Schluessel wird nicht sauber abgewiesen.");
|
|
ausgang = 1;
|
|
}
|
|
|
|
/* Jetzt der richtige. */
|
|
await tafelSeite.goto(`${BASIS}/workspace/tafel.html?s=${schluessel}`,
|
|
{ waitUntil: "domcontentloaded" });
|
|
await tafelSeite.waitForTimeout(1500);
|
|
const kekse = await fremd.cookies();
|
|
const grund = await tafelSeite.evaluate(() => ({
|
|
hintergrund: getComputedStyle(document.body).backgroundColor,
|
|
fehler: document.querySelector(".obs-fehler")?.textContent || "",
|
|
}));
|
|
console.log(` Kekse im Fenster: ${kekse.length} Hintergrund: ${grund.hintergrund}`);
|
|
if (kekse.length !== 0) {
|
|
console.log(" ACHTUNG: Die Quelle hat sich angemeldet -- das soll sie nicht muessen.");
|
|
ausgang = 1;
|
|
}
|
|
if (!/rgba\(0, 0, 0, 0\)|transparent/.test(grund.hintergrund)) {
|
|
console.log(` ACHTUNG: Der Grund ist nicht durchsichtig (${grund.hintergrund}).`);
|
|
ausgang = 1;
|
|
}
|
|
if (grund.fehler) {
|
|
console.log(` ACHTUNG: Die Tafel meldet einen Fehler: ${grund.fehler}`);
|
|
ausgang = 1;
|
|
}
|
|
|
|
/* ---------- Laeuft eine Karte an? --------------------------------------- */
|
|
await roh("/workspace/api/spenden", {
|
|
method: "POST", keks: k.admin,
|
|
rumpf: { dogen_mille: 2500, name: "Nachtfalter", gruss: "Gruss an die Buehne" },
|
|
});
|
|
await tafelSeite.waitForFunction(
|
|
() => document.querySelector('.spendenkarte[data-da="ja"]'), null, { timeout: 12000 },
|
|
).catch(() => {});
|
|
await tafelSeite.waitForTimeout(600);
|
|
const karte = await tafelSeite.evaluate(() => {
|
|
const k2 = document.querySelector(".spendenkarte");
|
|
if (!k2) return null;
|
|
const r = k2.getBoundingClientRect();
|
|
return {
|
|
/* Zahl, Wort und Zeichen getrennt -- `textContent` klebt sie
|
|
sonst zu „2,5Dogen" zusammen, und eine Suche darin traefe
|
|
frueher oder spaeter das Falsche. */
|
|
zahl: k2.querySelector(".dogen__zahl")?.textContent?.trim(),
|
|
wort: k2.querySelector(".dogen__wort")?.textContent?.trim(),
|
|
zeichen: !!k2.querySelector(".dogen__zeichen"),
|
|
wer: k2.querySelector(".spendenkarte__wer")?.textContent?.trim(),
|
|
stufe: k2.querySelector(".spendenkarte__stufe")?.textContent?.trim(),
|
|
breite: Math.round(r.width), hoehe: Math.round(r.height),
|
|
};
|
|
});
|
|
console.log(` Karte auf der Tafel: ${JSON.stringify(karte)}`);
|
|
if (!karte || karte.zahl !== "2,5" || karte.wort !== "Dogen" || !karte.zeichen) {
|
|
console.log(" ACHTUNG: Die Karte laeuft auf der Tafel nicht an -- "
|
|
+ "oder ohne Dogen und Zeichen.");
|
|
ausgang = 1;
|
|
}
|
|
/* IM STREAM DARF KEIN GELD STEHEN. Die Tafel ist die einzige
|
|
Flaeche, die wirklich jeder sieht. */
|
|
const geldImStream = await tafelSeite.evaluate(() =>
|
|
/€|EUR|Euro/.test(document.body.textContent || ""));
|
|
console.log(` Geld im Streambild: ${geldImStream}`);
|
|
if (geldImStream) {
|
|
console.log(" ACHTUNG: Auf der Buehnentafel steht Geld.");
|
|
ausgang = 1;
|
|
}
|
|
await tafelSeite.screenshot({ path: join(HIER, "mess-buehne-1-tafel.png") });
|
|
console.log(" Bild: server/mess-buehne-1-tafel.png");
|
|
|
|
/* ---------- Das Video als Quelle ---------------------------------------- */
|
|
const videoSeite = await fremd.newPage();
|
|
videoSeite.on("pageerror", (f) => konsole.push(String(f?.message || f)));
|
|
/* DIE ADRESSE GEHOERT IN DIE MELDUNG. Der Browser sagt nur
|
|
„Failed to load resource" -- das schickt einen auf die Suche,
|
|
ohne zu sagen wonach. Die uebrigen Seiten schreiben den Pfad
|
|
seit jeher mit; diese zwei nicht, und genau sie sind die
|
|
Quellen, die in den Stream gehen. */
|
|
videoSeite.on("response", (a) => {
|
|
if (a.status() >= 400) konsole.push(`${a.status()} ${a.url().replace(/^https?:\/\/[^/]+/, "")}`);
|
|
});
|
|
await videoSeite.goto(`${BASIS}/workspace/buehne.html?s=${schluessel}`,
|
|
{ waitUntil: "domcontentloaded" });
|
|
await videoSeite.waitForTimeout(6000);
|
|
const video = await videoSeite.evaluate(() => ({
|
|
rahmen: !!document.querySelector("#platz iframe"),
|
|
fehler: document.querySelector(".obs-fehler")?.textContent || "",
|
|
bedienung: document.body.innerHTML.includes("controls=1"),
|
|
}));
|
|
console.log(` Video-Quelle: Rahmen=${video.rahmen} Fehler=„${video.fehler}"`);
|
|
if (!video.rahmen || video.fehler) {
|
|
console.log(" ACHTUNG: Die Video-Quelle zeigt kein Video.");
|
|
ausgang = 1;
|
|
}
|
|
await videoSeite.screenshot({ path: join(HIER, "mess-buehne-2-video.png") });
|
|
console.log(" Bild: server/mess-buehne-2-video.png");
|
|
|
|
/* ---------- Ein neuer Schluessel macht die alten Adressen tot ----------- */
|
|
const neu = await roh("/workspace/api/buehne/schluessel", { method: "POST", keks: k.admin });
|
|
const neuerSchluessel = new URL(neu.j.video).searchParams.get("s");
|
|
console.log(` Neuer Schluessel: ${neuerSchluessel !== schluessel ? "anders" : "GLEICH"}`);
|
|
const altNochGut = await roh(`/workspace/api/buehne/lage?s=${schluessel}`, {});
|
|
console.log(` Alte Adresse antwortet: ${altNochGut.code}`);
|
|
if (neuerSchluessel === schluessel || altNochGut.code !== 404) {
|
|
console.log(" ACHTUNG: Der alte Schluessel wirkt noch.");
|
|
ausgang = 1;
|
|
}
|
|
const neuGut = await roh(`/workspace/api/buehne/lage?s=${neuerSchluessel}`, {});
|
|
console.log(` Neue Adresse antwortet: ${neuGut.code}`);
|
|
if (neuGut.code !== 200) {
|
|
console.log(" ACHTUNG: Der neue Schluessel wirkt nicht.");
|
|
ausgang = 1;
|
|
}
|
|
|
|
/* ---------- Die Werbeeinblendung im Stream ------------------------------ */
|
|
console.log("");
|
|
console.log("=== Die Werbung in der Buehnenquelle ===");
|
|
{
|
|
const setz = (f) => roh("/workspace/api/reaktion/werbung",
|
|
{ method: "POST", keks: k.admin, rumpf: f });
|
|
|
|
const an = await setz({ art: "laufband", tempo: 12, ecke_dogi: "or", ecke_gross: 1.4 });
|
|
console.log(` Einschalten: ${an.code}`);
|
|
if (an.code !== 200) {
|
|
console.log(" ACHTUNG: Die Werbung laesst sich nicht einschalten.");
|
|
ausgang = 1;
|
|
}
|
|
|
|
/* DIE QUELLE WIRD NEU GELADEN und nicht nur abgewartet: OBS haelt
|
|
sie dauerhaft offen, aber diese Messung soll zuerst beweisen,
|
|
dass die Einblendung ueberhaupt ankommt. Ob sie auch WAEHREND
|
|
des Laufens nachzieht, ist die zweite Frage -- die steht
|
|
darunter. */
|
|
await videoSeite.goto(`${BASIS}/workspace/buehne.html?s=${neuerSchluessel}`,
|
|
{ waitUntil: "domcontentloaded" });
|
|
await videoSeite.waitForTimeout(2500);
|
|
|
|
const w = await videoSeite.evaluate(() => {
|
|
const b = document.querySelector(".werbung__band");
|
|
const e = document.querySelector('.werbung__ecke--dogi:not([hidden])');
|
|
const bilder = [...document.querySelectorAll(".werbung img")];
|
|
const rb = b?.getBoundingClientRect();
|
|
const re = e?.getBoundingClientRect();
|
|
return {
|
|
band: !!b && !b.hidden && rb.height > 10,
|
|
bandHoch: rb ? Math.round(rb.height) : 0,
|
|
/* IM BILD, NICHT NUR IM DOKUMENT. Die Quelle ist genau so
|
|
gross wie der Stream; was darunter liegt, filmt OBS nicht
|
|
mit. */
|
|
bandImBild: !!rb && rb.bottom <= window.innerHeight + 2 && rb.top >= -2,
|
|
stuecke: document.querySelectorAll(".werbung__stueck").length,
|
|
code: document.querySelector(".werbung__code")?.textContent || "",
|
|
ecke: e ? e.dataset.platz : "",
|
|
eckeBreit: re ? Math.round(re.width) : 0,
|
|
eckeRechtsOben: !!re
|
|
&& (window.innerWidth - re.right) < re.left && re.top < (window.innerHeight - re.bottom),
|
|
/* `naturalWidth` und nicht „das Element ist da": Ein Bild mit
|
|
404 steht genauso im Dokument wie eines, das laedt -- im
|
|
Stream ist dort nichts. */
|
|
geladen: bilder.filter((i) => i.naturalWidth > 0).length,
|
|
gesamt: bilder.length,
|
|
/* DIE SCHRIFT MUSS GROESSER SEIN ALS IM SAAL. Ein Stream wird
|
|
auf einem Handy gesehen, oft in einem Viertel des Bildes.
|
|
Steht hier dieselbe Groesse wie am Rechner, ist der
|
|
Rabattcode nicht zu lesen und die Einblendung umsonst. */
|
|
schrift: Math.round(parseFloat(
|
|
getComputedStyle(document.querySelector(".werbung__text") || document.body).fontSize)),
|
|
};
|
|
});
|
|
console.log(` Band: ${w.band ? `${w.bandHoch} px` : "FEHLT"}, ${w.stuecke} Stuecke, `
|
|
+ `Schrift ${w.schrift} px, Code „${w.code}"`);
|
|
console.log(` Ecke: ${w.ecke || "FEHLT"}, ${w.eckeBreit} px, `
|
|
+ `rechts oben: ${w.eckeRechtsOben}, Bilder ${w.geladen}/${w.gesamt}`);
|
|
if (!w.band || !w.bandImBild || w.stuecke < 2) {
|
|
console.log(" ACHTUNG: Im Stream laeuft kein Band.");
|
|
ausgang = 1;
|
|
}
|
|
if (!w.code) {
|
|
console.log(" ACHTUNG: Der Rabattcode ist im Stream nicht hervorgehoben.");
|
|
ausgang = 1;
|
|
}
|
|
if (w.ecke !== "or" || !w.eckeRechtsOben) {
|
|
console.log(" ACHTUNG: Das Logo steht im Stream nicht oben rechts.");
|
|
ausgang = 1;
|
|
}
|
|
if (!w.gesamt || w.geladen < w.gesamt) {
|
|
console.log(` ACHTUNG: ${w.gesamt - w.geladen} Bild(er) laden im Stream nicht.`);
|
|
ausgang = 1;
|
|
}
|
|
if (w.schrift < 20) {
|
|
console.log(` ACHTUNG: ${w.schrift} px Schrift ist fuer einen Stream zu klein.`);
|
|
ausgang = 1;
|
|
}
|
|
await videoSeite.screenshot({ path: join(HIER, "mess-buehne-4-werbung.png") });
|
|
console.log(" Bild: server/mess-buehne-4-werbung.png");
|
|
|
|
/* ---- Zieht die OFFENE Quelle nach? -----------------------------
|
|
Das ist die eigentliche Betriebsfrage. OBS laedt diese Seite
|
|
einmal beim Start der Szene und danach nie wieder -- wenn
|
|
Filipe mitten in der Sendung umschaltet und die Quelle das
|
|
nicht mitbekommt, wirbt der Stream bis zum naechsten Neustart
|
|
weiter fuer das Falsche. */
|
|
await setz({ ecke_dogi: "ul" });
|
|
await videoSeite.waitForTimeout(2500);
|
|
const nach = await videoSeite.evaluate(() => {
|
|
const e = document.querySelector('.werbung__ecke--dogi:not([hidden])');
|
|
return e ? e.dataset.platz : "";
|
|
});
|
|
console.log(` Umgeschaltet auf „unten links", die offene Quelle zeigt: „${nach || "nichts"}"`);
|
|
if (nach !== "ul") {
|
|
console.log(" ACHTUNG: Die offene Buehnenquelle zieht eine Aenderung nicht nach.");
|
|
ausgang = 1;
|
|
}
|
|
|
|
/* ---- Und wieder aus ---- */
|
|
await setz({ art: "aus", ecke_dogi: "aus", ecke_hase: "aus" });
|
|
await videoSeite.waitForTimeout(2000);
|
|
const weg = await videoSeite.evaluate(() => ({
|
|
band: !!document.querySelector(".werbung__band:not([hidden])"),
|
|
ecken: document.querySelectorAll(".werbung__ecke:not([hidden])").length,
|
|
}));
|
|
console.log(` Abgeschaltet: Band ${weg.band ? "STEHT NOCH" : "weg"}, `
|
|
+ `${weg.ecken} Ecken uebrig`);
|
|
if (weg.band || weg.ecken) {
|
|
console.log(" ACHTUNG: Abgeschaltete Werbung bleibt im Stream stehen.");
|
|
ausgang = 1;
|
|
}
|
|
}
|
|
|
|
/* ---------- Der Buehnenmodus der Reaction-Seite -------------------------- */
|
|
const hostSeite = await alsWer("admin", 1280, 720);
|
|
await hostSeite.seite.goto(`${BASIS}/workspace/reaktion.html?nur=buehne`,
|
|
{ waitUntil: "domcontentloaded" });
|
|
await hostSeite.seite.waitForTimeout(3000);
|
|
const nurBild = await hostSeite.seite.evaluate(() => {
|
|
const sicht = (w) => {
|
|
const e = document.querySelector(w);
|
|
if (!e) return false;
|
|
const r = e.getBoundingClientRect();
|
|
return r.width > 0 && r.height > 0 && getComputedStyle(e).display !== "none";
|
|
};
|
|
return {
|
|
modus: document.body.dataset.nur,
|
|
kopf: sicht(".kopfleiste"), chat: sicht(".schiene"), pult: sicht(".pult"),
|
|
schild: sicht(".buehne-schild"), leinwand: sicht(".leinwand"),
|
|
saal: Math.round(document.getElementById("saal").getBoundingClientRect().height),
|
|
fenster: window.innerHeight,
|
|
};
|
|
});
|
|
console.log(` Buehnenmodus: Kopf=${nurBild.kopf} Chat=${nurBild.chat} `
|
|
+ `Pult=${nurBild.pult} Schild=${nurBild.schild} | Leinwand=${nurBild.leinwand} `
|
|
+ `Saal ${nurBild.saal} von ${nurBild.fenster}`);
|
|
if (nurBild.kopf || nurBild.chat || nurBild.pult || nurBild.schild || !nurBild.leinwand
|
|
|| nurBild.saal < nurBild.fenster - 4) {
|
|
console.log(" ACHTUNG: Der Buehnenmodus raeumt nicht sauber auf.");
|
|
ausgang = 1;
|
|
}
|
|
await hostSeite.seite.screenshot({ path: join(HIER, "mess-buehne-3-modus.png") });
|
|
console.log(" Bild: server/mess-buehne-3-modus.png");
|
|
|
|
/* ---------- Was in den Quellen schiefging -------------------------------
|
|
ZWEI FEHLSCHLAEGE SIND BESTELLT und stehen deshalb nicht in der
|
|
Meldung: der Abruf mit dem absichtlich falschen Schluessel und der
|
|
Nachfass der offenen Quelle auf den eben abgeloesten alten. Beide
|
|
sind das Gelingen einer Pruefung, nicht ihr Scheitern.
|
|
|
|
Sie standen bisher mit drin. Eine Meldung, die IMMER vier Zeilen
|
|
zeigt, wird nach dem zweiten Mal ueberlesen -- und ein echter 404
|
|
in genau der Seite, die in den Stream geht, faellt darin nicht
|
|
auf. Deshalb eng gefiltert, benannt, und der Rest ungekuerzt. */
|
|
const bestellt = [
|
|
(z) => z.includes("s=falschfalschfalsch"),
|
|
(z) => z.includes("/buehne/strom?s=") && z.includes(schluessel),
|
|
/* Dieselben Ereignisse ohne Adresse -- die Adresse steht seit
|
|
heute in der Zeile daneben. */
|
|
(z) => z.startsWith("Failed to load resource"),
|
|
];
|
|
const echt = konsole.filter((z) => !bestellt.some((f) => f(z)));
|
|
|
|
/* GEGENPROBE: Ein Filter, der alles durchlaesst, waere bequem und
|
|
wertlos. Diese vier Proben beweisen, dass er beide Richtungen
|
|
kann -- er nimmt die bestellten Faelle heraus UND laesst einen
|
|
echten 404 stehen, auch einen auf ein Werbebild. */
|
|
{
|
|
const proben = [
|
|
["404 /workspace/api/buehne/lage?s=falschfalschfalsch", false],
|
|
[`404 /workspace/api/buehne/strom?s=${schluessel}`, false],
|
|
["404 /workspace/assets/img/werbung-hase.webp", true],
|
|
["TypeError: x ist nicht definiert", true],
|
|
];
|
|
const daneben = proben.filter(([z, soll]) =>
|
|
(!bestellt.some((f) => f(z))) !== soll);
|
|
if (daneben.length) {
|
|
console.log(` ACHTUNG: Der Konsolenfilter ordnet ${daneben.length} Probe(n) falsch ein: `
|
|
+ daneben.map(([z]) => z.slice(0, 40)).join(" | "));
|
|
ausgang = 1;
|
|
}
|
|
}
|
|
if (echt.length) {
|
|
console.log(` KONSOLE: ${echt.length} unerwartete(r) Fehlschlag/Fehlschlaege`);
|
|
for (const z of echt.slice(0, 6)) console.log(` ${z}`);
|
|
ausgang = 1;
|
|
} else {
|
|
console.log(` Konsole sauber (${konsole.length} bestellte Fehlschlaege gefiltert).`);
|
|
}
|
|
|
|
await fremd.close();
|
|
await hostSeite.kontext.close();
|
|
await browser.close();
|
|
await new Promise((r) => vorbau.close(r));
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
console.log(`\nFertig. Rueckgabe ${ausgang}.`);
|
|
process.exit(ausgang);
|