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`. --> - - + + - +