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:
+89
-11
@@ -58,19 +58,49 @@
|
|||||||
Dreimal "@Anna" in einem Satz ist trotzdem eine Ansprache.
|
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
|
Hier stand bis heute: „KEIN @alle. Es wäre in fünf Minuten gebaut und
|
||||||
Weg, dass alle die Benachrichtigungen abschalten -- und dann kommt
|
ist der zuverlässigste Weg, dass alle die Benachrichtigungen
|
||||||
auch die an, die wirklich für einen bestimmten Menschen gedacht war.
|
abschalten -- und dann kommt auch die an, die wirklich für einen
|
||||||
Wenn Filipe es ausdrücklich will, gehört dazu eine Entscheidung, WER
|
bestimmten Menschen gedacht war. Wenn Filipe es ausdrücklich will,
|
||||||
es benutzen darf; das ist eine Frage an ihn, keine, die ich hier
|
gehört dazu eine Entscheidung, WER es benutzen darf; das ist eine
|
||||||
beantworte.
|
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):
|
AUFRUF (auch aus einer Prüfung heraus):
|
||||||
import { erwaehnungenFinden } from "./chat-erwaehnung.js";
|
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.
|
/** Ein Zeichen, das einen Namen fortsetzen könnte.
|
||||||
*
|
*
|
||||||
* BUCHSTABE ODER ZIFFER, und zwar auch mit Umlaut oder Akzent:
|
* 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
|
* @param {Array<{id:number,name:string}>} leute
|
||||||
* Die Teilnehmer DIESES Raums. Wer nicht darin steht, kann
|
* Die Teilnehmer DIESES Raums. Wer nicht darin steht, kann
|
||||||
* nicht gemeint sein -- siehe Regel (1) oben.
|
* 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
|
* Je Treffer die Person und die Stelle im Text (`von` bis
|
||||||
* `bis`, ohne das "@"-Zeichen gerechnet wie `slice`). Die
|
* `bis`, ohne das "@"-Zeichen gerechnet wie `slice`). Die
|
||||||
* Stellen braucht der Browser zum Hervorheben; der Server
|
* Stellen braucht der Browser zum Hervorheben; der Server
|
||||||
* benutzt nur `id`. Sortiert nach Vorkommen im Text.
|
* 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 || "");
|
const satz = String(text || "");
|
||||||
if (!satz.includes("@")) return [];
|
if (!satz.includes("@")) return [];
|
||||||
|
|
||||||
@@ -119,6 +157,18 @@ export function erwaehnungenFinden(text, leute) {
|
|||||||
/* REGEL (2): Was steht vor dem "@"? */
|
/* REGEL (2): Was steht vor dem "@"? */
|
||||||
if (wortzeichenBei(satz, i - 1)) continue;
|
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) {
|
for (const person of nachLaenge) {
|
||||||
const name = String(person.name);
|
const name = String(person.name);
|
||||||
const nk = name.toLowerCase();
|
const nk = name.toLowerCase();
|
||||||
@@ -146,7 +196,35 @@ export function erwaehnungenFinden(text, leute) {
|
|||||||
* ALS EIGENE FUNKTION, damit an der Aufrufstelle nicht steht
|
* ALS EIGENE FUNKTION, damit an der Aufrufstelle nicht steht
|
||||||
* `[...new Set(x.map(t => t.id))]`. Diese Zeile wäre an drei Orten
|
* `[...new Set(x.map(t => t.id))]`. Diese Zeile wäre an drei Orten
|
||||||
* gleich zu schreiben, und an einem davon irgendwann anders.
|
* 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) {
|
export function erwaehnteIds(text, leute, wie = {}) {
|
||||||
return [...new Set(erwaehnungenFinden(text, leute).map((t) => t.id))];
|
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
@@ -40,7 +40,7 @@ import { fileURLToPath } from "node:url";
|
|||||||
import { request as httpAnfrage } from "node:http";
|
import { request as httpAnfrage } from "node:http";
|
||||||
import { eigenerPort } from "./helfer-port.mjs";
|
import { eigenerPort } from "./helfer-port.mjs";
|
||||||
import { notbremse } from "./helfer-notbremse.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 HIER = dirname(fileURLToPath(import.meta.url));
|
||||||
const PORT = await eigenerPort(import.meta, "pruef-erwaehnung");
|
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.PORT = String(PORT);
|
||||||
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||||||
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
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 express = (await import("express")).default;
|
||||||
const ec = express.response.cookie;
|
const ec = express.response.cookie;
|
||||||
@@ -141,6 +154,73 @@ melde("\n=== 1. Wer ist gemeint? ===");
|
|||||||
immer jemanden findet, findet nichts. */
|
immer jemanden findet, findet nichts. */
|
||||||
ok(erwaehnteIds("@VanVan", []).length === 0,
|
ok(erwaehnteIds("@VanVan", []).length === 0,
|
||||||
"Gegenprobe: ohne Teilnehmerliste ist niemand gemeint");
|
"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(art?.vorgabe === true, " und ist von sich aus an");
|
||||||
ok(ARTEN.find((a) => a.schluessel === "chat_nachricht")?.schluessel !== art?.schluessel,
|
ok(ARTEN.find((a) => a.schluessel === "chat_nachricht")?.schluessel !== art?.schluessel,
|
||||||
" getrennt von der gewöhnlichen Chatnachricht — zwei Schalter, zwei Entscheidungen");
|
" 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,
|
ok(gleich === proben.length && proben.length >= 10,
|
||||||
`beide kommen bei allen ${proben.length} Proben auf dasselbe Ergebnis`);
|
`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,
|
liegt, wenn Enter statt zu wählen die halbe Nachricht abschickt,
|
||||||
oder wenn der Name am Ende grau bleibt wie jedes andere Wort.
|
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 ===");
|
melde("\n=== 6. Am echten Bildschirm ===");
|
||||||
const BASIS = `http://127.0.0.1:${PORT}`;
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
||||||
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
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();
|
const jetztRaus = await s.locator(".chat-nachricht").count();
|
||||||
ok(jetztRaus > vorher + 1, ` und Enter schickt wieder ab (${jetztRaus}, vorher ${vorher})`);
|
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 dogi.ctx.close();
|
||||||
await van.ctx.close();
|
await van.ctx.close();
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
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`);
|
console.log(`\n${fehler ? fehler + " FEHLER" : "Alles in Ordnung"} — ${geprueft} Prüfungen\n`);
|
||||||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||||||
process.exit(fehler ? 1 : 0);
|
process.exit(fehler ? 1 : 0);
|
||||||
|
|||||||
+136
-8
@@ -45,9 +45,10 @@ import {
|
|||||||
db, protokolliere, echteIp, sitzungLesen, istDogFather,
|
db, protokolliere, echteIp, sitzungLesen, istDogFather,
|
||||||
schreibbareIds, darfSchreibenMit, ROLLEN_SORTIERUNG, DATEN_ORDNER,
|
schreibbareIds, darfSchreibenMit, ROLLEN_SORTIERUNG, DATEN_ORDNER,
|
||||||
fuehrtTeamDogi, MODI_KATEGORIEN, ROLLEN_GRUPPE, TEAM_DOGI_ROLLEN,
|
fuehrtTeamDogi, MODI_KATEGORIEN, ROLLEN_GRUPPE, TEAM_DOGI_ROLLEN,
|
||||||
|
istTeamDogi,
|
||||||
} from "./workspace.js";
|
} 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";
|
import { KATALOG, ERLAUBT, VORSCHLAG, FAVORITEN_MAX } from "./workspace-reaktionen.js";
|
||||||
|
|
||||||
/* Der Treff-Chat: eigener Raum, eigene Nachtruhe, keine Anrufe.
|
/* 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(`
|
const reihen = db().prepare(`
|
||||||
SELECT n.id, n.person_id, n.text, n.erstellt, n.weg_am, p.name AS von, p.rolle,
|
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,
|
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_name, n.anhang_art, n.anhang_typ, n.anhang_groesse,
|
||||||
n.anhang_breite, n.anhang_hoehe,
|
n.anhang_breite, n.anhang_hoehe,
|
||||||
a.text AS zitat_text, a.weg_am AS zitat_weg, ap.name AS zitat_von,
|
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 Auskunft ueber das Haus an jeden Leser. Und es
|
||||||
waere eine zweite Wahrheit neben der Schranke im Server. */
|
waere eine zweite Wahrheit neben der Schranke im Server. */
|
||||||
darf_alles_loeschen: darfJedeNachrichtLoeschen(req.person),
|
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) => ({
|
angeheftet: angeheftet.map((a) => ({
|
||||||
id: a.id,
|
id: a.id,
|
||||||
von: a.von || "Gelöscht",
|
von: a.von || "Gelöscht",
|
||||||
@@ -1405,6 +1431,10 @@ chatRouter.get("/workspace/api/chat/raeume/:id/nachrichten", (req, res) => {
|
|||||||
erstellt: n.erstellt,
|
erstellt: n.erstellt,
|
||||||
selbst: n.person_id === req.person.id,
|
selbst: n.person_id === req.person.id,
|
||||||
angeheftet: !!n.angeheftet_am,
|
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
|
/* Das Zitat wird GEKUERZT geschickt, nicht ganz: Es ist ein
|
||||||
Hinweis auf die Stelle, nicht ihre Wiederholung. Und eine
|
Hinweis auf die Stelle, nicht ihre Wiederholung. Und eine
|
||||||
zurueckgenommene Nachricht bleibt auch im Zitat
|
zurueckgenommene Nachricht bleibt auch im Zitat
|
||||||
@@ -1530,6 +1560,11 @@ chatRouter.post("/workspace/api/chat/raeume/:id/nachrichten", gleicheHerkunft,
|
|||||||
zurueckgenommen: !!zitat.weg_am,
|
zurueckgenommen: !!zitat.weg_am,
|
||||||
} : null,
|
} : null,
|
||||||
anhang: 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) =====================
|
/* ==== 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
|
Die Regeln stehen in chat-erwaehnung.js, samt Begruendung je
|
||||||
Regel. Sie sind bewusst ohne Abhaengigkeiten, damit die
|
Regel. Sie sind bewusst ohne Abhaengigkeiten, damit die
|
||||||
Pruefung sie lesen kann, ohne einen Server hochzufahren. */
|
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 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
|
/* SICH SELBST ANSPRECHEN LOEST NICHTS AUS. Man schreibt den
|
||||||
eigenen Namen durchaus ("@Anna hat recht, ich auch") -- eine
|
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);
|
.filter((pid) => pid !== req.person.id);
|
||||||
if (erwaehnt.length) {
|
if (erwaehnt.length) {
|
||||||
const einf = d.prepare(
|
const einf = d.prepare(
|
||||||
"INSERT OR IGNORE INTO chat_erwaehnungen (nachricht_id, person_id) VALUES (?,?)");
|
"INSERT OR IGNORE INTO chat_erwaehnungen (nachricht_id, person_id) VALUES (?,?)");
|
||||||
for (const pid of erwaehnt) einf.run(id, pid);
|
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
|
/* Sofort an alle, die gerade zusehen -- und per Benachrichtigung
|
||||||
an die, die es nicht tun. Beides läuft über chatEreignis(),
|
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. */
|
abschaltet. */
|
||||||
const empfaenger = alleLeute.filter((t) => t.id !== req.person.id);
|
const empfaenger = alleLeute.filter((t) => t.id !== req.person.id);
|
||||||
chatEreignis(raumId, nachricht, empfaenger.map((t) => t.id), "neu",
|
chatEreignis(raumId, nachricht, empfaenger.map((t) => t.id), "neu",
|
||||||
new Set(erwaehnt));
|
new Set(erwaehnt), rudel);
|
||||||
|
|
||||||
res.status(201).json({ nachricht });
|
res.status(201).json({ nachricht });
|
||||||
} catch (fehler) {
|
} catch (fehler) {
|
||||||
@@ -2481,7 +2572,7 @@ const zuschauer = new Map();
|
|||||||
* bekommt was" gehoert an EINE Stelle.
|
* bekommt was" gehoert an EINE Stelle.
|
||||||
*/
|
*/
|
||||||
function chatEreignis(raumId, nachricht, benachrichtigen, art = "neu",
|
function chatEreignis(raumId, nachricht, benachrichtigen, art = "neu",
|
||||||
erwaehnte = new Set()) {
|
erwaehnte = new Set(), rudel = false) {
|
||||||
/* 1. An alle offenen Verbindungen der Beteiligten. */
|
/* 1. An alle offenen Verbindungen der Beteiligten. */
|
||||||
const leute = teilnehmerVon(raumId).map((t) => t.id);
|
const leute = teilnehmerVon(raumId).map((t) => t.id);
|
||||||
for (const personId of leute) {
|
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.
|
Wortlaut UND Art -- und die Art kann jeder einzeln abschalten.
|
||||||
Wer den Chat stumm stellt, weil dort viel geredet wird, will
|
Wer den Chat stumm stellt, weil dort viel geredet wird, will
|
||||||
trotzdem wissen, wenn ihn jemand direkt anspricht. */
|
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,
|
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 {
|
return {
|
||||||
titel: `Nachricht von ${wer}`,
|
titel: `Nachricht von ${wer}`,
|
||||||
/* Der Anfang der Nachricht steht in der Meldung -- eine
|
/* 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
|
Wer den lauten Chat stumm stellt, verliert damit weder Anrufe
|
||||||
noch die Faelle, in denen ihn jemand direkt anspricht. */
|
noch die Faelle, in denen ihn jemand direkt anspricht. */
|
||||||
const pushArt = art === "anruf" ? "anruf"
|
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";
|
: "chat_nachricht";
|
||||||
await benachrichtige(personId, pushArt, pushTextFuer(art, nachricht, raumId));
|
await benachrichtige(personId, pushArt, pushTextFuer(art, nachricht, raumId));
|
||||||
} catch (fehler) {
|
} catch (fehler) {
|
||||||
|
|||||||
+45
-8
@@ -753,10 +753,26 @@ export function ohneTeamDogi(praefix, spalten = ["creator_id", "erstellt_von"])
|
|||||||
return ohneRollen(praefix, spalten, TEAM_DOGI_ROLLEN);
|
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?
|
/** Darf diese Person ueberhaupt etwas sehen, das einem Modi gehoert?
|
||||||
* Nur die DogFather-Rolle und die Modis selbst. */
|
* Nur die DogFather-Rolle und die Modis selbst. */
|
||||||
export const siehtModis = (person) => !!person
|
export const siehtModis = istTeamDogi;
|
||||||
&& (person.rolle === "admin" || TEAM_DOGI_ROLLEN.has(person.rolle));
|
|
||||||
|
|
||||||
export const siehtAlles = (person) => !!person
|
export const siehtAlles = (person) => !!person
|
||||||
&& (person.rolle === "admin" || person.rolle === "spicy");
|
&& (person.rolle === "admin" || person.rolle === "spicy");
|
||||||
@@ -2207,9 +2223,7 @@ export function kanalVonHandle(handle) {
|
|||||||
* Liste gar nicht erst mit, statt mitzukommen und ausgeblendet zu
|
* Liste gar nicht erst mit, statt mitzukommen und ausgeblendet zu
|
||||||
* werden. */
|
* werden. */
|
||||||
export function kanaeleFuer(person) {
|
export function kanaeleFuer(person) {
|
||||||
if (!person) return [];
|
return istTeamDogi(person) ? KANAELE : [];
|
||||||
return TEAM_DOGI_ROLLEN.has(person.rolle) || person.rolle === "admin"
|
|
||||||
? KANAELE : [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =====================================================================
|
/* =====================================================================
|
||||||
@@ -2292,9 +2306,7 @@ export function loeschspurenAufraeumen(d, dbPfad = DB_PFAD) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function kategorienFuer(person) {
|
export function kategorienFuer(person) {
|
||||||
if (!person) return [];
|
return istTeamDogi(person) ? MODI_KATEGORIEN : [];
|
||||||
return TEAM_DOGI_ROLLEN.has(person.rolle) || person.rolle === "admin"
|
|
||||||
? MODI_KATEGORIEN : [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3186,6 +3198,31 @@ function umstellungen(d) {
|
|||||||
Gelegenheit fuer eine Sperre. */
|
Gelegenheit fuer eine Sperre. */
|
||||||
["chat_nachrichten", "antwort_auf", "INTEGER"],
|
["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).
|
/* GESPRAECH LOESCHEN -- NUR BEI MIR (09.09.2026).
|
||||||
|
|
||||||
Filipe: "man muss die chats auch geloescht bekommen!!!"
|
Filipe: "man muss die chats auch geloescht bekommen!!!"
|
||||||
|
|||||||
@@ -16,15 +16,15 @@
|
|||||||
verdeckt. Gefunden hat es pruef-struktur.
|
verdeckt. Gefunden hat es pruef-struktur.
|
||||||
|
|
||||||
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
|
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609230110" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609230135" />
|
||||||
<link rel="stylesheet" href="assets/css/haus.css?v=202609230110" />
|
<link rel="stylesheet" href="assets/css/haus.css?v=202609230135" />
|
||||||
<!-- Ohne diese drei nimmt ein iPhone einen Bildschirmausschnitt als
|
<!-- Ohne diese drei nimmt ein iPhone einen Bildschirmausschnitt als
|
||||||
Symbol, Android zeichnet die Leiste weiss, und die Seite oeffnet
|
Symbol, Android zeichnet die Leiste weiss, und die Seite oeffnet
|
||||||
ausserhalb der App -- auf einer Seite, die genau erklaeren soll,
|
ausserhalb der App -- auf einer Seite, die genau erklaeren soll,
|
||||||
WARUM das Telefon nicht klingelt, ist das besonders unpassend. -->
|
WARUM das Telefon nicht klingelt, ist das besonders unpassend. -->
|
||||||
<link rel="manifest" href="/workspace/crew.webmanifest" />
|
<link rel="manifest" href="/workspace/crew.webmanifest" />
|
||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609230110" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609230135" />
|
||||||
<style>
|
<style>
|
||||||
/* Bewusst in der Seite und nicht in einer eigenen Datei: Diese Seite
|
/* Bewusst in der Seite und nicht in einer eigenen Datei: Diese Seite
|
||||||
soll auch dann noch etwas anzeigen, wenn genau das schiefgeht,
|
soll auch dann noch etwas anzeigen, wenn genau das schiefgeht,
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script src="assets/js/anruf-probe.js?v=202609230110" defer></script>
|
<script src="assets/js/anruf-probe.js?v=202609230135" defer></script>
|
||||||
<script src="assets/js/installieren.js?v=202609230110" defer></script>
|
<script src="assets/js/installieren.js?v=202609230135" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -23,25 +23,25 @@
|
|||||||
},
|
},
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/assets/img/app-symbole/workspace-192.png?v=202609230110",
|
"src": "/assets/img/app-symbole/workspace-192.png?v=202609230135",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any"
|
"purpose": "any"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/assets/img/app-symbole/workspace-512.png?v=202609230110",
|
"src": "/assets/img/app-symbole/workspace-512.png?v=202609230135",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any"
|
"purpose": "any"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609230110",
|
"src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609230135",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "maskable"
|
"purpose": "maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609230110",
|
"src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609230135",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "maskable"
|
"purpose": "maskable"
|
||||||
|
|||||||
@@ -2393,6 +2393,67 @@
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- "@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."
|
||||||
|
|
||||||
|
ES MUSS ANDERS AUSSEHEN ALS DER EIGENE NAME. „Alle sind gemeint"
|
||||||
|
ist etwas anderes als „du bist gemeint" -- sähen beide gleich aus,
|
||||||
|
verlöre das zweite genau die Wirkung, für die es da ist.
|
||||||
|
|
||||||
|
ABER NICHT ÜBER EINE FESTE FARBE, und das ist hier der Kern:
|
||||||
|
Eine Nachricht liegt in der Blase, deren Farbe ihr Absender
|
||||||
|
AUSGESUCHT hat -- babyblau, veilchen, tinte, siebzehn Möglichkeiten.
|
||||||
|
Eine feste Farbe darauf ist eine Wette. Genau die habe ich am
|
||||||
|
22.09.2026 beim Löschknopf verloren: 1,91:1 auf Babyblau, gemessen,
|
||||||
|
nachdem es schon live war.
|
||||||
|
|
||||||
|
`--blase-text` ist die Schriftfarbe, die schriftFuer() in
|
||||||
|
workspace-chat.js FÜR DIESE BLASE ausgerechnet hat, mit mindestens
|
||||||
|
7:1 gegen ihren Hintergrund. Wer sie nimmt, kann nicht unlesbar
|
||||||
|
werden -- auf keiner der siebzehn Farben und auf keiner, die morgen
|
||||||
|
dazukommt.
|
||||||
|
|
||||||
|
Unterschieden wird deshalb über FORM statt über Farbton: kräftigere
|
||||||
|
Tönung, geschlossene Kante, fetter, und ein Zeichen davor. Das
|
||||||
|
überlebt auch einen Farbmodus, in dem Tönungen wegfallen.
|
||||||
|
|
||||||
|
AUGENSCHONEND: eine Tönung und eine Kante, kein Leuchten, keine
|
||||||
|
Bewegung. Ein Ruf ans ganze Team wird oft geschrieben, wenn es eilt
|
||||||
|
-- der Bildschirm muss deswegen nicht blinken. */
|
||||||
|
.chat-erwaehnung[data-rudel="ja"] {
|
||||||
|
color: var(--blase-text, var(--text));
|
||||||
|
font-weight: 700;
|
||||||
|
background: color-mix(in srgb, var(--blase-text, var(--text)) 20%, transparent);
|
||||||
|
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--blase-text, var(--text)) 52%, transparent);
|
||||||
|
}
|
||||||
|
/* Das Zeichen davor trägt die Unterscheidung mit, falls jemand
|
||||||
|
Tönungen gar nicht sieht. Es steht in derselben Farbe wie die
|
||||||
|
Schrift -- eine zweite Farbe wäre dieselbe Wette wie oben. */
|
||||||
|
.chat-erwaehnung[data-rudel="ja"]::before {
|
||||||
|
content: "C9 | ||||||