Chat: "@rudel" ruft das ganze Team -- und nur die vier duerfen es

Filipe: "dan will ich auch dass nur die modis, rechte hand, linke hand
und dogfather, alle auch auf einmal markieren koennen im chat mit
einem, @rudel ,dann sollen alle eine benarichtigung bekommen."

DAS WAR EINE OFFENE FRAGE, UND ER HAT SIE BEANTWORTET
-----------------------------------------------------
In chat-erwaehnung.js stand seit dem 20.09. woertlich: "KEIN @alle. Es
waere in fuenf Minuten gebaut und ist der zuverlaessigste Weg, dass alle
die Benachrichtigungen abschalten -- und dann kommt auch die an, die
wirklich fuer einen bestimmten Menschen gedacht war. Wenn Filipe es
ausdruecklich will, gehoert dazu eine Entscheidung, WER es benutzen
darf; das ist eine Frage an ihn, keine, die ich hier beantworte."

Seine Antwort ist genau diese Entscheidung, und sie ist die Sicherung:
Team Dogi und DogFather duerfen rufen, sonst niemand.

WEN DER RUF ERREICHT: DAS TEAM IM RAUM, NICHT DEN RAUM
------------------------------------------------------
"Rudel" heisst das Team -- dieselben vier Gruppen, die auch rufen
duerfen. In einem Team-Kanal ist das jeder Anwesende, also "alle auf
einmal". Sichtbar wird der Unterschied im TREFF, und dort haette die
andere Lesart wehgetan: Dort sitzt die Community. Ein Ruf, der jeden
Zuschauer weckt, waere etwas anderes als der bestellte -- und beim
zweiten Mal haetten sie die Benachrichtigungen abgeschaltet.

WO WAS ENTSCHIEDEN WIRD
-----------------------
chat-erwaehnung.js bleibt ohne Abhaengigkeiten (die Pruefung soll sie
lesen koennen, ohne einen Server hochzufahren). Sie sagt nur, DASS
gerufen wurde; der Aufrufer sagt ihr, ob der Schreibende darf. Wer zum
Rudel gehoert, entscheidet workspace-chat.js ueber istTeamDogi().

Das Schluesselwort gewinnt gegen einen Menschen, der "Rudel" heisst.
Heute heisst niemand so -- aber das ist eine Tatsache von heute, kein
Gesetz. So herum verliert niemand eine Meldung (wer so heisst, ist im
Rudel dabei); andersherum haette ein einziger Zugang den Ruf ans Team
stillschweigend abgeschaltet.

istTeamDogi() NEU IN workspace.js
---------------------------------
Derselbe Ausdruck ("admin oder TEAM_DOGI_ROLLEN") stand dort dreimal
wortgleich: siehtModis, kanaeleFuer, kategorienFuer. Drei gleiche
Ausdruecke sind drei Gelegenheiten, dass einer beim naechsten
Rollenzuschnitt nicht mitgeht. Jetzt eine Stelle, die drei benutzen.

DIE NACHRICHT MERKT SICH DEN RUF (Spalte chat_nachrichten.rudel)
----------------------------------------------------------------
Beim Lesen muesste der Browser sonst wissen, ob der Absender es DAMALS
durfte. Er kennt nur dessen heutige Rolle -- wechselt jemand die Rolle,
verschwaende die Hervorhebung rueckwirkend aus einem Satz von vorletzter
Woche. Und es waere die zweite Rechnung ueber dieselbe Frage.
Vorgabe 0; alle alten Nachrichten haben kein Rudel gerufen, und das ist
keine Annahme, sondern eine Tatsache: Das Wort gab es noch nicht.

DIE MELDUNG SAGT, WAS LOS IST
-----------------------------
"X hat das Rudel gerufen", nicht "X hat dich erwaehnt" -- letzteres
stimmt beim Rudel nicht, und wer dreimal liest, dass er gemeint sei,
und jedes Mal merkt, dass es alle betraf, glaubt beim vierten Mal auch
dem echten nicht mehr. Auf DEMSELBEN Schalter wie die Erwaehnung: Ein
vierter Schalter waere der, den jemand abschaltet und der dann genau im
wichtigen Moment fehlt. Jeder Ruf steht im Protokoll (chat_rudel) --
"@rudel wird zu oft benutzt" soll eine Zahl sein koennen, kein Gefuehl.

DIE FARBE WIRD ABGELEITET, NICHT GESETZT
----------------------------------------
Eine Nachricht liegt in der Blase, deren Farbe ihr Absender AUSGESUCHT
hat -- siebzehn Moeglichkeiten. Eine feste Farbe darauf ist eine Wette,
und genau die habe ich am 22.09. beim Loeschknopf verloren (1,91:1 auf
Babyblau, gemessen, nachdem es live war). Die Marke nimmt deshalb
`--blase-text` -- die Schrift, die schriftFuer() fuer DIESE Blase mit
mindestens 7:1 ausgerechnet hat. Unterschieden wird ueber Form statt
Farbton: Toenung, Kante, Gewicht 700, ein Zeichen davor. In der
Auswahlliste darf es einen eigenen Ton haben -- sie liegt auf der
Flaeche des Hauses, deren Farbe feststeht.

DER VORSCHLAG ERSCHEINT NUR, WO ER ETWAS BEWIRKT
------------------------------------------------
In einem Zweier-Gespraech mit einem Creator ist ausser mir niemand aus
dem Team. `darf_rudel` fragt deshalb beides: darf ich, und sitzt hier
noch jemand aus dem Team. Dieselbe Ueberlegung wie beim eigenen Namen,
den die Liste auch nicht anbietet. Die Schranke beim Schreiben haengt
nicht daran -- wer es von Hand tippt, ruft eben niemanden.

GEPRUEFT
--------
pruef-erwaehnung: 119 Pruefungen, 0 Fehler (vorher 66).
Neu darin, und die zweite ist die wichtigere:
  * der Modi ruft im Treff genau das Team -- die Liste wird aus der
    Besetzung ABGELEITET, nicht abgeschrieben
  * der Gast im selben Raum ruft NICHTS: kein Eintrag, kein Merkmal,
    kein Vorschlag. Ohne diese Pruefung stuende Filipes "nur die
    modis, rechte hand, linke hand und dogfather" bloss im Kommentar
  * beide Fassungen der Regel (Server und Browser) an 14 zusaetzlichen
    Proben nebeneinander, mit beiden Rechten -- samt Gegenprobe, dass
    der Vergleich einen Unterschied ueberhaupt sehen kann
  * am Bildschirm: der Ruf steht oben in der Liste, sagt daneben, was
    er bedeutet, Enter setzt ihn ein, und im Satz ist er an Gewicht
    und Rahmen erkennbar -- nicht nur an der Farbe

Beim Bauen gemessen statt vermutet: Ein Scout und ein Modi kommen gar
nicht in denselben Raum (403, Haeusertrennung) -- deshalb prueft der
Verhaltenstest im Treff. Und ein Gast meldet sich nur mit
Altersbestaetigung an (400 ohne).

Die Nachtruhe des Treffs wird in dieser Pruefung abgeschaltet (gleiche
Stunden = keine Nachtruhe). Sonst waere sie zwischen Mitternacht und
sechs rot und danach gruen -- ein Test, der die Wanduhr misst.

pruef-chat, pruef-treffchat (110), pruef-chat-kanaele (81),
pruef-chat-ausbau (64): alle unveraendert gruen.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-23 01:36:09 +02:00
co-authored by Claude Opus 5
parent 2a3058e98e
commit 28b0143166
45 changed files with 1427 additions and 628 deletions
+89 -11
View File
@@ -58,19 +58,49 @@
Dreimal "@Anna" in einem Satz ist trotzdem eine Ansprache.
---------------------------------------------------------------------
WAS ES BEWUSST NICHT GIBT
---------------------------------------------------------------------
"@RUDEL" -- ALLE AUF EINMAL (23.09.2026)
KEIN "@alle". Es wäre in fünf Minuten gebaut und ist der zuverlässigste
Weg, dass alle die Benachrichtigungen abschalten -- und dann kommt
auch die an, die wirklich für einen bestimmten Menschen gedacht war.
Wenn Filipe es ausdrücklich will, gehört dazu eine Entscheidung, WER
es benutzen darf; das ist eine Frage an ihn, keine, die ich hier
beantworte.
Hier stand bis heute: „KEIN @alle. Es wäre in fünf Minuten gebaut und
ist der zuverlässigste Weg, dass alle die Benachrichtigungen
abschalten -- und dann kommt auch die an, die wirklich für einen
bestimmten Menschen gedacht war. Wenn Filipe es ausdrücklich will,
gehört dazu eine Entscheidung, WER es benutzen darf; das ist eine
Frage an ihn, keine, die ich hier beantworte."
Filipe hat sie beantwortet, und zwar genau in dem Punkt, an dem sie
offen war: „dan will ich auch dass nur die modis, rechte hand, linke
hand und dogfather, alle auch auf einmal markieren können im chat mit
einem, @rudel ,dann sollen alle eine benarichtigung bekommen."
DIE EINSCHRÄNKUNG IST DIE ANTWORT AUF DIE SORGE. Nicht jeder kann
das Rudel rufen -- nur Team Dogi und DogFather. In einem Raum, in dem
auch Creator und Scouts sitzen, kann ein Creator „@rudel" schreiben,
und es passiert nichts: kein Ruf, keine Hervorhebung, kein Eintrag.
WER DARF, ENTSCHEIDET DIESE DATEI NICHT. Sie hat keine
Abhängigkeiten und soll keine bekommen (siehe oben). Der Aufrufer
sagt es ihr: `erwaehnungenFinden(text, leute, { rudel: true })`. Im
Server kommt das aus `istTeamDogi(person)`, der einen Stelle des
Hauses dafür.
DAS SCHLÜSSELWORT GEWINNT GEGEN EINEN MENSCHEN, DER "RUDEL" HEISST.
Heute heißt niemand so -- aber das ist eine Tatsache von heute, kein
Gesetz (dieselbe Überlegung wie bei Regel 4). Andersherum wäre der
Schaden größer: Ein einziger Zugang mit diesem Namen würde den Ruf
ans ganze Team stillschweigend abschalten, und niemand fände je
heraus, warum. So herum kostet es nichts -- wer „Rudel" heißt, ist im
Rudel ohnehin dabei und bekommt seine Meldung.
---------------------------------------------------------------------
AUFRUF (auch aus einer Prüfung heraus):
import { erwaehnungenFinden } from "./chat-erwaehnung.js";
===================================================================== */
/** Das Wort, das alle meint. Klein geschrieben -- verglichen wird
* ohnehin kleingeschrieben (Regel 5). */
export const RUDEL_WORT = "rudel";
/** Ein Zeichen, das einen Namen fortsetzen könnte.
*
* BUCHSTABE ODER ZIFFER, und zwar auch mit Umlaut oder Akzent:
@@ -93,13 +123,21 @@ function wortzeichenBei(text, i) {
* @param {Array<{id:number,name:string}>} leute
* Die Teilnehmer DIESES Raums. Wer nicht darin steht, kann
* nicht gemeint sein -- siehe Regel (1) oben.
* @returns {Array<{id:number, name:string, von:number, bis:number}>}
* @param {{rudel?: boolean}} [wie]
* `rudel: true` heisst: WER DAS HIER SCHREIBT, darf das ganze
* Rudel rufen. Vorgabe ist `false` -- wer nichts sagt, darf es
* nicht. Das ist Absicht: Eine vergessene Angabe soll zu wenig
* koennen, nicht zu viel.
* @returns {Array<{id:number|null, name:string, von:number, bis:number, rudel?:boolean}>}
* Je Treffer die Person und die Stelle im Text (`von` bis
* `bis`, ohne das "@"-Zeichen gerechnet wie `slice`). Die
* Stellen braucht der Browser zum Hervorheben; der Server
* benutzt nur `id`. Sortiert nach Vorkommen im Text.
* Beim Rudel ist `id` null und `rudel` true -- es ist keine
* Person, sondern alle.
*/
export function erwaehnungenFinden(text, leute) {
export function erwaehnungenFinden(text, leute, wie = {}) {
const darfRudel = wie?.rudel === true;
const satz = String(text || "");
if (!satz.includes("@")) return [];
@@ -119,6 +157,18 @@ export function erwaehnungenFinden(text, leute) {
/* REGEL (2): Was steht vor dem "@"? */
if (wortzeichenBei(satz, i - 1)) continue;
/* DAS RUDEL WIRD ZUERST GEPRUEFT -- und genau das ist die
Entscheidung aus dem Kopfkommentar: Das Schluesselwort gewinnt
gegen einen Menschen, der "Rudel" heisst. Die Regeln (2), (3)
und (5) gelten fuer es genauso wie fuer einen Namen, sonst
riefe eine Mailadresse "[email protected]" das ganze Team. */
if (darfRudel && klein.startsWith(RUDEL_WORT, i + 1)
&& !wortzeichenBei(satz, i + 1 + RUDEL_WORT.length)) {
treffer.push({ id: null, rudel: true, name: RUDEL_WORT,
von: i, bis: i + 1 + RUDEL_WORT.length });
continue;
}
for (const person of nachLaenge) {
const name = String(person.name);
const nk = name.toLowerCase();
@@ -146,7 +196,35 @@ export function erwaehnungenFinden(text, leute) {
* ALS EIGENE FUNKTION, damit an der Aufrufstelle nicht steht
* `[...new Set(x.map(t => t.id))]`. Diese Zeile wäre an drei Orten
* gleich zu schreiben, und an einem davon irgendwann anders.
*
* DAS RUDEL STEHT HIER NICHT DRIN, und das ist Absicht. "@rudel" ist
* keine Liste von Namen im Satz, sondern eine Gruppe -- und WER zu ihr
* gehört, ist eine Frage des Hauses, nicht des Textes. Diese Datei hat
* keine Abhängigkeiten und kann sie nicht beantworten (sie sähe einen
* Raum voller `{id, name}` und wüsste nichts von Rollen). Sie sagt nur,
* DASS gerufen wurde -- siehe rudelGerufen() darunter.
*/
export function erwaehnteIds(text, leute) {
return [...new Set(erwaehnungenFinden(text, leute).map((t) => t.id))];
export function erwaehnteIds(text, leute, wie = {}) {
return [...new Set(erwaehnungenFinden(text, leute, wie)
.filter((t) => t.id != null)
.map((t) => t.id))];
}
/** Wurde in diesem Text das ganze Rudel gerufen?
*
* ALS EIGENE FRAGE, weil der Server sie getrennt beantworten muss:
* Die Nachricht merkt sich, DASS das Rudel gerufen wurde (Spalte
* `rudel`), und daraus entsteht spaeter die Hervorhebung beim Lesen
* und der Wortlaut der Meldung. Aus der ID-Liste liesse sich das
* nicht ablesen -- in einem Raum zu zweit sieht "alle" genauso aus
* wie "einer".
*
* WEN ES DANN TRIFFT, steht in workspace-chat.js: die Leute im Raum,
* die zu Team Dogi gehoeren (istTeamDogi). NICHT jeder im Raum -- im
* Treff sitzt die Community, und "Rudel" heisst das Team. Ein Ruf,
* der zweihundert Zuschauer weckt, waere etwas anderes als der, den
* Filipe bestellt hat.
*/
export function rudelGerufen(text, leute, wie = {}) {
return erwaehnungenFinden(text, leute, wie).some((t) => t.rudel === true);
}
+425 -1
View File
@@ -40,7 +40,7 @@ import { fileURLToPath } from "node:url";
import { request as httpAnfrage } from "node:http";
import { eigenerPort } from "./helfer-port.mjs";
import { notbremse } from "./helfer-notbremse.mjs";
import { erwaehnungenFinden, erwaehnteIds } from "./chat-erwaehnung.js";
import { erwaehnungenFinden, erwaehnteIds, rudelGerufen, RUDEL_WORT } from "./chat-erwaehnung.js";
const HIER = dirname(fileURLToPath(import.meta.url));
const PORT = await eigenerPort(import.meta, "pruef-erwaehnung");
@@ -50,6 +50,19 @@ process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = String(PORT);
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
/* KEINE NACHTRUHE WAEHREND DER PRUEFUNG (23.09.2026).
Abschnitt 7 schreibt im Treff -- und dort gilt von 0 bis 6 Uhr
Schreibverbot. Eine Pruefung, die zwischen Mitternacht und sechs
scheitert und danach wieder gruen ist, misst die Uhr, nicht das
Programm. Genau diese Falle steht in den Hausregeln ("Ein Test, der
die Wanduhr als Annahme benutzt, misst irgendwann das Gegenteil").
Gleiche Werte heissen "keine Nachtruhe" -- so steht es in
istNachtruhe(), erste Zeile. Die Nachtruhe SELBST wird in
pruef-treffchat geprueft, dort gehoert sie hin. */
process.env.TREFF_NACHT_AB = "0";
process.env.TREFF_NACHT_BIS = "0";
const express = (await import("express")).default;
const ec = express.response.cookie;
@@ -141,6 +154,73 @@ melde("\n=== 1. Wer ist gemeint? ===");
immer jemanden findet, findet nichts. */
ok(erwaehnteIds("@VanVan", []).length === 0,
"Gegenprobe: ohne Teilnehmerliste ist niemand gemeint");
/* ---- "@RUDEL" — ALLE AUF EINMAL (23.09.2026) ------------------
Filipe: „dan will ich auch dass nur die modis, rechte hand, linke
hand und dogfather, alle auch auf einmal markieren können im chat
mit einem, @rudel ,dann sollen alle eine benarichtigung bekommen."
DIE WICHTIGSTE PRÜFUNG HIER IST DIE ZWEITE: dass es NICHTS tut,
wenn der Schreibende nicht darf. Sonst hätte die Einschränkung,
die Filipe ausdrücklich genannt hat, nur im Kommentar gestanden.
Die Grenzregeln (2) und (3) gelten für das Schlüsselwort genauso
wie für einen Namen — sonst ruft eine Mailadresse das ganze Team. */
const rudelFaelle = [
["@rudel alle mal herhören", true, true, "der einfache Fall"],
["@RUDEL", true, true, "gross geschrieben trifft auch"],
["@rudel alle mal herhören", false, false,
"wer nicht darf, ruft nichts — und merkt nichts davon"],
["schreib an [email protected]", true, false,
"eine Mailadresse ruft das Rudel nicht"],
["@rudeltier", true, false, "ein längeres Wort ist kein Ruf"],
["das rudel ohne at", true, false, "ohne @ ist es nur ein Wort"],
["(@rudel)", true, true, "in Klammern"],
["@Tili und @rudel", true, true, "neben einem Namen"],
];
let rudelStimmt = 0;
for (const [text, darf, soll, was] of rudelFaelle) {
const ist = rudelGerufen(text, leute, { rudel: darf });
if (ist === soll) rudelStimmt++;
ok(ist === soll, `@rudel: ${was}${ist === soll ? "" : ` (${JSON.stringify(text)}, darf=${darf} -> ${ist})`}`);
}
ok(rudelStimmt === rudelFaelle.length && rudelFaelle.length >= 8,
`alle ${rudelFaelle.length} Rudel-Fälle stimmen`);
/* OHNE ANGABE DARF MAN NICHT. Eine vergessene Angabe soll zu wenig
können, nicht zu viel — sonst ruft der erste Aufruf, der die
Option nicht kennt, aus Versehen das ganze Team. */
ok(rudelGerufen("@rudel", leute) === false,
"ohne Angabe gilt: darf nicht (eine vergessene Angabe kann zu wenig, nie zu viel)");
/* NEBEN EINEM NAMEN BLEIBT DER NAME. "@Tili und @rudel" ist beides. */
ok(JSON.stringify(erwaehnteIds("@Tili und @rudel", leute, { rudel: true })) === "[3]",
"neben dem Ruf bleibt der einzeln Genannte einzeln genannt");
/* DAS SCHLÜSSELWORT GEWINNT GEGEN EINEN MENSCHEN NAMENS "RUDEL".
Heute heisst niemand so — aber das ist eine Tatsache von heute.
So herum verliert niemand eine Meldung: Wer "Rudel" heisst, ist
im Rudel ohnehin dabei. Andersherum wäre der Ruf ans Team durch
einen einzigen Zugang stillschweigend abgeschaltet. */
const mitRudel = [{ id: 20, name: "Rudel" }, { id: 21, name: "Anna" }];
ok(rudelGerufen("@Rudel hallo", mitRudel, { rudel: true }) === true,
"heisst jemand „Rudel“, gewinnt trotzdem das Schlüsselwort");
ok(JSON.stringify(erwaehnteIds("@Rudel hallo", mitRudel, { rudel: false })) === "[20]",
" und wer nicht rufen darf, spricht damit genau diesen Menschen an");
/* DAS WORT STEHT AN EINER STELLE. Stünde es zweimal, hiesse es nach
einer Umbenennung an einer davon anders. */
ok(RUDEL_WORT === "rudel", `das Wort kommt aus der Datei („${RUDEL_WORT}“)`);
/* GEGENPROBE: Die Auflösung liefert beim Rudel KEINE Personennummer
— es ist keine Person. Wer daraus eine ID läse, bekäme `null` in
die Empfängerliste und benachrichtigte niemanden. */
const rt = erwaehnungenFinden("@rudel", leute, { rudel: true });
ok(rt.length === 1 && rt[0].rudel === true && rt[0].id === null,
"der Ruf ist keine Person (id null, rudel true)");
ok(erwaehnteIds("@rudel", leute, { rudel: true }).length === 0,
" und taucht deshalb in der Namensliste gar nicht auf");
}
/* =====================================================================
@@ -391,6 +471,22 @@ melde("\n=== 4. Die Meldung sagt, dass DU gemeint bist ===");
ok(art?.vorgabe === true, " und ist von sich aus an");
ok(ARTEN.find((a) => a.schluessel === "chat_nachricht")?.schluessel !== art?.schluessel,
" getrennt von der gewöhnlichen Chatnachricht — zwei Schalter, zwei Entscheidungen");
/* ---- UND BEIM RUDEL SAGT SIE, DASS ALLE GEMEINT SIND ----------
Filipe: „dann sollen alle eine benarichtigung bekommen."
WARUM NICHT DERSELBE SATZ WIE BEI DER ERWÄHNUNG: Weil er nicht
stimmt. Beim Rudel ist man mitgemeint, nicht gemeint — und wer
dreimal „hat dich erwähnt" liest und jedes Mal merkt, dass es
alle betraf, glaubt beim vierten Mal auch dem echten nicht mehr. */
const gerufen = pushTextFuer("rudel", n, raumId);
ok(/Rudel gerufen/.test(gerufen.titel), `-> ${gerufen.titel}`);
ok(gerufen.titel !== erw.titel && gerufen.titel !== normal.titel,
" und unterscheidet sich von BEIDEN anderen Meldungen");
ok(gerufen.text.includes("schaust du mal"),
" der Anfang der Nachricht steht auch hier darunter");
ok(gerufen.ziel === normal.ziel, " und führt in dasselbe Gespräch");
}
/* =====================================================================
@@ -451,6 +547,43 @@ melde("\n=== 5. Server und Browser rechnen gleich ===");
}
ok(gleich === proben.length && proben.length >= 10,
`beide kommen bei allen ${proben.length} Proben auf dasselbe Ergebnis`);
/* ---- UND DASSELBE MIT "@RUDEL" (23.09.2026) ----------------
DER DRITTE PARAMETER MUSS MITGEPRUEFT WERDEN, sonst waere er die
Stelle, an der die beiden Fassungen auseinanderlaufen -- und
zwar unbemerkt, weil die elf Proben darueber ihn gar nicht
benutzen. Beide Wahrheitswerte, damit auch das Nichtstun
verglichen wird. */
const rudelProben = [
"@rudel alle mal herhoeren", "@RUDEL", "[email protected]",
"@rudeltier", "(@rudel)", "@Tili und @rudel", "kein at rudel",
];
let rGleich = 0, rVerglichen = 0;
for (const t of rudelProben) {
for (const darf of [true, false]) {
rVerglichen++;
const a = JSON.stringify(erwaehnungenFinden(t, leute, { rudel: darf }));
const b = JSON.stringify(browserFassung(t, leute, { rudel: darf }));
if (a === b) rGleich++;
else {
console.log(` ungleich bei ${JSON.stringify(t)} (darf=${darf})`);
console.log(` Server: ${a}`);
console.log(` Browser: ${b}`);
}
}
}
ok(rGleich === rVerglichen && rVerglichen >= 14,
`auch beim Rudel dasselbe Ergebnis (${rVerglichen} Proben, beide Rechte)`);
/* GEGENPROBE DES VERGLEICHS: Er muss einen Unterschied auch SEHEN.
Ohne das waere er eine Schleife, die immer „gleich“ sagt -- und
genau so eine stand am 01.09. gruen da, ohne je etwas anzusehen. */
const ungleichErkannt =
JSON.stringify(erwaehnungenFinden("@rudel", leute, { rudel: true }))
!== JSON.stringify(browserFassung("@rudel", leute, { rudel: false }));
ok(ungleichErkannt,
"Gegenprobe: bei verschiedenen Rechten kommt Verschiedenes heraus — der Vergleich kann Unterschiede sehen");
}
}
@@ -467,6 +600,39 @@ melde("\n=== 5. Server und Browser rechnen gleich ===");
liegt, wenn Enter statt zu wählen die halbe Nachricht abschickt,
oder wenn der Name am Ende grau bleibt wie jedes andere Wort.
===================================================================== */
/* EIN RAUM, IN DEM DER RUF ETWAS BEWIRKT (23.09.2026).
Fuer den Bildschirmteil wird ein Gespraech gebraucht, in dem ausser
mir noch jemand aus dem Team sitzt -- sonst bietet die Liste den Ruf
gar nicht erst an (und das ist richtig so: ein Vorschlag, der
niemanden erreicht, ist ein Vorschlag, der nichts tut).
ZWEI ZUGAENGE MIT DER DOGFATHER-ROLLE, und zwar bewusst statt eines
Modis: Der Browser dieser Pruefung laeuft auf 127.0.0.1, und dort
steht die Agentur-Wand -- die Rolle "modi" gibt es auf ihr nicht
(siehe der Kommentar ganz oben). Wer hier das Team braucht, nimmt
die einzige Team-Rolle, die auf beiden Waenden vorkommt. */
let raumTeam = null;
{
const d3 = new DatabaseSync(process.env.WORKSPACE_DB);
const sch = d3.prepare("SELECT wert FROM einstellungen WHERE schluessel = ?")
.get("code_kennung_schluessel").wert;
const salt = randomBytes(16).toString("hex");
const hash = scryptSync("CODE-DOGI-0002", salt, 64,
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d3.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
+ " VALUES (?,?,?,?,?,?,1,?)")
.run("Zweiter", "admin", hash, salt, 32768,
createHmac("sha256", sch).update("CODE-DOGI-0002").digest("hex"), jetzt);
const idZwei = d3.prepare("SELECT last_insert_rowid() AS id").get().id;
d3.close();
const g = await ruf("/workspace/api/chat/gruppe", aDogi.keks, "POST",
{ name: "Team intern", mit: [idZwei] });
raumTeam = json(g).raum_id ?? null;
ok(Number.isInteger(raumTeam),
`ein Raum, in dem noch jemand aus dem Team sitzt (Raum ${raumTeam})`);
}
melde("\n=== 6. Am echten Bildschirm ===");
const BASIS = `http://127.0.0.1:${PORT}`;
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
@@ -631,12 +797,270 @@ try {
const jetztRaus = await s.locator(".chat-nachricht").count();
ok(jetztRaus > vorher + 1, ` und Enter schickt wieder ab (${jetztRaus}, vorher ${vorher})`);
/* ---- "@RUDEL" AM BILDSCHIRM (23.09.2026) -----------------------
Alles bisher hat bewiesen, dass der Server den Richtigen ruft.
Hier geht es um die andere Haelfte: Findet man es, und sieht man
es? Ein Ruf, den niemand vorgeschlagen bekommt und der im Satz
grau bleibt wie jedes andere Wort, ist eine Attrappe. */
if (Number.isInteger(raumTeam)) {
await s.goto(`${BASIS}/workspace/chat.html?raum=${raumTeam}`, { waitUntil: "networkidle" });
await s.waitForTimeout(1200);
await s.click("#text");
await s.type("#text", "@", { delay: 40 });
await s.waitForTimeout(400);
const eintraege = await liste.locator(".erwaehn-liste__eintrag").count();
const ersterName = await liste.locator(".erwaehn-liste__name").first().innerText();
const ersterZusatz = await liste.locator(".erwaehn-liste__rolle").first().innerText();
ok(eintraege >= 2 && ersterName === "rudel",
`der Ruf steht ganz oben in der Liste (${eintraege} Eintraege, erster: ${ersterName})`);
/* WAS ES BEDEUTET, MUSS DANEBENSTEHEN. "rudel" allein sagt keinem
Neuen, dass damit das ganze Team gemeint ist -- und wer das erst
nach dem Absenden erfaehrt, hat schon alle geweckt. */
ok(/alle im Team/i.test(ersterZusatz),
` und daneben steht, was es bedeutet ("${ersterZusatz}")`);
const alsPerson = await liste.locator('.erwaehn-liste__eintrag[data-rudel="ja"]').count();
ok(alsPerson === 1, ` genau einmal, als eigene Sorte Eintrag (${alsPerson})`);
await s.keyboard.press("Enter");
await s.waitForTimeout(300);
ok((await s.inputValue("#text")) === "@rudel ",
`Enter setzt den Ruf ein: ${JSON.stringify(await s.inputValue("#text"))}`);
await s.type("#text", "bitte alle kurz schauen", { delay: 10 });
await s.keyboard.press("Enter");
await s.waitForTimeout(1500);
const marke = await s.evaluate(() => {
const m = document.querySelector('.chat-erwaehnung[data-rudel="ja"]');
if (!m) return null;
const st = getComputedStyle(m);
const normal = document.querySelector(".chat-nachricht__text");
return {
wort: m.textContent.trim(),
gewicht: st.fontWeight,
schatten: st.boxShadow !== "none",
eigen: m.dataset.ich === "ja",
farbeGleichText: st.color === getComputedStyle(normal).color,
};
});
ok(!!marke, "der Ruf ist im Satz hervorgehoben");
/* Das Zeichen steht per Stilvorlage davor (::before) und zaehlt
nicht zum Textinhalt -- geprueft wird deshalb das Wort. */
ok(marke?.wort === "@rudel", ` und zwar genau: ${marke?.wort}`);
/* NICHT NUR DIE FARBE. Wer Farben schlecht unterscheidet, muss es
trotzdem sehen -- dieselbe Regel wie beim eigenen Namen. */
ok(Number(marke?.gewicht) >= 600 && marke?.schatten === true,
` erkennbar an Gewicht (${marke?.gewicht}) und Rahmen (${marke?.schatten}), nicht nur an der Farbe`);
/* GEGENPROBE: Es ist NICHT die Marke fuer "du bist gemeint". Saehen
beide gleich aus, verloere die persoenliche ihre Wirkung. */
ok(marke?.eigen === false,
" und es ist nicht die Marke fuer den eigenen Namen — alle gemeint ist etwas anderes als du gemeint");
} else {
ok(false, "ohne Team-Raum ist der Ruf am Bildschirm nicht messbar");
}
await dogi.ctx.close();
await van.ctx.close();
} finally {
await browser.close();
}
/* =====================================================================
7. "@RUDEL" DURCH DIE SCHNITTSTELLE (23.09.2026)
Filipe: „dan will ich auch dass nur die modis, rechte hand, linke
hand und dogfather, alle auch auf einmal markieren können im chat
mit einem, @rudel ,dann sollen alle eine benarichtigung bekommen."
ALLES BISHER WAR RECHNEN OHNE SERVER. Hier geht es durch die
richtige Tür.
GEPRÜFT WIRD IM TREFF, UND DAS IST KEINE BEQUEMLICHKEIT. Der Treff
ist der einzige Raum des Hauses, in dem Team Dogi und Menschen
sitzen, die NICHT dazugehören (Rolle „gast" -- die Community). In
einem gewöhnlichen Gespräch geht das gar nicht: Die Häusertrennung
lässt einen Scout und einen Modi nicht in denselben Raum, das ist
beim Bauen dieser Prüfung gemessen worden (403, „Mit mindestens
einer Person kannst du nicht schreiben").
Damit ist der Treff die einzige Stelle, an der der Unterschied
zwischen „alle im Raum" und „alle im Team" überhaupt sichtbar wird
-- und zugleich die, an der die falsche Lesart wehtäte: Sie hätte
jeden Zuschauer geweckt.
ZWEI FRAGEN, UND DIE ZWEITE IST DIE WICHTIGERE:
1. Ruft der Modi wirklich sein Team?
2. Ruft ein Gast wirklich NICHTS? -- Ohne diese Prüfung stünde
Filipes „nur die modis, rechte hand, linke hand und dogfather"
bloss im Kommentar.
===================================================================== */
melde("\n" + "=== 7. Der Ruf ans Rudel geht durch die Tür ===");
{
/* Die Anmeldewand entscheidet am HOST, in welchem Haus sie steht.
Team Dogi gibt es nur auf crew.… -- ohne diesen Kopf käme jede
Anfrage auf der Agenturseite an, und die Rolle "modi" gibt es
dort gar nicht. `fetch` verwirft den Host-Kopf, deshalb node:http. */
const CREW = "crew.dogfather-universe.com";
function crewRuf(pfad, keks, methode = "GET", koerper = null) {
return new Promise((fertig, schief) => {
const daten = koerper === null ? null : Buffer.from(JSON.stringify(koerper));
const a = httpAnfrage({
host: "127.0.0.1", port: PORT, path: pfad, method: methode,
headers: {
Host: CREW, Origin: `https://${CREW}`, Referer: `https://${CREW}/workspace/`,
...(daten ? { "Content-Type": "application/json", "Content-Length": daten.length } : {}),
...(keks ? { Cookie: keks } : {}),
},
}, (antwort) => {
const st = [];
antwort.on("data", (x) => st.push(x));
antwort.on("end", () => {
const text = Buffer.concat(st).toString("utf8");
let j = null; try { j = JSON.parse(text); } catch { /* kein JSON */ }
fertig({
code: antwort.statusCode, json: j, text,
keks: [].concat(antwort.headers["set-cookie"] || [])
.map((c) => c.split(";")[0]).join("; "),
});
});
});
a.on("error", schief);
if (daten) a.write(daten);
a.end();
});
}
/* Drei weitere Zugänge: ein Modi, die linke Hand und ein Gast. */
const d2 = new DatabaseSync(process.env.WORKSPACE_DB);
const schluessel2 = d2.prepare(
"SELECT wert FROM einstellungen WHERE schluessel = ?").get("code_kennung_schluessel").wert;
const anlegen2 = (name, rolle, code) => {
const salt = randomBytes(16).toString("hex");
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d2.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
+ " VALUES (?,?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768,
createHmac("sha256", schluessel2).update(code).digest("hex"), jetzt);
return d2.prepare("SELECT last_insert_rowid() AS id").get().id;
};
const idModi = anlegen2("Frida", "modi", "CODE-MODI-0001");
const idLinke = anlegen2("Lino", "linke", "CODE-LINK-0001");
const idGast = anlegen2("Zuschauer Zoe", "gast", "CODE-GAST-0001");
d2.close();
const aModi = await crewRuf("/workspace/api/anmelden", null, "POST",
{ rolle: "modi", code: "CODE-MODI-0001" });
const aLinke = await crewRuf("/workspace/api/anmelden", null, "POST",
{ rolle: "linke", code: "CODE-LINK-0001" });
/* EIN GAST BESTAETIGT SEIN ALTER. Ohne `alter_ok` kommt 400 --
gemessen, nicht vermutet; der erste Anlauf dieser Pruefung ist
genau daran haengengeblieben. Das Feld gilt NUR fuer die
Community-Kachel, fuer alle anderen Rollen verlangt der Server es
gar nicht erst. Dieselbe Stelle steht in pruef-treff. */
const aGast = await crewRuf("/workspace/api/anmelden", null, "POST",
{ rolle: "gast", code: "CODE-GAST-0001", alter_ok: true });
ok(!!aModi.keks && !!aLinke.keks && !!aGast.keks,
`Modi, linke Hand und ein Gast sind angemeldet (${aModi.code}/${aLinke.code}/${aGast.code})`);
/* DER TREFF FUELLT SICH, WENN MAN IHN AUFMACHT. treffAngleichen()
laeuft beim Abrufen der Gespraechsliste -- also einmal je Zugang. */
for (const k of [aModi.keks, aLinke.keks, aGast.keks]) {
await crewRuf("/workspace/api/chat/raeume", k);
}
const drin = new DatabaseSync(process.env.WORKSPACE_DB);
const treff = drin.prepare(
"SELECT id FROM chat_raeume WHERE art = 'kanal' AND kategorie = ?").get("treff")?.id;
/* DRITTER AUSGANG: Gibt es den Treff nicht, steht hier warum --
statt dass die folgenden Pruefungen an `undefined` arbeiten und
Fehler melden, die mit dem Rudel nichts zu tun haben. */
ok(Number.isInteger(treff), `der Treff steht (Raum ${treff})`);
const besetzung = Number.isInteger(treff) ? drin.prepare(
`SELECT t.person_id AS id, p.rolle FROM chat_teilnehmer t
JOIN personen p ON p.id = t.person_id
WHERE t.raum_id = ? AND t.raus_am IS NULL`).all(treff) : [];
const gaeste = besetzung.filter((x) => x.rolle === "gast").map((x) => x.id);
/* DIE ZAHL GEHOERT IN DIE BEDINGUNG. Saesse kein Gast im Treff,
waere die Gegenprobe weiter unten leer und trotzdem gruen --
genau der Fehler, der am 01.09. dreimal gruen dastand. */
ok(gaeste.includes(idGast) && besetzung.length >= 3,
`im Treff sitzen ${besetzung.length} Leute, darunter ein Gast`);
/* WER ERWARTET WIRD, WIRD ABGELEITET -- nicht abgeschrieben. Eine
feste Liste hier waere beim naechsten Rollenzuschnitt still
falsch, und die Pruefung haette den Fehler mitgemacht. */
const TEAM = new Set(["admin", "hand", "linke", "modi"]);
const erwartet = besetzung
.filter((x) => TEAM.has(x.rolle) && x.id !== idModi)
.map((x) => x.id).sort((x, y) => x - y);
/* ---- 1. DER MODI RUFT ------------------------------------------ */
const ruf1 = await crewRuf(`/workspace/api/chat/raeume/${treff}/nachrichten`,
aModi.keks, "POST", { text: "@rudel bitte alle kurz herschauen" });
ok(ruf1.code === 201, `der Modi darf im Treff schreiben (${ruf1.code} ${ruf1.code === 201 ? "" : ruf1.text.slice(0, 90)})`);
ok(ruf1.json?.nachricht?.rudel === true,
" und die Nachricht weiss, dass sie das Rudel gerufen hat");
const nrId = ruf1.json?.nachricht?.id;
const gemeint = drin.prepare(
"SELECT person_id FROM chat_erwaehnungen WHERE nachricht_id = ? ORDER BY person_id")
.all(nrId).map((x) => x.person_id).sort((x, y) => x - y);
ok(gemeint.length > 0 && JSON.stringify(gemeint) === JSON.stringify(erwartet),
`gerufen wird genau das Team im Raum (${JSON.stringify(gemeint)}, erwartet ${JSON.stringify(erwartet)})`);
/* DIE EIGENTLICHE AUSSAGE. "Rudel" heisst das Team -- nicht jeder im
Raum. Im Treff sitzt die Community; ein Ruf, der dort jeden
Zuschauer weckt, waere etwas anderes als der bestellte. */
ok(!gemeint.includes(idGast),
" der Gast im selben Raum aber NICHT — das Rudel ist das Team, nicht der Raum");
ok(!gemeint.includes(idModi),
" und der Rufer selbst auch nicht — man ruft nicht sich");
ok(gemeint.includes(idLinke),
" die linke Hand ist dabei — sie gehoert zum Team");
/* Die Spalte bleibt stehen -- sie ist es, aus der beim Lesen die
Hervorhebung entsteht. */
const gespeichert = drin.prepare(
"SELECT rudel FROM chat_nachrichten WHERE id = ?").get(nrId)?.rudel;
ok(gespeichert === 1, `und es steht an der Nachricht (rudel = ${gespeichert})`);
const verlauf = await crewRuf(`/workspace/api/chat/raeume/${treff}/nachrichten`, aModi.keks);
const geladen = (verlauf.json?.nachrichten || []).find((x) => x.id === nrId);
ok(geladen?.rudel === true, "der Verlauf bringt es mit — auch nach dem Neuladen");
ok(verlauf.json?.darf_rudel === true, "und der Modi bekommt gesagt, dass er rufen darf");
ok(verlauf.json?.rudel_wort === RUDEL_WORT,
`das Wort kommt vom Server, nicht aus dem Browser (${verlauf.json?.rudel_wort})`);
/* ---- 2. GEGENPROBE: DER GAST RUFT INS LEERE --------------------- */
const ruf2 = await crewRuf(`/workspace/api/chat/raeume/${treff}/nachrichten`,
aGast.keks, "POST", { text: "@rudel und ihr alle mal" });
ok(ruf2.code === 201, `der Gast darf im Treff schreiben wie alle (${ruf2.code})`);
const nr2 = ruf2.json?.nachricht?.id;
ok(ruf2.json?.nachricht?.rudel !== true, " aber sein @rudel ruft niemanden");
const gemeint2 = drin.prepare(
"SELECT person_id FROM chat_erwaehnungen WHERE nachricht_id = ?").all(nr2);
ok(gemeint2.length === 0,
` kein einziger Eintrag (${gemeint2.length}) — nur Team Dogi und DogFather duerfen rufen`);
const gespeichert2 = drin.prepare(
"SELECT rudel FROM chat_nachrichten WHERE id = ?").get(nr2)?.rudel;
ok(!gespeichert2, ` und an der Nachricht steht nichts (rudel = ${gespeichert2})`);
const verlauf2 = await crewRuf(`/workspace/api/chat/raeume/${treff}/nachrichten`, aGast.keks);
ok(verlauf2.json?.darf_rudel === false,
"dem Gast sagt der Server, dass er nicht rufen darf — der Vorschlag erscheint gar nicht erst");
/* ---- 3. EIN NAME WIRKT WEITERHIN FUER ALLE ---------------------- */
const ruf3 = await crewRuf(`/workspace/api/chat/raeume/${treff}/nachrichten`,
aGast.keks, "POST", { text: "@Frida schaust du mal?" });
const gemeint3 = drin.prepare(
"SELECT person_id FROM chat_erwaehnungen WHERE nachricht_id = ?")
.all(ruf3.json?.nachricht?.id).map((x) => x.person_id);
ok(gemeint3.length === 1 && gemeint3[0] === idModi,
"wer nicht rufen darf, kann trotzdem einzeln ansprechen — das bleibt unberuehrt");
drin.close();
}
console.log(`\n${fehler ? fehler + " FEHLER" : "Alles in Ordnung"} — ${geprueft} Prüfungen\n`);
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
process.exit(fehler ? 1 : 0);
+136 -8
View File
@@ -45,9 +45,10 @@ import {
db, protokolliere, echteIp, sitzungLesen, istDogFather,
schreibbareIds, darfSchreibenMit, ROLLEN_SORTIERUNG, DATEN_ORDNER,
fuehrtTeamDogi, MODI_KATEGORIEN, ROLLEN_GRUPPE, TEAM_DOGI_ROLLEN,
istTeamDogi,
} from "./workspace.js";
import { erwaehnteIds } from "./chat-erwaehnung.js";
import { erwaehnteIds, rudelGerufen, RUDEL_WORT } from "./chat-erwaehnung.js";
import { KATALOG, ERLAUBT, VORSCHLAG, FAVORITEN_MAX } from "./workspace-reaktionen.js";
/* Der Treff-Chat: eigener Raum, eigene Nachtruhe, keine Anrufe.
@@ -1296,7 +1297,7 @@ chatRouter.get("/workspace/api/chat/raeume/:id/nachrichten", (req, res) => {
const reihen = db().prepare(`
SELECT n.id, n.person_id, n.text, n.erstellt, n.weg_am, p.name AS von, p.rolle,
p.bild AS von_bild, p.chat_kachel AS von_kachel,
n.antwort_auf, n.angeheftet_am,
n.antwort_auf, n.angeheftet_am, n.rudel,
n.anhang_name, n.anhang_art, n.anhang_typ, n.anhang_groesse,
n.anhang_breite, n.anhang_hoehe,
a.text AS zitat_text, a.weg_am AS zitat_weg, ap.name AS zitat_von,
@@ -1368,6 +1369,31 @@ chatRouter.get("/workspace/api/chat/raeume/:id/nachrichten", (req, res) => {
waere eine Auskunft ueber das Haus an jeden Leser. Und es
waere eine zweite Wahrheit neben der Schranke im Server. */
darf_alles_loeschen: darfJedeNachrichtLoeschen(req.person),
/* DARF ICH DAS RUDEL RUFEN? (23.09.2026)
Vom Server, aus derselben Funktion wie die Schranke beim
Schreiben (istTeamDogi). Die Oberflaeche koennte
`ich.rolle === 'modi' || ...` vergleichen -- das waere eine
zweite Wahrheit, und assets/js/chat.js bekommt jeder, der die
Seite oeffnet. Ein Ja/Nein verraet nichts ueber das Haus.
DAS WORT KOMMT MIT. Stuende "rudel" im Browser fest, hiesse es
nach einer Umbenennung an einer Stelle anders als an der
anderen -- und der Vorschlag beim Tippen setzte etwas ein, das
der Server nicht mehr erkennt. */
/* UND OB ER HIER ueberhaupt jemanden erreichen wuerde. In einem
Zweier-Gespraech mit einem Creator ist ausser mir niemand aus
dem Team -- der Vorschlag "rudel, alle im Team" waere dort ein
Eintrag, der nichts tut. Dieselbe Ueberlegung wie beim eigenen
Namen, den die Liste auch nicht anbietet.
DIE SCHRANKE BEIM SCHREIBEN HAENGT NICHT DARAN. Wer den Ruf
von Hand tippt, ruft eben niemanden -- genau wie ein "@Anna",
das Anna nicht beantwortet. Verboten ist es nicht, es ist nur
keine Hilfe, es vorzuschlagen. */
darf_rudel: istTeamDogi(req.person)
&& leute.some((t) => t.id !== req.person.id && istTeamDogi(t)),
rudel_wort: RUDEL_WORT,
angeheftet: angeheftet.map((a) => ({
id: a.id,
von: a.von || "Gelöscht",
@@ -1405,6 +1431,10 @@ chatRouter.get("/workspace/api/chat/raeume/:id/nachrichten", (req, res) => {
erstellt: n.erstellt,
selbst: n.person_id === req.person.id,
angeheftet: !!n.angeheftet_am,
/* WURDE HIER DAS RUDEL GERUFEN? Steht an der Nachricht, weil
der Browser sonst die damalige Rolle des Absenders kennen
muesste -- siehe die Spalte in workspace.js. */
rudel: !!n.rudel,
/* Das Zitat wird GEKUERZT geschickt, nicht ganz: Es ist ein
Hinweis auf die Stelle, nicht ihre Wiederholung. Und eine
zurueckgenommene Nachricht bleibt auch im Zitat
@@ -1530,6 +1560,11 @@ chatRouter.post("/workspace/api/chat/raeume/:id/nachrichten", gleicheHerkunft,
zurueckgenommen: !!zitat.weg_am,
} : null,
anhang: null,
/* Wird gleich gesetzt, wenn das Rudel gerufen wurde. Steht
hier schon drin, damit das Feld in JEDER Nachricht vorkommt
-- eine Eigenschaft, die mal da ist und mal nicht, ist die,
die im Browser irgendwann `undefined` heisst. */
rudel: false,
};
/* ==== WER IST MIT "@" GEMEINT? (20.09.2026) =====================
@@ -1548,17 +1583,73 @@ chatRouter.post("/workspace/api/chat/raeume/:id/nachrichten", gleicheHerkunft,
Die Regeln stehen in chat-erwaehnung.js, samt Begruendung je
Regel. Sie sind bewusst ohne Abhaengigkeiten, damit die
Pruefung sie lesen kann, ohne einen Server hochzufahren. */
/* ==== "@RUDEL" -- ALLE AUF EINMAL (23.09.2026) =================
Filipe: „dan will ich auch dass nur die modis, rechte hand,
linke hand und dogfather, alle auch auf einmal markieren
koennen im chat mit einem, @rudel ,dann sollen alle eine
benarichtigung bekommen."
WER DARF, WIRD HIER ENTSCHIEDEN -- nicht im Browser und nicht
in chat-erwaehnung.js. Der Browser schickt nur Text; die Regeln
haben keine Abhaengigkeiten und sollen keine bekommen. Bleibt
diese Stelle, und sie fragt die eine Funktion des Hauses:
istTeamDogi(). Schreibt ein Creator "@rudel", passiert nichts
-- kein Ruf, kein Eintrag, keine Hervorhebung. */
const darfRudel = istTeamDogi(req.person);
const alleLeute = teilnehmerVon(raumId);
const erwaehnt = erwaehnteIds(text, alleLeute)
const rudel = rudelGerufen(text, alleLeute, { rudel: darfRudel });
/* WEN DER RUF ERREICHT: die Leute IN DIESEM RAUM, die zu Team
Dogi gehoeren -- nicht jeden im Raum.
Das Wort sagt es selbst. Ein Rudel ist das Team, und Filipe
nennt in einem Atemzug dieselben vier Gruppen, die es auch
rufen duerfen. In einem reinen Team-Kanal ist das jeder
Anwesende, also genau "alle auf einmal".
DER UNTERSCHIED WIRD IM TREFF SICHTBAR, und dort haette die
andere Lesart wehgetan: Dort sitzt die Community. "@rudel"
haette zweihundert Zuschauer gleichzeitig geweckt -- und beim
zweiten Mal haetten sie die Benachrichtigungen abgeschaltet.
Genau davor warnt der erste Absatz dieser Datei seit dem
20.09. Ein Moderator, der im Treff "@rudel" schreibt, erreicht
damit seine Kollegen; die Zuschauer lesen es einfach mit. */
const ausDemRudel = rudel
? alleLeute.filter((t) => istTeamDogi(t)).map((t) => t.id)
: [];
const erwaehnt = [...new Set([
...erwaehnteIds(text, alleLeute, { rudel: darfRudel }),
...ausDemRudel,
])]
/* SICH SELBST ANSPRECHEN LOEST NICHTS AUS. Man schreibt den
eigenen Namen durchaus ("@Anna hat recht, ich auch") -- eine
Meldung an sich selbst waere Unsinn. */
Meldung an sich selbst waere Unsinn. Beim Rudel genauso: Wer
das Team ruft, ruft nicht sich. */
.filter((pid) => pid !== req.person.id);
if (erwaehnt.length) {
const einf = d.prepare(
"INSERT OR IGNORE INTO chat_erwaehnungen (nachricht_id, person_id) VALUES (?,?)");
for (const pid of erwaehnt) einf.run(id, pid);
}
if (rudel) {
/* DIE NACHRICHT MERKT SICH, DASS DAS RUDEL GERUFEN WURDE.
Daraus entsteht beim Lesen die Hervorhebung -- ohne dass der
Browser die Rolle des Absenders von damals nachschlagen
muesste. Begruendung steht bei der Spalte in workspace.js. */
d.prepare("UPDATE chat_nachrichten SET rudel = 1 WHERE id = ?").run(id);
nachricht.rudel = true;
/* INS PROTOKOLL. Ein Ruf ans ganze Team ist der eine Handgriff
im Chat, der alle gleichzeitig unterbricht. Wer ihn benutzt,
soll das nicht heimlich tun koennen -- nicht als Misstrauen,
sondern weil "@rudel wird zu oft benutzt" sonst ein Gefuehl
bleibt statt einer Zahl. */
protokolliere("chat_rudel", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
detail: `#${id} an ${erwaehnt.length} im Raum ${raumId}`.slice(0, 120),
});
}
/* Sofort an alle, die gerade zusehen -- und per Benachrichtigung
an die, die es nicht tun. Beides läuft über chatEreignis(),
@@ -1570,7 +1661,7 @@ chatRouter.post("/workspace/api/chat/raeume/:id/nachrichten", gleicheHerkunft,
abschaltet. */
const empfaenger = alleLeute.filter((t) => t.id !== req.person.id);
chatEreignis(raumId, nachricht, empfaenger.map((t) => t.id), "neu",
new Set(erwaehnt));
new Set(erwaehnt), rudel);
res.status(201).json({ nachricht });
} catch (fehler) {
@@ -2481,7 +2572,7 @@ const zuschauer = new Map();
* bekommt was" gehoert an EINE Stelle.
*/
function chatEreignis(raumId, nachricht, benachrichtigen, art = "neu",
erwaehnte = new Set()) {
erwaehnte = new Set(), rudel = false) {
/* 1. An alle offenen Verbindungen der Beteiligten. */
const leute = teilnehmerVon(raumId).map((t) => t.id);
for (const personId of leute) {
@@ -2520,8 +2611,19 @@ function chatEreignis(raumId, nachricht, benachrichtigen, art = "neu",
Wortlaut UND Art -- und die Art kann jeder einzeln abschalten.
Wer den Chat stumm stellt, weil dort viel geredet wird, will
trotzdem wissen, wenn ihn jemand direkt anspricht. */
/* DREI STUFEN STATT ZWEI (23.09.2026): nur dabei -- angesprochen
-- das ganze Rudel gerufen. Der Unterschied steht im WORTLAUT,
nicht in einer zweiten Meldung: "Anna hat das Rudel gerufen"
sagt sofort, dass es alle betrifft und nicht nur mich.
AUF DEMSELBEN SCHALTER WIE DIE ERWAEHNUNG, mit Absicht (siehe
meldeNeueNachricht). Ein Ruf ans Team IST eine Erwaehnung -- nur
eben von allen. Ein eigener vierter Schalter waere der, den
jemand abschaltet und der dann genau im wichtigen Moment fehlt. */
void meldeNeueNachricht(personId, raumId, nachricht,
art === "neu" && erwaehnte.has(personId) ? "erwaehnung" : art);
art === "neu" && erwaehnte.has(personId)
? (rudel ? "rudel" : "erwaehnung")
: art);
}
}
@@ -2591,6 +2693,27 @@ export function pushTextFuer(art, nachricht, raumId) {
};
}
/* ---- DAS GANZE RUDEL (23.09.2026) ----------------------------
Filipe: „dann sollen alle eine benarichtigung bekommen."
WARUM NICHT EINFACH „hat dich erwähnt": Weil es nicht stimmt.
Beim Rudel ist man nicht gemeint, sondern mitgemeint -- und wer
dreimal „hat dich erwähnt" liest und jedes Mal feststellt, dass
es alle betraf, glaubt beim vierten Mal auch dem echten nicht
mehr. Der Wortlaut sagt deshalb, was los ist.
ES IST TROTZDEM DIESELBE ART von Meldung (chat_erwaehnung, siehe
meldeNeueNachricht) -- ein Ruf ans Team ist eine Erwähnung, nur
eben von allen. */
if (art === "rudel") {
return {
titel: `${wer} hat das Rudel gerufen`,
text: String(nachricht.text || "").slice(0, 120),
ziel,
};
}
return {
titel: `Nachricht von ${wer}`,
/* Der Anfang der Nachricht steht in der Meldung -- eine
@@ -2636,7 +2759,12 @@ async function meldeNeueNachricht(personId, raumId, nachricht, art = "neu") {
Wer den lauten Chat stumm stellt, verliert damit weder Anrufe
noch die Faelle, in denen ihn jemand direkt anspricht. */
const pushArt = art === "anruf" ? "anruf"
: art === "erwaehnung" ? "chat_erwaehnung"
/* Der Ruf ans Rudel laeuft ueber DENSELBEN Schalter wie die
Erwaehnung. Kein vierter Schalter: Wer "@rudel" stummschaltet,
schaltet den Fall stumm, in dem das ganze Team gebraucht wird
-- und genau dafuer gibt es ihn. Wem es zu viel wird, der sagt
es demjenigen, der ruft; im Protokoll steht, wer das war. */
: (art === "erwaehnung" || art === "rudel") ? "chat_erwaehnung"
: "chat_nachricht";
await benachrichtige(personId, pushArt, pushTextFuer(art, nachricht, raumId));
} catch (fehler) {
+45 -8
View File
@@ -753,10 +753,26 @@ export function ohneTeamDogi(praefix, spalten = ["creator_id", "erstellt_von"])
return ohneRollen(praefix, spalten, TEAM_DOGI_ROLLEN);
}
/** Gehoert dieser Mensch zu Team Dogi -- DogFather eingerechnet?
*
* DAS SIND DIE VIER, DIE FILIPE IMMER WIEDER NENNT: „nur die modis
* rechte linke hand und dogfather". Genau diese Aufzaehlung stand
* hier dreimal wortgleich als Ausdruck da (siehtModis, kanaeleFuer,
* kategorienFuer) und waere mit dem naechsten Wunsch ein viertes Mal
* dazugekommen. Drei gleiche Ausdruecke sind drei Gelegenheiten, dass
* einer beim naechsten Rollenzuschnitt nicht mitgeht.
*
* ABGELEITET, NICHT ABGESCHRIEBEN: TEAM_DOGI_ROLLEN (hand, linke,
* modi) ist die eine Liste des Hauses; DogFather kommt dazu, weil er
* dazugehoert -- er steht nicht darueber, er ist dabei. Kaeme morgen
* eine weitere Team-Rolle, waere sie hier von selbst dabei.
*/
export const istTeamDogi = (person) => !!person
&& (person.rolle === "admin" || TEAM_DOGI_ROLLEN.has(person.rolle));
/** Darf diese Person ueberhaupt etwas sehen, das einem Modi gehoert?
* Nur die DogFather-Rolle und die Modis selbst. */
export const siehtModis = (person) => !!person
&& (person.rolle === "admin" || TEAM_DOGI_ROLLEN.has(person.rolle));
export const siehtModis = istTeamDogi;
export const siehtAlles = (person) => !!person
&& (person.rolle === "admin" || person.rolle === "spicy");
@@ -2207,9 +2223,7 @@ export function kanalVonHandle(handle) {
* Liste gar nicht erst mit, statt mitzukommen und ausgeblendet zu
* werden. */
export function kanaeleFuer(person) {
if (!person) return [];
return TEAM_DOGI_ROLLEN.has(person.rolle) || person.rolle === "admin"
? KANAELE : [];
return istTeamDogi(person) ? KANAELE : [];
}
/* =====================================================================
@@ -2292,9 +2306,7 @@ export function loeschspurenAufraeumen(d, dbPfad = DB_PFAD) {
}
export function kategorienFuer(person) {
if (!person) return [];
return TEAM_DOGI_ROLLEN.has(person.rolle) || person.rolle === "admin"
? MODI_KATEGORIEN : [];
return istTeamDogi(person) ? MODI_KATEGORIEN : [];
}
/**
@@ -3186,6 +3198,31 @@ function umstellungen(d) {
Gelegenheit fuer eine Sperre. */
["chat_nachrichten", "antwort_auf", "INTEGER"],
/* WURDE HIER DAS GANZE RUDEL GERUFEN? (23.09.2026)
Filipe: „dan will ich auch dass nur die modis, rechte hand,
linke hand und dogfather, alle auch auf einmal markieren koennen
im chat mit einem, @rudel ,dann sollen alle eine benarichtigung
bekommen."
WARUM DAS GESPEICHERT WIRD UND NICHT BEIM LESEN NACHGERECHNET:
Beim Lesen muesste der Browser wissen, ob der ABSENDER es damals
durfte. Er weiss nur dessen heutige Rolle. Wechselt jemand die
Rolle, aenderte sich rueckwirkend, ob ein alter Satz das Rudel
gerufen hat -- die Hervorhebung verschwaende aus einer Nachricht
von vorletzter Woche, in der sie damals richtig war.
Und es waere die zweite Rechnung ueber dieselbe Frage: Der
Server entscheidet beim Schreiben, wer gerufen wird. Rechnete
der Browser beim Lesen noch einmal, haetten wir genau die Lage,
vor der chat-erwaehnung.js im ersten Absatz warnt -- die Seite
hebt jemanden hervor, den niemand benachrichtigt hat.
VORGABE 0. Alle Nachrichten von vor heute haben kein Rudel
gerufen, und das ist keine Annahme, sondern eine Tatsache: Es
gab das Wort noch nicht. */
["chat_nachrichten", "rudel", "INTEGER NOT NULL DEFAULT 0"],
/* GESPRAECH LOESCHEN -- NUR BEI MIR (09.09.2026).
Filipe: "man muss die chats auch geloescht bekommen!!!"