From 0fbd3f45b34336a43a050d33a779463c5651134e Mon Sep 17 00:00:00 2001 From: Dogfather Date: Wed, 30 Sep 2026 02:36:13 +0200 Subject: [PATCH] Die Chatkiste steht immer, Beenden raeumt auf -- und eine verspaetete Antwort ueberschreibt nichts Neueres mehr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filipe, 30.09.2026: „wenn eine sendung beendet wurde soll alles automatisch verschwinden. also von daten die da stehen von dem video." Und: „mach dass ich die chat kiste immer sehe bitte und nicht erst wenn das video läuft." ===================================================================== 1. BEENDET HEISST LEER ===================================================================== Beim Beenden verschwinden Titel, YouTube-Adresse, Videotitel, Vorschaubild, Beginn und das zweite Video samt seiner Stelle. Vorher blieb alles stehen; beim naechsten Aufmachen stand dort der Titel der letzten Sendung und ein Beginn, der in der Vergangenheit liegt. ERST DER VERLAUF, DANN DAS LEEREN -- weg vom Schreibtisch heisst nicht weg aus der Welt. Geprueft: Nach dem Beenden steht die Sendung weiter in `reaktion_verlauf`, samt Video. EINE FELDLISTE, NICHT ZWEI. Die Aufzaehlung stand in der Zuruecksetzen-Route; jetzt brauchen sie zwei Wege. `SENDUNGS_FELDER` und `PULT_EINSTELLUNGEN` stehen einmal oben. Zwei Abschriften waeren die, die beim naechsten Spaltenzuwachs auseinanderlaufen -- genau so sind am 11.09. drei Spalten mit Inhalt verlorengegangen. Einstellungen (Bildaufteilung, Kameragroesse, Chatmodus) und die Warteschlange bleiben -- die raeumt weiter nur „Alles zuruecksetzen" ab. Wer nur vorbereitet und dann zumacht, behaelt seine Eingaben; das ist eine Entscheidung und steht als Pruefung fest. DAS FELD „BEGINN" WURDE NIE GELEERT, nur gefuellt: `if (lage.beginnt_am && document.activeElement !== …)` -- zwei Fragen in einer Bedingung, und die zweite beantwortete die erste stillschweigend mit nein. Das betraf auch den vorhandenen Zuruecksetzen-Knopf. ===================================================================== 2. DIE CHATKISTE STEHT IMMER ===================================================================== Sie lag in `#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 (201, geprueft); zu sehen war der Chat trotzdem nicht. Die drei Haeute und die Schiene liegen jetzt in einem gemeinsamen `.raum`: links wechseln die Haeute, rechts steht der Chat. Das Raster wandert mit nach oben statt sich zu verdoppeln -- zwei `grid-template-columns` fuer dieselbe Frage waren am 28.09. der Grund, warum die Regieleiste null Pixel bekam. Wer nicht schreiben darf, liest den Grund im Feld („Der Saal ist zu"). DER UMBAU HAT DREI DINGE VERSCHOBEN, und die Messung hat sie sofort gefunden: Kino 0 px hoch, Spendenkarte bei x=1458 ausserhalb des Bildes, „Kamera aus"-Schild kam nicht an. Eine Ursache: Zwei Regeln (`grid-row: 2` und `grid-area: kino`) zielten auf die drei Haeute, weil die frueher unmittelbar im Saal lagen. Gefunden hat das nicht das Nachdenken, sondern eine Messung der ganzen Kette -- `teil-live 0x0 @1440,742` lag NEBEN dem Raum. ===================================================================== 3. „DER SAAL IST ZU" ERREICHTE NIEMANDEN, DER DARIN SASS ===================================================================== `stand: "zu"` loescht `reaktion_dabei`, und `melden()` suchte seine Empfaenger erst danach -- in genau dieser geleerten Liste. Die Seiten der Zuschauer blieben auf „live" stehen. `melden(art, daten, wer)` nimmt jetzt eine Empfaengerliste; die Stand-Route bestimmt sie VOR jeder Aenderung. UND EINE ZUGESPERRTE SEITE VERSTUMMTE FUER IMMER. War der Saal zu, hielt `anschliessen()` jeden Takt an -- und Rundrufe bekam sie auch keine, weil sie in keiner Liste mehr stand. Machte der Host wieder auf, passierte bei allen mit offenem Reiter NICHTS. Nicht in dreissig Sekunden: nie, bis jemand von Hand neu lud. Gemessen: Server sagt „vorbereitung", die Seite zeigt „zu", auch nach 40 Sekunden. Jetzt fragt eine geschlossene Seite alle 15 Sekunden nach -- der ruhigste Zustand des Hauses, dort kostet das nichts. ===================================================================== 4. WER ZULETZT ANTWORTET, HAT NICHT RECHT ===================================================================== Eine Abfrage ist Sekundenbruchteile unterwegs. Kommt in dieser Zeit ein Rundruf an, ist SEIN Stand der neuere -- und trotzdem hat die verspaetete Antwort ihn ueberschrieben. Gemessen: Die Regie schaltet die Werbung auf Wechsel, der Rundruf zeichnet ihn, und einen Augenblick spaeter steht wieder Laufband. Eine feste Wartezeit in der Messung hatte das zugedeckt; es fiel erst auf, als ich sie durch eine echte Bedingung ersetzte. Drei Runden Raten lagen daneben. Gefunden hat es eine Mitschrift der Aufrufkette: wechsel:2 @ EventSource laufband:2 @ zeichnen < standHolen < signalEmpfangen Eine Folgenummer zaehlt jeden angekommenen Rundruf. Jede Abfrage, die die Seite VON SICH AUS macht -- nach dem Verbindungsaufbau, im geschlossenen Saal, bei jedem Verbindungssignal, nach einer abgelehnten Anwesenheitsmeldung, und der Anwesenheitstakt selbst -- verwirft ihr Ergebnis, wenn inzwischen etwas Neueres da war. Abfragen nach einer EIGENEN Handlung bleiben hart: Sie bringen Dinge mit, die kein Rundruf traegt (die Verwaltungslisten der Regie). Die Unterscheidung steht am Aufruf, nicht in einer Bedingung im Inneren. ===================================================================== 5. UND DIE MESSUNGEN WARTEN NICHT MEHR AUF DIE UHR ===================================================================== Feste Wartezeiten sind dieselbe Falle wie feste Schwellen: Sie stimmen, bis daneben etwas langsamer wird, und melden dann einen Fehler, den es nicht gibt. Zweimal ist mir das in einer Stunde passiert -- einmal beim Band, einmal bei den Ecken. Der Werbeblock wartet jetzt fuenfmal auf das Ergebnis, der Gleichlauf viermal; laeuft eine Frist ab, ist es ein echter Befund. Die erste Fassung der Gleichlauf-Gegenprobe war selbst falsch -- sie hielt das Selbstheilen des Systems fuer einen Fehler. Neu in mess-reaktion: „Die Chatkiste in jedem Zustand" (alle drei, mit Breite und Sperrgrund) und „Beendet heisst leer (im Pult)" -- dort gemessen, wo Filipe hinsieht, nicht in der Datenbank. Neu in pruef-reaktion (+14): das Leeren, die Gegenprobe „vorher war etwas drin", der Verlauf bleibt, und die Entscheidung „wer nur vorbereitet hat, behaelt seine Eingaben". Gemessen: mess-reaktion dreimal hintereinander 0/0, pruef-reaktion 407/0, pruef-css-klassen 37/0, pruef-struktur 44/0, pruef-tippziele 13/0, pruef-haus-seiten 38/0, pruef-haus-trennung 100/0, pruef-buehne 38/0, pruef-push-ziel 38/0, mess-buehne 0. Co-Authored-By: Claude Opus 5 --- server/mess-reaktion.mjs | 326 +++++++++++++++++++- server/pruef-reaktion.mjs | 99 +++++++ server/workspace-reaktion.js | 103 +++++-- workspace/anruf-probe.html | 10 +- workspace/app.webmanifest | 8 +- workspace/assets/css/reaktion.css | 70 ++++- workspace/assets/js/reaktion.js | 203 ++++++++++++- workspace/aufgaben.html | 38 +-- workspace/automation.html | 32 +- workspace/befinden.html | 34 +-- workspace/bereich.html | 44 +-- workspace/bewerben.html | 38 +-- workspace/bewerbungen.html | 36 +-- workspace/buehne.html | 6 +- workspace/calls.html | 34 +-- workspace/chat.html | 40 +-- workspace/content.html | 34 +-- workspace/crew-index.html | 12 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 34 +-- workspace/entwicklung.html | 36 +-- workspace/hilfe.html | 30 +- workspace/index.html | 10 +- workspace/kalender.html | 34 +-- workspace/leistung.html | 32 +- workspace/material.html | 32 +- workspace/notizen.html | 28 +- workspace/personen.html | 32 +- workspace/profil.html | 36 +-- workspace/reaktion.html | 474 +++++++++++++++--------------- workspace/rechte.html | 34 +-- workspace/report.html | 36 +-- workspace/scouting.html | 32 +- workspace/start.html | 32 +- workspace/startcheck.html | 34 +-- workspace/steckbrief.html | 36 +-- workspace/support.html | 28 +- workspace/tafel.html | 6 +- workspace/talente.html | 36 +-- workspace/team.html | 32 +- workspace/teamlage.html | 34 +-- workspace/teilen.html | 30 +- workspace/treff-moderation.html | 32 +- workspace/treff-regeln.html | 32 +- workspace/uebersicht.html | 32 +- workspace/unsere-seiten.html | 30 +- workspace/unterstuetzen.html | 32 +- workspace/werdegang.html | 32 +- workspace/willkommen.html | 30 +- workspace/wissen.html | 34 +-- 50 files changed, 1644 insertions(+), 933 deletions(-) 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`. --> - - + + - +