diff --git a/server/chat-erwaehnung.js b/server/chat-erwaehnung.js index c3929a1a..2e874408 100644 --- a/server/chat-erwaehnung.js +++ b/server/chat-erwaehnung.js @@ -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 "post@rudelhaus.de" 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); } diff --git a/server/pruef-erwaehnung.mjs b/server/pruef-erwaehnung.mjs index 84affaf8..3bd51faf 100644 --- a/server/pruef-erwaehnung.mjs +++ b/server/pruef-erwaehnung.mjs @@ -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 post@rudelhaus.de", 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", "post@rudelhaus.de", + "@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); diff --git a/server/workspace-chat.js b/server/workspace-chat.js index a7d252e5..7f5af58c 100644 --- a/server/workspace-chat.js +++ b/server/workspace-chat.js @@ -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) { diff --git a/server/workspace.js b/server/workspace.js index d34a6de9..10745f83 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -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!!!" diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index 0a2fb104..f779ed3e 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,15 +16,15 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +