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
+278 -14
View File
@@ -44,10 +44,16 @@ import express from "express";
import { db, sitzungLesen, protokolliere, echteIp } from "./workspace.js";
import { stromAn } from "./workspace-chat.js";
import { buehneMelden } from "./workspace-buehne.js";
/* Der Spender-Glanz im Chat. EINE RICHTUNG: Die Reaction fragt die
Spenden, nie umgekehrt -- `workspace-spenden.js` kennt diese Datei
nicht und soll sie nicht kennen, sonst stünden sich beim Laden
zwei Dateien gegenseitig im Weg. */
import { spenderGlanz } from "./workspace-spenden.js";
import { einstellung } from "./workspace.js";
import { mitZugangsdaten, wegeAusfaechern } from "./workspace-turn.js";
import {
SICHT_PLAETZE, GAESTE_MAX, TEXT_MAX, SCHREIB_BREMSE, STAENDE, LAYOUTS, TEMPI,
SICHT_PLAETZE, GAESTE_MAX, EINLADUNG_SEKUNDEN, TEXT_MAX, SCHREIB_BREMSE,
STAENDE, LAYOUTS, TEMPI,
KAMERA_GROESSEN, KAMERA_ECKEN,
MELDUNG_TAGE,
VERLAUF_TAGE, LISTE_MAX, CHAT_MODI, STUMM_MAX_MINUTEN, MASSNAHMEN,
@@ -313,13 +319,66 @@ function weiterZu(zeile, personId) {
return s;
}
/** Wer wirklich auf der Buehne steht.
*
* NUR `dabei`. Seit dem 08.10.2026 gibt es Zeilen, die noch keine
* Gaeste sind (gefragt) und welche, die keine mehr werden
* (abgelehnt, keine Antwort). Haetten die hier gestanden, saehe der
* ganze Saal ein Kamerafenster fuer jemanden, der die Frage noch
* gar nicht gelesen hat -- und die Anruflogik haette versucht, ihn
* anzurufen. */
function gaesteListe() {
return db().prepare(`
SELECT g.person_id AS id, g.stumm, g.platz, p.name, p.rolle
FROM reaktion_gaeste g JOIN personen p ON p.id = g.person_id
WHERE g.stand = 'dabei'
ORDER BY g.platz`).all();
}
/** Die offenen und beantworteten Einladungen -- nur fuer den Host.
*
* ==== DIE FRIST WIRD BEIM LESEN GEZOGEN UND NICHT VON EINER UHR
*
* Ein eigener Zeitgeber waere ein zweiter Ort, an dem dieselbe
* Entscheidung faellt -- und er liefe auch dann, wenn niemand
* hinsieht. Hier gilt: Wer die Liste liest, bekommt den Stand, der
* jetzt gilt. Das ist dieselbe Form wie bei `dabeiListe()`, die
* ihre Karteileichen auch beim Lesen wegraeumt.
*
* UND SIE WIRD NICHT GELOESCHT, SONDERN UMGESCHRIEBEN. Eine
* verfallene Einladung, die verschwindet, sieht fuer den Host aus
* wie „ich habe mich verklickt". */
function einladungenListe() {
const grenze = new Date(Date.now() - EINLADUNG_SEKUNDEN * 1000).toISOString();
db().prepare(`UPDATE reaktion_gaeste SET stand = 'keine_antwort'
WHERE stand = 'gefragt' AND gefragt_am < ?`).run(grenze);
return db().prepare(`
SELECT g.person_id AS id, g.stand, g.gefragt_am, p.name
FROM reaktion_gaeste g JOIN personen p ON p.id = g.person_id
WHERE g.stand <> 'dabei'
ORDER BY g.gefragt_am`).all()
.map((z) => ({
id: z.id, name: z.name, stand: z.stand,
/* ==== ALS REST UND NICHT ALS UHRZEIT ====================
Der Empfaenger bekommt „noch so viele Sekunden" statt eines
Zeitpunkts. Eine Uhrzeit muesste gegen die Uhr seines
Geraets gerechnet werden, und die kann falsch gehen --
dieselbe Lehre wie beim Gleichlauf des Videos am
30.09.2026. Ein Handy, das zwei Minuten vorgeht, saehe
sonst jede Einladung als abgelaufen. */
noch_sek: z.stand === "gefragt"
? Math.max(0, Math.round(
(Date.parse(z.gefragt_am) + EINLADUNG_SEKUNDEN * 1000 - Date.now()) / 1000))
: 0,
}));
}
/** Wie viele Plaetze gerade belegt oder vorgemerkt sind. */
function gaesteBelegt() {
return db().prepare(`SELECT COUNT(*) AS n FROM reaktion_gaeste
WHERE stand IN ('dabei','gefragt')`).get()?.n || 0;
}
/** Wer gerade dabei ist -- und ob noch ein Kameraplatz frei war.
*
* ZWEI MINUTEN OHNE LEBENSZEICHEN HEISST WEG. Ein Browser, der
@@ -413,6 +472,13 @@ function lage(person) {
})),
chat_modus: s.chat_modus || "offen",
chat_modi: CHAT_MODI,
/* ==== DIE OFFENEN EINLADUNGEN (08.10.2026) ==================
Nur fuer den Host. Wer gefragt wurde und noch nicht geantwortet
hat, ist noch kein Gast -- und dass jemand gefragt WURDE, geht
den Saal nichts an. Wird in `oeffentlich()` ausdruecklich
entfernt. */
einladungen: istHost ? einladungenListe() : undefined,
einladung_sekunden: EINLADUNG_SEKUNDEN,
/* ==== DIE WERBUNG SIEHT JEDER =============================
Sie ist das Einzige auf dieser Seite, das ausdruecklich fuer
alle gedacht ist -- eine Einblendung, die nur der Host sieht,
@@ -480,6 +546,32 @@ function lage(person) {
rolle: person.rolle,
host: istHost,
gast: binGast,
/* ==== BIN ICH GERADE GEFRAGT? (08.10.2026) ==============
Die eine Auskunft, aus der die Seite den Vorhang baut:
„DogFather will dich auf die Bühne". Sie steht an `ich` und
nicht in einer Liste -- jeder bekommt nur seine eigene, und
dass jemand anderes gefragt wurde, geht ihn nichts an.
`noch_sek` reist mit, damit die Seite den Countdown ohne
eine eigene Uhr zeichnen kann. */
einladung: (() => {
const e = db().prepare(`SELECT gefragt_am FROM reaktion_gaeste
WHERE person_id = ? AND stand = 'gefragt'`).get(person.id);
if (!e) return null;
const rest = Math.round(
(Date.parse(e.gefragt_am) + EINLADUNG_SEKUNDEN * 1000 - Date.now()) / 1000);
/* Abgelaufen heißt „keine Einladung mehr". Die Zeile wird
beim nächsten Lesen der Hostliste umgeschrieben; hier
reicht es, sie nicht mehr zu zeigen -- sonst stünde vor
jemandem ein Vorhang mit „noch -40 Sekunden". */
if (rest <= 0) return null;
/* WER EINLÄDT, STEHT SCHON IM STAND (`lage.host.name`). Den
Namen hier ein zweites Mal mitzuschicken wäre eine zweite
Antwort auf dieselbe Frage -- und die beiden könnten
auseinanderlaufen, sobald jemand mitten in der Sendung die
Führung übernimmt. */
return { noch_sek: rest };
})(),
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),
@@ -939,8 +1031,106 @@ reaktionRouter.post("/workspace/api/reaktion/gast", nurHost, express.json({ limi
if (!p) return res.status(404).json({ fehler: "nicht_gefunden" });
if (id === req.person.id) return res.status(400).json({ fehler: "host_ist_kein_gast" });
const da = db().prepare("SELECT COUNT(*) AS n FROM reaktion_gaeste").get()?.n || 0;
if (da >= GAESTE_MAX) return res.status(409).json({ fehler: "gaeste_voll" });
/* Verfallene Einladungen erst wegräumen — sonst hält eine
Frage von vor einer Stunde noch einen Platz besetzt. */
einladungenListe();
if (gaesteBelegt() >= GAESTE_MAX) {
return res.status(409).json({ fehler: "gaeste_voll" });
}
/* ==== GEFRAGT, NICHT GEHOLT (08.10.2026) =================
Filipe: „wenn gäste eingeladen werden, sollen die
akzeptieren oder ablehnen können. bevor die auch angezeigt
werden sollen die auswählen können ob die kamera an ist oder
aus ist. also iohre eigene."
Vorher stand hier ein Platz und eine Zeile — und die Kamera
des Gastes sprang im selben Augenblick an. Jetzt entsteht
erst eine FRAGE. Der Platz wird beim Zusagen vergeben, nicht
jetzt: Wer ablehnt, soll Platz 1 nicht für zwei Minuten
blockiert haben.
`INSERT OR REPLACE` räumt dabei eine alte Antwort derselben
Person weg — wer eben abgelehnt hat und jetzt noch einmal
gefragt wird, steht nicht zweimal in der Liste. */
db().prepare(`INSERT OR REPLACE INTO reaktion_gaeste
(person_id, seit, stumm, platz, stand, gefragt_am)
VALUES (?,?,0,0,'gefragt',?)`).run(id, jetzt(), jetzt());
protokolliere("reaktion_gast_gefragt", {
personId: req.person.id,
detail: p.name,
ip: echteIp(req),
});
const l = lage(req.person);
/* KEIN RUNDRUF AN DEN SAAL. Es hat sich für ihn nichts
geändert — auf der Bühne steht niemand Neues. Ein Rundruf
hier hieße, dass alle erfahren, wer gefragt wurde. */
stromAn([id], "reaktion", { was: "du_bist_eingeladen" });
res.status(201).json(l);
} catch (fehler) {
console.error("[reaktion] Gast:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* ---------- Die Antwort des Gastes -------------------------------------
Filipe: „sollen die akzeptieren oder ablehnen können. bevor die
auch angezeigt werden sollen die auswählen können ob die kamera an
ist oder aus ist."
ZWEI DINGE IN EINEM AUFRUF, und das ist Absicht: Die Zusage UND
die Vorwahl von Kamera und Mikro kommen zusammen an. Zwei Aufrufe
hintereinander hätten ein Fenster dazwischen, in dem jemand schon
auf der Bühne steht, dessen Kamera noch auf „an" steht — und genau
diese Sekunde ist das, was abgestellt werden soll.
DAS IST KEIN HOST-WEG. Hier antwortet der Gefragte selbst, für
sich. `nurHost` wäre hier falsch herum.
----------------------------------------------------------------------- */
reaktionRouter.post("/workspace/api/reaktion/gast/antwort",
express.json({ limit: "2kb" }), (req, res) => {
try {
const e = db().prepare(`SELECT * FROM reaktion_gaeste
WHERE person_id = ? AND stand = 'gefragt'`).get(req.person.id);
if (!e) return res.status(404).json({ fehler: "keine_einladung" });
/* Die Frist gilt auch hier und nicht nur beim Anzeigen. Sonst
könnte eine Seite, die seit einer Stunde offen liegt, eine
längst verfallene Einladung annehmen — und stünde plötzlich
im Bild, ohne dass jemand damit rechnet. */
if (Date.parse(e.gefragt_am) + EINLADUNG_SEKUNDEN * 1000 <= Date.now()) {
db().prepare("UPDATE reaktion_gaeste SET stand = 'keine_antwort' WHERE person_id = ?")
.run(req.person.id);
return res.status(409).json({ fehler: "einladung_abgelaufen" });
}
const ja = req.body?.ja === true;
if (!ja) {
db().prepare("UPDATE reaktion_gaeste SET stand = 'abgelehnt' WHERE person_id = ?")
.run(req.person.id);
protokolliere("reaktion_gast_abgelehnt", {
personId: req.person.id, detail: req.person.name, ip: echteIp(req),
});
/* Nur der Host erfährt es — und er erfährt es SOFORT, sonst
wartet er zwei Minuten auf eine Antwort, die schon da
ist. */
const s = sendung();
if (s?.host_id) {
stromAn([s.host_id], "reaktion",
{ was: "einladung_beantwortet", name: req.person.name, ja: false });
}
return res.json(lage(req.person));
}
/* Zwischen Frage und Zusage kann ein zweiter Gast zugesagt
haben. Ohne diese Prüfung stünden drei auf einer Bühne für
zwei. */
const dabeiJetzt = db().prepare(`SELECT COUNT(*) AS n FROM reaktion_gaeste
WHERE stand = 'dabei'`).get()?.n || 0;
if (dabeiJetzt >= GAESTE_MAX) {
return res.status(409).json({ fehler: "gaeste_voll" });
}
/* Der Platz ist die kleinste freie Zahl -- nicht „Anzahl + 1".
Wer Gast 1 entfernt und dann jemanden holt, bekaeme sonst
@@ -949,19 +1139,62 @@ reaktionRouter.post("/workspace/api/reaktion/gast", nurHost, express.json({ limi
let platz = 1;
while (belegt.has(platz)) platz++;
db().prepare("INSERT OR REPLACE INTO reaktion_gaeste (person_id, seit, stumm, platz) VALUES (?,?,0,?)")
.run(id, jetzt(), platz);
/* ==== DIE VORWAHL WIRD GESETZT, BEVOR DIE BÜHNE AUFGEHT ===
Sie steht in `reaktion_dabei` — dort, wo auch der
Senderknopf sie später hinschreibt. Eine zweite Spalte an
der Gastzeile wäre eine zweite Antwort auf dieselbe Frage,
und spätestens beim ersten Klick auf „Kamera" liefen beide
auseinander.
FEHLT DIE ANGABE, IST SIE AN. Das ist der Zustand, den jeder
erwartet, der gerade „ja" gesagt hat — und die Seite
schickt sie immer mit. */
const kameraAus = req.body?.kamera === false ? 1 : 0;
const mikroAus = req.body?.mikro === false ? 1 : 0;
db().exec("BEGIN IMMEDIATE");
try {
db().prepare(`UPDATE reaktion_gaeste SET stand = 'dabei', platz = ?, seit = ?
WHERE person_id = ?`).run(platz, jetzt(), req.person.id);
/* ==== EINFÜGEN ODER ÄNDERN, ABER NIE ÜBERSCHREIBEN ====
Wer antwortet, sitzt im Saal und steht damit längst in
`reaktion_dabei` — der Normalfall ist also ein ÄNDERN.
Ein reines `UPDATE` hätte aber einen stillen Ausgang: Ist
die Zeile zwischendurch weggeräumt worden (zwei Minuten
ohne Lebenszeichen genügen), ändert es null Zeilen, meldet
nichts, und der Gast stünde mit laufender Kamera im Bild,
obwohl er sie abgewählt hat. Genau die Sorte Fehler, die
man erst im Stream sieht.
`ON CONFLICT` setzt deshalb nur die beiden Felder und
lässt `seit`, `gesehen` und `sicht` in Ruhe. */
db().prepare(`INSERT INTO reaktion_dabei
(person_id, seit, gesehen, sicht, kamera_aus, mikro_aus)
VALUES (?,?,?,0,?,?)
ON CONFLICT(person_id) DO UPDATE SET
kamera_aus = excluded.kamera_aus, mikro_aus = excluded.mikro_aus`)
.run(req.person.id, jetzt(), jetzt(), kameraAus, mikroAus);
db().exec("COMMIT");
} catch (f) {
db().exec("ROLLBACK");
throw f;
}
protokolliere("reaktion_gast_dazu", {
personId: req.person.id,
detail: `${p.name} (Platz ${platz})`,
detail: `${req.person.name} (Platz ${platz}, Kamera ${kameraAus ? "aus" : "an"})`,
ip: echteIp(req),
});
const l = lage(req.person);
melden("reaktion", { was: "gaeste", lage: oeffentlich(l) });
stromAn([id], "reaktion", { was: "du_bist_gast", lage: oeffentlich(l) });
res.status(201).json(l);
const s = sendung();
if (s?.host_id) {
stromAn([s.host_id], "reaktion",
{ was: "einladung_beantwortet", name: req.person.name, ja: true });
}
res.json(l);
} catch (fehler) {
console.error("[reaktion] Gast:", fehler?.message);
console.error("[reaktion] Gastantwort:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
@@ -1459,8 +1692,10 @@ reaktionRouter.post("/workspace/api/reaktion/senden", express.json({ limit: "2kb
if (s.stand === "zu") return res.status(409).json({ fehler: "saal_zu" });
const istHost = HOST_ROLLEN.has(req.person.rolle);
const binGast = !!db().prepare("SELECT 1 FROM reaktion_gaeste WHERE person_id = ?")
.get(req.person.id);
/* `stand = 'dabei'` seit dem 08.10.2026: Wer erst GEFRAGT ist,
sendet noch nicht und darf hier auch nichts schalten. */
const binGast = !!db().prepare("SELECT 1 FROM reaktion_gaeste"
+ " WHERE person_id = ? AND stand = 'dabei'").get(req.person.id);
if (!istHost && !binGast) return res.status(403).json({ fehler: "sendest_nicht" });
/* TEILWEISE AENDERN IST ERLAUBT. Wer nur die Kamera schaltet,
@@ -1917,15 +2152,37 @@ const CHAT_ABZEICHEN = {
* und die Meldung beim Schreiben. Zwei Fassungen waeren zwei
* Gelegenheiten, eine davon zu vergessen -- und vergessen wuerde man
* die, die seltener angefasst wird. */
function fuerDenSaal(b) {
function fuerDenSaal(b, glanz) {
if (!b) return b;
const { rolle, ...rest } = b;
/* ==== DER SPENDER-GLANZ (08.10.2026) ==========================
Filipe: „die leute die spenden gemacht haben sollen auch für
eine kurze zeit speziell aussehen je nach höhe von spende."
DIE KARTE WIRD EINMAL JE ANFRAGE GEBAUT und hereingereicht --
der Chat lädt 120 Beiträge, und eine Abfrage je Beitrag wären
120 Abfragen für eine Auskunft, die sich dazwischen gar nicht
ändert. Fehlt sie (einzelner neuer Beitrag), holt diese
Funktion sie selbst; dann ist es genau eine.
WARUM DER GLANZ AM BEITRAG HÄNGT UND NICHT AN DER PERSON: Ein
Beitrag von vor einer Stunde soll nicht nachträglich leuchten,
wenn jemand eben gespendet hat. Er bekommt den Stand, der beim
Laden gilt -- und beim nächsten Laden den dann gültigen. */
const k = (glanz || spenderGlanz()).get(b.person_id);
return {
...rest,
abzeichen: CHAT_ABZEICHEN[rolle] || null,
/* „Gehoert zum Team" war vorher `b.rolle !== 'gast'` in der
Oberflaeche. Dieselbe Frage, nur ohne den Namen. */
team: !!rolle && rolle !== "gast",
spende: k
? {
stufe: k.stufe, farbe: k.farbe, dogen_mille: k.dogen_mille,
muenze: k.muenze, noch_ms: k.noch_ms,
}
: null,
};
}
@@ -1934,7 +2191,8 @@ reaktionRouter.get("/workspace/api/reaktion/chat", (req, res) => {
const reihen = db().prepare(`
SELECT id, person_id, name, rolle, text, erstellt
FROM reaktion_chat WHERE weg_am IS NULL ORDER BY id DESC LIMIT 120`).all();
res.json({ beitraege: reihen.reverse().map(fuerDenSaal) });
const glanz = spenderGlanz();
res.json({ beitraege: reihen.reverse().map((b) => fuerDenSaal(b, glanz)) });
} catch (fehler) {
console.error("[reaktion] Chat:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
@@ -2093,7 +2351,13 @@ function oeffentlich(l) {
laufenden Botschaften stehen ohnehin unter `werbung.band` und
gehoeren allen; ein Entwurf, den jemand bewusst ausgeschaltet
hat, gehoert niemandem. */
const { zuschauer, ich, liste, meldungen, massnahmen, zweit,
/* `einladungen` KAM AM 08.10.2026 DAZU. Dass jemand gefragt wurde
und noch nicht geantwortet hat, geht den Saal nichts an -- und
erst recht nicht, dass jemand ABGELEHNT hat. Der Rundruf wird
aus dem Stand dessen gebaut, der gerade etwas getan hat; war das
der Host, ginge die ganze Liste an jeden im Saal. Genau dieselbe
Falle wie bei `meldungen` am 28.09. */
const { zuschauer, ich, liste, meldungen, massnahmen, zweit, einladungen,
banner_alle: _verwaltung, ...rest } = l;
return rest;
}