Eine Arbeitsdatei war mitgewandert
tools/.messkopf.txt ist ein Zwischenstand beim Bauen der Messung -- sie gehoert nicht in die Geschichte. Die uebrigen Zwischenstaende stehen schon in .gitignore; dieser Name fehlte.
This commit is contained in:
@@ -135,3 +135,5 @@ tools/.vid/
|
|||||||
tools/.commit-*
|
tools/.commit-*
|
||||||
tools/.dreh*
|
tools/.dreh*
|
||||||
tools/.nach-*
|
tools/.nach-*
|
||||||
|
|
||||||
|
tools/.messkopf.txt
|
||||||
|
|||||||
@@ -1,226 +0,0 @@
|
|||||||
/* =====================================================================
|
|
||||||
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";
|
|
||||||
|
|
||||||
const HIER = dirname(fileURLToPath(import.meta.url));
|
|
||||||
const ordner = mkdtempSync(join(tmpdir(), "mess-reakt-"));
|
|
||||||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
||||||
|
|
||||||
const PORT = 5483;
|
|
||||||
const HTTPS_PORT = PORT + 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");
|
|
||||||
execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes",
|
|
||||||
"-keyout", schluesselDatei, "-out", zertDatei, "-days", "2",
|
|
||||||
"-subj", `/CN=${CREW}`, "-addext", `subjectAltName=DNS:${CREW}`], { stdio: "ignore" });
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user