Der Treff bekommt einen Chat -- mit Nachtruhe, ohne Telefon

Filipe, auf die Frage ob die Community einen Chat bekommen soll:
"eine geile mischung von punkt 2 und drei. mach es perfekt so das quasi
nach mitternacht bis morgens 6 uhr keiner schreiben kann damit auch die
privatsphaere beruecksichtig wird ueber nacht ... und die sollen nicht
anrufen koennen. nur schreiben."

Zur Auswahl standen "gar kein Chat", "nur mit Oeffnungszeiten" und
"offen wie im Team". Seine Antwort ist eine vierte, die nicht auf dem
Zettel stand, und sie ist die bessere.

1. WARUM DIE NACHTRUHE MEHR IST ALS EINE EINSTELLUNG

Sein Grund war Privatsphaere -- und zwar die der Leute, die moderieren.
Die Forschung zu ehrenamtlichen Moderatoren sagt genau das: Die beiden
meistgenannten Gruende fuers Aufhoeren sind ZU WENIG ZEIT und STREIT IM
TEAM. Ein Chat, der nachts um drei weiterlaeuft, erzeugt beides.

SIE GILT FUER ALLE, AUCH FUER DIE LEITUNG. Das ist die unbequeme
Entscheidung: Der bequeme Weg waere, DogFather auszunehmen. Aber wenn er
um halb vier schreibt, liest es jemand -- und wer liest, fuehlt sich
zustaendig. Eine Nachtruhe, von der die Leitung ausgenommen ist, ist
keine, sondern eine Bitte.

MODERIEREN GEHT WEITER. Verbergen, loeschen, herausnehmen sind keine
Nachrichten. Wer nachts etwas Schlimmes sieht, kann es wegnehmen -- er
kann nur nicht darueber diskutieren. Und der vertrauliche Meldeweg
bleibt offen: Er ist ein Fall mit einer Zustaendigkeit, kein Raum mit
dreissig Leuten.

GERECHNET IN ORTSZEIT, nicht in UTC. `getHours()` auf einem UTC-Server
haette den Chat im Sommer von 2 bis 8 geschlossen -- zwei Stunden
daneben, und nur denen sichtbar, die um diese Zeit wach sind. Dieses
Haus ist mit der Rechnung schon zweimal hereingefallen.

DER RIEGEL SITZT IM SERVER, nicht im Browser. Ein ausgegrautes Feld ist
eine Hoeflichkeit; wer die Adresse kennt, spricht die Schnittstelle
direkt an. Nachrichten UND Anhaenge werden abgelehnt -- ein offener
Anhangweg waere der Umweg, den jeder findet, der es einmal versucht.

2. KEIN TELEFON -- UND WARUM DAS EINEN EIGENEN RIEGEL BRAUCHT

Ein Anruf haengt in diesem Haus an der RAUMMITGLIEDSCHAFT, nicht an der
Rolle. Solange die Community keinen Raum hatte, war die Frage muessig.
Seit sie einen hat, waere Telefonieren erlaubt -- nicht, weil es jemand
entschieden haette, sondern weil es niemand verboten hat.

Der Riegel steht ganz vorne am Anruf-Router, nicht in den neun Routen
dahinter. Alle sieben Wege sind geprueft, einzeln.

3. DER FUND, DER DIE GANZE NACHTRUHE UMGANGEN HAETTE

Ein Gast konnte ein PRIVATES ZWEIER-GESPRAECH mit DogFather aufmachen.
Die Route antwortete mit 200 und legte den Raum an.

`ohneAussen` nimmt die Community aus den Listen ANDERER heraus -- damit
kein Creator einen Zuschauer anschreibt. Die Gegenrichtung kam nie vor,
weil die Chatseite fuer 'gast' gar nicht offen war. Seit heute ist sie
offen, und damit war die Erlaubnis da.

Ein Zweier-Gespraech ist kein Kanal: keine Nachtruhe, kein Modi liest
mit, niemand koennte moderieren. Aus "die Community bekommt einen Chat"
waere ungewollt "jeder Zuschauer bekommt eine Standleitung zu
DogFather" geworden.

GEFUNDEN HAT ES DIE PRUEFUNG, NICHT DAS LESEN -- und zwar in dem Moment,
in dem ich zwei Dateien weiter selbst vor genau dieser Sorte Erlaubnis
gewarnt hatte.

4. ZWEI TOTE WEGE, EINER DAVON MEINER

pruef-community-sicht sucht nach sichtbaren Wegen, die ins Leere
fuehren. Sie fand zwei:

  a) "Bei dir klingelt nichts" -> anruf-probe.html. Der Hinweis von
     heute Vormittag, und die Seite steht einem Gast ausdruecklich NICHT
     offen. Mein Fehler, und einer, der jedem naechsten Hinweis genauso
     passiert waere.

     AN DER WURZEL BEHOBEN: Die Hinweise pruefen jetzt auch die ROLLE
     (`darfSeite`), nicht nur die Adresse. Das ist der 15.09. noch
     einmal, eine Ebene tiefer -- damals die Adresse, diesmal die Rolle,
     und dieselbe Lehre: Die Hinweise sind eine DRITTE Stelle neben
     Seiten und Schnittstellen.

  b) Der Verweis "Kalender" im Tagesblick. Steht fest im HTML, fuehrt
     fuer die Community ins Leere -- und zwar SCHON VOR den Aenderungen
     dieses Tages. Aufgefallen nur, weil (a) daneben stand. Der Weg
     wird jetzt abgeleitet: Wer die Kachel hat, hat den Weg.

5. DIE PRUEFUNG LAEUFT ZWEIMAL

Die Nachtruhe laesst sich nicht in EINEM Lauf in beiden Zustaenden
pruefen -- die Zeiten werden beim Laden gelesen, und ein zweiter Server
braeuchte denselben Port. pruef-treffchat ruft sich deshalb selbst noch
einmal auf, mit einem Fenster, das jetzt gilt. Dieselben Pruefungen,
andere Lage.

DIE UHR WIRD NICHT GEFAELSCHT. Verschoben wird die GRENZE, nicht die
Zeit -- eine Pruefung, die an der Systemzeit dreht, faelscht danach auch
anderes mit und ist nur nachts gruen.

6. WAS SICH NEBENBEI GEAENDERT HAT

- pruef-treff war SCHON VORHER ROT (8 Fehler): Sie erwartete sieben
  Kacheln fuer die Community, es waren gestern Nacht schon zehn. Feste
  Zahlen, Rechnung von gestern. Jetzt abgeleitet -- 72 Pruefungen statt
  66, alle gruen.
- Ein 404 bei jedem Seitenaufruf der Community: `anruf.js` holte die
  Verbindungsadressen, bevor es wusste, wer fragt. Der Browser
  protokolliert das, BEVOR JavaScript abfangen kann -- also darf die
  Anfrage gar nicht erst gestellt werden. `/api/ich` sagt es jetzt
  vorher. Ein Fehler, der immer kommt, macht den naechsten echten
  unsichtbar.
- Reaktionen (Daumen) bleiben nachts erlaubt. Ausdruecklich, mit
  Begruendung im Code: Ein Daumen ist keine Nachricht, er loest keine
  Meldung aus und kann keinen Streit anfangen. Eine Luecke ohne
  Begruendung wird beim naechsten Durchsehen entweder geschlossen oder
  uebersehen -- beides falsch.

GEMESSEN: pruef-treffchat 108/0 (neu, laeuft zweimal), pruef-treff 72/0
(vorher 66 mit 8 Fehlern), pruef-chat gruen, pruef-rechtetafel 19/0,
pruef-community-sicht gruen.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-19 11:47:31 +02:00
co-authored by Claude Opus 5
parent 790b77a73d
commit 7be85c265b
50 changed files with 2066 additions and 461 deletions
+127 -14
View File
@@ -290,13 +290,30 @@ melde("=== 1b. Eine Wand für beide ===");
2. DIE KACHELN: sieben, und sonst nichts
======================================================================= */
melde("");
melde("=== 2. Sieben Kacheln — und keine achte ===");
melde("=== 2. Die Kacheln der Community ===");
let namen = [];
{
const a = await roh("/workspace/api/ich", CREW, { cookie: gast.keks });
const j = JSON.parse(a.text);
ok(Array.isArray(j.bereiche) && j.bereiche.length === 7,
`die Community bekommt sieben Kacheln (${j.bereiche?.length})`);
/* ==== ABGELEITET STATT ABGESCHRIEBEN (19.09.2026) ================
Hier stand siebenmal die Zahl SIEBEN und einmal die Namensliste
ausgeschrieben. Beides war zu diesem Zeitpunkt schon falsch:
Gestern Nacht kamen "Unsere Seiten", "Vertraulich melden" und
"Mein Steckbrief" dazu -- die Pruefung war also bereits rot, ohne
dass irgendetwas kaputt war, und heute kam der Chat als elfte
hinzu.
Genau die Sorte Zahl, vor der die Hausregel warnt: eine Rechnung
von gestern, die still falsch wird. Gezaehlt wird ab jetzt gegen
die Liste, die der Server wirklich fuehrt. Was diese Pruefung
sichern soll, ist nicht die ZAHL, sondern dass die Community
genau das bekommt, was fuer sie gebaut wurde -- und nichts vom
Team. */
const { bereicheFuer } = await import("./workspace.js");
const erwartet = bereicheFuer({ rolle: "gast" }).map((k) => k.name);
ok(Array.isArray(j.bereiche) && j.bereiche.length === erwartet.length,
`die Community bekommt ${erwartet.length} Kacheln (${j.bereiche?.length})`);
/* Die Namen werden im naechsten Block noch einmal gebraucht -- dort
wird gegen sie geprueft, statt sie ein zweites Mal zu tippen. Zwei
Abschriften derselben sieben Namen waeren zwei Gelegenheiten,
@@ -315,12 +332,37 @@ let namen = [];
Wer sie wieder aendert, aendert beides. `pruef-kachelraster`
rechnet zusaetzlich nach, ob daraus ein Loch entsteht. */
ok(namen.join(" | ") === "Der Treff | Anschlagbrett | Was ansteht | Wunschliste | Highlights | Regeln & Hilfe | Mitmachen",
ok(namen.join(" | ") === erwartet.join(" | "),
`und zwar diese: ${namen.join(", ")}`);
ok((j.bereiche || []).every((k) => k.gruppe === "Community"),
"alle in der Gruppe „Community“");
/* DIE BREITE KACHEL STEHT VORNE, und das bleibt eine echte Aussage:
Das Raster hat drei Spalten, "Der Treff" ist doppelt breit. An
dritter Stelle passt sie nicht mehr in die erste Reihe und reisst
ein Loch hinein -- genau das, was Filipe am 17.09. auf dem
Bildschirmfoto gesehen hat. */
ok(namen[0] === "Der Treff", `und „Der Treff“ steht vorne (${namen[0]})`);
/* NICHTS VOM TEAM. Der eigentliche Zweck dieses Abschnitts: Eine
Kachel des Teams bei der Community waere lautlos falsch -- sie
saehe aus wie die anderen, und erst beim Klicken sagte der Server
404. */
const teamKacheln = ["Aufgaben", "Kalender", "Dateien", "Personen & Zugänge",
"Eure Aufgaben", "Talente", "Entwicklung", "Meldungen & Maßnahmen"];
ok(!namen.some((n) => teamKacheln.includes(n)),
"und keine einzige Kachel des Teams ist dabei");
/* ZWEI STEHEN IN "FUER DICH", NICHT IN "COMMUNITY" -- und das ist
die Aussage, nicht die Ausnahme: "Vertraulich melden" und "Mein
Steckbrief" gehoeren der einzelnen Person, nicht dem Raum. Sie
zwischen die Bretter zu stellen hiesse, dass eine vertrauliche
Meldung aussieht wie ein oeffentliches Brett. */
const PERSOENLICH = ["Vertraulich melden", "Mein Steckbrief"];
const bretter = (j.bereiche || []).filter((k) => !PERSOENLICH.includes(k.name));
ok(bretter.every((k) => k.gruppe === "Community"),
`die ${bretter.length} Bretter stehen in der Gruppe „Community“`);
ok((j.bereiche || []).filter((k) => PERSOENLICH.includes(k.name))
.every((k) => k.gruppe === "Für dich"),
"und die zwei persönlichen in „Für dich“ — eine Meldung ist kein Brett");
const toene = new Set((j.bereiche || []).map((k) => k.ton));
ok(toene.size === 7, `sieben verschiedene Farben (${[...toene].join(", ")})`);
ok(toene.size === (j.bereiche || []).length,
`${toene.size} Kacheln, ${toene.size} verschiedene Farben (${[...toene].join(", ")})`);
ok(j.rolle_name === "Community", `die Rolle heißt „${j.rolle_name}“`);
}
{
@@ -341,16 +383,75 @@ let namen = [];
const MOD = "Meldungen & Ma\u00dfnahmen";
const a = JSON.parse((await roh("/workspace/api/ich", CREW, { cookie: kModi })).text);
const beimModi = (a.bereiche || []).filter((k) => k.gruppe === "Community").map((k) => k.name);
const fehlt = namen.filter((n) => !beimModi.includes(n));
/* ==== ZWEI KACHELN SIEHT EIN MODI AUSDRUECKLICH NICHT ============
Bis zum 18.09. stimmte "ein Modi sieht alle Kacheln der Community
mit". Seitdem gibt es zwei, die ihm NICHT gehoeren -- und beide
sind Absicht, keine Luecke:
Vertraulich melden Sehr oft geht es in diesen Meldungen um eine
Moderationsentscheidung. Wer beteiligt ist,
darf die Beschwerde ueber sich nicht lesen.
Mein Steckbrief Jeder hat seinen eigenen. "Der von jemand
anderem" ist hier gar nicht gemeint.
Die Pruefung war dadurch seit gestern rot, ohne dass etwas kaputt
war. Jetzt steht die Ausnahme ausdruecklich da -- und sie ist
eine ECHTE Pruefung: Taucht "Vertraulich melden" eines Tages bei
einem Modi auf, wird diese Zeile rot. */
const NUR_COMMUNITY = ["Vertraulich melden", "Mein Steckbrief"];
/* Verglichen wird die GRUPPE "Community" auf beiden Seiten -- die
zwei persoenlichen Kacheln stehen dort ohnehin nicht drin. */
const sollteSehen = namen.filter((n) => !NUR_COMMUNITY.includes(n));
const fehlt = sollteSehen.filter((n) => !beimModi.includes(n));
ok(fehlt.length === 0,
`ein Modi sieht alle sieben der Community mit${fehlt.length ? " -- FEHLT: " + fehlt.join(", ") : ""}`);
ok(beimModi.includes(MOD) && !namen.includes(MOD),
`und genau eine mehr: ${beimModi.filter((n) => !namen.includes(n)).join(", ")}`);
`ein Modi sieht die ${sollteSehen.length} Bretter der Community mit`
+ `${fehlt.length ? " -- FEHLT: " + fehlt.join(", ") : ""}`);
const zuViel = NUR_COMMUNITY.filter((n) => beimModi.includes(n));
ok(zuViel.length === 0,
`aber NICHT den vertraulichen Meldeweg${zuViel.length ? " -- DURCH: " + zuViel.join(", ") : ""}`);
/* "Meldungen & Massnahmen" steht in der Gruppe MODERATION, nicht in
"Community" -- gemessen, nicht angenommen. Gesucht wird deshalb in
seiner ganzen Liste. Dass sie eine EIGENE Gruppe hat, ist richtig:
Sie ist kein Brett der Community, sondern das Werkzeug derer, die
sie betreuen. */
const modiAlles = [...(a.bereiche || []), ...(a.bereiche_zusatz || [])]
.map((k) => k.name);
ok(modiAlles.includes(MOD) && !namen.includes(MOD),
`und genau eine mehr, die die Community NICHT hat: ${MOD}`);
const b = JSON.parse((await roh("/workspace/api/ich", HAUS, { cookie: kDogi })).text);
const beiDogi = (b.bereiche_zusatz || []).filter((k) => k.gruppe === "Community").map((k) => k.name);
ok(namen.every((n) => beiDogi.includes(n)) && beiDogi.includes(MOD),
`und DogFather auf seiner Adresse auch (${beiDogi.length})`);
/* DogFather sieht den Meldeweg sehr wohl -- er beantwortet ihn. */
/* Auch hier: MOD steht in der Gruppe "Moderation", nicht in
"Community". `beiDogi` ist nach Gruppe gefiltert und kann ihn
deshalb gar nicht enthalten -- die alte Bedingung war unerfuellbar,
sobald die Moderationskachel ihre eigene Gruppe bekam. */
const dogiAlles = [...(b.bereiche || []), ...(b.bereiche_zusatz || [])]
.map((k) => k.name);
ok(sollteSehen.every((n) => beiDogi.includes(n)),
`und DogFather sieht auf seiner Adresse dieselben ${beiDogi.length} Bretter`);
ok(dogiAlles.includes(MOD), `und die Moderationskachel dazu (${MOD})`);
/* DER MELDEWEG BEI DOGFATHER: Er beantwortet ihn, also muss er ihn
haben -- aber er steht bei ihm NICHT in der Gruppe "Community",
sondern dort, wo seine eigenen Werkzeuge stehen. Gesucht wird
deshalb in seiner GANZEN Liste, nicht nur in einer Gruppe. */
/* DER MELDEWEG LEBT AUF DER TEAM-ADRESSE, nicht auf der Agentur-.
Gemessen: Auf crew. hat DogFather ihn in der Gruppe "Fuer dich";
auf workspace. steht er nicht, und das ist richtig -- dort geht es
um die Agentur, nicht um den Treff. Geprueft wird deshalb dort, wo
er hingehoert. */
const cDogi = JSON.parse((await roh("/workspace/api/ich", CREW, { cookie: kDogi })).text);
const dogiAufCrew = [...(cDogi.bereiche || []), ...(cDogi.bereiche_zusatz || [])]
.map((k) => k.name);
ok(dogiAufCrew.includes("Vertraulich melden"),
"auf der Team-Adresse steht der vertrauliche Meldeweg bei ihm — er beantwortet ihn");
/* GEGENPROBE: bei einem Modi nicht -- und zwar auf DERSELBEN
Adresse. Ohne diese Zeile waere die darueber auch gruen, wenn ihn
jeder saehe; mit einer anderen Adresse waere sie gruen, ohne
etwas zu beweisen. */
ok(!modiAlles.includes("Vertraulich melden"),
"bei einem Modi auf derselben Adresse nicht — er ist oft der Anlass");
const c = JSON.parse((await roh("/workspace/api/ich", HAUS, { cookie: kMana })).text);
const beiMana = (c.bereiche_zusatz || []).filter((k) => k.gruppe === "Community");
@@ -552,7 +653,19 @@ for (const [name, pfad] of [
["Termine", "/workspace/api/termine"],
["Dateien", "/workspace/api/dateien"],
["Personen", "/workspace/api/personen"],
["Gespräche", "/workspace/api/chat/raeume"],
/* "Gespräche" IST HIER HERAUSGENOMMEN (19.09.2026).
Bis heute stimmte die Zeile: Die Community hatte keinen Chat, also
musste dort nichts herauskommen. Filipe hat das geändert -- sie
bekommt genau EINEN Raum, den Treff, mit Nachtruhe und ohne
Anrufe.
SIE FÄLLT NICHT WEG, SIE ZIEHT UM: Was der Chat für die Community
darf und was nicht, prüft jetzt `pruef-treffchat` -- und zwar
genauer, als diese Zeile es je konnte (ein Raum und nicht mehr,
keine Zweier-Gespräche, kein Telefon, Nachtruhe am Server).
Sie hier stehen zu lassen hiesse, eine bestellte Funktion für
einen Fehler zu halten. */
]) {
const a = await roh(pfad, CREW, { cookie: gast.keks });
let leer = a.code !== 200;