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`. --> - - + + - +