From 99e62eebbe53cd7a234bb9678737dd3007df28a0 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Mon, 28 Sep 2026 20:03:38 +0200 Subject: [PATCH] Regieplatz, Bildschirm teilen, Zuruecksetzen und der Preis Vier Sachen aus Filipes Ansagen vom 28.09.2026. ======================================================================= 1. DER REGIEPLATZ "kann das nicht bissl aufgeteilt sein auf links und rechts und eine barre unten in der mitte. kannst du das nicht hoch professionell machen und hochwertig vom aussehen?" Die Teilung folgt dem, was WANN gebraucht wird: LINKS Was laeuft. Das liest man. RECHTS Was eingetragen wird. Das tippt man VOR der Sendung. UNTEN Der Transport. Den fasst man WAEHREND der Sendung an. Vorher stand alles in einem Stapel, und wer im Live an den Play-Knopf wollte, musste an den Eingabefeldern vorbeiscrollen. Der Play-Knopf ist jetzt rund und 58 px -- der einzige, den man blind treffen muss, und die Form unterscheidet ihn schon vor dem Hinsehen. UND EIN FUND, DEN NUR DIE MESSUNG FAND: Nach dem Umbau lag die Leiste bei y=986 in einem 900 Pixel hohen Fenster. Die Rechnung ging auf (links, rechts, Leiste darunter) und das Ergebnis war trotzdem falsch. Jetzt klebt sie (`position: sticky`) -- unten, wie gewollt, und immer sichtbar. Ihr Grund musste dafuer dicht werden: eine halbdurchsichtige Leiste, durch die Text scrollt, ist die Flaeche, auf der man sich im Live verliest. ======================================================================= 2. DEN BILDSCHIRM TEILEN "waere es nicht einfach eine bildschirm uebertragung zu installieren und es zu perfektionieren?" Ja -- der Weg war schon da: Die Kamerabilder laufen als Direktverbindung von Mensch zu Mensch. Geteilt wird deshalb AN STELLE der Kamera; `replaceTrack` tauscht die Bildspur in jeder bestehenden Leitung aus, ohne dass eine einzige neu ausgehandelt werden muss. Eine zweite Spur daneben waere eine zweite Verhandlung je Zuschauer, und jede davon kann scheitern. Vier Dinge, die sonst schiefgegangen waeren: - Wer waehrenddessen dazukommt, bekommt den Bildschirm und nicht das Gesicht. - Das eigene Fenster zeigt, was die anderen sehen -- sonst waere es die eine Anzeige, der man nicht trauen kann. - Der Stopp-Knopf des Browsers wird gehoert; sonst bliebe die Seite auf "teilt" stehen und sendete ein totes Bild. - Der Kameraknopf wird grau, solange geteilt wird. Er haette keine Wirkung mehr -- und ein Knopf, der still nichts tut, ist schlimmer als keiner. Ein Bildschirm wird ausserdem NICHT zugeschnitten: `cover` ist fuer ein Gesicht richtig, bei einem Schreibtisch faellt links und rechts genau das weg, worum es geht. UND DIE WAHRHEIT STEHT AN DER BEDIENUNG: Netflix, Disney+ und Prime bleiben beim Teilen schwarz (Widevine schaltet den Videobereich ab -- auf Discord und Zoom ist es genauso), und einen Film weiterzusenden waere eine oeffentliche Wiedergabe. Wer das erst erfaehrt, nachdem im Stream zehn Minuten ein schwarzes Rechteck stand, erfaehrt es zu spaet. ======================================================================= 3. ALLES ZURUECKSETZEN "brauch ich auch noch einen button wo ich alles easy zuruecksetzen kann." "Alles" heisst: der Schreibtisch, nicht das Gedaechtnis. Geleert werden Titel, Video, zweites Video, Vorschaubild, Beginn, Warteschlange, Gaesteliste; Anordnung, Kameragroesse, Ecke, Tempo und Chatmodus gehen auf Vorgabe. NICHT ANGETASTET werden Spenden, die Dogen der Leute, der Chatverlauf und die Massnahmen der Moderation. Eine bezahlte Spende aus den Buechern zu nehmen waere eine Faelschung; eine stillschweigend aufgehobene Sperre waere eine Entscheidung, die niemand getroffen hat. Beides steht nebeneinander im Dialog, BEVOR etwas passiert -- ein "Bist du sicher?" ohne diese Liste ist keine Frage, sondern eine Huerde. Im Live ist der Knopf grau und sagt warum. Und die Spalten stehen einzeln da statt als "alles ausser": Eine Ausnahmeliste waechst still mit jeder neuen Spalte mit, und dann loescht das Zuruecksetzen irgendwann etwas, das es nie loeschen sollte. ======================================================================= 4. DER PREIS BEI DEN DOGEN "da muss ich auch sehen so viel dogen sind so viel euro. damit ich auch immer weiss was es ist. aber nur ich. die leute sollen nur sehen was dogen kosten." Das ist keine Ruecknahme von "nie Geld", sondern ihre Grenze. Die Regel war richtig fuer alles, was ANZEIGE ist -- Karte, Stream, Chat, Punktestand -- und falsch fuer die eine Stelle, an der jemand KAUFT. GENAU ZWEI AUSNAHMEN, und die Pruefung nennt sie beim Namen statt die Regel aufzuweichen: knoepfe[].cent fuer alle -- der Preis am Kaufknopf kurs_cent_je_doge nur fuer die Leitung DIE ERLAUBNIS STECKT IN DEN DATEN UND NICHT IN EINEM `if`. Ein Zuschauer hat den Kurs nicht und kann deshalb GAR KEINEN Preis ausrechnen -- auch nicht, wenn eine spaetere Zeile es versuchte. Eine Abfrage "darf der das sehen?" in der Oberflaeche waere eine Regel, die man vergessen kann; eine fehlende Zahl ist eine, die man nicht vergessen kann. Eine Zahl statt dreissig Einzelumrechnungen: Wer je Zeile einen Cent mitschickt, hat dreissig Gelegenheiten, eine zu vergessen. ======================================================================= FUENF BEFUNDE GEGEN DIE EIGENEN PRUEFUNGEN 1. Eine Pruefung fragte "liegt die Leiste unter beiden Spalten?" -- das tut eine klebende Leiste beim Hochscrollen absichtlich nicht. Sie stellte die Frage von vorher. Jetzt zaehlt die Reihenfolge im Dokument, die beim Scrollen wie beim Stillstand gilt. 2. Eine Messung klickte auf einen Namen und wartete 400 ms auf die UHR -- und verschluckte den Klickfehler still. Derselbe Lauf war dreimal gruen und beim vierten rot, ohne Codeaenderung. Jetzt wird auf das Merkmal gewartet, bis zu dreimal, und die Zahl der Anlaeufe steht im Protokoll. 3. Eine Pruefung loeschte erst selbst den Chat (eine Sendung zu beenden tut das mit Absicht) und fragte dann, ob er noch da ist. 4. "Die Dogen sind unberuehrt (0 Staende)" -- null bleibt auch dann null, wenn das Zuruecksetzen sie mitnaehme. Jetzt steht vorher eine echte Spende da, und die Zahl gehoert in die BEDINGUNG. 5. `knopf-still--haupt` stand seit Wochen im HTML und war in KEINEM Stilblatt definiert -- eine Klasse, die aussieht, als sei etwas hervorgehoben, und auf dem Bildschirm ist es das nicht. Gefunden beim Nachsehen, ob es sie gibt, bevor ich sie ein zweites Mal benutze. Und eine neue Pruefung, die es vorher nicht gab: ALLE 118 Kennungen, die das Programm mit `$('...')` anspricht, werden gegen das HTML gehalten. Nach einem Umbau, der die halbe Tafel neu sortiert, waere eine verlorene Kennung KEIN Fehler beim Laden -- `$()` gibt still `null` zurueck, und der Fehler erscheint erst, wenn jemand mitten in der Sendung den Knopf drueckt. pruef-reaktion 365/0 (vorher 321) - pruef-spenden 172/0 (vorher 167) - pruef-buehne 36/0 - pruef-css-klassen ok - pruef-tippziele 11/0 - mess-reaktion und mess-buehne ohne Beanstandung Co-Authored-By: Claude Opus 5 --- server/mess-reaktion.mjs | 210 ++++++++++++++++++++- server/pruef-reaktion.mjs | 259 ++++++++++++++++++++++++++ server/pruef-spenden.mjs | 81 +++++++- server/reaktion-tabellen.js | 10 + server/workspace-reaktion.js | 85 ++++++++- server/workspace-spenden.js | 39 +++- workspace/anruf-probe.html | 10 +- workspace/app.webmanifest | 8 +- workspace/assets/css/reaktion.css | 276 ++++++++++++++++++++++++++-- workspace/assets/js/meldung.js | 3 + workspace/assets/js/reaktion.js | 269 ++++++++++++++++++++++++++- workspace/assets/js/spendenkarte.js | 27 ++- 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 | 2 +- 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 | 262 +++++++++++++++++--------- 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 ++-- 55 files changed, 2043 insertions(+), 776 deletions(-) diff --git a/server/mess-reaktion.mjs b/server/mess-reaktion.mjs index a1b18ffa..8f365aaf 100644 --- a/server/mess-reaktion.mjs +++ b/server/mess-reaktion.mjs @@ -396,6 +396,143 @@ await host.seite.evaluate(() => { }); await host.seite.waitForTimeout(900); +/* ---- Der Regieplatz: links, rechts und die Leiste unten ---------- + + Filipe: „kann das nicht bissl aufgeteilt sein auf links und rechts + und eine barre unten in der mitte." + + GEMESSEN WIRD DIE LAGE UND NICHT DIE REGEL. Dass im Stilblatt + `grid-template-areas` steht, sagt nichts darueber, ob die rechte + Spalte auch wirklich rechts landet -- eine spaetere Regel, eine zu + breite Zelle, ein langer Titel, und sie steht darunter. Geprueft + wird deshalb an den Kanten: + linke Spalte endet, BEVOR die rechte anfaengt + die Leiste liegt UNTER beiden und ist so breit wie beide + Und zusaetzlich, dass nichts seitlich hinauslaeuft: Der Umbau hat + eine 16-rem-Spalte gegen einen Videotitel ohne Grenze gestellt. */ +{ + const platz = await host.seite.evaluate(() => { + const k = (w) => { + const e = document.querySelector(w); + if (!e) return null; + const r = e.getBoundingClientRect(); + return { + l: Math.round(r.left), r: Math.round(r.right), + o: Math.round(r.top), u: Math.round(r.bottom), + b: Math.round(r.width), h: Math.round(r.height), + }; + }; + const innen = document.getElementById("pult-innen"); + return { + links: k(".regieplatz__seite:not(.regieplatz__seite--rechts)"), + rechts: k(".regieplatz__seite--rechts"), + leiste: k(".transport"), + spiel: k("#v-spiel"), + /* Laeuft etwas seitlich hinaus? `scrollWidth` groesser als die + sichtbare Breite heisst genau das. */ + ueber: innen ? innen.scrollWidth - innen.clientWidth : -1, + /* ==== UND IST DIE LEISTE UEBERHAUPT ZU SEHEN? ============ + Beim ersten Bau lag sie bei y=986 in einem 900 Pixel hohen + Fenster -- die Rechnung ging auf, und man kam trotzdem nur + mit Scrollen an den Play-Knopf. Geprueft wird deshalb + gegen den SICHTBAREN Ausschnitt der Regie, nicht gegen die + Seite. */ + sichtbar: (() => { + const e = document.querySelector(".transport"); + if (!e || !innen) return false; + const r = e.getBoundingClientRect(); + const i = innen.getBoundingClientRect(); + return r.bottom <= i.bottom + 1 && r.top >= i.top - 1; + })(), + /* ==== UND SIE IST DIE LETZTE ZEILE ======================== + „Unten" laesst sich bei einer KLEBENDEN Leiste nicht mehr an + den Kanten ablesen: Beim Hochscrollen schwebt sie + absichtlich ueber dem unteren Teil der Spalten. Die Frage + „liegt sie unter beiden?" war die von vorher -- sie hat nach + der Reparatur einen Fehler gemeldet, wo das Verhalten + richtig ist. + Was „unten" wirklich heisst, steht in der Reihenfolge: Sie + kommt im Dokument NACH beiden Spalten. Das gilt beim + Scrollen wie beim Stillstand. */ + letzte: (() => { + const e = document.querySelector(".transport"); + const r = document.querySelector(".regieplatz__seite--rechts"); + if (!e || !r) return false; + /* 4 = DOCUMENT_POSITION_FOLLOWING */ + return !!(r.compareDocumentPosition(e) & 4); + })(), + klebt: (() => { + const e = document.querySelector(".transport"); + return e ? getComputedStyle(e).position : "(?)"; + })(), + fenster: window.innerHeight, + }; + }); + const p = platz; + console.log(` Regieplatz: links ${p.links?.l}..${p.links?.r} · ` + + `rechts ${p.rechts?.l}..${p.rechts?.r} · ` + + `Leiste ${p.leiste?.l}..${p.leiste?.r} bei y=${p.leiste?.o} (${p.leiste?.h} px hoch)`); + console.log(` Play-Knopf ${p.spiel?.b}x${p.spiel?.h} px · seitlich ueber ${p.ueber} px ` + + `· Leiste ohne Scrollen sichtbar: ${p.sichtbar} ` + + `(${p.klebt}, nach beiden Spalten: ${p.letzte})`); + if (!p.sichtbar) { + console.log(" ACHTUNG: Die Transportleiste ist nur mit Scrollen zu erreichen."); + ausgang = 1; + } + if (!p.links || !p.rechts || !p.leiste) { + console.log(" ACHTUNG: Der Regieplatz fehlt."); + ausgang = 1; + } else { + if (p.links.r > p.rechts.l + 1) { + console.log(" ACHTUNG: Links und rechts stehen nicht nebeneinander."); + ausgang = 1; + } + if (!p.letzte || p.klebt !== "sticky") { + console.log(` ACHTUNG: Die Leiste ist nicht die letzte Zeile ` + + `(danach=${p.letzte}, position=${p.klebt}).`); + ausgang = 1; + } + /* Sie soll ueber die GANZE Breite gehen -- eine Leiste, die nur + so breit ist wie eine Spalte, ist keine Leiste unten in der + Mitte, sondern eine dritte Spalte. */ + if (p.leiste.l > p.links.l + 1 || p.leiste.r < p.rechts.r - 1) { + console.log(" ACHTUNG: Die Leiste geht nicht ueber beide Spalten."); + ausgang = 1; + } + /* Der Play-Knopf ist der einzige, den man im Live blind treffen + muss. 44 px ist die Hausgrenze; er soll deutlich darueber + liegen. */ + if ((p.spiel?.b || 0) < 52 || (p.spiel?.h || 0) < 52) { + console.log(" ACHTUNG: Der Play-Knopf ist zu klein zum Blindtreffen."); + ausgang = 1; + } + if (p.ueber > 1) { + console.log(` ACHTUNG: Der Regieplatz laeuft ${p.ueber} px seitlich hinaus.`); + ausgang = 1; + } + } + + /* ---- Und der Knopf zum Teilen ---- */ + const teilen = await host.seite.evaluate(() => { + const e = document.getElementById("s-teilen"); + if (!e) return null; + const r = e.getBoundingClientRect(); + return { + da: r.width > 0 && r.height > 0, + b: Math.round(r.width), h: Math.round(r.height), + wort: document.getElementById("s-teilen-wort")?.textContent || "", + gedrueckt: e.getAttribute("aria-pressed"), + }; + }); + console.log(` Bildschirm teilen: „${teilen?.wort}" ${teilen?.b}x${teilen?.h} px, ` + + `gedrueckt=${teilen?.gedrueckt}`); + if (!teilen?.da || teilen.h < 40 || teilen.wort !== "Bildschirm") { + console.log(" ACHTUNG: Der Knopf zum Teilen fehlt oder ist zu klein."); + ausgang = 1; + } + await bild(host.seite, "23-regieplatz"); +} + const regie = await host.seite.evaluate(() => { const t = (id) => document.getElementById(id)?.textContent?.trim() || ""; const sicht = (w) => { @@ -639,17 +776,48 @@ const geben = await zuschauer.seite.evaluate(() => { if (!b) return false; return b.tagName.toLowerCase() === "img" ? b.naturalWidth > 0 : true; }).length, - /* Und nirgends ein Euro. */ - geld: /€|EUR|Euro/.test(k2?.textContent || ""), + /* ==== HIER MUSS DER PREIS STEHEN ========================== + Filipe: „die leute sollen nur sehen was dogen kosten. sonst + wissen die ja nicht wie viel es kosten wird." + Hier stand bis zum 28.09. das Gegenteil -- „nirgends ein + Euro". Das war fuer jede ANZEIGE richtig und fuer den einen + Kaufknopf falsch. */ + preise: knoepfe.map((a) => a.querySelector(".geben__preis")?.textContent?.trim() || ""), + /* ==== UND SONST NIRGENDS AUF DER SEITE ==================== + Die staerkste Form der Frage: den ganzen sichtbaren Text + nehmen, den Geben-Kasten herausschneiden und nachsehen, ob + irgendwo noch ein Eurozeichen uebrig ist. Eine Pruefung, die + nur den Kasten ansieht, uebersieht genau die Stelle, an der + es versehentlich landet. */ + geldSonstwo: (() => { + const alles = document.body.textContent || ""; + const kasten = k2?.textContent || ""; + return /€|EUR/.test(alles.split(kasten).join(" ")); + })(), }; }); console.log(` Geben-Kasten: ${geben.knoepfe} Knoepfe (${geben.zahlen.join(" · ")}), ` - + `${geben.zeichen} mit Zeichen, Geld im Text: ${geben.geld}`); -if (!geben.da || geben.knoepfe !== 4 || geben.zeichen !== 4 || geben.geld + + `${geben.zeichen} mit Zeichen`); +console.log(` Preise am Knopf: ${geben.preise.join(" · ")} ` + + `| Geld sonstwo auf der Seite: ${geben.geldSonstwo}`); +if (!geben.da || geben.knoepfe !== 4 || geben.zeichen !== 4 || geben.zahlen[0] !== "0,2") { console.log(" ACHTUNG: Der Geben-Kasten zeigt nicht, was er soll."); ausgang = 1; } +/* WER BEZAHLT, MUSS VORHER WISSEN, WAS ER ZAHLT. Vier Knoepfe, vier + Preise -- ein Knopf ohne Preis waere der eine, bei dem es niemand + merkt. */ +if (geben.preise.some((p) => !/\d/.test(p) || !p.includes("€"))) { + console.log(` ACHTUNG: An einem Knopf fehlt der Preis (${geben.preise.join(" | ")}).`); + ausgang = 1; +} +/* UND SONST NIRGENDS. Ein Zuschauer sieht den Preis dort, wo er + kauft -- und sonst nur Dogen. */ +if (geben.geldSonstwo) { + console.log(" ACHTUNG: Auf der Zuschauerseite steht Geld ausserhalb der Kaufknoepfe."); + ausgang = 1; +} /* ==== DIE PROBE AENDERT NUR DAS, WAS SIE AENDERN MUSS ============ Der Knopf ist ein Verweis mit `target="_blank"` und fuehrt zu @@ -1837,8 +2005,38 @@ if (modiSicht !== 0 || abz.namensknoepfe < 3) { /* DAS MENUE MUSS INS BILD PASSEN. Ein Menue, das unten oder rechts heraussteht, ist auf dem Handy die Haelfte der Zeit nicht bedienbar -- und man sieht es erst, wenn man es aufmacht. */ -await modi.seite.click("#chat-liste .beitrag:last-child .beitrag__name").catch(() => {}); -await modi.seite.waitForTimeout(400); +/* ==== AUF DAS MENUE WARTEN, NICHT AUF DIE UHR =================== + + Hier stand `click(...).catch(() => {})` und danach 400 ms Pause. + Beide Haelften waren falsch: + + Der `.catch()` verschluckte einen fehlgeschlagenen Klick still. + Die Chatliste wird bei jeder Meldung neu gezeichnet, und trifft + das genau zwischen Auswahl und Klick, ist der Knoten weg. + + Und 400 ms sind eine Wette. Gemessen: Derselbe Lauf war am + 28.09. dreimal gruen und beim vierten Mal rot -- geaendert + hatte sich am Programm nichts, nur an dieser Messung. Ein + Ergebnis, das ohne Codeaenderung kippt, ist ein Fehler der + Messung und kein Befund. + + Jetzt wird auf das MERKMAL gewartet und bis zu dreimal neu + angesetzt -- und die Zahl der Versuche steht im Protokoll. Ein + stiller Wiederholungsversuch waere nur eine leisere Wette. */ +let menueVersuche = 0; +let menueOffen = false; +while (menueVersuche < 3 && !menueOffen) { + menueVersuche += 1; + await modi.seite.click("#chat-liste .beitrag:last-child .beitrag__name") + .catch(() => {}); + menueOffen = await modi.seite.waitForSelector(".modmenue", { timeout: 3000 }) + .then(() => true).catch(() => false); +} +if (menueVersuche > 1) { + console.log(` (Das Menue brauchte ${menueVersuche} Anlaeufe -- die Chatliste ` + + `wurde dazwischen neu gezeichnet.)`); +} +await modi.seite.waitForTimeout(250); const menue = await modi.seite.evaluate(() => { const m = document.querySelector(".modmenue"); if (!m) return null; diff --git a/server/pruef-reaktion.mjs b/server/pruef-reaktion.mjs index 5e018db8..71053b62 100644 --- a/server/pruef-reaktion.mjs +++ b/server/pruef-reaktion.mjs @@ -1895,6 +1895,265 @@ melde("=== 19. Groesse und Ecke der Kamerafenster ==="); } } + +/* ======================================================================= + 20. DER REGIEPLATZ UND DAS TEILEN DES BILDSCHIRMS + + Filipe, 28.09.2026: + „kann das nicht bissl aufgeteilt sein auf links und rechts und + eine barre unten in der mitte." + „waere es nicht einfach eine bildschirm uebertragung zu + installieren und es zu perfektionieren?" + ======================================================================= */ +melde(""); +melde("=== 20. Regieplatz und Bildschirm ==="); +{ + const html = readFileSync(join(HIER, "..", "workspace", "reaktion.html"), "utf8"); + const css = readFileSync(join(HIER, "..", "workspace", "assets", "css", "reaktion.css"), "utf8"); + const js = readFileSync(join(HIER, "..", "workspace", "assets", "js", "reaktion.js"), "utf8"); + const regeln = css.replace(/\/\*[\s\S]*?\*\//g, " "); + + /* ==== KEINE KENNUNG DARF BEIM UMBAU VERLORENGEHEN ============== + + Das ist die wichtigste Pruefung dieses Abschnitts. Der + Regieplatz hat die halbe Sendung-Tafel neu sortiert, und jedes + `$('...')` im Programm zeigt auf ein Element im HTML. Faellt + eines heraus, ist es KEIN Fehler beim Laden -- `$()` gibt `null` + zurueck, und der Fehler erscheint erst, wenn jemand den Knopf + drueckt. Also mitten in einer Sendung. + + Gezaehlt wird gegen die Kennungen im HTML. Was das Programm + selbst anlegt, steht darunter namentlich -- eine Liste von + Ausnahmen, die man SIEHT, ist besser als eine Pruefung, die + schweigt. */ + { + const selbstGebaut = new Set([ + /* Diese legt das Programm an oder sie stehen in anderen + Dateien der Kopfleiste. */ + ]); + const gebraucht = [...js.matchAll(/\$\('([a-z0-9-]+)'\)/g)].map((m) => m[1]); + const einmalig = [...new Set(gebraucht)].filter((x) => !selbstGebaut.has(x)); + const fehlend = einmalig.filter((x) => !html.includes(`id="${x}"`)); + ok(einmalig.length > 40, `das Programm greift auf ${einmalig.length} Kennungen zu`); + ok(fehlend.length === 0, + fehlend.length + ? `ES FEHLEN IM HTML: ${fehlend.join(", ")}` + : "und jede davon gibt es im HTML"); + /* Gegenprobe: Eine erfundene Kennung muss auffallen -- sonst + koennte die Liste auch leer sein und die Pruefung gruen. */ + ok(!html.includes('id="gibtesnicht"'), + "Gegenprobe: eine erfundene Kennung steht in keinem HTML"); + } + + /* ---- LINKS, RECHTS UND EINE LEISTE UNTEN ---- */ + ok(/grid-template-areas:\s*"links rechts"\s*"leiste leiste"/.test(regeln.replace(/\s+/g, " ") + .replace(/" "/g, '" "')) || /links rechts[\s\S]{0,40}leiste leiste/.test(regeln), + "der Regieplatz hat links, rechts und eine Leiste unten"); + const platz = [...regeln.matchAll(/([^{}]*\.regieplatz[^{}]*)\{([^}]*)\}/g)] + .filter((m) => /grid-template-areas/.test(m[2])); + /* ZWEI REGELN FUER DIESELBE FRAGE SIND DIE GARANTIE, DASS EINE + FALSCH GEWINNT -- heute schon viermal passiert. Die Handyansicht + darf die Bereiche neu setzen; deshalb zwei, nicht eine. */ + ok(platz.length === 2, + `${platz.length} Regeln ordnen den Regieplatz (Rechner und Handy)`); + + /* Die Leiste traegt den ganzen Transport -- und zwar in EINER. */ + const iLeiste = html.indexOf('class="transport"'); + const iEnde = html.indexOf("", iLeiste); + const inLeiste = html.slice(iLeiste, iEnde); + for (const k of ["v-band", "v-zeit", "v-zurueck", "v-spiel", "v-vor", "v-naechstes", "tempo"]) { + ok(inLeiste.includes(`id="${k}"`), `„${k}" liegt in der Transportleiste`); + } + ok(!inLeiste.includes('id="p-titel"'), + "und die Eingabefelder liegen NICHT darin"); + + /* ---- DER BILDSCHIRM ---- */ + ok(/getDisplayMedia/.test(js), "die Seite kann den Bildschirm holen"); + ok(/replaceTrack/.test(js), + "und tauscht die Bildspur aus, statt eine zweite Leitung auszuhandeln"); + ok(/audio:\s*false/.test(js), + "der Rechnerton kommt NICHT mit -- sonst waere alles doppelt"); + ok(/addEventListener\('ended'/.test(js), + "der eigene Stopp-Knopf des Browsers wird gehoert"); + ok(/bildschirmSpur \|\| meinStrom\.getVideoTracks\(\)\[0\]/.test(js), + "auch wer waehrenddessen dazukommt, bekommt den Bildschirm"); + ok(/\.kamera\[data-bildschirm="ja"\] video/.test(regeln) + && /object-fit:\s*contain/.test(regeln), + "ein geteilter Bildschirm wird nicht zugeschnitten"); + ok(html.includes('id="s-teilen"'), "es gibt den Knopf in der Senderleiste"); + + /* ==== UND DIE WAHRHEIT STEHT AN DER BEDIENUNG ================== + Netflix und Disney+ bleiben beim Teilen schwarz -- das ist ihr + Kopierschutz und keine Einstellung. Wer das erst erfaehrt, + nachdem im Stream zehn Minuten ein schwarzes Rechteck stand, + erfaehrt es zu spaet. Deshalb ist der Hinweis Teil der Seite + und nicht Teil einer Hilfe, die niemand oeffnet. */ + ok(/Netflix[\s\S]{0,80}schwarz/.test(html), + "und die Seite sagt, dass Netflix schwarz bleibt"); + ok(/öffentliche Wiedergabe/.test(html), + "und warum ein Film nicht weitergesendet wird"); + + /* ---- AM SERVER ---- */ + { + const an = await roh(CREW, "/workspace/api/reaktion/senden", { + method: "POST", keks: k.admin, rumpf: { bildschirm: true }, + }); + ok(an.code === 200 && an.j?.ich?.bildschirm === true, + `der Host meldet das Teilen (${an.code})`); + const beiAnderen = await roh(CREW, "/workspace/api/reaktion", { keks: k.gast }); + ok(beiAnderen.j?.host?.bildschirm === true, + "und alle sehen es an seinem Fenster"); + const aus = await roh(CREW, "/workspace/api/reaktion/senden", { + method: "POST", keks: k.admin, rumpf: { bildschirm: false }, + }); + ok(aus.code === 200 && aus.j?.ich?.bildschirm === false, + `und das Beenden auch (${aus.code})`); + const fremd = await roh(CREW, "/workspace/api/reaktion/senden", { + method: "POST", keks: k.gast, rumpf: { bildschirm: true }, + }); + ok(fremd.code === 403, + `wer nicht sendet, teilt auch nicht (${fremd.code})`); + } +} + + +/* ======================================================================= + 21. ALLES ZURUECKSETZEN + + Filipe, 28.09.2026: „brauch ich auch noch einen button wo ich alles + easy zuruecksetzen kann." + + Die wichtigste Frage dieses Abschnitts ist nicht, ob es leert -- + sondern ob es das Richtige STEHEN LAESST. Ein Zuruecksetzen, das + eine bezahlte Spende mitnimmt, ist keine Bequemlichkeit, sondern + ein Datenverlust mit Knopf. + ======================================================================= */ +melde(""); +melde("=== 21. Alles zuruecksetzen ==="); +{ + /* Erst etwas hinstellen, das verschwinden soll -- und etwas, das + bleiben muss. Ohne beides prueft man ein leeres Haus. + + ==== „VORBEREITUNG" UND NICHT „ZU" -- UND DAS IST DER PUNKT + + Beim ersten Lauf stand hier „zu", und die Pruefung meldete + danach „der Chatverlauf steht nicht mehr". Der Befund war + richtig und die Pruefung trotzdem falsch: Eine Sendung zu + BEENDEN loescht den Live-Chat seit jeher mit Absicht (sonst + finge die naechste mit den Rufen der letzten an). Sie hat also + erst selbst geloescht und dann gefragt, ob noch etwas da ist -- + und haette dieselbe Antwort auch gegeben, wenn das + Zuruecksetzen den Chat mitnaehme. + + In „vorbereitung" bleibt der Chat stehen, und erst dann sagt + die Antwort etwas ueber das Zuruecksetzen. */ + await roh(CREW, "/workspace/api/reaktion/stand", { + method: "POST", keks: k.admin, rumpf: { stand: "vorbereitung" }, + }); + await roh(CREW, "/workspace/api/reaktion/chat", { + method: "POST", keks: k.admin, rumpf: { text: "Der bleibt stehen" }, + }); + await roh(CREW, "/workspace/api/reaktion", { + method: "PUT", keks: k.admin, + rumpf: { titel: "Wird geleert", video: "dQw4w9WgXcQ", vorschau: "https://x/y.jpg" }, + }); + await roh(CREW, "/workspace/api/reaktion/liste", { + method: "POST", keks: k.admin, rumpf: { video: "dQw4w9WgXcQ" }, + }); + await roh(CREW, "/workspace/api/reaktion/layout", { + method: "POST", keks: k.admin, + rumpf: { layout: "gleich", kamera_gross: 2, kamera_ecke: "ol" }, + }); + /* ==== ES MUSS AUCH ETWAS DA SEIN, DAS BLEIBEN KANN =========== + Beim ersten Lauf stand hier „die Dogen sind unberuehrt (0 + Staende)". Null bleibt aber auch dann null, wenn das + Zuruecksetzen sie mitnaehme -- die Zeile war gruen und wertlos. + Die Zahl der geprueften Dinge gehoert in die BEDINGUNG und + nicht nur in den Meldetext. */ + await roh(CREW, "/workspace/api/spenden", { + method: "POST", keks: k.admin, + rumpf: { dogen_mille: 500, name: "Nachtfalter", person_id: IDS.gast }, + }); + const vorher = await roh(CREW, "/workspace/api/reaktion", { keks: k.admin }); + const spendenVorher = await roh(CREW, "/workspace/api/spenden", { keks: k.admin }); + const dogenVorher = spendenVorher.j?.staende?.length || 0; + const letzteVorher = spendenVorher.j?.letzte?.length || 0; + ok(!!vorher.j?.titel && (vorher.j?.liste || []).length > 0, + `vorher steht etwas da (Titel „${vorher.j?.titel}", ` + + `${vorher.j?.liste?.length} in der Warteschlange)`); + + const weg = await roh(CREW, "/workspace/api/reaktion/zuruecksetzen", { + method: "POST", keks: k.admin, + }); + ok(weg.code === 200, `zuruecksetzen geht (${weg.code})`); + ok(weg.j?.titel === "" && weg.j?.video === "", + "Titel und Video sind leer"); + ok((weg.j?.liste || []).length === 0, "die Warteschlange ist leer"); + ok((weg.j?.gaeste || []).length === 0, "die Gaesteliste ist leer"); + ok(weg.j?.layout === "kino" && weg.j?.kamera_gross === 1 + && weg.j?.kamera_ecke === "ur" && weg.j?.tempo === 1, + `das Bild steht wieder auf Vorgabe (${weg.j?.layout}, ` + + `${weg.j?.kamera_gross}x, ${weg.j?.kamera_ecke})`); + ok(weg.j?.stand === "zu", "und der Saal ist zu"); + + /* ==== UND DAS WICHTIGERE: WAS BLEIBEN MUSS, BLEIBT ============ */ + const spendenNachher = await roh(CREW, "/workspace/api/spenden", { keks: k.admin }); + ok(dogenVorher > 0 && letzteVorher > 0, + `es gab vorher etwas zu verlieren (${dogenVorher} Staende, ` + + `${letzteVorher} Spenden)`); + ok(dogenVorher > 0 && (spendenNachher.j?.staende?.length || 0) === dogenVorher, + `die Dogen der Leute sind unberuehrt (${dogenVorher} Staende)`); + ok(letzteVorher > 0 && (spendenNachher.j?.letzte?.length || 0) === letzteVorher, + `und die Spenden auch (${letzteVorher} gezeigte)`); + const chat = await roh(CREW, "/workspace/api/reaktion/chat", { keks: k.admin }); + ok((chat.j?.beitraege || []).some((b) => b.text === "Der bleibt stehen"), + `der Chatverlauf steht noch (${chat.j?.beitraege?.length} Beitraege)`); + + /* ---- NICHT WAEHREND EINER SENDUNG ---- + Mitten im Live zurueckzusetzen zieht allen das Video unter den + Fuessen weg -- und zwar ohne Rueckweg. */ + await roh(CREW, "/workspace/api/reaktion", { + method: "PUT", keks: k.admin, rumpf: { titel: "Laeuft", video: "dQw4w9WgXcQ" }, + }); + await roh(CREW, "/workspace/api/reaktion/stand", { + method: "POST", keks: k.admin, rumpf: { stand: "live" }, + }); + const imLive = await roh(CREW, "/workspace/api/reaktion/zuruecksetzen", { + method: "POST", keks: k.admin, + }); + ok(imLive.code === 409 && imLive.j?.fehler === "laeuft_noch", + `waehrend der Sendung wird abgelehnt (${imLive.code})`); + const nochDa = await roh(CREW, "/workspace/api/reaktion", { keks: k.admin }); + ok(nochDa.j?.titel === "Laeuft", + "und es ist dabei auch wirklich nichts passiert"); + + /* ---- UND NUR DER HOST ---- */ + const fremd = await roh(CREW, "/workspace/api/reaktion/zuruecksetzen", { + method: "POST", keks: k.modi, + }); + ok(fremd.code === 403, `ein Modi setzt nichts zurueck (${fremd.code})`); + + /* ---- DER KNOPF SAGT VORHER, WAS ER TUT ---- + Ein „Bist du sicher?" ohne die Liste ist keine Frage, sondern + eine Huerde: Man klickt sie weg und erfaehrt erst danach, was + fehlt. */ + { + const html = readFileSync(join(HIER, "..", "workspace", "reaktion.html"), "utf8"); + const js = readFileSync(join(HIER, "..", "workspace", "assets", "js", "reaktion.js"), "utf8"); + ok(html.includes('id="p-zuruecksetzen"'), "es gibt den Knopf"); + ok(/frageNach\(\{[\s\S]{0,600}?bleibt:/.test(js), + "und er sagt vorher, was BLEIBT -- nicht nur, was verschwindet"); + ok(/Spenden[\s\S]{0,80}Dogen[\s\S]{0,120}unberührt/.test(js), + "und nennt Spenden und Dogen dabei ausdruecklich"); + ok(/p-zuruecksetzen'\)\.disabled = laeuftGerade/.test(js), + "im Live ist er grau"); + } + + await roh(CREW, "/workspace/api/reaktion/stand", { + method: "POST", keks: k.admin, rumpf: { stand: "live" }, + }); +} + melde(""); melde(`${geprueft} Pruefungen, ${fehler} Fehler`); melde(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG"); diff --git a/server/pruef-spenden.mjs b/server/pruef-spenden.mjs index f4e60981..4d8d4e4a 100644 --- a/server/pruef-spenden.mjs +++ b/server/pruef-spenden.mjs @@ -670,22 +670,74 @@ melde("=== 9. Dogen, eigener Text und Punkte ==="); } return raus; }; - for (const [wer, keks] of [["die Leitung", k.admin], ["ein Zuschauer", k.gast]]) { + /* ==== ZWEI AUSNAHMEN, UND NUR DIESE ZWEI ===================== + + Am 28.09. stand hier „kein Geldfeld, fuer niemanden". Das war + fuer alles richtig, was ANZEIGE ist -- und falsch fuer die eine + Stelle, an der jemand KAUFT. Filipe: „die leute sollen nur + sehen was dogen kosten. sonst wissen die ja nicht wie viel es + kosten wird." + + Erlaubt sind jetzt GENAU zwei Dinge, und die Pruefung nennt sie + beim Namen statt die Regel aufzuweichen: + `knoepfe[].cent` fuer alle -- der Preis am Kaufknopf + `kurs_cent_je_doge` nur fuer die Leitung + Alles andere bleibt verboten. Eine Ausnahmeliste, die man + aufzaehlen kann, ist eine Regel; „irgendwo darf Geld stehen" + waere keine. */ + const ERLAUBT = new Set(["cent", "kurs_cent_je_doge"]); + for (const [wer, keks, kursErlaubt] of [ + ["die Leitung", k.admin, true], + ["ein Zuschauer", k.gast, false], + ]) { const a = await roh(CREW, "/workspace/api/spenden", { keks }); const felder = namen(a.j); - const geld = [...felder].filter((n) => /cent|waehrung|euro|eur$/i.test(n)); + const geld = [...felder].filter((n) => /cent|waehrung|euro|eur$/i.test(n)) + .filter((n) => !ERLAUBT.has(n)); ok(geld.length === 0, - geld.length ? `GELD IM STAND (${wer}): ${geld.join(", ")}` - : `kein Geldfeld im Stand (${wer}, ${felder.size} Felder)`); + geld.length ? `UNERLAUBTES GELDFELD (${wer}): ${geld.join(", ")}` + : `kein unerlaubtes Geldfeld im Stand (${wer}, ${felder.size} Felder)`); ok(felder.has("dogen_max_mille") && felder.has("gruss_max"), "und die Grenzen stehen als Dogen und Zeichen darin"); - /* Ein Eurozeichen darf nirgends stehen -- auch nicht in einem - Text, den irgendwer einmal eingetippt hat. */ - ok(!JSON.stringify(a.j).includes("€"), `kein Eurozeichen (${wer})`); + + /* ---- Der Preis steht am Kaufknopf -- fuer alle ---- */ + const knopf = a.j?.knoepfe?.[0]; + ok(typeof knopf?.cent === "number" && knopf.cent > 0, + `der Preis steht am Knopf (${wer}: ${knopf?.cent} Cent)`); + + /* ---- Und der Kurs nur bei der Leitung ---- + Er ist der Generalschluessel: Wer ihn hat, kann JEDEN + Dogenwert in Geld umrechnen. Genau deshalb steckt die + Erlaubnis in den Daten und nicht in einem `if` der + Oberflaeche -- eine fehlende Zahl kann man nicht vergessen. */ + const kurs = a.j?.kurs_cent_je_doge; + ok(kursErlaubt ? typeof kurs === "number" && kurs > 0 : kurs === null, + `der Kurs ${kursErlaubt ? "kommt mit" : "bleibt weg"} (${wer}: ${kurs})`); + + /* Ausser an den Knoepfen darf nirgends ein Eurozeichen stehen -- + auch nicht in einem Text, den jemand eingetippt hat. */ + const ohneKnoepfe = { ...a.j, knoepfe: undefined }; + ok(!JSON.stringify(ohneKnoepfe).includes("€"), `kein Eurozeichen (${wer})`); } ok([...namen({ a: [{ betrag_cent: 1 }] })].includes("betrag_cent"), "Gegenprobe: ein Geldfeld wuerde gefunden"); + /* ==== DIE KARTE TRAEGT WEITERHIN KEINEN CENT ================== + Sie geht auch an die Buehnentafel, und die steht im Stream. Ein + Betrag, der dort ankommt, waere Geld im Bild -- genau das, was + nirgends stehen soll. Der Preis gehoert an den Kaufknopf und + nirgendwo sonst. */ + { + const probe = await roh(CREW, "/workspace/api/spenden/probe", { + method: "POST", keks: k.admin, rumpf: { dogen_mille: 500 }, + }); + const felder = namen(probe.j?.karte); + const geld = [...felder].filter((n) => /cent|waehrung|euro|eur$/i.test(n)); + ok(geld.length === 0, + geld.length ? `GELD AUF DER KARTE: ${geld.join(", ")}` + : `die Karte traegt keinen Betrag (${felder.size} Felder)`); + } + /* ---- DIE KNOEPFE FUEHREN ZU EINEM EURO-BETRAG, ZEIGEN ABER DOGEN ---- Der Euro entsteht erst in der PayPal-Adresse, weil PayPal ihn braucht. Das ist der einzige Ort, an dem er ueberhaupt noch @@ -696,8 +748,19 @@ melde("=== 9. Dogen, eigener Text und Punkte ==="); ok(knopf?.mille === KNOEPFE_MILLE[0], `der erste Knopf steht auf ${knopf?.mille} Tausendsteln`); ok(knopf?.ziel?.endsWith("EUR"), - `und fuehrt trotzdem zu einer Zahlung (${knopf?.ziel})`); - ok(knopf?.cent === undefined, "der Euro-Betrag selbst kommt nicht mit"); + `und fuehrt zu einer Zahlung (${knopf?.ziel})`); + /* ==== UND DER PREIS STEHT DABEI ============================ + Hier stand bis zum 28.09. das Gegenteil: „der Euro-Betrag + selbst kommt nicht mit". Das war die Regel „nie Geld", bis + eine Stelle ihr widersprach -- die einzige, an der jemand + BEZAHLT. Wer auf „0,2 Dogen" drueckt, muss vorher wissen, + dass ihn das zwei Euro kostet; den Preis erst auf der + Zahlseite zu erfahren ist keiner. + Die Zahl muss zur Adresse PASSEN -- sonst steht am Knopf ein + Preis und bei PayPal ein anderer, und das merkt niemand + vorher. */ + ok(knopf?.cent === milleInCent(knopf?.mille), + `und der Preis dazu (${knopf?.cent} Cent) passt zur Zahlung`); } /* ---- DER TEXT KOMMT VON DEM, DER GIBT ---- */ diff --git a/server/reaktion-tabellen.js b/server/reaktion-tabellen.js index 8af22ec8..53a53f54 100644 --- a/server/reaktion-tabellen.js +++ b/server/reaktion-tabellen.js @@ -360,6 +360,16 @@ export function reaktionTabellen(d) { } const dabei = new Set( d.prepare("PRAGMA table_info(reaktion_dabei)").all().map((z) => z.name)); + if (!dabei.has("bildschirm")) { + /* WER GERADE SEINEN BILDSCHIRM TEILT. + Es ist eine eigene Spalte und nicht „kamera_aus umgedreht": + Beim Teilen geht ein Bild hinaus, es ist nur ein anderes. Wer + beides in eine Spalte legte, koennte nachher nicht mehr + sagen, ob da ein schwarzes Fenster steht oder ein + Schreibtisch. */ + d.exec("ALTER TABLE reaktion_dabei ADD COLUMN bildschirm INTEGER NOT NULL DEFAULT 0"); + console.log("[reaktion] Spalte bildschirm angelegt."); + } if (!dabei.has("kamera_aus")) { /* Wer sein Bild gerade abgeschaltet hat. AN DIESER TABELLE UND NICHT AN DEN GAESTEN: Hier steht, wer diff --git a/server/workspace-reaktion.js b/server/workspace-reaktion.js index 728c63d0..e7fd5db4 100644 --- a/server/workspace-reaktion.js +++ b/server/workspace-reaktion.js @@ -220,7 +220,7 @@ function dabeiListe() { const grenze = new Date(Date.now() - 120_000).toISOString(); db().prepare("DELETE FROM reaktion_dabei WHERE gesehen < ?").run(grenze); return db().prepare(` - SELECT d.person_id AS id, d.sicht, d.seit, d.kamera_aus, d.mikro_aus, + SELECT d.person_id AS id, d.sicht, d.seit, d.kamera_aus, d.mikro_aus, d.bildschirm, p.name, p.rolle FROM reaktion_dabei d JOIN personen p ON p.id = d.person_id ORDER BY d.seit`).all(); @@ -277,13 +277,24 @@ function lage(person) { const p = db().prepare("SELECT id, name, rolle FROM personen WHERE id = ?").get(s.host_id); if (!p) return null; const z = dabei.find((d) => d.id === p.id); - return { ...p, kamera_aus: !!z?.kamera_aus, mikro_aus: !!z?.mikro_aus }; + return { + ...p, + kamera_aus: !!z?.kamera_aus, + mikro_aus: !!z?.mikro_aus, + bildschirm: !!z?.bildschirm, + }; })() : null, gaeste: gaeste.map((g) => ({ id: g.id, name: g.name, rolle: g.rolle, stumm: !!g.stumm, platz: g.platz, kamera_aus: !!(dabei.find((d) => d.id === g.id)?.kamera_aus), mikro_aus: !!(dabei.find((d) => d.id === g.id)?.mikro_aus), + /* WER TEILT, MUSS ERKENNBAR SEIN. Ohne diese Auskunft waere + ein geteilter Bildschirm ein Kamerafenster, in dem ploetzlich + ein Schreibtisch steht -- und das Fenster wuerde ihn + beschneiden, weil eine Kamera hochkant zugeschnitten wird + und ein Bildschirm nicht. */ + bildschirm: !!(dabei.find((d) => d.id === g.id)?.bildschirm), })), chat_modus: s.chat_modus || "offen", chat_modi: CHAT_MODI, @@ -336,6 +347,7 @@ function lage(person) { moderiert: MODERIERT.has(person.rolle), sicht: !!(dabei.find((d) => d.id === person.id)?.sicht), kamera_aus: !!(dabei.find((d) => d.id === person.id)?.kamera_aus), + bildschirm: !!(dabei.find((d) => d.id === person.id)?.bildschirm), mikro_aus: !!(dabei.find((d) => d.id === person.id)?.mikro_aus), darf_schreiben: darfSchreiben(person, s), /* Der Grund steht daneben -- die Seite muss ihn nicht raten. */ @@ -462,6 +474,72 @@ reaktionRouter.get("/workspace/api/reaktion", (req, res) => { } }); +/* ====================================================================== + ALLES ZURUECKSETZEN + + Filipe: „brauch ich auch noch einen button wo ich alles easy + zuruecksetzen kann." + + --------------------------------------------------------------------- + DER SCHREIBTISCH, NICHT DAS GEDAECHTNIS + + Geleert wird, was zur naechsten Sendung gehoert: Titel, Video, + zweites Video, Vorschaubild, Beginn, Warteschlange, Gaesteliste -- + und das Bild geht auf seine Vorgaben zurueck. + + NICHT ANGETASTET wird, was Menschen getan haben: Spenden, die + Dogen der Leute, der Chatverlauf, die Massnahmen der Moderation. + Eine bezahlte Spende aus den Buechern zu nehmen waere eine + Faelschung; eine stillschweigend aufgehobene Sperre waere eine + Entscheidung, die niemand getroffen hat. „Alles" heisst hier + „alles, was ich selbst eingetragen habe". + + NICHT WAEHREND EINER SENDUNG. Mitten im Live zurueckzusetzen + zieht allen das Video unter den Fuessen weg -- und zwar ohne dass + es rueckgaengig zu machen waere. + ====================================================================== */ +reaktionRouter.post("/workspace/api/reaktion/zuruecksetzen", nurHost, (req, res) => { + try { + const s = sendung(); + if (s.stand === "live") return res.status(409).json({ fehler: "laeuft_noch" }); + + 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()); + db().prepare("DELETE FROM reaktion_liste").run(); + db().prepare("DELETE FROM reaktion_gaeste").run(); + db().exec("COMMIT"); + } catch (f) { + db().exec("ROLLBACK"); + throw f; + } + + protokolliere("reaktion_zurueckgesetzt", { + personId: req.person.id, + detail: "Pult geleert (Spenden, Dogen, Chat und Massnahmen unberuehrt)", + ip: echteIp(req), + }); + const l = lage(req.person); + melden("reaktion", { was: "stand", lage: oeffentlich(l) }); + res.json(l); + } catch (fehler) { + console.error("[reaktion] Zuruecksetzen:", fehler?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } +}); + /** Der Host stellt ein: Titel, Video, Vorschaubild, Beginn. */ reaktionRouter.put("/workspace/api/reaktion", nurHost, express.json({ limit: "32kb" }), async (req, res) => { @@ -1226,6 +1304,9 @@ reaktionRouter.post("/workspace/api/reaktion/senden", express.json({ limit: "2kb if (req.body?.mikro_aus !== undefined) { felder.push("mikro_aus = ?"); werte.push(req.body.mikro_aus ? 1 : 0); } + if (req.body?.bildschirm !== undefined) { + felder.push("bildschirm = ?"); werte.push(req.body.bildschirm ? 1 : 0); + } if (!felder.length) return res.status(400).json({ fehler: "nichts_zu_tun" }); const geschrieben = db().prepare( diff --git a/server/workspace-spenden.js b/server/workspace-spenden.js index d19e105f..efa98d1b 100644 --- a/server/workspace-spenden.js +++ b/server/workspace-spenden.js @@ -45,7 +45,7 @@ import { BETRAG_MAX_CENT, NAME_MAX, GRUSS_MAX, KNOEPFE_MILLE, SPENDEN_TAGE, VORLAGEN, STUFEN_MAX, DAUER_MS_MIN, DAUER_MS_MAX, GROESSE_MIN, GROESSE_MAX, BILD_MAX_BYTES, - centInMille, milleInCent, ZEICHEN_NAME, + centInMille, milleInCent, ZEICHEN_NAME, CENT_JE_DOGE, DOGEN_SAETZE, DOGEN_STUFEN, SATZ_NAME, muenzeAdresse, } from "./spenden-tabellen.js"; @@ -443,12 +443,22 @@ spendenRouter.get("/workspace/api/spenden", (req, res) => { try { const pp = paypalAdresse(); const darf = GELD_ROLLEN.has(req.person.rolle); - /* DER EURO ENTSTEHT ERST IN DER ADRESSE. `mitBetrag` braucht - ihn, weil PayPal ihn braucht -- an den Browser geht nur die - fertige Adresse und die Zahl in Dogen. */ + /* ==== HIER DARF DER PREIS STEHEN -- UND NUR HIER =========== + + Filipe: „die leute sollen nur sehen was dogen kosten. sonst + wissen die ja nicht wie viel es kosten wird." + + Das ist die eine Stelle, an der jemand KAUFT. Wer auf „0,2 + Dogen" drueckt, muss vorher wissen, dass ihn das zwei Euro + kostet. Ueberall sonst ist Geld eine Anzeige, hier ist es ein + Preis -- und ein Preis, den man erst auf der Zahlseite + erfaehrt, ist keiner. */ const knoepfe = pp - ? KNOEPFE_MILLE.map((m) => ({ mille: m, ziel: mitBetrag(pp.ziel, milleInCent(m)) })) - .filter((k) => k.ziel) + ? KNOEPFE_MILLE.map((m) => ({ + mille: m, + cent: milleInCent(m), + ziel: mitBetrag(pp.ziel, milleInCent(m)), + })).filter((k) => k.ziel) : []; res.json({ paypal: pp ? { ziel: pp.ziel, knopf: pp.knopf } : null, @@ -475,6 +485,23 @@ spendenRouter.get("/workspace/api/spenden", (req, res) => { groesse_min: GROESSE_MIN, groesse_max: GROESSE_MAX, bild_max_mb: Math.round(BILD_MAX_BYTES / 1024 / 1024), }, + /* ==== DER KURS GEHT NUR AN DIE LEITUNG ================== + + Filipe: „damit ich auch immer weiss was es ist. aber nur + ich." + + Mit dieser einen Zahl kann die Oberflaeche jeden Dogenwert + in Geld umrechnen -- an der Stufenleiter, beim Eintragen, + in den Listen. Ein Zuschauer bekommt sie nicht: Fuer ihn + ist der Preis an den Knoepfen die ganze Auskunft, die er + braucht, und alles darueber hinaus waere Geld auf dem + Bildschirm ohne Anlass. + + EINE ZAHL UND NICHT DREISSIG EINZELUMRECHNUNGEN. Wer je + Zeile einen Cent mitschickt, hat dreissig Gelegenheiten, + eine zu vergessen -- und dann steht an einer Stelle ein + Preis von gestern. */ + kurs_cent_je_doge: darf ? CENT_JE_DOGE : null, /* Die Grenzen fuer die Felder -- in Dogen und in Zeichen. Sie kommen vom Server, damit nicht zwei Zahlen dieselbe Frage beantworten und eine davon veraltet. */ diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index 7b851ceb..be38ce9f 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`. --> - - + + - +