diff --git a/server/mess-reaktion.mjs b/server/mess-reaktion.mjs
index c8da797e..ab23c98e 100644
--- a/server/mess-reaktion.mjs
+++ b/server/mess-reaktion.mjs
@@ -455,8 +455,23 @@ console.log("=== Der Gleichlauf des Videos ===");
Fuenf-Sekunden-Taktes. Die erste Fassung dieser Gegenprobe hat
genau daran Alarm geschlagen: Sie hielt das Selbstheilen des
Systems fuer einen Fehler. */
+ /* GEWARTET WIRD AUF DEN ZUSTAND, NICHT AUF DIE UHR. Eine feste
+ Wartezeit stimmt meistens -- und wenn der Fuenf-Sekunden-Takt
+ des Hosts dazwischenfaellt, hinkt die Zuschauerin ein Ereignis
+ hinterher und die Messung meldet einen Fehler, den es nicht
+ gibt. Genau das ist einmal passiert. */
+ const warteAuf = async (seite, wert, frist = 9000) => {
+ try {
+ await seite.waitForFunction(
+ (w) => document.getElementById("video-platz")?.dataset.spieler === w,
+ wert, { timeout: frist });
+ return true;
+ } catch { return false; }
+ };
+
await host.seite.evaluate(() => document.getElementById("v-spiel")?.click());
- await host.seite.waitForTimeout(1800);
+ await warteAuf(host.seite, "pause");
+ await warteAuf(zuschauer.seite, "pause");
const haltHost = await stand(host.seite);
const haltZu = await stand(zuschauer.seite);
console.log(` Host haelt wirklich an: Host=${haltHost} Zuschauerin=${haltZu}`);
@@ -468,7 +483,8 @@ console.log("=== Der Gleichlauf des Videos ===");
/* Und wieder los, mit demselben Knopf. */
await host.seite.evaluate(() => document.getElementById("v-spiel")?.click());
- await host.seite.waitForTimeout(2000);
+ await warteAuf(host.seite, "laeuft");
+ await warteAuf(zuschauer.seite, "laeuft");
const weiterHost = await stand(host.seite);
const weiterZu = await stand(zuschauer.seite);
console.log(` Und wieder los: Host=${weiterHost} Zuschauerin=${weiterZu}`);
@@ -1663,16 +1679,29 @@ console.log(" --- Die Regieleiste ---");
/* ---- WAS SIE DEM VIDEO KOSTET ----
Die eigentliche Frage bei einer Leiste am oberen Rand. */
const platz = await host.seite.evaluate(() => {
+ /* SPUR: die ganze Kette vom Saal bis zur Leinwand. Wer nur die
+ Hoehe des Kinos kennt, raet, an welchem Glied sie verloren
+ geht. */
+ window.__spur = ["saal", "raum", "teil-live", "leinwand", "schiene"].map((id) => {
+ const e = document.getElementById(id);
+ if (!e) return `${id}=FEHLT`;
+ const r = e.getBoundingClientRect();
+ const c = getComputedStyle(e);
+ return `${id} ${Math.round(r.width)}x${Math.round(r.height)} `
+ + `@${Math.round(r.left)},${Math.round(r.top)} ${c.display}`;
+ }).join(" | ");
const l = document.getElementById("regieleiste").getBoundingClientRect();
const k = document.getElementById("teil-live").getBoundingClientRect();
const s2 = document.getElementById("saal").getBoundingClientRect();
return {
+ spur: window.__spur,
leiste: Math.round(l.height), kino: Math.round(k.height), saal: Math.round(s2.height),
ueber: Math.max(0, document.documentElement.scrollHeight
- document.documentElement.clientHeight),
};
});
- console.log(` Saal ${platz.saal} px = Leiste ${platz.leiste} + Kino ${platz.kino} `
+ console.log(` [Kette] ${platz.spur}`);
+console.log(` Saal ${platz.saal} px = Leiste ${platz.leiste} + Kino ${platz.kino} `
+ `+ Regie, senkrecht ueber ${platz.ueber} px`);
if (platz.ueber > 6 || platz.kino < 200) {
console.log(" ACHTUNG: Die Leiste nimmt dem Bild zu viel oder die Seite laeuft ueber.");
@@ -2534,11 +2563,24 @@ console.log("=== Die Werbeeinblendung ===");
bleibt, steht als offener Punkt in der Liste; hier still darum
herumzumessen waere das Verstecken eines Befunds. */
await zuschauer.seite.reload({ waitUntil: "domcontentloaded" });
+ /* GEWARTET WIRD AUF DEN STAND, NICHT AUF DIE SEITE. `#leinwand`
+ steht im HTML und ist sofort da; `body[data-stand]` setzt erst
+ `zeichnen()`, also nachdem der Stand angekommen UND verarbeitet
+ ist. Vorher hat diese Messung gemessen, waehrend die Seite noch
+ nichts wusste -- und das sah aus wie Flatterhaftigkeit. */
await zuschauer.seite.waitForFunction(
() => !document.querySelector(".raus__titel")
- && !!document.getElementById("leinwand"),
+ && !!document.getElementById("leinwand")
+ && !!document.body.dataset.stand
+ /* UND DIE WERBEFLAECHE IST EINGEHAENGT. Hier standen
+ vorher 600 Millisekunden „zur Sicherheit". Das ist
+ keine Bedingung, sondern eine Annahme -- und dieselbe
+ Sorte Annahme hat in diesem Block zweimal einen Fehler
+ gemeldet, den es nicht gab. Die Flaeche entsteht in
+ `werbungZeichnen()`, also nachdem der Stand angekommen
+ und verarbeitet ist. */
+ && !!document.querySelector(".werbung"),
null, { timeout: 15000 });
- await zuschauer.seite.waitForTimeout(600);
const setz = (felder) => host.seite.evaluate(async (f) => {
const a = await fetch("/workspace/api/reaktion/werbung", {
@@ -2549,11 +2591,27 @@ console.log("=== Die Werbeeinblendung ===");
return { code: a.status, text: (await a.text()).slice(0, 120) };
}, felder);
+ /* AUF DAS ERGEBNIS WARTEN, NICHT AUF DIE UHR. Feste Wartezeiten
+ stimmen meistens -- und melden, wenn daneben etwas langsamer
+ wird, einen Fehler, den es nicht gibt. Genau das ist hier
+ zweimal passiert. Laeuft die Frist ab, ist es ein echter
+ Befund. */
+ const warte = async (seite, fn, was, frist = 9000) => {
+ try { await seite.waitForFunction(fn, null, { timeout: frist }); return true; }
+ catch {
+ console.log(` ACHTUNG: ${was} ist nach ${frist / 1000} s nicht angekommen.`);
+ ausgang = 1;
+ return false;
+ }
+ };
+
/* ---- 1. Das Laufband ---- */
const an = await setz({ art: "laufband", tempo: 12 });
console.log(` Band einschalten: ${an.code}`);
- await host.seite.waitForTimeout(700);
- await zuschauer.seite.waitForTimeout(900);
+ await warte(zuschauer.seite,
+ () => document.querySelector('.werbung[data-art="laufband"]')
+ && document.querySelectorAll(".werbung__stueck").length > 0,
+ "das Laufband");
const band = await zuschauer.seite.evaluate(() => {
const w = document.querySelector(".werbung");
@@ -2620,7 +2678,10 @@ console.log("=== Die Werbeeinblendung ===");
/* ---- 2. Die Ecken ---- */
const ecken = await setz({ ecke_dogi: "ol", ecke_hase: "ur", ecke_gross: 1.4 });
console.log(` Ecken setzen: ${ecken.code}`);
- await zuschauer.seite.waitForTimeout(800);
+ await warte(zuschauer.seite,
+ () => document.querySelector('.werbung__ecke--dogi[data-platz="ol"]:not([hidden])')
+ && document.querySelector('.werbung__ecke--hase[data-platz="ur"]:not([hidden])'),
+ "die zwei Eckenbilder");
const lage = await zuschauer.seite.evaluate(() => {
const lein = document.getElementById("leinwand")?.getBoundingClientRect();
@@ -2669,7 +2730,25 @@ console.log("=== Die Werbeeinblendung ===");
/* ---- 4. Der Wechsel ---- */
await setz({ art: "wechsel", tempo: 8 });
- await zuschauer.seite.waitForTimeout(900);
+ await warte(zuschauer.seite,
+ () => document.querySelector('.werbung[data-art="wechsel"]')
+ && document.querySelectorAll(".werbung__stueck--wechsel").length > 1,
+ "der Wechsel");
+ /* SPUR: Was passiert in der Sekunde nach dem Umschalten? Ein
+ einzelner Blick sagt nicht, ob etwas nie da war oder gleich
+ wieder weg -- und das sind zwei ganz verschiedene Ursachen. */
+ {
+ const folge = [];
+ for (let i = 0; i < 6; i++) {
+ folge.push(await zuschauer.seite.evaluate(() => {
+ const w = document.querySelector(".werbung");
+ return `${w?.dataset.art || "?"}/`
+ + `${document.querySelectorAll(".werbung__stueck--wechsel").length}`;
+ }));
+ await zuschauer.seite.waitForTimeout(250);
+ }
+ console.log(` [Spur] Art/Stuecke: ${folge.join(" -> ")}`);
+ }
const wechsel = await zuschauer.seite.evaluate(() => {
const w = document.querySelector(".werbung");
const alle = [...document.querySelectorAll(".werbung__stueck--wechsel")];
@@ -2725,12 +2804,17 @@ console.log("=== Die Werbeeinblendung ===");
/* ---- 5. Ein Bild davon ---- */
await setz({ art: "laufband", ecke_hase: "ur" });
- await zuschauer.seite.waitForTimeout(900);
+ await warte(zuschauer.seite,
+ () => document.querySelector('.werbung[data-art="laufband"]'),
+ "das Laufband fuer das Bildschirmfoto");
await bild(zuschauer.seite, "24-werbung");
/* ---- 6. Wieder aus ---- */
const aus = await setz({ art: "aus", ecke_dogi: "aus", ecke_hase: "aus" });
- await zuschauer.seite.waitForTimeout(700);
+ await warte(zuschauer.seite,
+ () => !document.querySelector(".werbung__band:not([hidden])")
+ && !document.querySelector(".werbung__ecke:not([hidden])"),
+ "das Abschalten");
const weg = await zuschauer.seite.evaluate(() => {
const b = document.querySelector(".werbung__band");
const e = [...document.querySelectorAll(".werbung__ecke")].filter((x) => !x.hidden);
@@ -2744,6 +2828,226 @@ console.log("=== Die Werbeeinblendung ===");
}
}
+/* =====================================================================
+ BEENDET HEISST LEER -- auch im Kasten
+ =====================================================================
+ Filipe, 30.09.2026: „wenn eine sendung beendet wurde soll alles
+ automatisch verschwinden. also von daten die da stehen von dem
+ video."
+
+ GEMESSEN WIRD DAS EINGABEFELD, nicht die Spalte. Genau dazwischen
+ lag der zweite Fehler: `p-beginn` wurde nur gefuellt, nie geleert
+ -- die Spalte war leer, im Pult stand der Termin weiter, bis
+ jemand die Seite neu lud. Das betraf auch den vorhandenen Knopf
+ „Alles zuruecksetzen".
+ ===================================================================== */
+console.log("");
+console.log("=== Beendet heisst leer (im Pult) ===");
+{
+ const felder = () => host.seite.evaluate(() => ({
+ titel: document.getElementById("p-titel")?.value ?? null,
+ video: document.getElementById("p-video")?.value ?? null,
+ beginn: document.getElementById("p-beginn")?.value ?? null,
+ zweit: document.getElementById("p-zweit")?.value ?? null,
+ }));
+
+ /* Erst fuellen -- sonst misst die Pruefung leere Felder und nennt
+ das Erfolg. */
+ await host.seite.evaluate(async () => {
+ const wann = new Date(Date.now() + 3600000);
+ const p = (n) => String(n).padStart(2, "0");
+ await fetch("/workspace/api/reaktion", {
+ method: "PUT", credentials: "same-origin",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ titel: "Test", video: "dQw4w9WgXcQ",
+ beginnt_am: wann.toISOString(),
+ }),
+ });
+ await fetch("/workspace/api/reaktion/stand", {
+ method: "POST", credentials: "same-origin",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ stand: "live" }),
+ });
+ });
+ await host.seite.waitForTimeout(1500);
+
+ const voll = await felder();
+ console.log(` Vorher im Pult: Titel="${voll.titel}" Video="${voll.video}" `
+ + `Beginn="${voll.beginn}"`);
+ if (!voll.titel || !voll.video || !voll.beginn) {
+ console.log(" ACHTUNG: Die Felder liessen sich nicht fuellen -- "
+ + "so ist die Frage nicht zu beantworten.");
+ ausgang = 1;
+ } else {
+ await host.seite.evaluate(() => fetch("/workspace/api/reaktion/stand", {
+ method: "POST", credentials: "same-origin",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ stand: "zu" }),
+ }));
+ await host.seite.waitForTimeout(1800);
+
+ const leer = await felder();
+ console.log(` Nach dem Beenden: Titel="${leer.titel}" Video="${leer.video}" `
+ + `Beginn="${leer.beginn}" Zweites="${leer.zweit}"`);
+ for (const [name, wert] of Object.entries(leer)) {
+ if (wert) {
+ console.log(` ACHTUNG: Im Feld „${name}" steht nach dem Beenden `
+ + `weiter „${wert}".`);
+ ausgang = 1;
+ }
+ }
+ }
+
+ /* Wiederherstellen -- die Messungen danach brauchen eine laufende
+ Sendung. */
+ await host.seite.evaluate(async () => {
+ await fetch("/workspace/api/reaktion", {
+ method: "PUT", credentials: "same-origin",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ titel: "Reaction", video: "dQw4w9WgXcQ" }),
+ });
+ await fetch("/workspace/api/reaktion/stand", {
+ method: "POST", credentials: "same-origin",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ stand: "live" }),
+ });
+ });
+ await host.seite.waitForTimeout(1200);
+}
+
+/* =====================================================================
+ DIE CHATKISTE STEHT IMMER (30.09.2026)
+ =====================================================================
+ Filipe: „mach dass ich die chat kiste immer sehe bitte und nicht
+ erst wenn das video läuft."
+
+ Sie lag im Kino (`#teil-live`) und ging mit ihm weg. Im
+ Wartebereich stand dabei woertlich „Der Chat ist schon offen" --
+ der Satz stimmte sogar, der Server laesst dort schreiben; zu sehen
+ war der Chat trotzdem nicht.
+
+ GEMESSEN WIRD IN ALLEN DREI ZUSTAENDEN. Nur im Live zu messen
+ waere die Pruefung, die den Fehler nicht sehen kann.
+ ===================================================================== */
+console.log("");
+console.log("=== Die Chatkiste in jedem Zustand ===");
+{
+ const kiste = (seite) => seite.evaluate(() => {
+ const e = document.getElementById("schiene");
+ if (!e) return null;
+ const r = e.getBoundingClientRect();
+ const feld = document.getElementById("chat-feld");
+ const fr = feld ? feld.getBoundingClientRect() : null;
+ return {
+ stand: document.body.dataset.stand || "(nichts)",
+ breit: Math.round(r.width), hoch: Math.round(r.height),
+ /* SICHTBAR HEISST: hat Flaeche UND ist nicht weggeschaltet.
+ `display: none` gibt 0x0 -- beides zusammen laesst keinen
+ Zwischenweg offen. */
+ sichtbar: getComputedStyle(e).display !== "none" && r.width > 0 && r.height > 0,
+ feldBreit: fr ? Math.round(fr.width) : 0,
+ grund: feld ? feld.placeholder : "",
+ gesperrt: feld ? !!feld.disabled : null,
+ };
+ });
+
+ const stellen = async (stand, mitVideo) => {
+ const antwort = await host.seite.evaluate(async (w) => {
+ if (w.video) {
+ await fetch("/workspace/api/reaktion", {
+ method: "PUT", credentials: "same-origin",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ titel: "Chatprobe", video: "dQw4w9WgXcQ" }),
+ });
+ }
+ const a = await fetch("/workspace/api/reaktion/stand", {
+ method: "POST", credentials: "same-origin",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ stand: w.stand }),
+ });
+ /* DIE ANTWORT WIRD ZURUECKGEGEBEN. Ohne sie meldet die Messung
+ nur „steht auf live statt auf zu" und verschweigt, dass der
+ Server 409 gesagt hat -- man sucht dann am falschen Ende. */
+ return { code: a.status, text: (await a.text()).slice(0, 80) };
+ }, { stand, video: mitVideo });
+ if (antwort.code !== 200) {
+ console.log(` Der Server nimmt „${stand}" nicht an: `
+ + `${antwort.code} ${antwort.text}`);
+ }
+ /* 40 SEKUNDEN, UND DAS HAT EINEN GRUND. „zu" kommt sofort an --
+ die Empfaenger werden seit dem 30.09. vor dem Leeren der
+ Dabei-Liste bestimmt. Die RUECKKEHR nicht: Wer im
+ geschlossenen Saal sitzt, steht in keiner Liste mehr und
+ bekommt keinen Rundruf. Seine Seite faengt sich selbst, aber
+ erst mit dem eigenen Dreissig-Sekunden-Takt.
+
+ Gewartet wird auf den Zustand, nicht die volle Zeit: Kommt er
+ frueher, geht es frueher weiter. */
+ try {
+ await zuschauer.seite.waitForFunction(
+ (w) => document.body.dataset.stand === w, stand, { timeout: 40_000 });
+ } catch { /* unten faellt es auf */ }
+ await zuschauer.seite.waitForTimeout(400);
+ };
+
+ let fehlt = 0;
+ for (const [stand, wort, mitVideo] of [
+ ["zu", "der Saal ist zu", false],
+ ["vorbereitung", "im Wartebereich", true],
+ ["live", "waehrend der Sendung", true],
+ ]) {
+ await stellen(stand, mitVideo);
+ /* SPUR: Was sagt der Server, und was zeigt ihre Seite? Ohne
+ beides nebeneinander sucht man am falschen Ende. */
+ const spur = await zuschauer.seite.evaluate(async () => {
+ const a = await fetch("/workspace/api/reaktion", { credentials: "same-origin" });
+ const j = await a.json().catch(() => null);
+ return { code: a.status, server: j?.stand, seite: document.body.dataset.stand,
+ raus: document.body.dataset.raus || "nein" };
+ });
+ console.log(` [Spur] Server sagt „${spur.server}" (${spur.code}), `
+ + `Seite zeigt „${spur.seite}", raus=${spur.raus}`);
+ const k = await kiste(zuschauer.seite);
+ if (!k) {
+ console.log(` ACHTUNG: ${wort}: die Chatkiste gibt es gar nicht.`);
+ ausgang = 1; fehlt++;
+ continue;
+ }
+ console.log(` ${wort.padEnd(22)} Stand=${k.stand.padEnd(13)} `
+ + `Kiste ${k.breit}x${k.hoch} px, Feld ${k.feldBreit} px`
+ + `${k.gesperrt ? ` (gesperrt: „${k.grund}")` : ""}`);
+ if (k.stand !== stand) {
+ console.log(` ACHTUNG: ${wort}: die Seite steht auf „${k.stand}" `
+ + `statt auf „${stand}" -- so ist die Frage nicht zu beantworten.`);
+ ausgang = 1; fehlt++;
+ continue;
+ }
+ /* 200 px und nicht „groesser als null": Eine Spalte, die auf
+ zwanzig Pixel zusammenfaellt, ist rechnerisch da und auf dem
+ Bildschirm weg. */
+ if (!k.sichtbar || k.breit < 200 || k.hoch < 100 || k.feldBreit < 100) {
+ console.log(` ACHTUNG: ${wort}: die Chatkiste ist nicht zu sehen `
+ + `(${k.breit}x${k.hoch}, Feld ${k.feldBreit}).`);
+ ausgang = 1; fehlt++;
+ }
+ /* WER NICHT SCHREIBEN DARF, LIEST DEN GRUND. Ein graues Feld
+ ohne Erklaerung laesst einen an sich selbst zweifeln. */
+ if (k.gesperrt && !k.grund) {
+ console.log(` ACHTUNG: ${wort}: das Feld ist gesperrt und sagt nicht, warum.`);
+ ausgang = 1; fehlt++;
+ }
+ }
+ if (!fehlt) console.log(" In allen drei Zustaenden steht die Kiste.");
+ offenePunkte.push("Wird der Saal wieder aufgemacht, erfahren die, die im "
+ + "geschlossenen Saal sitzen, es erst mit ihrem eigenen 30-Sekunden-Takt "
+ + "-- sie stehen in keiner Dabei-Liste mehr, an die ein Rundruf gehen "
+ + "koennte. „Zu\" kommt seit dem 30.09. sofort an.");
+
+ /* Und wieder auf Sendung -- was danach kommt, braucht sie. */
+ await stellen("live", true);
+}
+
await modi.kontext.close();
await host.kontext.close();
await gast2.kontext.close();
diff --git a/server/pruef-reaktion.mjs b/server/pruef-reaktion.mjs
index 1d887a93..9b5085f1 100644
--- a/server/pruef-reaktion.mjs
+++ b/server/pruef-reaktion.mjs
@@ -2475,6 +2475,14 @@ melde("=== Die Einladung ===");
live, und eingeladen wird nur beim Uebergang. */
await roh(CREW, "/workspace/api/reaktion/stand",
{ method: "POST", keks: k.admin, rumpf: { stand: "zu" } });
+ /* UND DANN EIN NEUES VIDEO. Seit dem 30.09. raeumt das Beenden die
+ Sendungsdaten weg -- ohne Video gibt es kein „auf Sendung" (400
+ kein_video). Genau das tut ein Mensch auch: Nach der einen
+ Sendung traegt er fuer die naechste ein neues Video ein. */
+ await roh(CREW, "/workspace/api/reaktion", {
+ method: "PUT", keks: k.admin,
+ rumpf: { titel: "Die naechste Sendung", video: "dQw4w9WgXcQ" },
+ });
const db2 = new DatabaseSync(process.env.WORKSPACE_DB);
/* Je ein Geraet fuer JEDEN -- auch fuer die, die keine Einladung
@@ -2559,6 +2567,97 @@ melde("=== Die Einladung ===");
await new Promise((r) => dienst.close(r));
}
+/* =======================================================================
+ BEENDET HEISST LEER -- aber nicht vergessen
+ =======================================================================
+
+ Filipe, 30.09.2026, mit einem Bild des Pults („Titel der Sendung:
+ Test / YouTube: bm0sK2-KkW8 / Beginn: 29.09.2026 18:36"):
+ „wenn eine sendung beendet wurde soll alles automatisch
+ verschwinden. also von daten die da stehen von dem video."
+
+ Vorher blieb alles stehen. Beim naechsten Aufmachen stand dort der
+ Titel der letzten Sendung und ein Beginn, der in der Vergangenheit
+ liegt.
+ ======================================================================= */
+melde("");
+melde("=== Beendet heisst leer ===");
+{
+ /* Die Sendung laeuft an dieser Stelle noch (aus dem Abschnitt
+ darueber). Erst werden die Felder gefuellt, damit es ueberhaupt
+ etwas zu leeren gibt. */
+ const beginn = new Date(Date.now() + 3600_000).toISOString();
+ await roh(CREW, "/workspace/api/reaktion", {
+ method: "PUT", keks: k.admin,
+ rumpf: { titel: "Die letzte Sendung", video: "dQw4w9WgXcQ",
+ vorschau: "https://example.invalid/bild.jpg", beginnt_am: beginn },
+ });
+ await roh(CREW, "/workspace/api/reaktion/zweitvideo", {
+ method: "POST", keks: k.admin, rumpf: { video: "bm0sK2-KkW8" },
+ });
+
+ /* DIE VORAUSSETZUNG WIRD HERGESTELLT UND GEPRUEFT.
+ Ohne das misst dieser Abschnitt bei der kleinsten Aenderung
+ weiter oben etwas ganz anderes -- und meldet es als Befund. */
+ await roh(CREW, "/workspace/api/reaktion/stand",
+ { method: "POST", keks: k.admin, rumpf: { stand: "live" } });
+
+ /* ---- DIE GEGENPROBE STEHT VORNE ----------------------------
+ Ohne sie bestuende die Pruefung darunter auch dann, wenn nie
+ etwas in den Feldern stand. */
+ const vorher = await roh(CREW, "/workspace/api/reaktion", { keks: k.admin });
+ ok(vorher.j?.titel === "Die letzte Sendung" && !!vorher.j?.video,
+ `vorher stehen Titel und Video im Pult ("${vorher.j?.titel}")`);
+ ok(!!vorher.j?.beginnt_am, "und ein Beginn");
+ ok(!!vorher.j?.zweit?.video, `und ein zweites Video (${vorher.j?.zweit?.video})`);
+ ok(vorher.j?.stand === "live",
+ `und die Sendung laeuft wirklich -- sonst misst dieser Abschnitt `
+ + `etwas anderes (${vorher.j?.stand})`);
+
+ const anAus = await roh(CREW, "/workspace/api/reaktion/stand",
+ { method: "POST", keks: k.admin, rumpf: { stand: "zu" } });
+ ok(anAus.code === 200, `die Sendung wird beendet (${anAus.code})`);
+
+ const nachher = await roh(CREW, "/workspace/api/reaktion", { keks: k.admin });
+ ok(!nachher.j?.titel, `der Titel ist weg ("${nachher.j?.titel}")`);
+ ok(!nachher.j?.video, `das Video ist weg ("${nachher.j?.video}")`);
+ ok(!nachher.j?.vorschau, "das Vorschaubild ist weg");
+ ok(!nachher.j?.beginnt_am, `der Beginn ist weg (${nachher.j?.beginnt_am})`);
+ ok(!nachher.j?.zweit?.video, "das zweite Video ist weg");
+ ok(!nachher.j?.laeuft && !nachher.j?.sekunde, "und die Stelle steht auf null");
+
+ /* ---- WEG VOM SCHREIBTISCH, NICHT AUS DER WELT ---------------
+ Der Verlaufseintrag wird VOR dem Leeren geschrieben. Schoebe
+ jemand das Leeren nach vorn, stuende dort ein leerer Titel --
+ und es faellt erst auf, wenn jemand nachsehen will, was letzte
+ Woche lief. */
+ const dbv = new DatabaseSync(process.env.WORKSPACE_DB);
+ const letzte = dbv.prepare(
+ "SELECT titel, video FROM reaktion_verlauf ORDER BY id DESC LIMIT 1").get();
+ dbv.close();
+ ok(letzte?.titel === "Die letzte Sendung",
+ `im Verlauf steht sie weiter ("${letzte?.titel}")`);
+ ok(letzte?.video === "dQw4w9WgXcQ", `samt Video (${letzte?.video})`);
+
+ /* ---- WER NUR VORBEREITET HAT, BEHAELT SEINE EINGABEN --------
+ Eine Entscheidung, und deshalb steht sie hier: Aus der
+ Vorbereitung zumachen heisst „ich hoere auf vorzubereiten",
+ nicht „ich habe gesendet". Ohne diese Zeile nimmt eine spaetere,
+ gut gemeinte Vereinfachung („leeren wir doch bei jedem zu") die
+ Eingaben unbemerkt weg. */
+ await roh(CREW, "/workspace/api/reaktion", {
+ method: "PUT", keks: k.admin,
+ rumpf: { titel: "Nur vorbereitet", video: "dQw4w9WgXcQ" },
+ });
+ await roh(CREW, "/workspace/api/reaktion/stand",
+ { method: "POST", keks: k.admin, rumpf: { stand: "vorbereitung" } });
+ await roh(CREW, "/workspace/api/reaktion/stand",
+ { method: "POST", keks: k.admin, rumpf: { stand: "zu" } });
+ const probe = await roh(CREW, "/workspace/api/reaktion", { keks: k.admin });
+ ok(probe.j?.titel === "Nur vorbereitet",
+ `wer nur vorbereitet hat, behaelt seine Eingaben ("${probe.j?.titel}")`);
+}
+
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG");
diff --git a/server/workspace-reaktion.js b/server/workspace-reaktion.js
index ec69db4a..83bbb294 100644
--- a/server/workspace-reaktion.js
+++ b/server/workspace-reaktion.js
@@ -98,6 +98,41 @@ export const REAKTION_HOST_ROLLEN = HOST_ROLLEN;
const jetzt = () => new Date().toISOString();
+/* ======================================================================
+ WAS ZU EINER EINZELNEN SENDUNG GEHOERT (30.09.2026)
+
+ Filipe: „wenn eine sendung beendet wurde soll alles automatisch
+ verschwinden. also von daten die da stehen von dem video."
+
+ Diese Aufzaehlung stand bis dahin EINMAL mitten in der
+ Zuruecksetzen-Route. Jetzt brauchen sie zwei Wege -- das Beenden
+ und der Knopf --, und zwei Abschriften waeren die, die beim
+ naechsten Spaltenzuwachs auseinanderlaufen. Genau so sind am
+ 11.09. drei Spalten mit Inhalt verlorengegangen.
+
+ SIE STEHT EINZELN DA UND NICHT ALS „alles ausser". Eine Liste von
+ Ausnahmen waechst still mit jeder neuen Spalte mit -- und dann
+ loescht das Aufraeumen etwas, das es nie loeschen sollte. Was hier
+ fehlt, bleibt stehen, und das ist die sichere Richtung.
+ ====================================================================== */
+const SENDUNGS_FELDER = {
+ titel: "", video: "", video_titel: "", vorschau: "",
+ beginnt_am: null, laeuft: 0, sekunde: 0, gestartet_am: null,
+ zweit_video: "", zweit_titel: "", zweit_sekunde: 0,
+};
+
+/* UND WAS EINSTELLUNG IST -- nur beim ausdruecklichen Zuruecksetzen.
+
+ Bildaufteilung, Kameragroesse und Chatmodus gelten fuer alle
+ Sendungen. Wer sie nach jedem Beenden neu setzen muesste, haette
+ nach der dritten Sendung genug davon -- und schaltet dann lieber
+ gar nicht mehr ab. Das ist der Unterschied zwischen Aufraeumen und
+ Abraeumen. */
+const PULT_EINSTELLUNGEN = {
+ tempo: 1, layout: "kino", kamera_gross: 1, kamera_ecke: "ur",
+ chat_modus: "offen",
+};
+
/* ---------- Schranken --------------------------------------------------- */
function angemeldet(req, res, next) {
@@ -484,8 +519,15 @@ function darfSchreiben(person, stand) {
return schreibGrund(person, stand) === null;
}
-function melden(art, daten) {
- stromAn(empfaenger(), art, daten);
+/** Rundruf an alle, die von einer Aenderung erfahren sollen.
+ *
+ * `wer` ist ausdruecklich ueberschreibbar (30.09.2026): Eine
+ * Aenderung, die die Zuschauerliste LEERT, muss ihre Empfaenger
+ * vorher bestimmt haben. Sonst sucht der Rundruf sie in einer
+ * Liste, die es nicht mehr gibt -- und ausgerechnet die Nachricht
+ * „der Saal ist zu" erreicht niemanden, der darin sitzt. */
+function melden(art, daten, wer) {
+ stromAn(wer || empfaenger(), art, daten);
/* UND AN DIE BUEHNENQUELLEN. Sie haengen an keiner Person -- OBS
meldet sich mit einem Schluessel, nicht mit einem Zugang. Beide
Aufrufe stehen hier, an EINER Stelle: zwei Stellen, die je einen
@@ -539,19 +581,16 @@ reaktionRouter.post("/workspace/api/reaktion/zuruecksetzen", nurHost, (req, res)
db().exec("BEGIN IMMEDIATE");
try {
- /* DIE SPALTEN STEHEN EINZELN DA UND NICHT ALS „alles ausser".
- Eine Liste von Ausnahmen waechst still mit jeder neuen
- Spalte mit -- und dann loescht das Zuruecksetzen etwas, das
- es nie loeschen sollte. Was hier fehlt, bleibt stehen, und
- das ist die sichere Richtung. */
- db().prepare(`UPDATE reaktion SET
- stand = 'zu', titel = '', video = '', video_titel = '', vorschau = '',
- beginnt_am = NULL, laeuft = 0, sekunde = 0, tempo = 1,
- layout = 'kino', kamera_gross = 1, kamera_ecke = 'ur',
- chat_modus = 'offen',
- zweit_video = '', zweit_titel = '', zweit_sekunde = 0,
- gestartet_am = NULL, stand_seit = ?, geaendert = ?
- WHERE id = 1`).run(jetzt(), jetzt());
+ /* BEIDE LISTEN, und beide stehen oben bei den Tabellen -- der
+ Knopf raeumt mehr weg als das blosse Beenden: auch die
+ Einstellungen und die Warteschlange. */
+ const felder = {
+ ...SENDUNGS_FELDER, ...PULT_EINSTELLUNGEN,
+ stand: "zu", stand_seit: jetzt(), geaendert: jetzt(),
+ };
+ const spalten = Object.keys(felder).map((k) => `${k} = ?`).join(", ");
+ db().prepare(`UPDATE reaktion SET ${spalten} WHERE id = 1`)
+ .run(...Object.values(felder));
db().prepare("DELETE FROM reaktion_liste").run();
db().prepare("DELETE FROM reaktion_gaeste").run();
db().exec("COMMIT");
@@ -635,6 +674,13 @@ reaktionRouter.post("/workspace/api/reaktion/stand", nurHost, express.json({ lim
if (!STAENDE.includes(neu)) return res.status(400).json({ fehler: "stand_unbekannt" });
const s = sendung();
+ /* WER DAVON ERFAHREN SOLL, STEHT JETZT FEST -- vor jeder
+ Aenderung. „zu" loescht `reaktion_dabei`; wuerde der Rundruf
+ seine Empfaenger erst danach suchen, ginge ausgerechnet die
+ Nachricht „der Saal ist zu" an niemanden, der darin sitzt.
+ Ihre Seite blieb dann auf „live" stehen, bis sie neu luden. */
+ const erfahren = empfaenger();
+
/* OHNE VIDEO KEIN LIVE. Der Knopf waere sonst da, das Ergebnis
ein schwarzes Rechteck -- und die Leute sitzen davor. */
if (neu === "live" && !s.video) {
@@ -664,9 +710,28 @@ reaktionRouter.post("/workspace/api/reaktion/stand", nurHost, express.json({ lim
dabeiListe().length, zahl?.n || 0);
db().exec("DELETE FROM reaktion_chat");
db().exec("DELETE FROM reaktion_gaeste");
- felder.laeuft = 0;
- felder.sekunde = 0;
- felder.gestartet_am = null;
+
+ /* UND DER SCHREIBTISCH WIRD LEER (30.09.2026).
+
+ Filipe: „wenn eine sendung beendet wurde soll alles
+ automatisch verschwinden. also von daten die da stehen von
+ dem video."
+
+ Vorher blieben Titel, Video, Vorschau, Beginn und das
+ zweite Video stehen. Beim naechsten Aufmachen stand dort
+ der Titel der letzten Sendung und ein Beginn, der in der
+ Vergangenheit liegt -- man musste jedes Feld einzeln
+ leeren, bevor man anfangen konnte.
+
+ ERST DER VERLAUF, DANN DAS LEEREN. Die Zeile darueber
+ schreibt Titel und Video in `reaktion_verlauf`; umgekehrt
+ stuende dort nichts. Verloren geht also nichts -- es wandert
+ nur dorthin, wo Vergangenes hingehoert.
+
+ Die Einstellungen (Bildaufteilung, Kameragroesse,
+ Chatmodus) und die Warteschlange bleiben. Wer sie auch weg
+ haben will, hat dafuer den Knopf „Alles zuruecksetzen". */
+ Object.assign(felder, SENDUNGS_FELDER);
}
if (neu === "zu") db().exec("DELETE FROM reaktion_dabei");
@@ -680,7 +745,7 @@ reaktionRouter.post("/workspace/api/reaktion/stand", nurHost, express.json({ lim
ip: echteIp(req),
});
const l = lage(req.person);
- melden("reaktion", { was: "stand", lage: oeffentlich(l) });
+ melden("reaktion", { was: "stand", lage: oeffentlich(l) }, erfahren);
/* ERST JETZT EINLADEN, nach dem Schreiben und nach `melden`.
Wer die Meldung antippt, landet auf einer Seite, die den
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index 4e7fda57..e8719f86 100644
--- a/workspace/anruf-probe.html
+++ b/workspace/anruf-probe.html
@@ -16,8 +16,8 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
-
-
+
+
-
+