diff --git a/server/pruef-modi-verborgen.mjs b/server/pruef-modi-verborgen.mjs index 4d59cbb1..a7bf517f 100644 --- a/server/pruef-modi-verborgen.mjs +++ b/server/pruef-modi-verborgen.mjs @@ -802,6 +802,98 @@ console.log("\n=== 8b. Der Hinweis nach zwei Fehlversuchen ===\n"); diese Person in jeder Liste DANACH auf -- und jeder spaetere Abschnitt maesse einen Zustand, den es im Betrieb nicht gibt. -------------------------------------------------------------------- */ +/* --------------------------------------------------------------------- + 8b. DER LIVE-CHAT VERRAET DIE ROLLE NICHT (07.10.2026) + + DIE LUECKE, DIE ES BIS HEUTE GAB: `pruef-modi-wortleck` durchsucht + ausgelieferte DATEIEN. Sie kann nicht sehen, was der Server in einer + Antwort mitschickt -- und genau dort stand der Rollenname. + + `/workspace/api/reaktion/chat` waehlte `rolle` aus der Tabelle und + gab sie unveraendert heraus. Eine Nachricht eines Modis trug damit + `"rolle":"modi"` an jeden im Saal, auch an Creator und Scouts. Ein + Blick in die Netzwerkspur genuegte; im Quelltext war nichts zu sehen, + weil nichts im Quelltext stand. + + GEMESSEN WIRD DER ROHE TEXT der Antwort, nicht das ausgewertete + Objekt: Eine Rolle kann auch in einem Feld stecken, nach dem niemand + fragt. Was nicht im Text steht, steht nirgends. + + DIE ZEILE KOMMT PER DATENBANK HINEIN und nicht ueber das Schreibfeld: + Ob ein Modi gerade schreiben DARF, haengt am Zustand der Sendung + (zu / nur Team / offen). Diese Pruefung fragt aber nicht, wer + schreiben darf, sondern was beim HERAUSGEHEN passiert -- und das soll + sie auch dann beantworten koennen, wenn der Saal geschlossen ist. + -------------------------------------------------------------------- */ +console.log("\n=== 8b. Der Live-Chat gibt die Rolle nicht heraus ===\n"); +{ + const dc = new DatabaseSync(process.env.WORKSPACE_DB); + dc.prepare(`INSERT INTO reaktion_chat (person_id, name, rolle, text, erstellt) + VALUES (?,?,?,?,?)`) + .run(idModi1, "Modi Marina", "modi", "Hallo aus dem Saal", jetzt); + dc.close(); + + const anC = await anmelden("creator", "CODE-ANNA-0001"); + if (anC.status !== 200) { + unklar(`Creator: Anmeldung misslang (${anC.status}) -- der Chat blieb ungeprueft`); + } else { + const a = await fetch(BASIS + "/workspace/api/reaktion/chat", + { headers: { cookie: anC.keks } }); + const roh = await a.text(); + ok(a.status === 200, `der Creator bekommt den Chat (${a.status})`); + /* DIE ZAHL IN DER BEDINGUNG: Eine leere Antwort enthaelt den Namen + auch nicht -- und bewiese nichts. Der Beitrag MUSS drin sein. */ + const drin = roh.includes("Hallo aus dem Saal"); + ok(drin, `der Beitrag des Modis steht wirklich darin (${roh.length} Zeichen)`); + /* GEPRUEFT WIRD DER SCHLUESSEL, NICHT DAS WORT -- und der + Unterschied ist der ganze Punkt dieser Pruefung. + + Beim ersten Anlauf stand hier `/\bmodis?\b/i` und meldete rot. + Der Fund war echt und trotzdem kein Mangel: In der Antwort steht + `"abzeichen":{"wort":"Modi",...}` -- das Wort, das im Chat + SICHTBAR neben dem Namen klebt, weil Filipe das ausdruecklich so + wollte („die modis, linke hand und rechte hand soll im chat extra + aussehen"). Ein Wort, das auf dem Bildschirm steht, kann man + nicht aus der Antwort heraushalten, in der es steht. + + WAS WIRKLICH GEHEIM IST, ist der Schluessel: `modi` als Wert in + `rolle`, als Vergleich im Code, als Kennung im CSS. Der ist im + ganzen Haus KLEINGESCHRIEBEN -- in der Datenbank, in den + Rollenlisten, in den Auswahlen. Das Anzeigewort ist + grossgeschrieben. Deshalb hier ausdruecklich OHNE `i`. + + Und weil Grossschreibung allein eine duenne Grenze waere, kommt + die zweite Bedingung dazu: Das Feld `rolle` darf in der Antwort + gar nicht vorkommen. Beides zusammen faengt sowohl den alten + Fall (`"rolle":"modi"`) als auch einen neuen, der ihn anders + schreibt. */ + const schluesselDrin = /\bmodis?\b/.test(roh) || /"rolle"/.test(roh); + ok(drin && !schluesselDrin, + `der Rollenschluessel steht nirgends in der Antwort` + + (schluesselDrin ? ` -- GEFUNDEN in: ${roh.slice(0, 220)}` : "")); + /* GEGENPROBE ZUM MESSGERAET: Genau der Text, der frueher + herausging, MUSS anschlagen. Ohne sie hiesse „nichts gefunden" + auch „die Suche findet nie etwas". */ + const alteAntwort = '{"beitraege":[{"id":1,"name":"Marina","rolle":"modi"}]}'; + ok(/\bmodis?\b/.test(alteAntwort) && /"rolle"/.test(alteAntwort), + "Gegenprobe: die alte Antwort wuerde erkannt"); + ok(!/\bmodis?\b/.test('{"wort":"Modi"}'), + "Gegenprobe: das sichtbare Anzeigewort schlaegt nicht an"); + /* UND DAS ABZEICHEN IST TROTZDEM DA. Ohne diese Zeile waere der + bequemste Weg zu einem gruenen Haken, das Abzeichen ganz + wegzulassen -- dann verraet nichts mehr etwas, und Filipes Wunsch + („die modis ... soll im chat extra aussehen") waere stillschweigend + weg. */ + let geparst = null; + try { geparst = JSON.parse(roh); } catch { /* bleibt null */ } + const meiner = (geparst?.beitraege || []).find((b) => b.text === "Hallo aus dem Saal"); + ok(!!meiner?.abzeichen?.wort, + `das Abzeichen kommt trotzdem mit (${JSON.stringify(meiner?.abzeichen)})`); + ok(meiner?.rolle === undefined, + `und das Feld "rolle" gibt es gar nicht mehr (${JSON.stringify(meiner?.rolle)})`); + } +} + console.log("\n=== 9. Gegenprobe: kann diese Pruefung ueberhaupt anschlagen? ===\n"); { const d2 = new DatabaseSync(process.env.WORKSPACE_DB); diff --git a/server/pruef-reaktion.mjs b/server/pruef-reaktion.mjs index 7fe4c79b..c8d58397 100644 --- a/server/pruef-reaktion.mjs +++ b/server/pruef-reaktion.mjs @@ -1510,23 +1510,61 @@ melde("=== 16. Rollen und Moderation auf der Seite ==="); /* JEDE ROLLE MIT EIGENEM AUSSEHEN BEKOMMT AUCH EINE REGEL. Ein Abzeichen ohne Stil ist ein Wort ohne Kasten -- es faellt nicht auf, und damit ist der ganze Zweck weg. */ - const abzeichen = [...js.matchAll(/^ {4}(admin|hand|linke|modi): \['([^']+)', '([^']+)'\],/gm)]; - ok(abzeichen.length === 4, `vier Rollen tragen ein Abzeichen (${abzeichen.length})`); - for (const [, rolle, wort, marke] of abzeichen) { - ok(wort.length >= 3 && wort.length <= 6, `„${wort}" fuer ${rolle} ist kurz genug`); - ok(css.includes(`.beitrag__rolle[data-rolle="${marke}"]`), - `und „${marke}" hat eine eigene Farbe`); + /* ==== DIE ABZEICHEN STEHEN SEIT DEM 07.10.2026 AUF DEM SERVER ==== + + Sie standen in reaktion.js -- und damit stand der Schluessel einer + verborgenen Rolle in einer Datei, die jeder bekommt. Jetzt + entscheidet der Server (CHAT_ABZEICHEN in workspace-reaktion.js) + und schickt Wort und Farbkennung fertig mit. + + ==== UND DIESE PRUEFUNG KANN JETZT NICHT MEHR AUSSETZEN ========== + + Vorher lief sie ueber die GEFUNDENEN Treffer: `for (const … of + abzeichen)`. Als die Liste umzog, fand das Suchmuster nichts -- + und mit ihm verschwanden ZWOELF Pruefungen lautlos. Der Lauf fiel + von 421 auf 409, und nur EINE Zeile wurde rot. Genau davor warnt + das Projektgedaechtnis (28.08.2026): „bedingte Bloecke sind stille + Aussetzer; findet die Pruefung ihre Voraussetzung nicht, muss das + ein Fehler sein, kein Ueberspringen." + + Deshalb laeuft die Schleife jetzt ueber die ERWARTETEN vier Rollen, + nicht ueber die gefundenen. Fehlt eine, wird ihre Zeile rot -- + und die Anzahl bleibt dieselbe. */ + const dienst = readFileSync(join(HIER, "workspace-reaktion.js"), "utf8"); + const ERWARTET = ["admin", "hand", "linke", "modi"]; + const gefunden = new Map([...dienst.matchAll( + /^\s{2}(admin|hand|linke|modi):\s*\{\s*wort:\s*"([^"]+)",\s*ton:\s*"([^"]+)"\s*\},/gm)] + .map((m) => [m[1], { wort: m[2], ton: m[3] }])); + ok(gefunden.size === ERWARTET.length, + `vier Rollen tragen ein Abzeichen (${gefunden.size})`); + + /* DAS WORT IST NICHT DER RANG. Filipe steht nie ueber seinem Team -- + Woerter wie „Chef" oder „Leitung" haetten hier nichts zu suchen. */ + const rangwoerter = ["Chef", "Boss", "Leiter", "Leitung", "Kopf"]; + for (const rolle of ERWARTET) { + const a = gefunden.get(rolle); + ok(!!a && a.wort.length >= 3 && a.wort.length <= 6, + `„${a?.wort ?? "fehlt"}" fuer ${rolle} ist kurz genug`); + /* Ein Abzeichen ohne Stil ist ein Wort ohne Kasten -- es faellt + nicht auf, und damit ist der ganze Zweck weg. */ + ok(!!a && css.includes(`.beitrag__rolle[data-rolle="${a.ton}"]`), + `und „${a?.ton ?? "fehlt"}" hat eine eigene Farbe`); + ok(!!a && !rangwoerter.includes(a.wort), + `„${a?.wort ?? "fehlt"}" ist kein Rang`); } /* GEGENPROBE: Kann diese Pruefung „nein" sagen? */ ok(!css.includes('.beitrag__rolle[data-rolle="gibtesnicht"]'), "Gegenprobe: eine erfundene Marke hat keine Regel"); - /* DAS WORT IST NICHT DER RANG. Filipe steht nie ueber seinem Team -- - Woerter wie „Chef" oder „Leitung" haetten hier nichts zu suchen. */ - const rangwoerter = ["Chef", "Boss", "Leiter", "Leitung", "Kopf"]; - for (const [, , wort] of abzeichen) { - ok(!rangwoerter.includes(wort), `„${wort}" ist kein Rang`); - } + /* UND DIE OBERFLAECHE KENNT KEINE ROLLENNAMEN MEHR. Das ist der + Punkt der ganzen Umstellung -- ohne diese Zeile koennte jemand die + Liste spaeter „der Einfachheit halber" zurueckholen, und alles + waere wieder offen. `pruef-modi-wortleck` faende es zwar auch, + aber hier steht der Grund dabei. */ + ok(!/ABZEICHEN\s*=\s*\{/.test(js), + "die Seite fuehrt keine eigene Abzeichenliste mehr"); + ok(js.includes("b.abzeichen"), + "sie nimmt, was der Server schickt"); ok(html.includes('id="chat-meldungen"'), "der Meldungs-Knopf steht in der Seite"); ok(css.includes(".schiene__meldungen"), "und hat eine Regel"); diff --git a/server/workspace-reaktion.js b/server/workspace-reaktion.js index 3ed9f590..19567000 100644 --- a/server/workspace-reaktion.js +++ b/server/workspace-reaktion.js @@ -1876,12 +1876,65 @@ reaktionRouter.post("/workspace/api/reaktion/banner/:id(\\d+)/schieben", nurHost /* ---------- Der Live-Chat ----------------------------------------------- */ +/* ===================================================================== + DAS ABZEICHEN STATT DER ROLLE (07.10.2026) + ===================================================================== + + GEFUNDEN BEIM NACHGEHEN EINES ROTEN LAUFS, nicht gesucht: + `pruef-modi-wortleck` meldete drei Fundstellen des Rollennamens in + ausgelieferten Dateien. Beim Nachsehen, wie der Name dorthin kam, + stellte sich heraus, dass er gar nicht erst dort stehen muss -- und + dass er ausserdem an einer Stelle steht, die jene Pruefung gar nicht + sehen KANN: in den DATEN. + + Hier stand `SELECT id, person_id, name, rolle, ...` und darunter ein + schlichtes `res.json`. Eine Chatnachricht eines Modis trug damit + `"rolle":"modi"` an JEDEN im Saal -- auch an Creator und Scouts, vor + denen der Zugang verborgen sein soll. Ein Blick in die Netzwerkspur + genuegte. + + DIE OBERFLAECHE BRAUCHTE DIE ROLLE NIE. Sie tat damit genau zwei + Dinge: ein Abzeichen malen und "gehoert zum Team" setzen. Beides + wird jetzt HIER entschieden und fertig verschickt. + + WARUM DAS WORT „Modi" TROTZDEM MITGEHT: Es steht ohnehin sichtbar + auf dem Abzeichen -- Filipe wollte das ausdruecklich („die modis, + linke hand und rechte hand soll im chat extra aussehen"), und das + Modi-Team ist oeffentlich. Verborgen ist nicht das TEAM, sondern + dass es einen eigenen ZUGANG gibt; und genau der Schluessel `modi` + verlaesst das Haus jetzt nicht mehr. Die Farbkennung heisst + `moderation` und benennt die Aufgabe, nicht den Zugang. */ +const CHAT_ABZEICHEN = { + admin: { wort: "Dogi", ton: "dogi" }, + hand: { wort: "Team", ton: "team" }, + linke: { wort: "Team", ton: "team" }, + modi: { wort: "Modi", ton: "moderation" }, +}; + +/** Ein Beitrag, wie ihn der Saal sehen darf -- ohne Rollenschluessel. + * + * AN EINER STELLE, weil es zwei Ausgaenge gibt: die Liste beim Laden + * 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) { + if (!b) return b; + const { rolle, ...rest } = b; + 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", + }; +} + reaktionRouter.get("/workspace/api/reaktion/chat", (req, res) => { try { 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() }); + res.json({ beitraege: reihen.reverse().map(fuerDenSaal) }); } catch (fehler) { console.error("[reaktion] Chat:", fehler?.message); res.status(503).json({ fehler: "nicht_verfuegbar" }); @@ -1905,9 +1958,13 @@ reaktionRouter.post("/workspace/api/reaktion/chat", express.json({ limit: "8kb" const { lastInsertRowid } = db().prepare(`INSERT INTO reaktion_chat (person_id, name, rolle, text, erstellt) VALUES (?,?,?,?,?)`) .run(req.person.id, req.person.name, req.person.rolle, text, jetzt()); - const beitrag = db().prepare( + const beitrag = fuerDenSaal(db().prepare( "SELECT id, person_id, name, rolle, text, erstellt FROM reaktion_chat WHERE id = ?") - .get(Number(lastInsertRowid)); + .get(Number(lastInsertRowid))); + /* BEIDE WEGE, nicht nur einer: `melden` schiebt den Beitrag ueber + den Ereignisstrom an alle im Saal, `res.json` an den Schreiber + selbst. Wer nur den einen saeubert, hat die Rolle weiterhin bei + allen anderen -- also genau dort, wo sie nicht hingehoert. */ melden("reaktion_chat", { was: "neu", beitrag }); res.status(201).json(beitrag); } catch (fehler) { diff --git a/workspace/anleitung.html b/workspace/anleitung.html index d2a66c7a..579cd84d 100644 --- a/workspace/anleitung.html +++ b/workspace/anleitung.html @@ -7,17 +7,17 @@ - - - - - + + + + + - - + +
@@ -289,16 +289,16 @@ Und anleitung.js braucht es doppelt: Symbol, Farbe, Name und Adresse jeder Karte kommen von dort, nicht vom Server. --> - + - - - - - - + + + + + +