Reaction: Material statt Kaesten, Spende in der Mitte, Gaeste werden gefragt

Nach Filipes sieben Bildschirmfotos vom 07.10.2026. Sieben Wuensche,
und einer davon ("alles viel hochwertiger") betrifft alle anderen --
deshalb steht das Fundament zuerst und nicht am Ende.

DAS AUSSEHEN HAT EINEN WERKSTOFF. Jede Flaeche sagt jetzt, ob sie oben
oder unten liegt: Was obenauf liegt, bekommt oben eine helle Kante und
darunter einen Schatten; was vertieft ist, genau umgekehrt. Dazwischen
gibt es nichts mehr. Vorher stand im ganzen Pult zweimal eine
Lichtkante und sonst nirgends -- genau deshalb sahen diese zwei Stellen
gut aus und alles daneben danebengestellt. Dazu: jeder Knopf senkt sich
beim Druecken, jedes Feld ist ein Loch und kein Kasten, und wer mit der
Tastatur bedient, wird zum ersten Mal ueberhaupt gesehen.

UND EIN FUND DABEI: Die drei Kacheln des Regieplatzes hiessen in der
HTML anders als im Stilblatt (kachel--laeuft gegen regiekachel--laeuft,
regieregiekachel__kopf gegen regiekachel__kopf). Die Aufteilung "links
was laeuft, rechts was man eintraegt" hat damit NIE gegriffen -- und
mess-reaktion hat den falschen Zustand gemessen und gruen gemeldet,
weil sie gegen die HTML geschrieben war.

DIE SPENDENKARTE STEHT IN DER MITTE und kommt aus der Tiefe nach vorn.
Das dreht eine Entscheidung vom 28.09. um ("sie deckt nie die Mitte
zu"); die Begruendung war richtig und ist es noch -- sie schuetzt das
Video, und Filipe sieht die Spende als das, was in dem Augenblick
zaehlt. Der alte Platz bleibt einen Knopf weit weg und gilt fuer Saal
UND OBS-Tafel. Die Karte besteht dafuer aus zwei Teilen: `opacity`
unter 1 zwingt ein Element auf `transform-style: flat`, eine Karte mit
Deckkraft-Uebergang waere also genau waehrend des Hereinkommens flach
und spraenge am Ende in einem Bild in die Tiefe.

FERTIGE SAETZE GEHEN SOFORT INS BILD. Der Browser schickt nur die
Kennung, den Text holt der Server -- deshalb darf hier die Bestaetigung
entfallen und beim freien Text nicht. Zwei Schranken bleiben: sofort
nur mit einem der vier Knopfbetraege (Filipes Ansage galt dem SATZ,
nicht der Summe), und GEZEIGT IST NICHT GEBUCHT -- die Dogen gibt es
erst nach dem Tipp der Leitung, sonst stellt sich jeder seine eigene
Waehrung aus.

WER GESPENDET HAT, GLAENZT IM CHAT -- je nach Hoehe. Die Dauer haengt
an der Stufe und nicht an einer zweiten Leiter; 0 heisst "gar nicht".

GAESTE WERDEN GEFRAGT, NICHT GEHOLT. Vorher sprang die Kamera eines
Zuschauers an, sobald jemand "Dazuholen" drueckte -- er hat es daran
gemerkt, dass er sich selbst sah. Jetzt gibt es einen Vorraum: eigenes
Bild, Pegel, Kamera und Mikro vorwaehlen, Geraete waehlen, dann "Dabei
sein" oder "Lieber nicht". Zwei Minuten Frist, und sie verfaellt
SICHTBAR -- eine verschwundene Zeile sieht fuer den Host aus wie ein
Verklicker.

DAS MISCHPULT. Hier stand fest `echoCancellation: true,
noiseSuppression: true`. Fuer ein Laptopmikrofon richtig, ueber einem
RODECaster falsch: Das Pult hat Gate, Kompressor und EQ schon gemacht,
und die Echo-Unterdrueckung legt sich danach als zweite Automatik
darueber und duckt, sobald von der Gegenseite Ton kommt -- genau das
Pumpen vom 14.09. Jetzt: Geraetewahl fuer Mikro, Kamera und Ausgabe
(auch fuer Gaeste), ein Pultmodus, und darunter GEMESSEN, was wirklich
ankommt. Mit und ohne Pult.

DAS ZWEITE VIDEO war nie gesperrt -- es war nur unsichtbar. Jetzt
stehen Vorschaubild, Titel und die gemerkte Stelle da, "Jetzt wechseln"
auch von hier aus, und aus der Warteschlange legt ein Knopf eines
bereit.

UND EIN FUND AUF DEM BILDSCHIRMFOTO: Das Band "Ton einschalten" lag auf
derselben Hoehe wie die Senderleiste. Bei vier Knoepfen reichte die
Leiste bis zur Mitte -- vom Mikro-Knopf war nur noch ein "t" zu sehen.
Das war schon vorher so; der fuenfte Knopf hat es sichtbar gemacht.
Dieselbe Lehre wie am 06.09.: keine neue Zahl suchen, sondern die
Begegnung unmoeglich machen. mess-reaktion misst das jetzt als
UEBERLAPPUNG.

GEPRUEFT: pruef-reaktion 423 -> 478 (0 Fehler), pruef-spenden 172 -> 215
(0), pruef-reaktion-schmal 44 (0), pruef-tippziele 13 (0), dazu gruen:
pruef-css-klassen, pruef-alle-wege, pruef-haus-trennung, pruef-agentur,
pruef-deutsche-texte. Keine Zahl ist gefallen.

Drei Doppelungen hat erst das Pruefen gefunden: `satz_unbekannt` gab es
zweimal (Muenzsatz), `saetze` im Spendenstand war belegt (ebenfalls
Muenzsaetze), und `glanz_minuten` waere in einem frischen Haus immer 0
geblieben -- die Spalte kam per ALTER TABLE, und das laeuft VOR dem
Anlegen der Werksstufen.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-10-08 01:31:24 +02:00
co-authored by Claude Opus 5
parent 2764aea61c
commit 996231af6e
63 changed files with 5783 additions and 841 deletions
+303
View File
@@ -1103,6 +1103,309 @@ melde("=== 10. Die drei Muenzsaetze ===");
}
}
/* =======================================================================
11. DIE FERTIGEN SAETZE (08.10.2026)
Filipe: „es soll fertige sätze geben und wenn jemand die auswählt
muss keiner es bestätigen dan wird es sofort angezeigt und wenn
jemand selber was schreibt dan muss bestätigt werden damit es
angezeigt wird."
---------------------------------------------------------------------
WAS HIER WIRKLICH AUF DEM SPIEL STEHT
Eine Karte, die OHNE Bestaetigung in einen Livestream geht, ist die
offenste Tuer des ganzen Hauses. Sie darf nur deshalb offen sein,
weil der Text NICHT vom Absender kommt, sondern aus der Datenbank.
Genau das wird hier angegriffen: Es wird versucht, neben der
Satzkennung einen eigenen Text einzuschmuggeln.
======================================================================= */
melde("");
melde("=== 11. Die fertigen Saetze ===");
{
const { db } = await import("./workspace.js");
/* Die Bremse „eine Meldung je Minute und Person" steht allen
folgenden Pruefungen im Weg -- sie brauchen mehrere Meldungen
derselben Person. Zurueckdatieren statt warten: Eine Pruefung,
die Minuten wartet, wird beim naechsten Mal uebersprungen. */
const bremseLoesen = (personId) => {
db().prepare("UPDATE spenden SET erstellt = ? WHERE person_id = ?")
.run(new Date(Date.now() - 600_000).toISOString(), personId);
};
const lage = await roh(CREW, "/workspace/api/spenden", { keks: k.admin });
const saetze = lage.j?.fertige_saetze || [];
ok(saetze.length >= 3,
`ab Werk stehen ${saetze.length} fertige Saetze zum Aussuchen bereit`);
ok(saetze.every((s) => s.id && s.text),
"jeder hat eine Kennung und einen Text");
const beimGast = await roh(CREW, "/workspace/api/spenden", { keks: k.gast });
ok((beimGast.j?.fertige_saetze || []).length === saetze.length,
"ein Zuschauer bekommt sie auch -- er soll ja einen aussuchen");
/* ==== SOFORT INS BILD, ABER NICHT INS BUCH ===================== */
bremseLoesen(IDS.gast);
const standVorher = (await roh(CREW, "/workspace/api/spenden", { keks: k.gast }))
.j?.ich?.dogen_mille ?? -1;
const sofort = await roh(CREW, "/workspace/api/spenden/melden", {
method: "POST", keks: k.gast,
rumpf: { dogen_mille: 500, satz_id: saetze[0].id, gruss: "HIER STEHT ETWAS ANDERES" },
});
ok(sofort.code === 201 && sofort.j?.sofort === true,
`ein fertiger Satz geht sofort ins Bild (${sofort.code}/${sofort.j?.sofort})`);
ok(sofort.j?.wartet === false,
"und er wartet ausdruecklich NICHT auf eine Bestaetigung");
const imPult = await roh(CREW, "/workspace/api/spenden", { keks: k.admin });
const frisch = (imPult.j?.offen || []).find((x) => x.dogen_mille === 500);
ok(!!frisch, "sie steht trotzdem im Pult");
ok(frisch?.schon_gezeigt === true,
"und zwar mit dem Vermerk, dass sie schon gelaufen ist");
/* ==== DER ANGRIFF: EIN EIGENER TEXT NEBEN DER KENNUNG =========
Wer `gruss` mitschickt, hat wieder ein freies Feld -- nur mit
einem Haken daneben, auf dem „ist ja ein fertiger" steht. Der
Text MUSS aus der Datenbank kommen. */
ok(frisch?.gruss === saetze[0].text,
`der Text kommt aus der Datenbank, nicht aus der Leitung ("${frisch?.gruss}")`);
ok(frisch?.gruss !== "HIER STEHT ETWAS ANDERES",
"Gegenprobe: der mitgeschickte eigene Text gilt NICHT");
/* ==== GEZEIGT IST NICHT GEBUCHT ==============================
Sonst stellte sich jede Minute jeder seine eigene Waehrung aus. */
const standNachher = (await roh(CREW, "/workspace/api/spenden", { keks: k.gast }))
.j?.ich?.dogen_mille ?? -1;
ok(standNachher === standVorher,
`gezeigt, aber noch nicht gutgeschrieben (${standVorher} -> ${standNachher})`);
/* ==== DIE BESTAETIGUNG BUCHT -- UND ZEIGT NICHT NOCH EINMAL === */
const vorBest = db().prepare("SELECT gezeigt_am FROM spenden WHERE id = ?").get(frisch.id);
const best = await roh(CREW, `/workspace/api/spenden/${frisch.id}/bestaetigen`, {
method: "POST", keks: k.admin, rumpf: {},
});
ok(best.code === 200, `die Leitung bestaetigt sie nachtraeglich (${best.code})`);
const standBest = (await roh(CREW, "/workspace/api/spenden", { keks: k.gast }))
.j?.ich?.dogen_mille ?? -1;
ok(standBest === standVorher + 500,
`erst jetzt gibt es die Dogen (${standVorher} -> ${standBest})`);
const nachBest = db().prepare("SELECT gezeigt_am FROM spenden WHERE id = ?").get(frisch.id);
ok(nachBest?.gezeigt_am === vorBest?.gezeigt_am,
"und die Karte laeuft NICHT ein zweites Mal (der Zeitpunkt steht still)");
/* ==== EIN FREIER TEXT WARTET WEITERHIN ======================== */
bremseLoesen(IDS.gast2);
const frei = await roh(CREW, "/workspace/api/spenden/melden", {
method: "POST", keks: k.gast2,
rumpf: { dogen_mille: 500, gruss: "Selbst getippt" },
});
ok(frei.code === 201 && frei.j?.sofort !== true && frei.j?.wartet === true,
`ein selbst geschriebener Text wartet (${frei.code}/sofort=${frei.j?.sofort})`);
const freiImPult = (await roh(CREW, "/workspace/api/spenden", { keks: k.admin }))
.j?.offen?.find((x) => x.gruss === "Selbst getippt");
ok(freiImPult?.schon_gezeigt === false,
"und ist nicht im Bild gewesen");
/* ==== EINE FREIE ZAHL GEHT AUCH MIT SATZ NICHT SOFORT =========
Filipes Ansage galt dem SATZ, nicht der Summe. Wer eine eigene
Zahl eintraegt, behauptet eine Hoehe -- und „25 Dogen" ungesehen
gross im Stream ist der Fehler, den man nicht mehr einfangen
kann, wenn hundert Leute zusehen. */
bremseLoesen(IDS.gast2);
const krumm = await roh(CREW, "/workspace/api/spenden/melden", {
method: "POST", keks: k.gast2, rumpf: { dogen_mille: 777, satz_id: saetze[0].id },
});
ok(krumm.code === 201 && krumm.j?.sofort === false,
`ein Betrag, den kein Knopf anbietet, wartet trotz Satz (${krumm.j?.sofort})`);
/* GEGENPROBE: Mit einem Knopfbetrag waere derselbe Satz sofort
gelaufen -- sonst bewiese das Obige nur, dass irgendetwas
nicht sofort geht. */
bremseLoesen(IDS.gast2);
const rund = await roh(CREW, "/workspace/api/spenden/melden", {
method: "POST", keks: k.gast2, rumpf: { dogen_mille: 200, satz_id: saetze[0].id },
});
ok(rund.j?.sofort === true,
"Gegenprobe: mit einem Knopfbetrag geht derselbe Satz sofort");
/* ==== EINE KENNUNG, DIE ES NICHT GIBT ======================== */
bremseLoesen(IDS.gast2);
const falsch = await roh(CREW, "/workspace/api/spenden/melden", {
method: "POST", keks: k.gast2, rumpf: { dogen_mille: 200, satz_id: 999999 },
});
ok(falsch.code === 400 && falsch.j?.fehler === "fertigsatz_unbekannt",
`eine erfundene Satzkennung wird abgelehnt (${falsch.code}/${falsch.j?.fehler})`);
/* ==== DIE LEITUNG PFLEGT DIE LISTE =========================== */
const neu = await roh(CREW, "/workspace/api/spenden/fertige-saetze", {
method: "PUT", keks: k.admin,
rumpf: { fertige_saetze: ["Erster Satz", " ", "Zweiter Satz"] },
});
ok(neu.code === 200 && (neu.j?.fertige_saetze || []).length === 2,
`leere Zeilen fallen weg (${neu.j?.fertige_saetze?.length} statt 3)`);
ok(neu.j.fertige_saetze[0].text === "Erster Satz"
&& neu.j.fertige_saetze[1].text === "Zweiter Satz",
"und die Reihenfolge bleibt");
for (const rolle of ["linke", "modi", "gast"]) {
const a = await roh(CREW, "/workspace/api/spenden/fertige-saetze", {
method: "PUT", keks: k[rolle], rumpf: { fertige_saetze: ["Ich auch"] },
});
ok(a.code === 403, `${rolle}: darf die Saetze nicht aendern (${a.code})`);
}
const zuViele = await roh(CREW, "/workspace/api/spenden/fertige-saetze", {
method: "PUT", keks: k.admin,
rumpf: { fertige_saetze: Array.from({ length: 9 }, (_, i) => `Satz ${i}`) },
});
ok(zuViele.code === 400 && zuViele.j?.fehler === "zu_viele_saetze",
`neun sind zu viele (${zuViele.code}/${zuViele.j?.fehler})`);
}
/* =======================================================================
12. WO DIE KARTE STEHT UND WER IM CHAT GLAENZT (08.10.2026)
======================================================================= */
melde("");
melde("=== 12. Platz und Spender-Glanz ===");
{
const { db } = await import("./workspace.js");
const { spenderGlanz } = await import("./workspace-spenden.js");
/* ---- Der Platz ---- */
const l0 = await roh(CREW, "/workspace/api/spenden", { keks: k.admin });
ok(l0.j?.platz === "mitte", `die Vorgabe ist die Mitte (${l0.j?.platz})`);
ok((l0.j?.gestaltung?.plaetze || []).join(",") === "mitte,rand",
"und es gibt genau zwei Plaetze");
/* ==== DIESER ABSCHNITT SETZT SEINE EIGENE LEITER ==============
Die Abschnitte davor haben an der Stufenleiter gearbeitet --
verschoben, geloescht, neu angelegt. Sich darauf zu verlassen,
was dabei uebrig geblieben ist, waere die Kopplung, die spaeter
jemand aus Versehen zerreisst; dann faellt ein Abschnitt um, an
dem nichts falsch ist. (Genau so ist dieser Abschnitt beim
ersten Lauf rot geworden: zwei Stufen statt drei, beide ohne
Glanz.)
Die drei Werte sind dieselben wie ab Werk -- 5, 15, 45 --, damit
die Pruefung auch das misst, was ein frisches Haus mitbringt. */
const leiterSoll = [
{ id: 0, ab_mille: 100, name: "Danke", vorlage: "herz", farbe: "#8ec9ff",
dauer_ms: 6000, groesse: 1, glanz_minuten: 5 },
{ id: 0, ab_mille: 500, name: "Starke Runde", vorlage: "welle", farbe: "#a98bff",
dauer_ms: 8000, groesse: 1.15, glanz_minuten: 15 },
{ id: 0, ab_mille: 2000, name: "Rudel-Legende", vorlage: "krone", farbe: "#e0a13a",
dauer_ms: 11000, groesse: 1.45, glanz_minuten: 45 },
];
const gesetzt = await roh(CREW, "/workspace/api/spenden/stufen", {
method: "PUT", keks: k.admin, rumpf: { stufen: leiterSoll },
});
ok(gesetzt.code === 200 && (gesetzt.j?.stufen || []).length === 3,
`drei Stufen fuer diesen Abschnitt gesetzt (${gesetzt.j?.stufen?.length})`);
ok((gesetzt.j?.stufen || []).map((s) => s.glanz_minuten).join("/") === "5/15/45",
`und der Glanz kommt zurueck, wie er hingeschickt wurde `
+ `(${(gesetzt.j?.stufen || []).map((s) => s.glanz_minuten).join("/")})`);
const stufenJetzt = (gesetzt.j?.stufen || []).map((s) => ({
id: s.id, ab_mille: s.ab_mille, name: s.name, vorlage: s.vorlage,
farbe: s.farbe, dauer_ms: s.dauer_ms, groesse: s.groesse,
glanz_minuten: s.glanz_minuten,
}));
const amRand = await roh(CREW, "/workspace/api/spenden/stufen", {
method: "PUT", keks: k.admin, rumpf: { stufen: stufenJetzt, platz: "rand" },
});
ok(amRand.code === 200 && amRand.j?.platz === "rand",
`er laesst sich auf „rand" stellen (${amRand.j?.platz})`);
const falscherPlatz = await roh(CREW, "/workspace/api/spenden/stufen", {
method: "PUT", keks: k.admin, rumpf: { stufen: stufenJetzt, platz: "oben" },
});
ok(falscherPlatz.code === 400 && falscherPlatz.j?.fehler === "platz_unklar",
`ein erfundener Platz wird abgelehnt (${falscherPlatz.code}/${falscherPlatz.j?.fehler})`);
/* ==== DER PLATZ REIST MIT DER KARTE ==========================
Die Spendentafel in OBS hat keine Sitzung und kann nichts
nachfragen -- was sie nicht mitbekommt, kann sie nicht
anzeigen. */
const mitKarte = await roh(CREW, "/workspace/api/spenden", { keks: k.admin });
const eine = (mitKarte.j?.letzte || [])[0];
ok(eine?.platz === "rand", `jede Karte traegt den Platz mit (${eine?.platz})`);
await roh(CREW, "/workspace/api/spenden/stufen", {
method: "PUT", keks: k.admin, rumpf: { stufen: stufenJetzt, platz: "mitte" },
});
/* ---- Der Glanz im Chat ---- */
const werte = stufenJetzt.map((s) => s.glanz_minuten);
ok(werte.length >= 3 && werte[0] > 0 && werte[2] > werte[0],
`je hoeher die Stufe, desto laenger der Glanz (${werte.join(" / ")} Minuten)`);
const glanz = spenderGlanz();
const meiner = glanz.get(IDS.gast);
ok(!!meiner, "wer eben gespendet hat, glaenzt im Chat");
ok(meiner?.farbe && /^#[0-9a-fA-F]{6}$/.test(meiner.farbe),
`in der Farbe seiner Stufe (${meiner?.farbe})`);
ok(!!meiner?.stufe, `und mit ihrem Namen (${meiner?.stufe})`);
ok(meiner?.noch_ms > 0, `und mit einer Restdauer (${meiner?.noch_ms} ms)`);
/* ALS ALTER UND NICHT ALS UHRZEIT -- eine Uhr im Handy kann falsch
gehen, eine Dauer nicht. Dieselbe Lehre wie beim Gleichlauf des
Videos am 30.09.2026. */
ok(meiner?.noch_ms <= werte[1] * 60_000 + 1000,
"und die Restdauer passt zur Stufe");
/* ==== DIE STAERKSTE STUFE GEWINNT, NICHT DIE LETZTE ==========
Wer erst viel und dann wenig gibt, soll nicht herabgestuft
werden -- das laese sich wie eine Bestrafung dafuer, dass er
noch einmal gegeben hat. */
{
const gross = stufenJetzt[stufenJetzt.length - 1].ab_mille;
db().prepare(`INSERT INTO spenden
(betrag_cent, waehrung, name, gruss, person_id, herkunft, stand, erstellt, gezeigt_am)
VALUES (?,?,?,?,?,'hand','bestaetigt',?,?)`)
.run(gross * 10, "EUR", "Probe", "", IDS.gast,
new Date(Date.now() - 60_000).toISOString(),
new Date(Date.now() - 60_000).toISOString());
db().prepare(`INSERT INTO spenden
(betrag_cent, waehrung, name, gruss, person_id, herkunft, stand, erstellt, gezeigt_am)
VALUES (?,?,?,?,?,'hand','bestaetigt',?,?)`)
.run(100, "EUR", "Probe", "", IDS.gast, new Date().toISOString(),
new Date().toISOString());
const g2 = spenderGlanz().get(IDS.gast);
ok(g2?.stufe === stufenJetzt[stufenJetzt.length - 1].name,
`die hoechste noch gueltige Stufe gewinnt (${g2?.stufe})`);
}
/* ==== NULL MINUTEN HEISST GAR NICHT ==========================
Eine gueltige Einstellung und kein fehlender Wert. */
{
const aus = stufenJetzt.map((s) => ({ ...s, glanz_minuten: 0 }));
const a = await roh(CREW, "/workspace/api/spenden/stufen", {
method: "PUT", keks: k.admin, rumpf: { stufen: aus },
});
ok(a.code === 200, `alle Stufen auf null (${a.code})`);
ok(spenderGlanz().size === 0,
"dann glaenzt niemand mehr -- auch nicht, wer eben gespendet hat");
const zurueck = await roh(CREW, "/workspace/api/spenden/stufen", {
method: "PUT", keks: k.admin, rumpf: { stufen: stufenJetzt },
});
ok(zurueck.code === 200 && spenderGlanz().size > 0,
"Gegenprobe: mit Minuten glaenzt er wieder");
}
/* ==== UND NACH DER ZEIT IST SCHLUSS ========================== */
{
const lange = stufenJetzt[stufenJetzt.length - 1].glanz_minuten;
db().prepare("UPDATE spenden SET gezeigt_am = ? WHERE person_id = ?")
.run(new Date(Date.now() - (lange + 10) * 60_000).toISOString(), IDS.gast);
ok(!spenderGlanz().get(IDS.gast),
`nach ${lange} Minuten ist der Glanz vorbei`);
}
const grenze = await roh(CREW, "/workspace/api/spenden/stufen", {
method: "PUT", keks: k.admin,
rumpf: { stufen: stufenJetzt.map((s) => ({ ...s, glanz_minuten: 9999 })) },
});
ok(grenze.code === 400 && grenze.j?.fehler === "glanz_unklar",
`und 9999 Minuten sind zu viel (${grenze.code}/${grenze.j?.fehler})`);
}
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG");