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
+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) {