diff --git a/server/mess-reaktion.mjs b/server/mess-reaktion.mjs
index 4a411b8f..518bd754 100644
--- a/server/mess-reaktion.mjs
+++ b/server/mess-reaktion.mjs
@@ -460,6 +460,52 @@ if (regie.regler !== 4 || regie.pegel !== 4 || regie.knoepfe_sichtbar !== 3) {
ausgang = 1;
}
+/* ---- Kamera aus und Chat-Stufe ------------------------------------
+ Beides ist erst gebaut, wenn es beim ZUSCHAUER ankommt. */
+console.log("");
+console.log(" --- Kamera aus, Chat zu ---");
+
+await host.seite.click("#s-kamera").catch(() => {});
+await host.seite.waitForTimeout(1200);
+const nachKamera = await zuschauer.seite.evaluate(() => {
+ const f = document.querySelector('#kameras .kamera[data-kamera="aus"]');
+ return {
+ schild: f ? f.querySelector(".kamera__aus")?.textContent?.trim() : null,
+ wessen: f ? f.querySelector(".kamera__name")?.textContent?.trim() : null,
+ videoVersteckt: f ? getComputedStyle(f.querySelector("video")).opacity === "0" : false,
+ };
+});
+console.log(` Zuschauerin sieht: „${nachKamera.schild || "(nichts)"}" `
+ + `bei ${nachKamera.wessen || "(niemandem)"}, Bild verdeckt=${nachKamera.videoVersteckt}`);
+if (nachKamera.schild !== "Kamera aus" || !nachKamera.videoVersteckt) {
+ console.log(" ACHTUNG: Das abgeschaltete Bild kommt nicht als Schild an.");
+ ausgang = 1;
+}
+await host.seite.click("#s-kamera").catch(() => {});
+await host.seite.waitForTimeout(900);
+
+await host.seite.click('#chat-stufen .stufe[data-modus="team"]').catch(() => {});
+await host.seite.waitForTimeout(1200);
+const nachChat = await zuschauer.seite.evaluate(() => {
+ const f = document.getElementById("chat-feld");
+ return { gesperrt: !!f?.disabled, grund: f?.placeholder || "",
+ stufe: document.getElementById("schiene")?.dataset.chat };
+});
+console.log(` Zuschauerin: Feld gesperrt=${nachChat.gesperrt} `
+ + `Grund „${nachChat.grund}" Stufe=${nachChat.stufe}`);
+/* GEGENPROBE AM SERVER. Ein ausgegrautes Feld haelt niemanden auf,
+ der die Schnittstelle kennt -- also wird auch dort nachgefragt. */
+const trotzdem = await roh("/workspace/api/reaktion/chat", {
+ method: "POST", keks: k.gast, rumpf: { text: "geht das trotzdem?" },
+});
+console.log(` ... und der Server lehnt ab: ${trotzdem.code}/${trotzdem.j?.fehler}`);
+if (!nachChat.gesperrt || !nachChat.grund.includes("Team") || trotzdem.code !== 403) {
+ console.log(" ACHTUNG: Die Chat-Stufe kommt nicht an.");
+ ausgang = 1;
+}
+await host.seite.click('#chat-stufen .stufe[data-modus="offen"]').catch(() => {});
+await host.seite.waitForTimeout(900);
+
await vermessen(host.seite, "Host am Rechner");
await bild(host.seite, "3-live-host");
diff --git a/server/pruef-reaktion.mjs b/server/pruef-reaktion.mjs
index 3441881c..412b37d1 100644
--- a/server/pruef-reaktion.mjs
+++ b/server/pruef-reaktion.mjs
@@ -880,10 +880,163 @@ melde("=== 12. Die Regie ===");
}
/* =======================================================================
- 13. KEIN WEG HEISST ZWEIMAL GLEICH
+ 13. KAMERA UND CHAT — die zwei fehlenden Host-Steuerungen
+
+ Filipes Notiz nennt acht: Kamera, Mikrofon, Video, Gaeste,
+ Lautstaerke, Chat, Layout, Start/Ende. Nachgezaehlt war sechsmal
+ etwas da und zweimal nichts.
======================================================================= */
melde("");
-melde("=== 13. Keine doppelten Wege ===");
+melde("=== 13. Kamera und Chat ===");
+{
+ /* ABSCHNITT 9 HAT DIE SENDUNG BEENDET -- also wird hier neu
+ aufgemacht. Sich auf den Stand eines frueheren Abschnitts zu
+ verlassen ist die Kopplung, die spaeter jemand aus Versehen
+ zerreisst; dann faellt ein Abschnitt um, an dem nichts falsch
+ ist. */
+ await roh(CREW, "/workspace/api/reaktion", {
+ method: "PUT", keks: k.admin,
+ rumpf: { titel: "Kamera und Chat", video: "dQw4w9WgXcQ" },
+ });
+ await roh(CREW, "/workspace/api/reaktion/stand", {
+ method: "POST", keks: k.admin, rumpf: { stand: "live" },
+ });
+ for (const rolle of ["admin", "hand", "linke", "modi", "gast", "gast2"]) {
+ await roh(CREW, "/workspace/api/reaktion/dabei", { method: "POST", keks: k[rolle] });
+ }
+ await roh(CREW, "/workspace/api/reaktion/gast", {
+ method: "POST", keks: k.admin, rumpf: { person_id: IDS.gast2 },
+ });
+
+ const stand0 = await roh(CREW, "/workspace/api/reaktion", { keks: k.admin });
+ ok(stand0.j?.stand === "live" && stand0.j?.gaeste?.length === 1,
+ `die Buehne steht (${stand0.j?.stand}, ${stand0.j?.gaeste?.length} Gast)`);
+
+ /* ---- Kamera ---- */
+ const fremd = await roh(CREW, "/workspace/api/reaktion/senden", {
+ method: "POST", keks: k.gast, rumpf: { kamera_aus: true },
+ });
+ ok(fremd.code === 403 && fremd.j?.fehler === "sendest_nicht",
+ `wer nicht sendet, schaltet nichts (${fremd.code}/${fremd.j?.fehler})`);
+
+ const aus = await roh(CREW, "/workspace/api/reaktion/senden", {
+ method: "POST", keks: k.admin, rumpf: { kamera_aus: true },
+ });
+ ok(aus.code === 200 && aus.j?.ich?.kamera_aus === true,
+ `der Host schaltet sein Bild ab (${aus.code})`);
+ ok(aus.j?.host?.kamera_aus === true,
+ "und es steht an der Sendung, damit es ALLE sehen");
+
+ /* TEILWEISE AENDERN DARF DEN ANDEREN WERT NICHT ANFASSEN. Sonst
+ muesste jede Seite immer beide Werte mitschicken -- und wer das
+ vergisst, schaltet dem anderen versehentlich das Bild wieder an. */
+ const nurMikro = await roh(CREW, "/workspace/api/reaktion/senden", {
+ method: "POST", keks: k.admin, rumpf: { mikro_aus: true },
+ });
+ ok(nurMikro.j?.ich?.mikro_aus === true && nurMikro.j?.ich?.kamera_aus === true,
+ "nur das Mikro zu schalten laesst die Kamera in Ruhe");
+
+ const leer = await roh(CREW, "/workspace/api/reaktion/senden", {
+ method: "POST", keks: k.admin, rumpf: {},
+ });
+ ok(leer.code === 400 && leer.j?.fehler === "nichts_zu_tun",
+ `ein Aufruf ohne Inhalt sagt das auch (${leer.code})`);
+
+ /* Ein Gast darf es fuer sich -- sonst muesste er den Host bitten. */
+ const gastAus = await roh(CREW, "/workspace/api/reaktion/senden", {
+ method: "POST", keks: k.gast2, rumpf: { kamera_aus: true },
+ });
+ ok(gastAus.code === 200,
+ `ein Gast schaltet sein eigenes Bild ab (${gastAus.code})`);
+ const beimHost = await roh(CREW, "/workspace/api/reaktion", { keks: k.admin });
+ ok(beimHost.j?.gaeste?.some((g) => g.id === IDS.gast2 && g.kamera_aus === true),
+ "und der Host sieht es an der Gastkarte");
+
+ /* Zurueck, damit der Rest der Pruefung von einem sauberen Stand
+ aus weiterlaeuft. */
+ await roh(CREW, "/workspace/api/reaktion/senden", {
+ method: "POST", keks: k.admin, rumpf: { kamera_aus: false, mikro_aus: false },
+ });
+ await roh(CREW, "/workspace/api/reaktion/senden", {
+ method: "POST", keks: k.gast2, rumpf: { kamera_aus: false },
+ });
+
+ /* ---- Chat ---- */
+ for (const rolle of ["linke", "modi", "gast"]) {
+ const a = await roh(CREW, "/workspace/api/reaktion/chatmodus", {
+ method: "POST", keks: k[rolle], rumpf: { modus: "zu" },
+ });
+ ok(a.code === 403, `${rolle}: stellt den Chat nicht um (${a.code})`);
+ }
+ const unsinn = await roh(CREW, "/workspace/api/reaktion/chatmodus", {
+ method: "POST", keks: k.admin, rumpf: { modus: "halboffen" },
+ });
+ ok(unsinn.code === 400 && unsinn.j?.fehler === "chatmodus_unbekannt",
+ `eine erfundene Stufe wird abgelehnt (${unsinn.code})`);
+
+ const offen = await roh(CREW, "/workspace/api/reaktion/chat", {
+ method: "POST", keks: k.gast, rumpf: { text: "offen geht" },
+ });
+ ok(offen.code === 201, `bei „offen" schreibt die Community (${offen.code})`);
+
+ const team = await roh(CREW, "/workspace/api/reaktion/chatmodus", {
+ method: "POST", keks: k.hand, rumpf: { modus: "team" },
+ });
+ ok(team.code === 200 && team.j?.chat_modus === "team",
+ `die rechte Hand stellt auf „Team" (${team.j?.chat_modus})`);
+
+ const gastTeam = await roh(CREW, "/workspace/api/reaktion/chat", {
+ method: "POST", keks: k.gast, rumpf: { text: "und jetzt?" },
+ });
+ ok(gastTeam.code === 403 && gastTeam.j?.fehler === "chat_nur_team",
+ `dann schweigt die Community (${gastTeam.code}/${gastTeam.j?.fehler})`);
+ const modiTeam = await roh(CREW, "/workspace/api/reaktion/chat", {
+ method: "POST", keks: k.modi, rumpf: { text: "wir ordnen kurz" },
+ });
+ ok(modiTeam.code === 201, `ein Modi schon -- er soll ja ordnen (${modiTeam.code})`);
+
+ /* DIE OBERFLAECHE MUSS DIESELBE ANTWORT BEKOMMEN wie der Server.
+ Sonst steht ein Feld da, das sich beschreiben laesst und beim
+ Absenden 403 sagt -- eine Falle, keine Bedienung. */
+ const sichtGast = await roh(CREW, "/workspace/api/reaktion", { keks: k.gast });
+ ok(sichtGast.j?.ich?.darf_schreiben === false,
+ "und die Oberflaeche erfaehrt es, bevor jemand tippt");
+ const sichtModi = await roh(CREW, "/workspace/api/reaktion", { keks: k.modi });
+ ok(sichtModi.j?.ich?.darf_schreiben === true,
+ "der Modi erfaehrt das Gegenteil");
+
+ const zu = await roh(CREW, "/workspace/api/reaktion/chatmodus", {
+ method: "POST", keks: k.admin, rumpf: { modus: "zu" },
+ });
+ ok(zu.j?.chat_modus === "zu", "die Stufe zu geht auch");
+ const modiZu = await roh(CREW, "/workspace/api/reaktion/chat", {
+ method: "POST", keks: k.modi, rumpf: { text: "auch ich?" },
+ });
+ ok(modiZu.code === 403 && modiZu.j?.fehler === "chat_zu",
+ `dann schweigt auch der Modi (${modiZu.code}/${modiZu.j?.fehler})`);
+ const hostZu = await roh(CREW, "/workspace/api/reaktion/chat", {
+ method: "POST", keks: k.admin, rumpf: { text: "ich sage noch etwas" },
+ });
+ ok(hostZu.code === 201,
+ `die zwei, die fuehren, koennen weiter reden (${hostZu.code})`);
+
+ /* GEGENPROBE: Es geht auch wieder auf. Eine Sperre, die man nicht
+ loesen kann, ist keine Stufe, sondern ein Ende. */
+ await roh(CREW, "/workspace/api/reaktion/chatmodus", {
+ method: "POST", keks: k.admin, rumpf: { modus: "offen" },
+ });
+ const wiederOffen = await roh(CREW, "/workspace/api/reaktion/chat", {
+ method: "POST", keks: k.gast, rumpf: { text: "wieder da" },
+ });
+ ok(wiederOffen.code === 201,
+ `Gegenprobe: zurueck auf „offen", und die Community schreibt wieder (${wiederOffen.code})`);
+}
+
+/* =======================================================================
+ 14. KEIN WEG HEISST ZWEIMAL GLEICH
+ ======================================================================= */
+melde("");
+melde("=== 14. Keine doppelten Wege ===");
{
/* Beim Notizblock hat genau das eine Stunde gekostet:
`DELETE /workspace/api/notizen/:id` gab es schon woanders, und
@@ -907,7 +1060,7 @@ melde("=== 13. Keine doppelten Wege ===");
ok(doppelt.length === 0,
doppelt.length ? `DOPPELT: ${[...new Set(doppelt)].join(", ")}` : "kein Weg kommt zweimal vor");
const reaktionswege = wege.filter((x) => x.includes("/api/reaktion")).length;
- ok(reaktionswege >= 15, `die Reaction bringt ${reaktionswege} eigene Wege mit`);
+ ok(reaktionswege >= 17, `die Reaction bringt ${reaktionswege} eigene Wege mit`);
}
melde("");
diff --git a/server/reaktion-tabellen.js b/server/reaktion-tabellen.js
index f765806a..cd21a4b5 100644
--- a/server/reaktion-tabellen.js
+++ b/server/reaktion-tabellen.js
@@ -83,6 +83,19 @@ export const STAENDE = ["zu", "vorbereitung", "live"];
/** Die Anordnungen des Bildes. */
export const LAYOUTS = ["kino", "gleich", "kamera"];
+/** Wer im Live-Chat schreiben darf.
+ *
+ * Filipes Notiz nennt „Chat" als eine der acht Host-Steuerungen.
+ * Gemeint ist nicht die Moderation -- die gab es schon -- sondern
+ * der Chat selbst: Einen Beitrag wegzunehmen, nachdem er stand, ist
+ * etwas anderes, als ihn gar nicht erst zuzulassen.
+ *
+ * DREI STUFEN UND NICHT ZWEI. „Zu" ist in einer Sendung fast immer
+ * zu viel -- dann sitzen alle vor einem stummen Fenster. Die
+ * mittlere Stufe ist die, die man wirklich braucht: Es wird laut,
+ * das Team ordnet kurz, danach geht es wieder auf. */
+export const CHAT_MODI = ["offen", "team", "zu"];
+
/** Hoechstens so viele Videos duerfen in der Warteschlange stehen.
*
* Die Zahl ist keine technische Grenze, sondern eine Ansage: Wer
@@ -216,6 +229,31 @@ export function reaktionTabellen(d) {
niemand pflegt, kann nicht veralten; `ADD COLUMN` braucht gar
keine. */
const da = new Set(d.prepare("PRAGMA table_info(reaktion)").all().map((z) => z.name));
+ if (!da.has("chat_modus")) {
+ /* Wer im Live-Chat schreiben darf. Gilt fuer die ganze Sendung,
+ nicht je Person -- deshalb hier und nicht an den Zusehenden. */
+ d.exec("ALTER TABLE reaktion ADD COLUMN chat_modus TEXT NOT NULL DEFAULT 'offen'");
+ console.log("[reaktion] Spalte chat_modus angelegt.");
+ }
+ const dabei = new Set(
+ d.prepare("PRAGMA table_info(reaktion_dabei)").all().map((z) => z.name));
+ if (!dabei.has("kamera_aus")) {
+ /* Wer sein Bild gerade abgeschaltet hat.
+ AN DIESER TABELLE UND NICHT AN DEN GAESTEN: Hier steht, wer
+ gerade da ist -- Host, Gaeste und Zuschauer in einer Liste.
+ Genau die richtige Lebensdauer: Wer geht, nimmt seinen Zustand
+ mit, und die Pflege raeumt ihn ohnehin weg. */
+ d.exec("ALTER TABLE reaktion_dabei ADD COLUMN kamera_aus INTEGER NOT NULL DEFAULT 0");
+ console.log("[reaktion] Spalte kamera_aus angelegt.");
+ }
+ if (!dabei.has("mikro_aus")) {
+ /* Und dasselbe fuers Mikro. Wer sich selbst stummschaltet und
+ trotzdem redet, saehe sonst bei allen anderen ein ganz
+ normales Fenster -- niemand merkt, dass nichts ankommt, und er
+ selbst am wenigsten. */
+ d.exec("ALTER TABLE reaktion_dabei ADD COLUMN mikro_aus INTEGER NOT NULL DEFAULT 0");
+ console.log("[reaktion] Spalte mikro_aus angelegt.");
+ }
if (!da.has("video_titel")) {
/* Der Name des laufenden Videos, einmal bei YouTube geholt.
Ohne ihn stuende im Pult und auf der Wartetafel nur die
diff --git a/server/workspace-reaktion.js b/server/workspace-reaktion.js
index 8ecc1fd8..476f0841 100644
--- a/server/workspace-reaktion.js
+++ b/server/workspace-reaktion.js
@@ -47,7 +47,7 @@ import { einstellung } from "./workspace.js";
import { mitZugangsdaten, wegeAusfaechern } from "./workspace-turn.js";
import {
SICHT_PLAETZE, GAESTE_MAX, TEXT_MAX, SCHREIB_BREMSE, STAENDE, LAYOUTS,
- VERLAUF_TAGE, LISTE_MAX,
+ VERLAUF_TAGE, LISTE_MAX, CHAT_MODI,
} from "./reaktion-tabellen.js";
export const reaktionRouter = express.Router();
@@ -212,7 +212,8 @@ 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, p.name, p.rolle
+ SELECT d.person_id AS id, d.sicht, d.seit, d.kamera_aus, d.mikro_aus,
+ p.name, p.rolle
FROM reaktion_dabei d JOIN personen p ON p.id = d.person_id
ORDER BY d.seit`).all();
}
@@ -244,10 +245,26 @@ function lage(person) {
laeuft: !!s.laeuft,
sekunde: s.sekunde,
gestartet_am: s.gestartet_am,
+ /* WER GERADE SEIN BILD AUS HAT -- fuer den Host und fuer jeden
+ Gast. Ohne diese Auskunft waere ein abgeschaltetes Bild von
+ einem kaputten nicht zu unterscheiden: beides ein schwarzes
+ Fenster. Mit ihr steht „Kamera aus" darin, und niemand fragt
+ nach. */
host: s.host_id
- ? db().prepare("SELECT id, name, rolle FROM personen WHERE id = ?").get(s.host_id)
+ ? (() => {
+ 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 };
+ })()
: null,
- gaeste: gaeste.map((g) => ({ id: g.id, name: g.name, rolle: g.rolle, stumm: !!g.stumm, platz: g.platz })),
+ 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),
+ })),
+ chat_modus: s.chat_modus || "offen",
+ chat_modi: CHAT_MODI,
dabei: dabei.length,
/* ==== WER DIE LISTE BEKOMMT -- UND WARUM GENAU DIESE ==========
@@ -280,6 +297,9 @@ function lage(person) {
gast: binGast,
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),
+ mikro_aus: !!(dabei.find((d) => d.id === person.id)?.mikro_aus),
+ darf_schreiben: darfSchreiben(person, s),
},
/* DIE WARTESCHLANGE SEHEN NUR DIE ZWEI. Sie ist Teil der
Vorbereitung, und Filipe hat dazu gesagt: „nur die zwei sollen
@@ -301,6 +321,26 @@ function lage(person) {
};
}
+/** Darf diese Person gerade in den Live-Chat schreiben?
+ *
+ * EINE STELLE FUER ZWEI FRAGEN. Der Server muss es wissen, um
+ * abzulehnen; die Oberflaeche muss es wissen, um das Feld gar nicht
+ * erst anzubieten. Zwei Rechnungen waeren zwei Gelegenheiten, dass
+ * ein Feld da ist und mit 403 antwortet -- und das ist eine Falle,
+ * keine Bedienung. */
+function darfSchreiben(person, stand) {
+ if (stand.stand === "zu") return false;
+ const modus = stand.chat_modus || "offen";
+ if (modus === "offen") return true;
+ /* „team": wer moderiert, darf. Das ist dieselbe Menge, die auch
+ Beitraege wegnehmen darf -- wer aufraeumen soll, muss dabei
+ reden koennen. */
+ if (modus === "team") return MODERIERT.has(person.rolle);
+ /* „zu": nur die zwei, die die Sendung fuehren. Sonst saesse der
+ Saal vor einem Fenster, in dem gar nichts mehr passiert. */
+ return HOST_ROLLEN.has(person.rolle);
+}
+
function melden(art, daten) {
stromAn(empfaenger(), art, daten);
}
@@ -752,6 +792,86 @@ reaktionRouter.post("/workspace/api/reaktion/weiter", nurHost, express.json({ li
}
});
+/* ---------- Kamera aus, Kamera an ---------------------------------------
+ Filipes Notiz nennt „Kamera" als erste der acht Host-Steuerungen.
+ Sie fehlte: Wer kurz aufstehen, trinken oder etwas holen wollte,
+ musste die Sendung verlassen oder sich dabei filmen lassen.
+
+ DAS BILD WIRD AM GERAET ABGESCHALTET, NICHT AM SERVER. Die Spur
+ wird stillgelegt (`track.enabled = false`); die Verbindung bleibt
+ stehen, der Ton laeuft weiter, und beim Wiedereinschalten ist das
+ Bild sofort da statt nach einem neuen Verbindungsaufbau. Dieser
+ Weg hier sagt nur allen ANDEREN Bescheid, damit im Fenster
+ „Kamera aus" steht und nicht ein schwarzes Rechteck, das nach
+ einem Fehler aussieht.
+
+ JEDER SENDER DARF DAS FUER SICH -- Host wie Gast. Ein Gast, der
+ sein eigenes Bild nicht abschalten kann, muesste den Host darum
+ bitten. */
+reaktionRouter.post("/workspace/api/reaktion/senden", express.json({ limit: "2kb" }),
+ (req, res) => {
+ try {
+ const s = sendung();
+ if (s.stand === "zu") return res.status(409).json({ fehler: "geschlossen" });
+
+ const istHost = HOST_ROLLEN.has(req.person.rolle);
+ const binGast = !!db().prepare("SELECT 1 FROM reaktion_gaeste WHERE person_id = ?")
+ .get(req.person.id);
+ if (!istHost && !binGast) return res.status(403).json({ fehler: "sendest_nicht" });
+
+ /* TEILWEISE AENDERN IST ERLAUBT. Wer nur die Kamera schaltet,
+ schickt nur die Kamera -- sonst muesste jede Seite immer
+ beide Werte mitschicken und koennte dabei den anderen
+ versehentlich zuruecksetzen. */
+ const felder = [];
+ const werte = [];
+ if (req.body?.kamera_aus !== undefined) {
+ felder.push("kamera_aus = ?"); werte.push(req.body.kamera_aus ? 1 : 0);
+ }
+ if (req.body?.mikro_aus !== undefined) {
+ felder.push("mikro_aus = ?"); werte.push(req.body.mikro_aus ? 1 : 0);
+ }
+ if (!felder.length) return res.status(400).json({ fehler: "nichts_zu_tun" });
+
+ const geschrieben = db().prepare(
+ `UPDATE reaktion_dabei SET ${felder.join(", ")} WHERE person_id = ?`)
+ .run(...werte, req.person.id);
+ /* WER NICHT IN DER LISTE STEHT, SITZT NICHT IM SAAL. Das ist
+ kein Sonderfall, sondern ein Hinweis: Die Seite hat sich
+ zwei Minuten nicht gemeldet. Stillschweigend nichts zu tun
+ waere ein Knopf, der nicht wirkt. */
+ if (!geschrieben.changes) return res.status(409).json({ fehler: "nicht_dabei" });
+
+ const l = lage(req.person);
+ melden("reaktion", { was: "senden", lage: oeffentlich(l) });
+ res.json(l);
+ } catch (fehler) {
+ console.error("[reaktion] Kamera:", fehler?.message);
+ res.status(503).json({ fehler: "nicht_verfuegbar" });
+ }
+ });
+
+/* ---------- Der Chat als Ganzes ----------------------------------------
+ „Chat" aus Filipes Liste der acht Steuerungen. Gemeint ist nicht
+ die Moderation -- die gab es schon -- sondern der Chat selbst.
+ Einen Beitrag wegzunehmen, nachdem er stand, ist etwas anderes,
+ als ihn gar nicht erst zuzulassen. */
+reaktionRouter.post("/workspace/api/reaktion/chatmodus", nurHost, express.json({ limit: "2kb" }),
+ (req, res) => {
+ try {
+ const modus = String(req.body?.modus ?? "");
+ if (!CHAT_MODI.includes(modus)) return res.status(400).json({ fehler: "chatmodus_unbekannt" });
+ db().prepare("UPDATE reaktion SET chat_modus = ? WHERE id = 1").run(modus);
+ protokolliere(req.person.id, "reaktion_chatmodus", modus, echteIp(req));
+ const l = lage(req.person);
+ melden("reaktion", { was: "chatmodus", lage: oeffentlich(l) });
+ res.json(l);
+ } catch (fehler) {
+ console.error("[reaktion] Chatmodus:", fehler?.message);
+ res.status(503).json({ fehler: "nicht_verfuegbar" });
+ }
+ });
+
reaktionRouter.post("/workspace/api/reaktion/layout", nurHost, express.json({ limit: "2kb" }),
(req, res) => {
try {
@@ -785,6 +905,14 @@ reaktionRouter.post("/workspace/api/reaktion/chat", express.json({ limit: "8kb"
try {
const s = sendung();
if (s.stand === "zu") return res.status(409).json({ fehler: "geschlossen" });
+ /* DIE STUFE GILT HIER, NICHT NUR IN DER OBERFLAECHE. Ein Feld,
+ das ausgegraut ist, haelt niemanden auf, der die Schnittstelle
+ kennt. */
+ if (!darfSchreiben(req.person, s)) {
+ return res.status(403).json({
+ fehler: s.chat_modus === "team" ? "chat_nur_team" : "chat_zu",
+ });
+ }
const text = String(req.body?.text ?? "").trim().slice(0, TEXT_MAX);
if (!text) return res.status(400).json({ fehler: "leer" });
if (zuSchnell(req.person.id)) return res.status(429).json({ fehler: "zu_schnell" });
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index 32561ef7..0bfb27cc 100644
--- a/workspace/anruf-probe.html
+++ b/workspace/anruf-probe.html
@@ -16,8 +16,8 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
-
-
+
+
-
+