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:
+265
-1
@@ -68,11 +68,19 @@ function anlegen(name, rolle, code) {
|
||||
createHmac("sha256", kenn).update(code).digest("hex"), ALT);
|
||||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
}
|
||||
const CODES = { admin: "CODE-M00-000001", gast: "CODE-M01-000001", gast2: "CODE-M02-000001" };
|
||||
const CODES = {
|
||||
admin: "CODE-M00-000001", gast: "CODE-M01-000001", gast2: "CODE-M02-000001",
|
||||
modi: "CODE-M03-000001", linke: "CODE-M04-000001",
|
||||
};
|
||||
const IDS = {
|
||||
admin: anlegen("DogFather", "admin", CODES.admin),
|
||||
gast: anlegen("Nachtfalter", "gast", CODES.gast),
|
||||
gast2: anlegen("Lenny", "gast", CODES.gast2),
|
||||
/* Fuer die Moderation. Sie ist der Teil, bei dem ein Fehler am
|
||||
meisten kostet -- also wird sie im Browser gemessen und nicht
|
||||
nur ueber die Schnittstelle. */
|
||||
modi: anlegen("Miss", "modi", CODES.modi),
|
||||
linke: anlegen("Funny", "linke", CODES.linke),
|
||||
};
|
||||
d.close();
|
||||
|
||||
@@ -111,6 +119,8 @@ const k = {
|
||||
admin: await anmelden("admin"),
|
||||
gast: await anmelden("gast"),
|
||||
gast2: await anmelden("gast2"),
|
||||
modi: await anmelden("modi"),
|
||||
linke: await anmelden("linke"),
|
||||
};
|
||||
|
||||
/* ---------- Die Buehne fuellen ------------------------------------------ */
|
||||
@@ -139,6 +149,12 @@ const REDEN = [
|
||||
["gast2", "Ich auch! Hab extra Popcorn geholt"],
|
||||
["gast", "Wie lange noch?"],
|
||||
["admin", "Zwei Minuten, ich richte noch die Kamera"],
|
||||
/* DAS TEAM SCHREIBT MIT. Ohne einen Beitrag von einer linken Hand
|
||||
und einem Modi liesse sich gar nicht messen, ob die Abzeichen
|
||||
ankommen -- und eine Messung, die ihren Gegenstand nicht
|
||||
herstellt, misst nichts. */
|
||||
["linke", "Ich pass auf den Chat auf, schreibt ruhig"],
|
||||
["modi", "Und ich auch 🙂"],
|
||||
["gast2", "Kein Stress, wir sitzen bequem"],
|
||||
["gast", "Letztes Mal war so gut, ich hab geheult"],
|
||||
];
|
||||
@@ -658,6 +674,254 @@ await vermessen(zuschauer.seite, "Zuschauerin am Rechner");
|
||||
await bild(zuschauer.seite, "4-live-zuschauer");
|
||||
if (zuschauer.konsole.length) console.log(" KONSOLE:", zuschauer.konsole.slice(0, 4).join(" | "));
|
||||
|
||||
/* ---- Rollen und Moderation im Chat --------------------------------
|
||||
|
||||
Filipe: „die modis, linke hand und rechte hand soll im chat extra
|
||||
aussehen und auch sachen wie stummschaltungen, sperrungen,
|
||||
meldunge ... machen koennen."
|
||||
|
||||
GEMESSEN UND NICHT HERGELEITET. Die Schiene ist die schmalste
|
||||
Flaeche der ganzen Seite, und in jede Beitragszeile ist gerade ein
|
||||
Abzeichen UND ein zweiter Knopf dazugekommen. Genau so ist am
|
||||
28.09. die Regie 197 px uebergelaufen: Die Rechnung ging auf, die
|
||||
Wirklichkeit nicht. */
|
||||
console.log("");
|
||||
console.log(" --- Rollen und Moderation ---");
|
||||
const modi = await alsWer("modi", 1440, 900);
|
||||
await modi.seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "domcontentloaded" });
|
||||
await modi.seite.waitForTimeout(2500);
|
||||
|
||||
const abz = await modi.seite.evaluate(() => {
|
||||
const liste = document.getElementById("chat-liste");
|
||||
const marken = [...liste.querySelectorAll(".beitrag__rolle")].map((m) => ({
|
||||
wort: m.textContent.trim(),
|
||||
rolle: m.dataset.rolle,
|
||||
schrift: Math.round(parseFloat(getComputedStyle(m).fontSize) * 100) / 100,
|
||||
breit: Math.round(m.getBoundingClientRect().width),
|
||||
}));
|
||||
/* LAEUFT EINE ZEILE UEBER? Nicht die Zahl aus dem Stilblatt, die
|
||||
tatsaechliche Breite gegen die tatsaechliche Liste. */
|
||||
const innen = liste.getBoundingClientRect();
|
||||
const ueber = [...liste.querySelectorAll(".beitrag")]
|
||||
.map((b) => Math.round(b.scrollWidth - b.clientWidth))
|
||||
.filter((x) => x > 0);
|
||||
/* Und die Kopfzeile selbst -- dort stehen Name, Abzeichen und
|
||||
Knopf nebeneinander. */
|
||||
const koepfe = [...liste.querySelectorAll(".beitrag__wer")]
|
||||
.map((h) => Math.round(h.scrollWidth - h.clientWidth))
|
||||
.filter((x) => x > 0);
|
||||
return {
|
||||
marken, ueber, koepfe,
|
||||
listenbreite: Math.round(innen.width),
|
||||
namensknoepfe: liste.querySelectorAll(".beitrag__name").length,
|
||||
beitraege: liste.children.length,
|
||||
};
|
||||
});
|
||||
console.log(` Schiene ${abz.listenbreite} px breit, ${abz.beitraege} Beitraege, `
|
||||
+ `${abz.marken.length} Abzeichen, ${abz.namensknoepfe} Namen zum Anfassen.`);
|
||||
for (const m of abz.marken) {
|
||||
console.log(` „${m.wort}" (${m.rolle}) ${m.schrift} px, ${m.breit} px breit`);
|
||||
}
|
||||
if (abz.marken.length < 3) {
|
||||
console.log(" ACHTUNG: Die Abzeichen kommen im Chat nicht an.");
|
||||
ausgang = 1;
|
||||
}
|
||||
/* 11,5 px ist die Untergrenze des Hauses (pruef-css-klassen). */
|
||||
if (abz.marken.some((m) => m.schrift < 11.5)) {
|
||||
console.log(" ACHTUNG: Ein Abzeichen ist kleiner als 11,5 px.");
|
||||
ausgang = 1;
|
||||
}
|
||||
if (abz.ueber.length || abz.koepfe.length) {
|
||||
console.log(` ACHTUNG: ${abz.ueber.length} Beitraege und ${abz.koepfe.length} Kopfzeilen `
|
||||
+ `laufen ueber (${[...abz.ueber, ...abz.koepfe].join(", ")} px).`);
|
||||
ausgang = 1;
|
||||
}
|
||||
/* EIN MODI MELDET NICHT -- er handelt. Der Melden-Knopf gehoert
|
||||
denen, die nichts anderes tun koennen. */
|
||||
const modiSicht = await modi.seite.evaluate(() =>
|
||||
document.querySelectorAll("#chat-liste .beitrag__melden").length);
|
||||
console.log(` Der Modi sieht ${modiSicht} Melden-Knoepfe (soll: 0) `
|
||||
+ `und ${abz.namensknoepfe} Namen zum Anfassen.`);
|
||||
if (modiSicht !== 0 || abz.namensknoepfe < 3) {
|
||||
console.log(" ACHTUNG: Melden und Moderieren sind vertauscht.");
|
||||
ausgang = 1;
|
||||
}
|
||||
|
||||
/* DAS MENUE MUSS INS BILD PASSEN. Ein Menue, das unten oder rechts
|
||||
heraussteht, ist auf dem Handy die Haelfte der Zeit nicht
|
||||
bedienbar -- und man sieht es erst, wenn man es aufmacht. */
|
||||
await modi.seite.click("#chat-liste .beitrag:last-child .beitrag__name").catch(() => {});
|
||||
await modi.seite.waitForTimeout(400);
|
||||
const menue = await modi.seite.evaluate(() => {
|
||||
const m = document.querySelector(".modmenue");
|
||||
if (!m) return null;
|
||||
const r = m.getBoundingClientRect();
|
||||
const knoepfe = [...m.querySelectorAll(".modmenue__knopf")].map((k2) => ({
|
||||
wort: k2.textContent.trim(),
|
||||
hoch: Math.round(k2.getBoundingClientRect().height),
|
||||
}));
|
||||
/* IST ES WIRKLICH ZU SEHEN? „Im Fenster" reicht nicht als Frage.
|
||||
Beim ersten Bau lag das Menue nachweislich 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. Gefunden hat das nicht die
|
||||
Messung, sondern das Bildschirmfoto.
|
||||
Deshalb jetzt der einzige Test, der die Frage beantwortet: An
|
||||
die Stelle jedes Knopfes fassen und nachsehen, was dort
|
||||
tatsaechlich liegt. */
|
||||
const greifbar = [...m.querySelectorAll(".modmenue__knopf")].filter((k2) => {
|
||||
const q = k2.getBoundingClientRect();
|
||||
const oben = document.elementFromPoint(q.left + q.width / 2, q.top + q.height / 2);
|
||||
return !!oben && (k2 === oben || k2.contains(oben));
|
||||
}).length;
|
||||
return {
|
||||
links: Math.round(r.left), rechts: Math.round(r.right),
|
||||
oben: Math.round(r.top), unten: Math.round(r.bottom),
|
||||
breit: Math.round(r.width), knoepfe, greifbar,
|
||||
fensterBreit: window.innerWidth, fensterHoch: window.innerHeight,
|
||||
};
|
||||
});
|
||||
if (!menue) {
|
||||
console.log(" ACHTUNG: Das Moderationsmenue geht nicht auf.");
|
||||
ausgang = 1;
|
||||
} else {
|
||||
console.log(` Menue ${menue.breit} px breit bei x=${menue.links}..${menue.rechts} `
|
||||
+ `(Fenster ${menue.fensterBreit}), y=${menue.oben}..${menue.unten}`);
|
||||
for (const b of menue.knoepfe) console.log(` „${b.wort}" ${b.hoch} px hoch`);
|
||||
console.log(` ${menue.greifbar} von ${menue.knoepfe.length} Knoepfen sind anfassbar`);
|
||||
const raus = menue.rechts > menue.fensterBreit || menue.links < 0
|
||||
|| menue.unten > menue.fensterHoch;
|
||||
if (raus) {
|
||||
console.log(" ACHTUNG: Das Menue steht aus dem Bild heraus.");
|
||||
ausgang = 1;
|
||||
}
|
||||
if (menue.greifbar !== menue.knoepfe.length) {
|
||||
console.log(" ACHTUNG: Ein Knopf des Menues ist verdeckt oder abgeschnitten.");
|
||||
ausgang = 1;
|
||||
}
|
||||
if (menue.knoepfe.length < 4 || menue.knoepfe.some((b) => b.hoch < 44)) {
|
||||
console.log(" ACHTUNG: Ein Knopf im Menue ist kleiner als 44 px.");
|
||||
ausgang = 1;
|
||||
}
|
||||
}
|
||||
await bild(modi.seite, "12-moderationsmenue");
|
||||
|
||||
/* STUMM SCHALTEN -- und beim Betroffenen nachsehen.
|
||||
Der Chat steht dabei auf „offen": genau der Fall, in dem die
|
||||
Auskunft vorher falsch war („Der Chat ist gerade zu"). */
|
||||
await modi.seite.click('.modmenue__knopf:text("10 Minuten stumm")').catch(async () => {
|
||||
await modi.seite.evaluate(() => {
|
||||
const k2 = [...document.querySelectorAll(".modmenue__knopf")]
|
||||
.find((x) => x.textContent.includes("stumm"));
|
||||
if (k2) k2.click();
|
||||
});
|
||||
});
|
||||
await modi.seite.waitForTimeout(1500);
|
||||
|
||||
const beimGast = await zuschauer.seite.evaluate(() => {
|
||||
const f = document.getElementById("chat-feld");
|
||||
return { gesperrt: !!f?.disabled, grund: f?.placeholder || "" };
|
||||
});
|
||||
console.log(` Die Stummgeschaltete sieht: gesperrt=${beimGast.gesperrt}, `
|
||||
+ `Grund „${beimGast.grund}"`);
|
||||
const serverSagt = await roh("/workspace/api/reaktion/chat", {
|
||||
method: "POST", keks: k.gast, rumpf: { text: "und jetzt?" },
|
||||
});
|
||||
console.log(` ... und der Server: ${serverSagt.code}/${serverSagt.j?.fehler}`);
|
||||
if (!beimGast.gesperrt || !beimGast.grund.includes("stumm")
|
||||
|| serverSagt.j?.fehler !== "stumm_geschaltet") {
|
||||
console.log(" ACHTUNG: Die Stummschaltung kommt nicht oder mit falschem Grund an.");
|
||||
ausgang = 1;
|
||||
}
|
||||
await roh(`/workspace/api/reaktion/massnahme/${IDS.gast}`, { method: "DELETE", keks: k.admin });
|
||||
await modi.seite.waitForTimeout(900);
|
||||
|
||||
/* MELDEN -- der Weg fuer die, die selbst nichts tun koennen. */
|
||||
const letzter = await roh("/workspace/api/reaktion/chat", { keks: k.admin });
|
||||
const zuMelden = (letzter.j?.beitraege || []).filter((b) => b.person_id === IDS.gast2).pop();
|
||||
if (zuMelden) {
|
||||
await roh(`/workspace/api/reaktion/chat/${zuMelden.id}/melden`, {
|
||||
method: "POST", keks: k.gast, rumpf: { grund: "unfreundlich" },
|
||||
});
|
||||
}
|
||||
await modi.seite.waitForTimeout(1800);
|
||||
const chip = await modi.seite.evaluate(() => {
|
||||
const k2 = document.getElementById("chat-meldungen");
|
||||
if (!k2) return null;
|
||||
const r = k2.getBoundingClientRect();
|
||||
return { versteckt: !!k2.hidden, wort: k2.textContent.trim(), hoch: Math.round(r.height) };
|
||||
});
|
||||
console.log(` Meldungs-Zeichen: „${chip?.wort}" (versteckt=${chip?.versteckt}, `
|
||||
+ `${chip?.hoch} px hoch)`);
|
||||
if (!chip || chip.versteckt || !/Meldung/.test(chip.wort)) {
|
||||
console.log(" ACHTUNG: Die Meldung erreicht die Moderation nicht.");
|
||||
ausgang = 1;
|
||||
}
|
||||
await modi.seite.click("#chat-meldungen").catch(() => {});
|
||||
await modi.seite.waitForTimeout(400);
|
||||
const liste = await modi.seite.evaluate(() => {
|
||||
const m = document.querySelector(".modmenue--liste");
|
||||
if (!m) return null;
|
||||
const r = m.getBoundingClientRect();
|
||||
const knoepfe = [...m.querySelectorAll(".modmenue__knopf")];
|
||||
const greifbar = knoepfe.filter((k2) => {
|
||||
const q = k2.getBoundingClientRect();
|
||||
const oben = document.elementFromPoint(q.left + q.width / 2, q.top + q.height / 2);
|
||||
return !!oben && (k2 === oben || k2.contains(oben));
|
||||
}).length;
|
||||
return {
|
||||
zeilen: m.querySelectorAll(".modmeldung").length,
|
||||
text: m.querySelector(".modmeldung__text")?.textContent || "",
|
||||
knoepfe: knoepfe.length, greifbar,
|
||||
rechts: Math.round(r.right), unten: Math.round(r.bottom),
|
||||
fensterBreit: window.innerWidth, fensterHoch: window.innerHeight,
|
||||
};
|
||||
});
|
||||
if (!liste || liste.zeilen < 1) {
|
||||
console.log(" ACHTUNG: Die Meldungsliste geht nicht auf.");
|
||||
ausgang = 1;
|
||||
} else {
|
||||
console.log(` ${liste.zeilen} Meldung(en), erste: „${liste.text}" -- `
|
||||
+ `${liste.greifbar}/${liste.knoepfe} Knoepfe anfassbar`);
|
||||
if (liste.rechts > liste.fensterBreit || liste.unten > liste.fensterHoch) {
|
||||
console.log(" ACHTUNG: Die Meldungsliste steht aus dem Bild heraus.");
|
||||
ausgang = 1;
|
||||
}
|
||||
if (liste.greifbar !== liste.knoepfe) {
|
||||
console.log(" ACHTUNG: Ein Knopf der Meldungsliste ist verdeckt.");
|
||||
ausgang = 1;
|
||||
}
|
||||
}
|
||||
await bild(modi.seite, "13-meldungen");
|
||||
if (modi.konsole.length) console.log(" KONSOLE:", modi.konsole.slice(0, 4).join(" | "));
|
||||
|
||||
/* AUS DER SENDUNG -- der haerteste Fall. Wer rausgenommen wird, muss
|
||||
es ERFAHREN; vorher lief bei ihm das Video weiter, und nur die
|
||||
Anwesenheitsmeldung schlug still fehl. */
|
||||
await roh("/workspace/api/reaktion/massnahme", {
|
||||
method: "POST", keks: k.admin, rumpf: { person_id: IDS.gast, art: "raus" },
|
||||
});
|
||||
await zuschauer.seite.waitForTimeout(2500);
|
||||
const rausgesetzt = await zuschauer.seite.evaluate(() => {
|
||||
const k2 = document.querySelector(".raus");
|
||||
return {
|
||||
da: !!k2, markiert: document.body.dataset.raus === "ja",
|
||||
titel: k2?.querySelector(".raus__titel")?.textContent || "",
|
||||
deckt: k2 ? Math.round(k2.getBoundingClientRect().height) : 0,
|
||||
fensterHoch: window.innerHeight,
|
||||
};
|
||||
});
|
||||
console.log(` Die Rausgenommene sieht: „${rausgesetzt.titel}" `
|
||||
+ `(${rausgesetzt.deckt} von ${rausgesetzt.fensterHoch} px)`);
|
||||
if (!rausgesetzt.da || !rausgesetzt.markiert
|
||||
|| rausgesetzt.deckt < rausgesetzt.fensterHoch - 2) {
|
||||
console.log(" ACHTUNG: Wer rausgenommen wird, merkt es nicht.");
|
||||
ausgang = 1;
|
||||
}
|
||||
await bild(zuschauer.seite, "14-aus-der-sendung");
|
||||
await roh(`/workspace/api/reaktion/massnahme/${IDS.gast}`, { method: "DELETE", keks: k.admin });
|
||||
|
||||
await modi.kontext.close();
|
||||
await host.kontext.close();
|
||||
await gast2.kontext.close();
|
||||
await zuschauer.kontext.close();
|
||||
|
||||
Reference in New Issue
Block a user