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:
+278
-14
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user