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]>
This commit is contained in:
2026-10-08 01:31:24 +02:00
co-authored by Claude Opus 5
parent 2764aea61c
commit 996231af6e
63 changed files with 5783 additions and 841 deletions
+345 -19
View File
@@ -277,6 +277,10 @@ for (const [wer, text] of REDEN) {
console.log("\n=== LIVE ===");
await roh("/workspace/api/reaktion/stand", { method: "POST", keks: k.admin, rumpf: { stand: "live" } });
await roh("/workspace/api/reaktion/gast", { method: "POST", keks: k.admin, rumpf: { person_id: IDS.gast2 } });
/* Und die Zusage: Seit dem 08.10.2026 wird GEFRAGT, nicht geholt --
wer nur gefragt ist, sendet noch nicht. */
await roh("/workspace/api/reaktion/gast/antwort",
{ method: "POST", keks: k.gast2, rumpf: { ja: true, kamera: true, mikro: true } });
/* ==== BEIDE SEITEN GLEICHZEITIG ====================================
Der Host sendet, die Zuschauerin empfaengt -- das geht nur, wenn
@@ -785,9 +789,9 @@ await host.seite.waitForTimeout(900);
Regiespalte stehen sie UNTEREINANDER -- das ist seit dem
Umbau „Regie links" der Sinn, nicht ein Fehler. Gefragt
ist deshalb: Gibt es alle drei, und hat jede Inhalt? */
laeuft: k(".regiekachel.kachel--laeuft"),
sendung: k(".regiekachel.kachel--sendung"),
bereit: k(".regiekachel.kachel--bereit"),
laeuft: k(".regiekachel.regiekachel--laeuft"),
sendung: k(".regiekachel.regiekachel--sendung"),
bereit: k(".regiekachel.regiekachel--bereit"),
leiste: k(".transport"),
spiel: k("#v-spiel"),
/* Laeuft etwas seitlich hinaus? `scrollWidth` groesser als die
@@ -825,7 +829,7 @@ await host.seite.waitForTimeout(900);
Scrollen wie beim Stillstand. */
letzte: (() => {
const e = document.querySelector(".transport");
const r = document.querySelector(".regiekachel.kachel--sendung");
const r = document.querySelector(".regiekachel.regiekachel--sendung");
if (!e || !r) return false;
/* 4 = DOCUMENT_POSITION_FOLLOWING */
return !!(r.compareDocumentPosition(e) & 4);
@@ -850,10 +854,47 @@ await host.seite.waitForTimeout(900);
const raus = [];
const leiste = document.querySelector(".transport");
if (!leiste) return raus;
/* ==== WAS WEGGEROLLT IST, IST NICHT VERDECKT (08.10.2026)
Die Regie-Schublade (`#pult-innen`) rollt: Ihr Inhalt ist 1228
Pixel hoch, sichtbar sind 342. Alles darunter ist
ABGESCHNITTEN -- man rollt es herunter, und dann ist es da.
`getBoundingClientRect()` meldet fuer solche Felder trotzdem
eine Lage im Fenster, und `elementFromPoint` findet dort die
Transportleiste. Die Messung meldete daraufhin „die Leiste
liegt ueber drei Bedienelementen" -- bei Feldern, die man nur
noch nicht heruntergerollt hat.
GEFUNDEN DURCH EINEN GEGENLAUF, nicht durch Nachdenken: Am
08.10. kam die Warnung neu, der Umbau sah wie die Ursache aus.
Ein `git worktree` auf den letzten Commit zeigte dieselbe Lage
-- dort lagen die Felder nur zufaellig ein paar Pixel tiefer,
unter dem Fensterrand, und `elementFromPoint` gab `null`
zurueck. Es war also nie eine Frage der Gestaltung, sondern
eine Luecke in der Messung.
Gefragt wird deshalb zuerst: Liegt das Feld ueberhaupt im
sichtbaren Teil seines Rollkastens? Nur dann kann es verdeckt
sein. */
const rollkasten = (e) => {
for (let p = e.parentElement; p; p = p.parentElement) {
const c = getComputedStyle(p);
if (/(auto|scroll)/.test(c.overflowY) || /(auto|scroll)/.test(c.overflowX)) return p;
}
return null;
};
for (const e of document.querySelectorAll("#pult-innen button, #pult-innen input,"
+ " #pult-innen select, #pult-innen label")) {
const r = e.getBoundingClientRect();
if (r.width === 0 || r.height === 0) continue;
const k = rollkasten(e);
if (k) {
const kr = k.getBoundingClientRect();
const mitte = r.top + r.height / 2;
/* Ganz oder halb aus dem Rollkasten heraus: weggerollt,
nicht verdeckt. */
if (mitte < kr.top || mitte > kr.bottom) continue;
}
const oben = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2);
if (!oben || e === oben || e.contains(oben)) continue;
if (!leiste.contains(oben)) continue; /* nur die Leiste zaehlt */
@@ -945,6 +986,56 @@ await host.seite.waitForTimeout(900);
console.log(" ACHTUNG: Der Knopf zum Teilen fehlt oder ist zu klein.");
ausgang = 1;
}
/* ==== NICHTS ÜBER DEM BILD DECKT EINEN KNOPF ZU (08.10.2026) ====
Über der Leinwand schweben drei Dinge: die Senderleiste (Kamera,
Bildschirm, Mikro, Geräte), das Band „Ton einschalten" und die
Spendenkarte. Sie liegen alle `absolute` und wissen nichts
voneinander.
GEFUNDEN AUF EINEM BILDSCHIRMFOTO, NICHT IN EINER ZAHL: Das Band
stand auf `left: 50%, bottom: 20px`, die Leiste auf `left: 16px,
bottom: 16px` — also auf derselben Höhe. Bei vier Knöpfen reichte
die Leiste bis zur Mitte, und vom Mikro-Knopf war nur noch ein
„t" zu sehen. Es war schon vorher so; mit dem fünften Knopf
wurde es nur sichtbar.
Gemessen wird nicht die Lage, sondern die ÜBERLAPPUNG — dieselbe
Form wie bei der Transportleiste. Eine Zahl („das Band muss 78
Pixel hoch liegen") wäre wieder die Schwelle, die beim nächsten
Knopf niemand nachzieht. */
const deckt = await host.seite.evaluate(() => {
const stuecke = [
...document.querySelectorAll(".senderleiste button"),
...document.querySelectorAll(".ton-an"),
].map((e) => ({ e, r: e.getBoundingClientRect() }))
.filter((x) => x.r.width > 0 && x.r.height > 0);
const raus = [];
for (let i = 0; i < stuecke.length; i++) {
for (let j = i + 1; j < stuecke.length; j++) {
const a = stuecke[i].r, b = stuecke[j].r;
const quer = Math.min(a.right, b.right) - Math.max(a.left, b.left);
const hoch = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top);
if (quer > 1 && hoch > 1) {
const n = (x) => x.e.id || x.e.className || x.e.tagName;
raus.push(`${n(stuecke[i])} / ${n(stuecke[j])}`);
}
}
}
return { zahl: stuecke.length, raus };
});
console.log(` Über dem Bild: ${deckt.zahl} Bedienteile, `
+ `${deckt.raus.length} Überlappung(en)`);
/* DIE ZAHL GEHOERT IN DIE BEDINGUNG. „null Ueberlappungen" ist auch
dann wahr, wenn gar nichts gemessen wurde -- und genau so sieht
ein gruener Haken aus, der nichts beweist. */
if (deckt.zahl < 4 || deckt.raus.length) {
console.log(` ACHTUNG: ${deckt.raus.length ? deckt.raus.join(" · ")
: "zu wenige Bedienteile gefunden -- die Messung greift ins Leere"}`);
ausgang = 1;
}
await bild(host.seite, "23-regieplatz");
}
@@ -1140,7 +1231,16 @@ await zuschauer.seite.evaluate(() => {
Versehen. */
muenze: (n.querySelector(".spendenkarte__bild img")
?.getAttribute("src") || "").split("/").pop(),
kleinWeg: n.dataset?.muenze === "ja",
/* ==== DIE MARKIERUNG SITZT AN DER KARTE, NICHT AN DER
HUELLE (08.10.2026)
Angehaengt wird seit heute die HUELLE -- sie macht das
Erscheinen, die Karte darin die Form. `n.dataset.muenze`
fragte also den falschen Knoten und war immer leer:
„Die Muenze steht zweimal auf derselben Karte", bei
einer Karte, auf der sie genau einmal stand. */
kleinWeg: (n.matches?.(".spendenkarte")
? n : n.querySelector?.(".spendenkarte"))?.dataset?.muenze === "ja",
});
}
}
@@ -1153,7 +1253,7 @@ await roh("/workspace/api/spenden", {
rumpf: { dogen_mille: 2500, name: "Nachtfalter", gruss: "Fuer das Rudel!" },
});
await zuschauer.seite.waitForFunction(
() => document.querySelector('.spendenkarte[data-da="ja"]'), null, { timeout: 8000 },
() => document.querySelector('.spendenkarte-auftritt[data-da="ja"] .spendenkarte'), null, { timeout: 8000 },
).catch(() => {});
await zuschauer.seite.waitForTimeout(700);
await bild(zuschauer.seite, "10-spendenkarte");
@@ -1477,13 +1577,13 @@ if (!wartet) {
naechste kommt erst, wenn die vorige abgelaufen ist. Also auf
das Merkmal warten, das nur die neue hat. */
await host.seite.waitForFunction(
() => [...document.querySelectorAll('.spendenkarte[data-da="ja"]')].some(
() => [...document.querySelectorAll('.spendenkarte-auftritt[data-da="ja"] .spendenkarte')].some(
(k2) => k2.querySelector(".spendenkarte__wer")?.textContent === "Probe"
&& k2.querySelector(".spendenkarte__bild img")),
null, { timeout: 20000 },
).catch(() => {});
const nachWechsel = await host.seite.evaluate(() => {
const k2 = [...document.querySelectorAll('.spendenkarte[data-da="ja"]')]
const k2 = [...document.querySelectorAll('.spendenkarte-auftritt[data-da="ja"] .spendenkarte')]
.find((x) => x.querySelector(".spendenkarte__wer")?.textContent === "Probe");
const b = k2?.querySelector(".spendenkarte__bild img");
/* Und auch hier: geladen, nicht nur vorhanden. */
@@ -1966,6 +2066,12 @@ await host.seite.waitForTimeout(1200);
const tafelnLeeren = async () => {
for (const s2 of [host.seite, zuschauer.seite]) {
await s2.evaluate(() => {
/* Die HUELLE wegnehmen und nicht nur die Karte darin: Seit dem
08.10.2026 macht die Huelle das Erscheinen, die Karte nur die
Form. Wer nur die Karte entfernt, laesst eine leere Huelle
stehen -- und misst gleich darauf, ob „schon eine Karte da
ist", mit einem Rest der vorigen im Bild. */
for (const k2 of document.querySelectorAll(".spendenkarte-auftritt")) k2.remove();
for (const k2 of document.querySelectorAll(".spendenkarte")) k2.remove();
});
}
@@ -1993,10 +2099,27 @@ const messeKarte = async (erwartet) => {
Sonst koennte die vorige Probe die Antwort geben, und die
Messung bestaetigte eine Aenderung, die gar nicht
angekommen ist. */
/* ==== GEWARTET WIRD AUF DEN FAKTOR, NICHT AUF EINE PIXELZAHL
(geaendert 08.10.2026)
Hier stand „warte, bis die Schrift 16 bzw. 32 Pixel misst". Das
war die Grundgroesse der Karte AM RAND. Seit heute steht sie in
der Mitte und ist dort groesser (1,3rem statt 1rem) -- und damit
wartete die Messung auf eine Zahl, die es nicht mehr gibt, lief
in die Frist und meldete „Die Probe zeigt keine Karte". Die
Karte war die ganze Zeit da.
DAS IST DIESELBE FALLE WIE EINE FESTE SCHWELLE IM STILBLATT:
eine Zahl, die irgendwo anders entsteht, hier abgeschrieben.
`--sp-gross` ist der Wert, um den es geht -- er kommt von der
Stufe, und die Messung fragt jetzt genau ihn ab. Wie viele
Pixel daraus werden, entscheidet die Gestaltung und darf sich
aendern. */
await host.seite.waitForFunction(
(soll) => [...document.querySelectorAll('.spendenkarte[data-da="ja"]')].some((k2) =>
k2.querySelector(".spendenkarte__wer")?.textContent === "Probe"
&& Math.abs(parseFloat(getComputedStyle(k2).fontSize) - soll) < 0.5),
(soll) => [...document.querySelectorAll('.spendenkarte-auftritt[data-da="ja"] .spendenkarte')]
.some((k2) => k2.querySelector(".spendenkarte__wer")?.textContent === "Probe"
&& Math.abs(parseFloat(
getComputedStyle(k2).getPropertyValue("--sp-gross")) - soll) < 0.01),
erwartet, { timeout: 16000 },
).catch(() => {});
/* DIE KARTE KOMMT VON LINKS HEREIN (450 ms). Wer sofort misst,
@@ -2012,7 +2135,7 @@ const messeKarte = async (erwartet) => {
-- gemessen wurden dadurch 413 statt 430 Pixel (genau das
0,96-fache) und ein linker Rand von -1. Beides war kein
Fehler der Karte, sondern ein Blick zur falschen Zeit. */
const k2 = [...document.querySelectorAll('.spendenkarte[data-da="ja"]')]
const k2 = [...document.querySelectorAll('.spendenkarte-auftritt[data-da="ja"] .spendenkarte')]
.find((x) => x.querySelector(".spendenkarte__wer")?.textContent === "Probe");
if (!k2) return null;
const r = k2.getBoundingClientRect();
@@ -2036,7 +2159,7 @@ const messeKarte = async (erwartet) => {
});
};
const kKlein = await messeKarte(16);
const kKlein = await messeKarte(1);
console.log(` Größe 1: Karte ${kKlein?.breit}x${kKlein?.hoch} px, Schrift ${kKlein?.schrift} px, `
+ `Betrag „${kKlein?.betragText}" bei ${kKlein?.betragSchrift} px, Stufe „${kKlein?.stufe}"`);
if (!kKlein || kKlein.wer !== "Probe") {
@@ -2065,7 +2188,7 @@ await host.seite.evaluate(() => {
document.getElementById("stufen-sichern").click();
});
await host.seite.waitForTimeout(1400);
const kGross = await messeKarte(32);
const kGross = await messeKarte(2);
console.log(` Größe 2: Karte ${kGross?.breit}x${kGross?.hoch} px, Schrift ${kGross?.schrift} px, `
+ `Betrag bei ${kGross?.betragSchrift} px, links ${kGross?.links} / oben ${kGross?.oben}, `
+ `ganz im Bild=${kGross?.imBild}`);
@@ -2100,7 +2223,7 @@ if (!kGross || !kKlein) {
/* ---- Und die Farbe? ---- */
const farbe = await host.seite.evaluate(() => {
const k2 = [...document.querySelectorAll('.spendenkarte[data-da="ja"]')]
const k2 = [...document.querySelectorAll('.spendenkarte-auftritt[data-da="ja"] .spendenkarte')]
.find((x) => x.querySelector(".spendenkarte__wer")?.textContent === "Probe");
return k2 ? getComputedStyle(k2).getPropertyValue("--sp-farbe").trim() : "";
});
@@ -2161,6 +2284,34 @@ if (tempoKnoepfe.length !== 6 || tempoKnoepfe.some((t) => t.breit < 44)) {
ausgang = 1;
}
/* ==== ERST AN DEN ANFANG, DANN MESSEN (08.10.2026) ================
Diese Messung braucht zwoelf Sekunden Videomaterial -- sechs bei
einfachem und sechs bei doppeltem Tempo, und bei doppeltem sind
das zwoelf Sekunden Film. Das Testvideo ist 3:34 lang, und was
davon noch uebrig ist, haengt davon ab, wie lange alle Abschnitte
davor gebraucht haben.
GENAU DAS HAT SIE FLATTERN LASSEN: Am 08.10. meldete sie in zwei
von vier Laeufen „Die Geschwindigkeit wirkt nicht" -- einmal mit
„2x: 0.00", einmal mit „2x: 0.50". Beides heisst nicht „zu
langsam", sondern „das Video war zu Ende". Im Gegenlauf auf dem
letzten Commit war es gruen, und auch das war Zufall: Dort lag der
Spieler zufaellig weit genug vorn.
Eine Pruefung, deren Ergebnis davon abhaengt, wie schnell der
Rechner die Abschnitte davor abgearbeitet hat, misst den Rechner.
Also: vorher an den Anfang springen. Dann sind immer 3:34
uebrig. */
await host.seite.evaluate(() => {
const band = document.getElementById("v-band");
if (!band) return;
band.value = "20"; /* 2 % von 3:34 ~ 4 s */
band.dispatchEvent(new Event("input", { bubbles: true }));
band.dispatchEvent(new Event("change", { bubbles: true }));
});
await host.seite.waitForTimeout(1500);
/* NORMALES TEMPO ZUERST MESSEN -- als Bezugswert. Ohne ihn wüsste man
nicht, ob 1,5x wirklich schneller ist oder ob das Video einfach
schon lief. */
@@ -2188,7 +2339,15 @@ const normal = (a1 !== null && a2 !== null) ? (a2 - a1) / 6 : null;
const schnell = (b1 !== null && b2 !== null) ? (b2 - b1) / 6 : null;
console.log(` 1x: ${normal === null ? "?" : normal.toFixed(2)} Sekunden je Sekunde · `
+ `2x: ${schnell === null ? "?" : schnell.toFixed(2)}`);
if (normal === null || schnell === null || schnell < normal * 1.5) {
/* UND DIE BEDINGUNG NENNT, WAS SIE GEMESSEN HAT. „schnell < normal *
1,5" ist auch dann wahr, wenn beide null sind -- also wenn der
Spieler ueberhaupt nicht lief. Das ist ein anderer Befund als
„das Tempo wirkt nicht", und er braucht einen anderen Satz. */
if (normal === null || schnell === null || normal < 0.5) {
console.log(" KONNTE NICHT NACHSEHEN: Das Video lief waehrend der Messung nicht "
+ `(1x: ${normal === null ? "?" : normal.toFixed(2)}). Das sagt nichts ueber das Tempo.`);
offenePunkte.push("Das Tempo war nicht messbar -- das Video lief nicht.");
} else if (schnell < normal * 1.5) {
console.log(" ACHTUNG: Die Geschwindigkeit wirkt nicht -- das Video laeuft gleich schnell.");
ausgang = 1;
}
@@ -2373,10 +2532,32 @@ if (nachHin.titel === nachZurueck.titel) {
console.log(" ACHTUNG: Der Umschalter wechselt nicht.");
ausgang = 1;
}
/* Die gemerkte Stelle: etwa dort weiter, wo es aufgehoert hat --
sechs Sekunden Spiel fuer das Laden des Videos. */
/* ==== DIE GEMERKTE STELLE -- UND WARUM NUR NACH UNTEN GEPRUEFT WIRD
(praezisiert 08.10.2026)
Hier stand `Math.abs(nachher - vorher) > 8`. Das misst zwei Dinge
auf einmal: ob die Stelle gemerkt wurde UND wie lange diese
Messung zwischendurch gewartet hat. Sie wartet 2x3 Sekunden, und
danach laeuft das zurueckgeholte Video ganz richtig WEITER --
gemessen wurden deshalb 86 -> 95, und die Pruefung nannte das
„verloren".
Im Gegenlauf auf dem letzten Commit stand dieselbe Zahl bei
213 -> 213, und das sah nach „alles in Ordnung" aus. Es war
Zufall: Das Video stand dort bei 213 von 214 Sekunden, also am
Ende, und konnte gar nicht weiterlaufen. Eine Pruefung, die nur
am Ende eines Videos gruen ist, prueft den Spieler und nicht die
Software.
WAS WIRKLICH SCHIEFGEHEN KANN, geht nach UNTEN: Die Stelle wird
vergessen (zurueck auf 0) oder es wird die Stelle des ANDEREN
Videos genommen (hier wenige Sekunden). Beides liegt deutlich
unter dem gemerkten Wert. Nach oben ist Weiterlaufen der
Normalfall; die Obergrenze bleibt nur als Netz gegen einen Sprung
ins Nichts. */
if (stelleVorher === null || stelleNachher === null
|| Math.abs(stelleNachher - stelleVorher) > 8) {
|| stelleNachher < stelleVorher - 2
|| stelleNachher > stelleVorher + 30) {
console.log(` ACHTUNG: Die gemerkte Stelle ging verloren `
+ `(${stelleVorher} -> ${stelleNachher}).`);
ausgang = 1;
@@ -2760,6 +2941,151 @@ await roh(`/workspace/api/reaktion/massnahme/${IDS.gast}`, { method: "DELETE", k
}
}
/* =====================================================================
DER VORRAUM FUER EINGELADENE GAESTE (08.10.2026)
Filipe: „wenn gäste eingeladen werden, sollen die akzeptieren oder
ablehnen können. bevor die auch angezeigt werden sollen die
auswählen können ob die kamera an ist oder aus ist."
---------------------------------------------------------------------
WARUM DAS HIER GEMESSEN WIRD UND NICHT NUR IN `pruef-reaktion`
Dort ist der ganze Weg geprueft -- fragen, zusagen, ablehnen,
Frist, Kameravorwahl. Was dort NICHT geprueft werden kann, ist, ob
der Vorhang auf dem Bildschirm auch wirklich erscheint. Ein
Zustand im Server, den die Seite nicht zeigt, ist fuer den
Gefragten dasselbe wie keine Einladung: Er sieht nichts, sagt
nichts, und der Host wartet zwei Minuten.
Deshalb wird hier gemessen, was ein Mensch sieht: Steht der
Vorhang da? Hat er BEIDE Ausgaenge? Laeuft das eigene Bild darin?
Und bringt „Dabei sein" ihn wirklich auf die Buehne?
===================================================================== */
console.log("");
console.log("=== Der Vorraum fuer eingeladene Gaeste ===");
{
/* Der Zuschauer war eben aus der Sendung genommen und
zurueckgeholt -- sein Vorhang steht noch (siehe der Abschnitt
darueber). Also neu laden, so wie es ein Mensch auch taete. */
await zuschauer.seite.reload({ waitUntil: "networkidle" });
await zuschauer.seite.waitForTimeout(900);
/* PLATZ SCHAFFEN. Die Buehne hat zwei Plaetze, und aus frueheren
Abschnitten steht dort noch jemand. Eine Einladung, die mit
„gaeste_voll" abgelehnt wird, maesse nichts ueber den Vorraum. */
const vorher = await roh("/workspace/api/reaktion", { keks: k.admin });
for (const g of (vorher.j?.gaeste || [])) {
await roh(`/workspace/api/reaktion/gast/${g.id}`, { method: "DELETE", keks: k.admin });
}
const ein = await roh("/workspace/api/reaktion/gast", {
method: "POST", keks: k.admin, rumpf: { person_id: IDS.gast },
});
console.log(` Eingeladen: ${ein.code} (${(ein.j?.einladungen || []).length} offene Frage)`);
await zuschauer.seite.waitForFunction(
() => { const v = document.getElementById("vorraum"); return !!v && !v.hidden; },
null, { timeout: 15000 },
).catch(() => {});
await zuschauer.seite.waitForTimeout(600);
const vr = await zuschauer.seite.evaluate(() => {
const v = document.getElementById("vorraum");
if (!v || v.hidden) return { da: false };
const sicht = (e) => {
if (!e) return null;
const r = e.getBoundingClientRect();
return {
b: Math.round(r.width), h: Math.round(r.height),
imBild: r.width > 0 && r.height > 0 && r.top >= -1 && r.left >= -1
&& r.bottom <= window.innerHeight + 1 && r.right <= window.innerWidth + 1,
};
};
const vid = document.getElementById("vorraum-video");
return {
da: true,
/* DECKT ER WIRKLICH ALLES ZU? Ein Vorhang, durch den man den
Saal weiterbedienen kann, ist keiner -- und ausgerechnet
hier soll man nicht nebenbei etwas anderes anklicken. */
ueberAllem: (() => {
const e = document.elementFromPoint(window.innerWidth / 2, 12);
return !!e && (e === v || v.contains(e));
})(),
ja: sicht(document.getElementById("vorraum-ja")),
nein: sicht(document.getElementById("vorraum-nein")),
kamera: sicht(document.getElementById("vorraum-kamera")),
mikro: sicht(document.getElementById("vorraum-mikro")),
/* LAEUFT DAS EIGENE BILD? `videoWidth` ist die einzige ehrliche
Antwort -- ein `<video>` ohne Strom steht genauso im
Dokument wie eines mit. */
bild: vid ? { breit: vid.videoWidth, laeuft: !vid.paused } : null,
satz: document.getElementById("vorraum-satz")?.textContent?.trim().slice(0, 60) || "",
frist: document.getElementById("vorraum-frist")?.textContent?.trim() || "",
geraete: document.querySelectorAll("#vorraum-geraete select").length,
};
});
console.log(` Vorhang: da=${vr.da} ueberAllem=${vr.ueberAllem} · `
+ `„Dabei sein" ${vr.ja?.b}x${vr.ja?.h} · „Lieber nicht" ${vr.nein?.b}x${vr.nein?.h}`);
console.log(` Eigenes Bild: ${vr.bild?.breit} px, laeuft=${vr.bild?.laeuft} · `
+ `${vr.geraete} Geraetelisten · ${vr.frist}`);
await bild(zuschauer.seite, "24-vorraum");
/* ZWEI AUSGAENGE, BEIDE ANFASSBAR, BEIDE AUF HAUSGROESSE. Ein
Vorhang mit nur einem Weg hinaus ist keine Frage, sondern eine
Huerde -- und er stuende vor jemandem, der nur zuschauen
wollte. */
if (!vr.da || !vr.ueberAllem
|| !vr.ja?.imBild || !vr.nein?.imBild
|| (vr.ja?.h || 0) < 44 || (vr.nein?.h || 0) < 44
|| !vr.kamera?.imBild || !vr.mikro?.imBild) {
console.log(" ACHTUNG: Der Vorraum fehlt, deckt nicht zu, oder einer seiner "
+ "zwei Ausgaenge ist nicht da.");
ausgang = 1;
}
/* DAS BILD IST DER SINN DER SACHE. Ohne es waere der Vorraum eine
Frage ohne die Auskunft, die man zum Antworten braucht. */
if (!vr.bild || !vr.bild.breit) {
console.log(" ACHTUNG: Im Vorraum laeuft kein eigenes Bild.");
ausgang = 1;
}
/* ---- Kamera abwaehlen und zusagen ---- */
await zuschauer.seite.click("#vorraum-kamera").catch(() => {});
await zuschauer.seite.waitForTimeout(300);
const nachKlick = await zuschauer.seite.evaluate(() => ({
wort: document.getElementById("vorraum-kamera")?.textContent?.trim(),
schild: !document.getElementById("vorraum-aus")?.hidden,
}));
console.log(` Kamera abgewaehlt: „${nachKlick.wort}", Schild im Bild=${nachKlick.schild}`);
if (nachKlick.wort !== "Kamera aus" || !nachKlick.schild) {
console.log(" ACHTUNG: Die Kamera laesst sich im Vorraum nicht abwaehlen.");
ausgang = 1;
}
await zuschauer.seite.click("#vorraum-ja").catch(() => {});
await zuschauer.seite.waitForTimeout(1500);
const danach = await roh("/workspace/api/reaktion", { keks: k.admin });
const alsGast = (danach.j?.gaeste || []).find((g) => g.id === IDS.gast);
const zu = await zuschauer.seite.evaluate(() =>
!!document.getElementById("vorraum")?.hidden);
console.log(` Nach „Dabei sein": auf der Buehne=${!!alsGast}, `
+ `Kamera aus=${alsGast?.kamera_aus}, Vorhang zu=${zu}`);
/* DIE VORWAHL MUSS AM SERVER ANKOMMEN. Nur im Browser gegolten
hiesse: Der ganze Saal sieht ein schwarzes Fenster ohne
Beschriftung, und der Gast glaubt, seine Kamera sei aus,
waehrend sein Bild hinausgeht. */
if (!alsGast || alsGast.kamera_aus !== true || !zu) {
console.log(" ACHTUNG: Die Zusage kommt nicht an oder die Kameravorwahl geht verloren.");
ausgang = 1;
}
/* Und wieder aufraeumen -- die folgenden Abschnitte erwarten einen
Zuschauer und keinen Gast. */
await roh(`/workspace/api/reaktion/gast/${IDS.gast}`, { method: "DELETE", keks: k.admin });
await zuschauer.seite.waitForTimeout(500);
}
/* =====================================================================
DIE WERBEEINBLENDUNG -- beim Zuschauer, nicht beim Host
===================================================================== */