Reaction: Rollen sichtbar, Moderation im Live-Chat
Filipes Wunsch: „die modis, linke hand und rechte hand soll im chat extra aussehen und auch sachen wie stummschaltungen, sperrungen, meldunge und alles mögliche im chat machen können falls sich jemand nicht benimmt." WER SPRICHT Jeder Beitrag aus dem Team trägt jetzt ein Abzeichen: „Dogi", „Team" (beide Hände) und „Modi". Ein WORT und nicht nur eine Farbe — rund acht Prozent der Männer unterscheiden Rot und Grün schlecht, und hier hängt an der Unterscheidung etwas. Gedeckte Töne auf schwacher Fläche, 11,52 px (die Hausgrenze ist 11,5), weil daneben ein Video läuft. Die Wörter sagen den RANG nicht: Filipe steht nie über seinem Team. Eine Prüfung schlägt an, wenn dort „Chef", „Boss" oder „Leitung" stünde. WAS DIE MODERATION KANN Am Namen öffnet sich ein Menü mit vier Wegen: Beitrag wegnehmen, 10 Minuten stumm, aus der Sendung — und der Verweis in den Treff, wo längere Maßnahmen hingehören (mit Begründungspflicht und Frist in Tagen). Die Reaction baut kein zweites Sperrsystem daneben: Wer im Treff eine Pause hat, schreibt hier auch nicht. Neu ist nur, was der Treff nicht hat — MINUTEN. Höchstens eine Stunde; wer länger etwas braucht, nimmt den Treff. Nicht gegen das Team: Wer einen Modi stummschalten könnte, hätte einen Weg, die Moderation selbst auszuschalten, mitten in der Sendung. MELDEN DARF JEDER Die Moderation sieht nicht alles, wer mitliest schon. Zweimal melden zählt einmal (sonst füllt einer allein die Liste). Bei der Moderation erscheint im Kopf der Schiene „1 Meldung" — nur wenn etwas offen ist; eine Zahl, die immer dasteht und meistens null ist, wird nach drei Tagen nicht mehr gelesen. VIER FEHLER, DIE DABEI AUFGEFALLEN SIND 1. `oeffentlich()` nahm `meldungen` und `massnahmen` NICHT aus dem Rundruf. Der Rundruf wird aus dem Stand dessen gebaut, der gerade etwas getan hat — bei einer Moderationshandlung wären der gemeldete Text, der Name des Gemeldeten und der Name des MELDERS an jeden im Saal gegangen. Wer meldet, muss sich darauf verlassen können, dass das niemand sieht. 2. `meldung.js` hatte zwei Schlüssel doppelt: `geschlossen` und `nur_leitung`. Der spätere gewinnt stillschweigend — in der Hilfe stand dadurch „Der Saal ist zu". Ein Wort, ein Satz; eine Prüfung hält das jetzt fest. 3. Wer stummgeschaltet war, bekam bei OFFENEM Chat „Der Chat ist gerade zu" — der Aufrufer reimte sich den Grund aus der Chat-Stufe zusammen. Jetzt gibt `schreibGrund()` den echten Grund zurück. 4. Wer rausgenommen wurde, merkte nichts: Das Video lief weiter, nur die Anwesenheitsmeldung schlug still fehl. Jetzt hält alles an und es steht ein Satz da, der sagt, dass es nur für heute gilt. UND EINER, DEN NUR DAS BILDSCHIRMFOTO GEFUNDEN HAT Das Menü lag messbar komplett im Fenster (x=1099..1315 von 1440, y=652..840 von 900) und war trotzdem abgeschnitten: Die Chatliste rollt, und ein rollender Vorfahre beschneidet sein Kind. „Im Fenster" und „sichtbar" sind zwei verschiedene Fragen, und ich hatte die falsche gemessen. Das Menü hängt jetzt fest am Fenster und klappt nach oben oder links, wo kein Platz ist. Die Messung fasst seitdem mit `elementFromPoint` an jeden Knopf an, statt Rechtecke zu vergleichen. GEPRUEFT pruef-reaktion: 216 Prüfungen (vorher 156), 0 Fehler — zwei neue Abschnitte. mess-reaktion misst die Moderation jetzt im echten Browser: Abzeichen, Überlauf der Schiene, Menü, Stummschaltung beim Betroffenen, Meldung bis zur Moderation, Rauswurf. Dazu grün: pruef-meldungen, pruef-struktur, pruef-css-klassen, pruef-tippziele, pruef-deutsche-texte, pruef-hilfe. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+263
-1
@@ -580,7 +580,7 @@ melde("=== 9. Beenden ===");
|
||||
const c = await roh(CREW, "/workspace/api/reaktion/chat", {
|
||||
method: "POST", keks: k.gast, rumpf: { text: "Hallo?" },
|
||||
});
|
||||
ok(c.code === 409 && c.j?.fehler === "geschlossen",
|
||||
ok(c.code === 409 && c.j?.fehler === "saal_zu",
|
||||
`und schreiben geht nicht mehr (${c.code}/${c.j?.fehler})`);
|
||||
}
|
||||
|
||||
@@ -1095,6 +1095,268 @@ melde("=== 14. Keine doppelten Wege ===");
|
||||
ok(reaktionswege >= 17, `die Reaction bringt ${reaktionswege} eigene Wege mit`);
|
||||
}
|
||||
|
||||
|
||||
/* =======================================================================
|
||||
15. MODERATION IM LIVE-CHAT
|
||||
|
||||
Der heikelste Teil der ganzen Reaction: Hier kann jemand einem
|
||||
anderen das Wort nehmen. Geprueft wird deshalb nicht nur, dass es
|
||||
GEHT, sondern vor allem, wer es NICHT darf und was dabei nicht
|
||||
herauskommen darf.
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 15. Moderation im Live-Chat ===");
|
||||
{
|
||||
/* Sauberer Anfang: Der Chat steht auf „offen". */
|
||||
await roh(CREW, "/workspace/api/reaktion/chatmodus", {
|
||||
method: "POST", keks: k.admin, rumpf: { modus: "offen" },
|
||||
});
|
||||
|
||||
/* ---- Wer eine Massnahme verhaengen darf ---- */
|
||||
const zuschauer = await roh(CREW, "/workspace/api/reaktion/massnahme", {
|
||||
method: "POST", keks: k.gast, rumpf: { person_id: IDS.gast2, art: "stumm", minuten: 10 },
|
||||
});
|
||||
ok(zuschauer.code === 403 && zuschauer.j?.fehler === "nur_moderation",
|
||||
`ein Zuschauer schaltet niemanden stumm (${zuschauer.code}/${zuschauer.j?.fehler})`);
|
||||
|
||||
/* ---- Nicht gegen das Team ----
|
||||
Wer einen Modi stummschalten koennte, haette einen Weg, die
|
||||
Moderation selbst auszuschalten -- mitten in der Sendung. */
|
||||
const gegenTeam = await roh(CREW, "/workspace/api/reaktion/massnahme", {
|
||||
method: "POST", keks: k.modi, rumpf: { person_id: IDS.linke, art: "stumm", minuten: 10 },
|
||||
});
|
||||
ok(gegenTeam.code === 403 && gegenTeam.j?.fehler === "nicht_gegen_team",
|
||||
`ein Modi schaltet die linke Hand nicht stumm (${gegenTeam.code}/${gegenTeam.j?.fehler})`);
|
||||
const gegenHost = await roh(CREW, "/workspace/api/reaktion/massnahme", {
|
||||
method: "POST", keks: k.modi, rumpf: { person_id: IDS.admin, art: "raus" },
|
||||
});
|
||||
ok(gegenHost.code === 403, `und DogFather erst recht nicht (${gegenHost.code})`);
|
||||
|
||||
/* ---- Die Dauer hat eine Obergrenze ---- */
|
||||
const { STUMM_MAX_MINUTEN, MASSNAHMEN } = await import("./reaktion-tabellen.js");
|
||||
for (const [min, wort] of [[0, "null"], [-5, "minus"], [STUMM_MAX_MINUTEN + 1, "zu lang"]]) {
|
||||
const a = await roh(CREW, "/workspace/api/reaktion/massnahme", {
|
||||
method: "POST", keks: k.modi, rumpf: { person_id: IDS.gast2, art: "stumm", minuten: min },
|
||||
});
|
||||
ok(a.code === 400 && a.j?.fehler === "dauer_unklar",
|
||||
`${min} Minuten (${wort}) wird abgelehnt (${a.code})`);
|
||||
}
|
||||
const quatsch = await roh(CREW, "/workspace/api/reaktion/massnahme", {
|
||||
method: "POST", keks: k.modi, rumpf: { person_id: IDS.gast2, art: "verbannen" },
|
||||
});
|
||||
ok(quatsch.code === 400 && quatsch.j?.fehler === "massnahme_unbekannt",
|
||||
`eine erfundene Massnahme wird abgelehnt (${quatsch.code})`);
|
||||
ok(MASSNAHMEN.length === 2 && MASSNAHMEN.includes("stumm") && MASSNAHMEN.includes("raus"),
|
||||
`es gibt genau zwei kurze Massnahmen: ${MASSNAHMEN.join(", ")}`);
|
||||
|
||||
/* ---- Stumm wirkt -- und sagt den richtigen Grund ----
|
||||
Der Chat steht dabei auf „offen". Genau das ist der Fall, der
|
||||
vorher die falsche Auskunft gab („Der Chat ist gerade zu"). */
|
||||
const vorher = await roh(CREW, "/workspace/api/reaktion/chat", {
|
||||
method: "POST", keks: k.gast2, rumpf: { text: "vorher geht es" },
|
||||
});
|
||||
ok(vorher.code === 201, `vor der Massnahme schreibt gast2 (${vorher.code})`);
|
||||
|
||||
const stumm = await roh(CREW, "/workspace/api/reaktion/massnahme", {
|
||||
method: "POST", keks: k.modi, rumpf: { person_id: IDS.gast2, art: "stumm", minuten: 10 },
|
||||
});
|
||||
ok(stumm.code === 200, `ein Modi schaltet einen Zuschauer stumm (${stumm.code})`);
|
||||
|
||||
const nachher = await roh(CREW, "/workspace/api/reaktion/chat", {
|
||||
method: "POST", keks: k.gast2, rumpf: { text: "und jetzt?" },
|
||||
});
|
||||
ok(nachher.code === 403 && nachher.j?.fehler === "stumm_geschaltet",
|
||||
`danach nicht mehr -- und der Grund stimmt (${nachher.code}/${nachher.j?.fehler})`);
|
||||
|
||||
const sichtGast2 = await roh(CREW, "/workspace/api/reaktion", { keks: k.gast2 });
|
||||
ok(sichtGast2.j?.ich?.darf_schreiben === false
|
||||
&& sichtGast2.j?.ich?.schreib_grund === "stumm_geschaltet",
|
||||
"und er steht auch in seinem eigenen Stand");
|
||||
ok(sichtGast2.j?.ich?.massnahme?.art === "stumm" && !!sichtGast2.j.ich.massnahme.bis,
|
||||
"mit Art und Ablaufzeitpunkt");
|
||||
|
||||
/* ZUSEHEN GEHT WEITER. „stumm" ist nicht „raus" -- wer das
|
||||
verwechselt, wirft jemanden fuer einen Satz aus der Sendung. */
|
||||
const nochDabei = await roh(CREW, "/workspace/api/reaktion/dabei", {
|
||||
method: "POST", keks: k.gast2,
|
||||
});
|
||||
ok(nochDabei.code === 200, `zusehen geht weiter (${nochDabei.code})`);
|
||||
|
||||
/* ---- Die Massnahmen sind keine oeffentliche Liste ----
|
||||
In `meldungen` steht der Name dessen, der gemeldet hat. Wer
|
||||
meldet, muss sich darauf verlassen koennen, dass das niemand
|
||||
sieht -- sonst meldet beim naechsten Mal keiner mehr. */
|
||||
ok(Array.isArray(stumm.j?.massnahmen) && stumm.j.massnahmen.length >= 1,
|
||||
"die Moderation sieht die laufenden Massnahmen");
|
||||
ok(sichtGast2.j?.massnahmen === null && sichtGast2.j?.meldungen === null,
|
||||
"ein Zuschauer sieht weder Massnahmen noch Meldungen");
|
||||
const sichtModi = await roh(CREW, "/workspace/api/reaktion", { keks: k.modi });
|
||||
ok(Array.isArray(sichtModi.j?.meldungen), "ein Modi schon");
|
||||
|
||||
/* GEGENPROBE AM RUNDRUF: `oeffentlich()` muss beide Felder
|
||||
wegnehmen. Es aus dem Quelltext zu lesen ist hier genauer als
|
||||
jede Messung -- der Rundruf geht an alle, und ein Fehler darin
|
||||
waere erst aufgefallen, wenn jemand mitgelesen haette. */
|
||||
{
|
||||
const quelle = readFileSync(join(HIER, "workspace-reaktion.js"), "utf8");
|
||||
const zeile = quelle.match(/const \{([^}]*)\} = l;/);
|
||||
const weg = (zeile ? zeile[1] : "").split(",").map((x) => x.trim().replace(/\.\.\..*/, ""));
|
||||
for (const feld of ["zuschauer", "ich", "liste", "meldungen", "massnahmen"]) {
|
||||
ok(weg.includes(feld), `oeffentlich() nimmt „${feld}" aus dem Rundruf`);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Wieder aufheben ---- */
|
||||
const auf = await roh(CREW, `/workspace/api/reaktion/massnahme/${IDS.gast2}`, {
|
||||
method: "DELETE", keks: k.gast,
|
||||
});
|
||||
ok(auf.code === 403, `ein Zuschauer hebt sie nicht auf (${auf.code})`);
|
||||
const auf2 = await roh(CREW, `/workspace/api/reaktion/massnahme/${IDS.gast2}`, {
|
||||
method: "DELETE", keks: k.modi,
|
||||
});
|
||||
ok(auf2.code === 200, `ein Modi schon (${auf2.code})`);
|
||||
const wieder = await roh(CREW, "/workspace/api/reaktion/chat", {
|
||||
method: "POST", keks: k.gast2, rumpf: { text: "wieder da" },
|
||||
});
|
||||
ok(wieder.code === 201, `Gegenprobe: danach schreibt er wieder (${wieder.code})`);
|
||||
|
||||
/* ---- Melden ---- */
|
||||
const zuMelden = wieder.j.id;
|
||||
const selbst = await roh(CREW, `/workspace/api/reaktion/chat/${zuMelden}/melden`, {
|
||||
method: "POST", keks: k.gast2, rumpf: {},
|
||||
});
|
||||
ok(selbst.code === 400 && selbst.j?.fehler === "eigener_beitrag",
|
||||
`den eigenen Beitrag meldet man nicht (${selbst.code})`);
|
||||
|
||||
const gemeldet = await roh(CREW, `/workspace/api/reaktion/chat/${zuMelden}/melden`, {
|
||||
method: "POST", keks: k.gast, rumpf: { grund: "unfreundlich" },
|
||||
});
|
||||
ok(gemeldet.code === 201, `ein Zuschauer darf melden (${gemeldet.code})`);
|
||||
|
||||
/* ZWEIMAL MELDEN IST EINMAL. Ohne die Sperre koennte einer allein
|
||||
die Liste fuellen, und die Moderation sieht die echte nicht. */
|
||||
const nochmal = await roh(CREW, `/workspace/api/reaktion/chat/${zuMelden}/melden`, {
|
||||
method: "POST", keks: k.gast, rumpf: {},
|
||||
});
|
||||
ok(nochmal.code === 200 && nochmal.j?.schon === true,
|
||||
`zweimal melden zaehlt einmal -- ohne Fehlermeldung (${nochmal.code})`);
|
||||
|
||||
const beiModi = await roh(CREW, "/workspace/api/reaktion", { keks: k.modi });
|
||||
const m = (beiModi.j?.meldungen || []).find((x) => x.beitrag_id === zuMelden);
|
||||
ok(!!m, "die Meldung steht bei der Moderation");
|
||||
ok(m?.text === "wieder da" && m?.wer_id === IDS.gast2,
|
||||
"mit dem Text und der Person, um die es geht");
|
||||
|
||||
/* ---- Erledigen ---- */
|
||||
const nein = await roh(CREW, `/workspace/api/reaktion/meldung/${m.id}/erledigt`, {
|
||||
method: "POST", keks: k.gast,
|
||||
});
|
||||
ok(nein.code === 403, `ein Zuschauer erledigt keine Meldung (${nein.code})`);
|
||||
const ja = await roh(CREW, `/workspace/api/reaktion/meldung/${m.id}/erledigt`, {
|
||||
method: "POST", keks: k.modi,
|
||||
});
|
||||
ok(ja.code === 200, `ein Modi schon (${ja.code})`);
|
||||
ok(!(ja.j?.meldungen || []).some((x) => x.id === m.id),
|
||||
"und sie ist aus der offenen Liste");
|
||||
const zweimal = await roh(CREW, `/workspace/api/reaktion/meldung/${m.id}/erledigt`, {
|
||||
method: "POST", keks: k.modi,
|
||||
});
|
||||
ok(zweimal.code === 409 && zweimal.j?.fehler === "nicht_offen",
|
||||
`zweimal erledigen geht nicht (${zweimal.code})`);
|
||||
|
||||
/* ---- „raus" heisst raus ---- */
|
||||
const raus = await roh(CREW, "/workspace/api/reaktion/massnahme", {
|
||||
method: "POST", keks: k.hand, rumpf: { person_id: IDS.gast2, art: "raus" },
|
||||
});
|
||||
ok(raus.code === 200, `die rechte Hand nimmt jemanden aus der Sendung (${raus.code})`);
|
||||
const rein = await roh(CREW, "/workspace/api/reaktion/dabei", {
|
||||
method: "POST", keks: k.gast2,
|
||||
});
|
||||
ok(rein.code === 403 && rein.j?.fehler === "aus_der_sendung",
|
||||
`er kommt nicht mehr herein -- und erfaehrt warum (${rein.code}/${rein.j?.fehler})`);
|
||||
|
||||
const weg2 = await roh(CREW, "/workspace/api/reaktion/chat", {
|
||||
method: "POST", keks: k.gast2, rumpf: { text: "hallo?" },
|
||||
});
|
||||
ok(weg2.code === 403 && weg2.j?.fehler === "aus_der_sendung",
|
||||
`und schreiben auch nicht (${weg2.code}/${weg2.j?.fehler})`);
|
||||
|
||||
await roh(CREW, `/workspace/api/reaktion/massnahme/${IDS.gast2}`, {
|
||||
method: "DELETE", keks: k.admin,
|
||||
});
|
||||
const zurueck = await roh(CREW, "/workspace/api/reaktion/dabei", {
|
||||
method: "POST", keks: k.gast2,
|
||||
});
|
||||
ok(zurueck.code === 200, `Gegenprobe: aufgehoben, und er ist wieder dabei (${zurueck.code})`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
16. WAS DIE SEITE DARAUS MACHT
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 16. Rollen und Moderation auf der Seite ===");
|
||||
{
|
||||
const js = readFileSync(join(HIER, "..", "workspace", "assets", "js", "reaktion.js"), "utf8");
|
||||
const css = readFileSync(join(HIER, "..", "workspace", "assets", "css", "reaktion.css"), "utf8");
|
||||
const html = readFileSync(join(HIER, "..", "workspace", "reaktion.html"), "utf8");
|
||||
const saetze = readFileSync(join(HIER, "..", "workspace", "assets", "js", "meldung.js"), "utf8");
|
||||
|
||||
/* 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`);
|
||||
}
|
||||
/* 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`);
|
||||
}
|
||||
|
||||
ok(html.includes('id="chat-meldungen"'), "der Meldungs-Knopf steht in der Seite");
|
||||
ok(css.includes(".schiene__meldungen"), "und hat eine Regel");
|
||||
ok(js.includes("meldungenZeichnen()") && js.includes("meldungenZeigen"),
|
||||
"die Seite zeichnet und oeffnet ihn");
|
||||
|
||||
/* DIE TIPPZIELE IM MODERATIONSMENUE. Hier wird jemand stumm
|
||||
geschaltet; ein Fehlgriff trifft den Falschen. */
|
||||
const knopf = css.match(/\.modmenue__knopf \{[^}]*\}/);
|
||||
ok(!!knopf && /min-height:\s*44px/.test(knopf[0]),
|
||||
"jeder Knopf im Moderationsmenue ist 44 px hoch");
|
||||
|
||||
/* JEDES FEHLERWORT DES SERVERS HAT EINEN DEUTSCHEN SATZ.
|
||||
Sonst steht im Chat ein Maschinenwort wie „nicht_gegen_team". */
|
||||
const server = readFileSync(join(HIER, "workspace-reaktion.js"), "utf8");
|
||||
const woerter = [...new Set([...server.matchAll(/fehler:\s*"([a-z_0-9]+)"/g)]
|
||||
.map((x) => x[1]))].filter((w) => w !== "nicht_verfuegbar");
|
||||
ok(woerter.length > 20, `${woerter.length} Fehlerwoerter im Server gefunden`);
|
||||
const ohneSatz = woerter.filter((w) => !new RegExp(`^\\s+${w}:`, "m").test(saetze));
|
||||
ok(ohneSatz.length === 0,
|
||||
ohneSatz.length ? `OHNE SATZ: ${ohneSatz.join(", ")}` : "alle haben einen deutschen Satz");
|
||||
/* Die Gegenprobe braucht ein Wort, das es WIRKLICH nicht gibt --
|
||||
„gibt_es_nicht" steht seit Langem in der Tafel und liess die
|
||||
Gegenprobe genau das melden, wovor sie warnen sollte. */
|
||||
ok(!/^\s+zwetschgenknoedel:/m.test(saetze),
|
||||
"Gegenprobe: ein erfundenes Wort hat keinen Satz");
|
||||
|
||||
/* KEIN SCHLUESSEL STEHT ZWEIMAL IN DER SATZTAFEL.
|
||||
Ein zweiter Eintrag mit demselben Namen gewinnt stillschweigend --
|
||||
genau so stand am 28.09. in der Hilfe „Der Saal ist zu". */
|
||||
const schluessel = [...saetze.matchAll(/^ {4}([a-z_0-9]+):/gm)].map((x) => x[1]);
|
||||
const doppelt = [...new Set(schluessel.filter((x, i) => schluessel.indexOf(x) !== i))];
|
||||
ok(doppelt.length === 0,
|
||||
doppelt.length ? `DOPPELT: ${doppelt.join(", ")}` : `${schluessel.length} Saetze, keiner doppelt`);
|
||||
}
|
||||
|
||||
melde("");
|
||||
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
||||
melde(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG");
|
||||
|
||||
Reference in New Issue
Block a user