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 <[email protected]>
This commit is contained in:
+204
-6
@@ -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;
|
||||
|
||||
@@ -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("</section>", 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");
|
||||
|
||||
@@ -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 ---- */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user