Der Treff: eine eigene Tür für die Community, und sie sieht nur, was dasteht

Filipe: "eine community rolle und community kategorie, wo die community
auch dann nur die sieht" · "wieso nur eine kachel — ich will mehrere,
mit mehreren optionen" · "egal welche rolle hinzugefügt wird soll immer
nur das sehen was ich erlaube".

DIE DRITTE ADRESSE. treff.dogfather-universe.com bekommt eine eigene
Weiche (treff-adresse.js), eine eigene Zugangswand, ein eigenes Manifest
und acht eigene Bühnenbilder. Ohne sie stünde dort die Wand der Agentur:
Spicy-Logo, "Creator Workspace", fünf Rollenkacheln mit den Namen Admin,
Manager, Scout, Creator — die komplette Struktur des Unternehmens, vor
der Anmeldung, für jeden mit der Adresse.

Welche Wand zu welcher Tür gehört, steht jetzt in EINER Tafel
(istFremdeWand) statt als Sonderfall im Code. Bei zwei Wänden war ein
Sonderfall richtig; bei drei wären es drei geworden, bei vier sechs.

SIEBEN BRETTER UND EIN ACHTES NUR FÜRS TEAM. Anschlagbrett, Was ansteht,
Der Treff, Wunschliste, Highlights, Regeln & Hilfe, Mitmachen — dazu
"Meldungen & Maßnahmen", das ausdrücklich NICHT in TREFF_BEREICHE steht:
dort wäre es lautlos bei der Community gelandet und hätte ausgesehen wie
die anderen sieben.

WAS AUF DEN BRETTERN PASSIERT: "Will ich auch" (eine Stimme je Person,
erzwungen durch den Schlüssel, nicht durch eine Prüfung) · Anheften,
höchstens drei, gezählt IN der Transaktion · Freigabe vor Sichtbarkeit
für Termine und Highlights, wobei Abwesenheit der Ruhezustand ist ·
Melden mit Pflicht-Grund (DSA Art. 16) · Entfernen mit Grund, der den
Beitrag ÜBERLEBT (DSA Art. 17) · drei Stufen, gerechnet statt
gespeichert · die Bannleiter: Modi bis 7 Tage, dauerhaft nur DogFather
(seine Entscheidung) · "Mitmachen" wird zum Talent, genau einmal.

Der Ausschluss wirkt in sitzungLesen() — also nicht nur beim Schreiben.
Wer ausgeschlossen ist, ist weg, nicht stumm.

ZWEI SEITEN MEHR. treff-regeln.html holt seine ZAHLEN vom Server
(Mindestalter 18, Fristen, Anschläge) — ein Regeltext, der eine andere
Zahl nennt als die Regel, ist schlimmer als keiner. rechte.html rechnet
"Wer sieht was" aus derselben Tafel aus, aus der die Schranke ihre
Entscheidung holt. Die Community steht dort bei 3 von 23.

SECHS BEFUNDE, KEINEN HAT DAS LESEN GEFUNDEN:
 · Ein Modi kam über den verborgenen Zugang in den Treff — die Bedingung
   war durch Ausschluss formuliert und nahm die neue Wand automatisch mit
 · /api/personen gab einem Mitglied Namen und Rolle von DogFather
 · Ein DELETE mit Körper wird von Nodes HTTP-Parser mit einem leeren 400
   abgewiesen, bevor Express ihn sieht (gemessen). Der Grund reist jetzt
   in der Adresse
 · Der Kommentar, der erklärt, warum die Zugangswand keine Rollennamen
   nennt, nannte selbst einen — in einer ausgelieferten Datei
 · Zwei Listen für "welche Seiten sind ohne Anmeldung offen". Sie
   stimmten, weil beide am selben Tag entstanden
 · Ein Portkonflikt, den ich selbst gebaut hatte (4391 gehört
   pruef-chat-aufloesen)

Dazu zwei feste Zahlen abgeschafft (die 20 in pruef-rechtetafel, die
Kachelzahl in pruef-treff) und zwei Werkzeuge, die jetzt SUCHEN statt
aufzuzählen: Der Versionsstempel findet seine Manifeste selbst — das
dritte hätte sonst keinen bekommen, und die neue App hätte bis zu vier
Stunden lang das falsche Zeichen getragen. Das Bildwerkzeug baut beide
Zugangswände aus einer Vorlage mit zwei Werten.

Zwei neue Kacheltöne, gemessen statt gewählt: #2f2f7f (Abstand 31,4 in
Lab) und #7c2771 (29,7). Das schwächste Paar der vorhandenen 34 liegt
bei 24,9.

Geprüft: treff 69 · treff-werkzeuge 60 · rollen 312 · start-ansicht 147 ·
crew-adresse 129 · sicht 84 · modi-verborgen 80 · haus-trennung 62 ·
bereiche-lesend 37 · css-klassen 30 · zwischenspeicher 21 · alle-wege 19 ·
rechtetafel 19 — alles grün.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-11 17:58:34 +02:00
co-authored by Claude Opus 5
parent 47a91e9708
commit 7b21f247eb
63 changed files with 5672 additions and 382 deletions
+396 -18
View File
@@ -27,8 +27,12 @@ import { dirname, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { mkdirSync } from "node:fs";
import { createRequire } from "node:module";
import { darfSeite } from "./rechte.js";
import { sitzungPasstZurAdresse, istOhneModiAdresse, istCrewAdresse,
import { darfSeite, OHNE_ANMELDUNG, SEITEN, ALLE_ROLLEN } from "./rechte.js";
/* Ein Modul ohne eigene Importe -- deshalb entsteht hier kein Kreis,
obwohl workspace-treff.js seinerseits aus dieser Datei importiert.
Begruendung im Kopf von treff-tabellen.js. */
import { treffTabellen, treffSperreLesen } from "./treff-tabellen.js";
import { sitzungPasstZurAdresse, istOhneModiAdresse, istCrewAdresse, istTreffAdresse, istPruefAdresse, AUSSEN_ROLLEN,
TEAM_DOGI_ROLLEN } from "./crew-adresse.js";
/* Weitergereicht, damit die Fachmodule sie wie alles andere aus
workspace.js beziehen und nicht wissen muessen, wo sie wohnt. */
@@ -63,7 +67,7 @@ const COOKIE = "dfw_sitzung";
const SITZUNG_STUNDEN = 12;
const VERSUCHE_MAX = 8; // pro IP
const VERSUCHE_FENSTER_MIN = 10;
const ROLLEN = new Set(["spicy", "admin", "manager", "scout", "creator", "hand", "modi"]);
const ROLLEN = new Set(["spicy", "admin", "manager", "scout", "creator", "hand", "modi", "gast"]);
/* Die Reihenfolge, in der Rollen ueberall erscheinen: Spicy Media
zuerst, dann DogFather, Manager, Scout, Creator. Steht hier einmal,
@@ -99,6 +103,19 @@ const ROLLEN_KACHEL = new Set(["spicy", "admin", "manager", "scout", "creator"])
* sondern ihr Sinn. */
const CREW_KACHEL = new Set(["admin", "hand", "modi"]);
/** Die Rollen mit einer Kachel auf der Zugangswand von treff.
*
* EINE, und deshalb zeigt die Wand dort gar keine Kachelreihe: Eine
* Auswahl mit einem einzigen Eintrag ist keine Auswahl, sondern eine
* Huerde. Dieselbe Ueberlegung wie bei der ersten Fassung der
* Crew-Wand -- nur ist sie hier dauerhaft richtig, weil es im Treff
* auch kuenftig nur eine Rolle gibt.
*
* Die Menge steht trotzdem hier: Die Anmeldung prueft gegen sie, und
* eine Wand ohne Kacheln darf nicht heissen, dass jede Rolle
* durchkommt. */
const TREFF_KACHEL = new Set(["gast"]);
/* Als SQL-Ausdruck fuer ORDER BY. "ORDER BY rolle" waere alphabetisch
(admin, creator, manager, scout, spicy) -- also fast genau falsch
herum. */
@@ -383,6 +400,15 @@ export const ROLLEN_NAME = {
creator: "Creator",
hand: "Rechte Hand",
modi: "Modi",
/* DIE COMMUNITY (11.09.2026). Der Name steht hier und NICHT in einer
Datei, die jeder herunterlaedt -- ausgeliefert wird er nur an den,
der ihn selbst traegt, und an die, die ihn sehen duerfen.
Filipe hat ausdruecklich gewaehlt, dass im Treff der volle
Rollenname neben einem Beitrag steht ("Modi", "Rechte Hand",
"DogFather"). Das bleibt damit vereinbar: Der Name kommt als DATEN
mit der Antwort, nicht als Konstante im Browser. */
gast: "Community",
};
/* DIE UEBERSCHRIFT UEBER EINER PERSONENLISTE (10.09.2026).
@@ -632,9 +658,110 @@ const PERSONEN_KACHEL = {
const HAND_BEREICHE = [...MODI_BEREICHE, EINGANG_KACHEL, PERSONEN_KACHEL,
ENTWICKLUNG_KACHEL, TALENTE_KACHEL];
/* =====================================================================
DIE SIEBEN BRETTER DES TREFFS (11.09.2026)
Filipe: "wieso nur eine kachel fuer die community ich will dass die
mehrere kacheln haben. mit mehreren optionen."
Eine Kachel war zu kurz gedacht: Ein Raum, in dem Ansagen, Fragen,
Wuensche und Clips durcheinandergehen, ist nach zwei Wochen
unbenutzbar -- die Ansage von gestern ist weg, die Frage von heute
geht unter.
SORTIERT NACH ZWECK, NICHT NACH THEMA, und in der Reihenfolge, in der
ein Neuer liest: erst was gilt, dann was laeuft, dann das Gespraech,
dann das Besondere.
Jede der sieben bedient eines der vier Dinge, die darueber
entscheiden, ob sich jemand zugehoerig fuehlt (Zugehoerigkeit,
Einfluss, Nutzen, geteilte Erlebnisse). Was keines bedient, ist nicht
dabei -- deshalb gibt es hier keine Bestenliste und keine Punkte.
VON OBEN NACH UNTEN GELESEN SIND SIE EIN WEG: lesen -> mitreden ->
mitgestalten -> dazugehoeren. Die letzte Kachel uebergibt an die
Talente-Kategorie, die es schon gibt.
DIE KACHELN STEHEN HIER UND NICHT IN assets/js/bereiche.js -- aus
demselben Grund wie bei den Modis: Diese Datei wird nicht
ausgeliefert, jene bekommt jeder Besucher. */
const TREFF_GRUPPE = {
gruppe: "Community",
gruppeUnter: "Der Treff \u2013 der Raum f\u00fcr die Zuschauer",
};
const TREFF_BEREICHE = [
{ ...TREFF_GRUPPE, name: "Anschlagbrett", unter: "Was gerade gilt",
zeichen: "reports", ton: 33, ziel: "bereich.html?b=anschlag", szene: "halle" },
{ ...TREFF_GRUPPE, name: "Was ansteht", unter: "Die n\u00e4chsten Streams und Events",
zeichen: "kalender", ton: 31, ziel: "bereich.html?b=ansteht", szene: "skyline" },
{ ...TREFF_GRUPPE, name: "Der Treff", unter: "Hallo sagen, fragen, loben",
zeichen: "community", ton: 34, gross: true, ziel: "bereich.html?b=treff", szene: "lounge" },
{ ...TREFF_GRUPPE, name: "Wunschliste", unter: "Was ihr euch w\u00fcnscht \u2013 und wer mitwill",
zeichen: "content", ton: 30, ziel: "bereich.html?b=wunsch", szene: "portal" },
{ ...TREFF_GRUPPE, name: "Highlights", unter: "Eure Clips und Bilder",
zeichen: "live", ton: 29, ziel: "bereich.html?b=highlight", szene: "arena" },
{ ...TREFF_GRUPPE, name: "Regeln & Hilfe", unter: "Wie es hier l\u00e4uft",
zeichen: "schutz", ton: 28, ziel: "bereich.html?b=regeln", szene: "studio" },
{ ...TREFF_GRUPPE, name: "Mitmachen", unter: "Wie du dazugeh\u00f6ren kannst",
zeichen: "personen", ton: 32, ziel: "bereich.html?b=mitmachen", szene: "garage" },
];
/* MELDUNGEN & MASSNAHMEN -- FUER DAS TEAM, NICHT FUER DIE COMMUNITY
(11.09.2026).
Diese Kachel steht ausdruecklich NICHT in TREFF_BEREICHE. Die Liste
dort ist das, was ein Mitglied sieht; sie wird dem Team nur
angehaengt. Eine achte Kachel darin waere auch bei der Community
gelandet -- und zwar lautlos, denn sie saehe dort aus wie die
anderen sieben. Erst beim Klicken haette der Server 404 gesagt, und
das sieht aus wie ein kaputtes Programm.
SIE GEHOERT AUCH NICHT IN TREFF_BRETTER: Sie ist kein Brett mit
Eintraegen, sondern eine Seite. Das Ziel ist deshalb ein
Dateiname und kein "bereich.html?b=...", und die Ableitung weiter
unten nimmt sie von selbst nicht mit. */
const TREFF_MODERATION_KACHEL = {
...TREFF_GRUPPE,
name: "Meldungen & Ma\u00dfnahmen", unter: "Was gemeldet wurde \u2013 und was daraus wurde",
zeichen: "schutz", ton: 36, ziel: "treff-moderation.html", szene: "studio",
};
/* Welche Bretter zum Treff gehoeren -- ABGELEITET aus den Kacheln, nicht
danebengeschrieben. Eine zweite Liste waere die, die beim naechsten
neuen Brett vergessen wird. */
export const TREFF_BRETTER = TREFF_BEREICHE
.map((k) => new URL("http://x/" + k.ziel).searchParams.get("b"))
.filter(Boolean);
/* Wer den Treff ueberhaupt sehen darf: die Community selbst und das
Team von Dogi. Die Agentur ausdruecklich nicht -- Creator, Scouts und
Spicy Media haben mit den Zuschauern dieses Streams nichts zu tun. */
export const TREFF_ROLLEN = new Set(["gast", "modi", "hand", "admin"]);
/* DAS TEAM SIEHT DEN TREFF MIT -- als eigene Gruppe unter seinen
uebrigen Kacheln.
Filipe: "die modis rechte hand und ich also dogfather sehen die aber
auch. die community aber nur die dann."
Zusammengesetzt und nicht in MODI_BEREICHE hineingeschrieben, weil
diese Liste weiter oben steht als die Kacheln des Treffs. Eine Kopie
dort waere die Stelle, an der beim naechsten neuen Brett eines fehlt. */
const MODI_MIT_TREFF = [...MODI_BEREICHE, ...TREFF_BEREICHE, TREFF_MODERATION_KACHEL];
const HAND_MIT_TREFF = [...HAND_BEREICHE, ...TREFF_BEREICHE, TREFF_MODERATION_KACHEL];
/* AUSSEN_ROLLEN kommt aus crew-adresse.js und wird hier nur
weitergereicht -- die Adressregel braucht sie zuerst, und diese Datei
importiert von dort. Eine zweite Menge waere die, die auseinanderlaeuft. */
export { AUSSEN_ROLLEN };
export function bereicheFuer(person) {
if (person?.rolle === "modi") return MODI_BEREICHE;
if (person?.rolle === "hand") return HAND_BEREICHE;
if (person?.rolle === "modi") return MODI_MIT_TREFF;
if (person?.rolle === "hand") return HAND_MIT_TREFF;
/* DIE COMMUNITY SIEHT DEN TREFF UND SONST NICHTS (11.09.2026). */
if (person?.rolle === "gast") return TREFF_BEREICHE;
/* AUF DER ADRESSE VON TEAM DOGI SIEHT AUCH DOGFATHER DIE KACHELN DES
TEAMS (10.09.2026).
@@ -653,10 +780,27 @@ export function bereicheFuer(person) {
Kacheln. Eine eigene Liste fuer ihn waere die, die beim naechsten
Umbau auseinanderlaeuft -- und sie stuende gegen die Hausregel,
ihn nicht ueber sein Team zu stellen. */
if (person?.haus === "crew") return HAND_BEREICHE;
return null;
if (person?.haus === "crew") return HAND_MIT_TREFF;
/* `null` HEISST "NIMM DIE LISTE AUS DEM BROWSER" -- und die enthaelt
fuenfundzwanzig Kacheln des Agenturhauses. Bis zum 11.09.2026 stand
hier nur `return null`, also bekam JEDE Rolle ohne eigenen Zweig
diese Liste angeboten. Gefiltert wurde sie danach zwar nach
`rollen:` an jeder Kachel, aber das ist eine zweite Schranke an
einer anderen Stelle -- und sie lebt in einer Datei, die jeder
herunterlaedt.
Jetzt bekommen nur die fuenf Rollen `null`, fuer die diese Liste
geschrieben wurde. Alles andere bekommt eine leere Liste: keine
Kachel, kein Weg, nichts zu filtern. */
if (AGENTUR_LISTE.has(person?.rolle)) return null;
return [];
}
/* Die fuenf, deren Kacheln in assets/js/bereiche.js stehen. Nicht zu
verwechseln mit AGENTUR_ROLLEN (dort fehlt `admin`, weil es dabei um
die Sichtbarkeit von Zeilen geht und nicht um Kacheln). */
const AGENTUR_LISTE = new Set(["spicy", "admin", "manager", "scout", "creator"]);
/* WELCHE BEREICHSSEITEN EIN MODI OEFFNEN DARF -- abgeleitet aus seinen
eigenen Kacheln und nicht danebengeschrieben.
@@ -670,7 +814,7 @@ export function bereicheFuer(person) {
-- ohne diese Einschraenkung kaeme er ueber die Adresszeile auch in
die Agentur-Ablage, die ihn nichts angeht. */
export const MODI_BEREICHE_ERLAUBT = new Set(
HAND_BEREICHE
HAND_MIT_TREFF
.map((k) => (/bereich\.html\?b=([a-z]+)/.exec(k.ziel) || [])[1])
.filter(Boolean));
@@ -942,8 +1086,51 @@ const ZUSATZ_BEREICHE = {
/* Eine eigene Gruppe, direkt hinter Team Dogi: Was ueber Menschen
aufgeschrieben wird, steht nicht zwischen den taeglichen
Werkzeugen. */
/* WER SIEHT WAS (11.09.2026).
Filipe: "egal welche Rolle oder Person hinzugefuegt wird, soll
immer nur das sehen, was ich erlaube."
Dazu gehoert, dass er es nachsehen kann. Die Seite rechnet die
Uebersicht aus derselben Tafel, aus der die Schranke ihre
Entscheidung holt (rechte.js) -- eine von Hand gepflegte
Uebersicht waere eine zweite Wahrheit, und ausgerechnet die, der
man glaubt.
IN "Team & System" UND NICHT BEI DEN PERSONEN: Es geht nicht um
Menschen, sondern um das Haus. */
{
...TEAM_GRUPPE,
name: "Wer sieht was", unter: "Jede Rolle, jede Seite – ausgerechnet, nicht notiert",
/* EIN EIGENER TON, UND ZWAR EIN NEUER (11.09.2026).
Erster Anlauf war Ton 26 -- das waere "Entwicklung" ein zweites
Mal gewesen. Zweiter Anlauf war Ton 22, weil er frei AUSSAH:
Ich hatte die Toene ueber bereicheFuer() gezaehlt, und das
liefert die Zusatzkacheln gar nicht mit. Beide Male haette
sich diese Kachel eine Farbe mit einer anderen geteilt, und
beide Male haette ich es fuer geprueft gehalten.
Gefunden hat es pruef-start-ansicht, die ueber die ECHTE
Ansicht zaehlt: "34 Farben auf 35 Kacheln". Der 35. Ton ist
jetzt gemessen (start.css, Abstand 31,4 in Lab zum naechsten
Nachbarn) -- und nicht mehr aus einer Luecke geraten. */
zeichen: "schutz", ton: 35, ziel: "rechte.html", szene: "studio",
},
{ ...ENTWICKLUNG_KACHEL, gruppeNach: TEAM_GRUPPE.gruppe },
{ ...TALENTE_KACHEL, gruppeNach: TEAM_GRUPPE.gruppe },
/* DER TREFF -- GANZ UNTEN, UND ZWAR ABSICHTLICH.
Filipe zu den beiden Gruppen darueber: "das sind private
informationen von meiner community." Dasselbe gilt hier, nur
noch deutlicher: Im Treff stehen Namen und Saetze von Leuten,
die nichts mit der Agentur zu tun haben.
Ohne `gruppeNach` haengt eine Gruppe hinten an -- genau richtig.
Wer sich den Bildschirm teilt oder jemandem ueber die Schulter
schauen laesst, hat sie dann nicht als Erstes im Blick. */
...TREFF_BEREICHE,
TREFF_MODERATION_KACHEL,
],
};
@@ -2086,6 +2273,11 @@ function umstellungen(d) {
["spicy", "admin", "manager", "scout", "creator", "modi"], jetztStempel);
checkListeErweitern(d, "personen", "rolle", "hand",
["spicy", "admin", "manager", "scout", "creator", "modi", "hand"], jetztStempel);
/* DIE COMMUNITY (11.09.2026). Dieselbe Umstellung wie die drei
darueber: Sicherung vorher, Neubau, Zaehlung innerhalb der
Transaktion, Indizes zurueck. */
checkListeErweitern(d, "personen", "rolle", "gast",
["spicy", "admin", "manager", "scout", "creator", "modi", "hand", "gast"], jetztStempel);
/* DER BEREICH FUER DAS IDEEN-BOARD (10.09.2026, Kapitel 5.6).
@@ -2118,6 +2310,18 @@ function umstellungen(d) {
checkListeErweitern(d, "eintraege", "bereich", "talente",
["live", "content", "technik", "community", "schutz", "agentur", "ideen",
"angebote", "rueckmeldung", "entwicklung", "talente"], jetztStempel);
/* DIE SIEBEN BRETTER DES TREFFS (11.09.2026).
EINE Umstellung fuer alle sieben, nicht sieben einzelne. Jeder
Aufruf baut die Tabelle neu auf -- siebenmal hintereinander waeren
sieben Sicherungen, sieben Neubauten und sieben Gelegenheiten, dass
etwas schiefgeht. Der Marker ist "treff"; ist der schon in der
CHECK-Liste, passiert gar nichts. */
checkListeErweitern(d, "eintraege", "bereich", "treff",
["live", "content", "technik", "community", "schutz", "agentur", "ideen",
"angebote", "rueckmeldung", "entwicklung", "talente",
"anschlag", "ansteht", "treff", "wunsch", "highlight", "regeln", "mitmachen"],
jetztStempel);
checkListeErweitern(d, "eintraege", "status", "angenommen",
["offen", "erledigt", "angenommen", "abgelehnt"], jetztStempel);
@@ -2644,6 +2848,10 @@ export function db() {
geaendert TEXT
);
CREATE INDEX IF NOT EXISTS idx_eintraege_bereich ON eintraege (bereich, creator_id);
/* Die Tabellen des Treffs folgen weiter unten, gleich nachdem
dieser Bauplan durch ist -- sie haengen an eintraege und
personen und koennen deshalb nicht davor stehen. Ihr SQL steht
in treff-tabellen.js, samt Begruendung je Tabelle. */
/* Wer darf welche Datei sehen. Bisher hatte eine Datei genau
EINEN Bereich (dateien.creator_id) -- damit liess sich eine
@@ -2889,6 +3097,11 @@ export function db() {
CREATE INDEX IF NOT EXISTS idx_versuche_ip ON versuche (ip, zeitpunkt);
CREATE INDEX IF NOT EXISTS idx_sitzungen_gueltig ON sitzungen (gueltig_bis);
`);
/* Die Tabellen des Treffs. Eigene Datei, weil workspace-treff.js
aus DIESER Datei importiert -- ein Importkreis waere die Folge,
und ueber einen Kreis kommen Konstanten als undefined an, ohne
dass irgendwo ein Fehler erscheint. */
treffTabellen(d);
umstellungen(d);
_db = d;
return d;
@@ -3074,7 +3287,34 @@ export function sitzungLesen(req) {
nicht, was er darf -- genau wie die Sicht eines anderen
(sichtPerson) das auch nicht tut. Wer hier etwas anderes einbaut,
macht aus einer Anzeigefrage eine Rechtefrage. */
const haus = istCrewAdresse(req.get?.("host")) ? "crew" : "agentur";
const haus = istCrewAdresse(req.get?.("host")) ? "crew"
: istTreffAdresse(req.get?.("host")) ? "treff" : "agentur";
/* DER AUSSCHLUSS WIRKT HIER -- an derselben Stelle wie alles andere
(11.09.2026).
Ein Ausschluss aus dem Treff ist nicht "darf nichts mehr
schreiben", sondern "ist nicht mehr da". Stuende die Pruefung an
den Schreibwegen, koennte ein Ausgeschlossener weiter mitlesen --
und genau das ist bei Belaestigung der Teil, der weh tut.
AN DIESER STELLE UND NICHT NUR BEIM ANMELDEN: Wer schon
angemeldet ist, hat ein Sitzungsplaetzchen, das Tage gilt. Eine
Pruefung beim Anmelden haette ihn bis zum Ablauf drin gelassen --
also ausgerechnet in den Stunden, in denen die Entscheidung
gefallen ist.
NUR FUER AUSSENROLLEN: Eine Massnahme gegen jemanden aus dem Team
gibt es nicht (siehe /api/treff/massnahme), und ein Fehler hier
duerfte niemals das Team aussperren. Die Frage wird deshalb gar
nicht erst gestellt.
WIRFT DIE FRAGE, greift der catch weiter unten und die Antwort
ist "null" -- also "nicht angemeldet". Das ist die sichere
Richtung: Wer nicht nachsehen kann, laesst niemanden herein. */
if (AUSSEN_ROLLEN.has(reihe.rolle) && treffSperreLesen(db(), reihe.id)?.art === "ausschluss") {
return null;
}
return { id: reihe.id, name: reihe.name, rolle: reihe.rolle, haus };
} catch { return null; }
@@ -3131,7 +3371,15 @@ export const workspaceRouter = express.Router();
/workspace/ -- und von dort wieder hierher. Der Browser bricht das
nach ein paar Runden mit "zu viele Weiterleitungen" ab, und man
sucht den Fehler ueberall, nur nicht in dieser Zeile. */
const OFFEN = new Set(["/workspace/index.html", "/workspace/crew-index.html"]);
/* SIE WIRD NICHT ZWEIMAL GESCHRIEBEN (11.09.2026).
Bis heute stand hier dieselbe Liste ein zweites Mal, von Hand. Sie
stimmte -- weil sie zwei Eintraege hatte und beide am selben Tag
entstanden. Beim dritten (der Wand des Treffs) waere genau das
passiert, wogegen die Rechtetafel gebaut wurde: Wer nur hier
nachtraegt, bekommt eine Wand, die die Pruefung als Luecke meldet;
wer nur dort nachtraegt, bekommt eine Endlosweiterleitung. Beides
faellt erst im Browser auf. Jetzt gibt es die Liste einmal. */
const OFFEN = new Set(OHNE_ANMELDUNG);
/** Der Pfad, wie ihn die Schranke ansehen muss.
*
@@ -3241,6 +3489,58 @@ function stillerZugang(code) {
}
}
/* =====================================================================
WER SIEHT WAS — die Tafel, wie sie wirklich ist (11.09.2026)
=====================================================================
Filipe: *"egal welche Rolle oder Person hinzugefuegt wird, soll immer
nur das sehen, was ich erlaube."*
Dazu gehoert, dass er es NACHSEHEN kann. Eine Regel, die man nur
glauben kann, ist eine Zusage; eine, die man ansehen kann, ist eine
Auskunft.
DIESE ANTWORT WIRD GERECHNET, NICHT GEPFLEGT. Sie kommt aus
derselben Tafel, aus der auch die Schranke ihre Entscheidung holt
(rechte.js). Eine von Hand geschriebene Uebersicht waere eine zweite
Wahrheit — und ausgerechnet die, der man glaubt, weil sie wie eine
Auskunft aussieht. Sie wuerde altern, und man wuerde es ihr nicht
ansehen.
NUR DogFather. Nicht aus Geheimniskraemerei, sondern weil diese
Antwort eine vollstaendige Karte des Hauses ist: jede Seite, jede
Rolle, und wo die Luecken sind. Wer sie hat, weiss, wo er es
versuchen muesste.
===================================================================== */
workspaceRouter.get("/workspace/api/rechte/tafel", (req, res) => {
const person = sitzungLesen(req);
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
if (person.rolle !== "admin") return res.status(404).json({ fehler: "nicht_gefunden" });
const seiten = Object.entries(SEITEN).map(([pfad, rollen]) => ({
pfad,
datei: pfad.replace("/workspace/", ""),
rollen: [...rollen],
})).sort((a, b) => a.datei.localeCompare(b.datei, "de"));
res.json({
rollen: ALLE_ROLLEN.map((r) => ({
schluessel: r,
name: ROLLEN_NAME[r] ?? r,
/* Wie viele der Seiten diese Rolle oeffnen darf. Die Zahl ist
die eigentliche Antwort auf "sieht die neue Rolle zu viel?" */
seiten: seiten.filter((s) => s.rollen.includes(r)).length,
})),
seiten,
ohne_anmeldung: [...OHNE_ANMELDUNG],
/* Die Zahl, die den Unterschied macht: Wie viele Seiten stehen
fuer NIEMANDEN offen, ohne dass jemand das entschieden haette?
Seit dem 11.09. ist sie immer 0 — es gibt keinen Zustand mehr,
der "alle angemeldeten" bedeutet, ohne dass es dasteht. */
ohne_eintrag: 0,
});
});
workspaceRouter.post("/workspace/api/anmelden", (req, res) => {
const ip = echteIp(req);
try {
@@ -3313,9 +3613,25 @@ workspaceRouter.post("/workspace/api/anmelden", (req, res) => {
zwar auch fuer die neue Adresse, denn die Sperre haengt an der
IP, nicht am Hostnamen. Das ist der Preis der Stille; er faellt
nur an, wenn jemand die alte Adresse mehrfach probiert. */
const still = codeBrauchbar && !istOhneModiAdresse(req.get("host"))
? stillerZugang(code)
: null;
/* UND AUCH NICHT AUF DER WAND DES TREFFS (11.09.2026).
Hier stand `!istOhneModiAdresse(...)` -- also "ueberall ausser auf
den alten Adressen". Als dritte Wand dazukam, hiess das
stillschweigend auch: dort. Ein Modi kam damit ueber den
verborgenen Weg in den Treff herein, obwohl die Kachelliste dort
nur `gast` kennt und die Adressregel ihn abweist.
Gefunden hat das nicht das Lesen, sondern eine Zeile in
pruef-treff, die das GEGENTEIL behauptete und rot wurde:
"auf treff. kommt sonst niemand herein -- auch DogFather nicht
(401/200)". DogFather wurde abgewiesen, der Modi nicht.
Die Lehre ist dieselbe wie bei `null` in der Rechtetafel: Eine
Bedingung, die durch AUSSCHLIESSEN formuliert ist, nimmt jede
neue Moeglichkeit automatisch mit auf. Jetzt steht dort, WO der
Weg gilt, statt wo er nicht gilt. */
const stilleWand = istCrewAdresse(req.get("host")) || istPruefAdresse(req.get("host"));
const still = codeBrauchbar && stilleWand ? stillerZugang(code) : null;
if (still) {
db().prepare("DELETE FROM versuche WHERE ip = ?").run(ip);
db().prepare("UPDATE personen SET letzter_login = ? WHERE id = ?").run(jetzt(), still.id);
@@ -3360,7 +3676,9 @@ workspaceRouter.post("/workspace/api/anmelden", (req, res) => {
Wand gerade dran ist. Ein Manager, der crew. erraet und seinen
Code eintippt, faellt dadurch in genau dieselbe Antwort wie
jemand mit einer erfundenen Rolle. */
const kachelSatz = istCrewAdresse(req.get("host")) ? CREW_KACHEL : ROLLEN_KACHEL;
const kachelSatz = istTreffAdresse(req.get("host")) ? TREFF_KACHEL
: istCrewAdresse(req.get("host")) ? CREW_KACHEL
: ROLLEN_KACHEL;
if (!kachelSatz.has(rolle) || !codeBrauchbar) {
db().prepare("INSERT INTO versuche (ip, zeitpunkt) VALUES (?,?)").run(ip, jetzt());
@@ -3417,6 +3735,33 @@ workspaceRouter.post("/workspace/api/anmelden", (req, res) => {
return res.status(401).json({ fehler: "ungueltig" });
}
/* AUSGESCHLOSSEN HEISST: DER CODE STIMMT NICHT MEHR (11.09.2026).
sitzungLesen() weist eine ausgeschlossene Person ohnehin ab --
jede Anfrage bekommt 401. Ohne diese Zeile hier waere die
Anmeldung aber TROTZDEM gelungen: Plaetzchen gesetzt, "willkommen"
geantwortet, Weiterleitung auf die Startseite -- und dort dann
eine Seite, auf der nichts geht. Das sieht nicht aus wie eine
Entscheidung, sondern wie ein kaputtes Programm, und man
probiert es zehnmal.
DIESELBE ANTWORT WIE BEI EINEM FALSCHEN CODE, und zwar
wortgleich: Wer ausgeschlossen wurde, hat den Grund bereits
bekommen (die Massnahme verlangt einen). Die Anmeldeseite muss
ihn nicht wiederholen -- und sie darf niemandem, der einen
fremden Code probiert, verraten, dass es diesen Zugang gibt.
NUR FUER AUSSENROLLEN. Gegen jemanden aus dem Team gibt es diese
Massnahme gar nicht (siehe /api/treff/massnahme); die Frage
wird deshalb nicht gestellt. Ein Fehler hier duerfte niemals
das Team aussperren. */
if (AUSSEN_ROLLEN.has(gefunden.rolle)
&& treffSperreLesen(db(), gefunden.id)?.art === "ausschluss") {
db().prepare("INSERT INTO versuche (ip, zeitpunkt) VALUES (?,?)").run(ip, jetzt());
protokolliere("anmeldung_gesperrt_treff", { personId: gefunden.id, rolle, ip });
return res.status(401).json({ fehler: "ungueltig" });
}
db().prepare("DELETE FROM versuche WHERE ip = ?").run(ip);
db().prepare("UPDATE personen SET letzter_login = ? WHERE id = ?").run(jetzt(), gefunden.id);
sitzungSetzen(res, gefunden, req);
@@ -4593,10 +4938,43 @@ const nurCreatorIds = (ids) => {
} catch { return ids; }
};
export const sichtbareCreatorIds = (person) => nurCreatorIds(nurHaus(ohneVerborgene(sichtbareCreatorIdsRoh(person), person), person));
export const sichtbarePersonenIds = (person) => nurHaus(ohneVerborgene(sichtbarePersonenIdsRoh(person), person), person);
export const betreuerIds = (person) => nurHaus(ohneVerborgene(betreuerIdsRoh(person), person), person);
export const einladbareIds = (person) => nurHaus(ohneVerborgene(einladbareIdsRoh(person), person), person);
/* WER VON AUSSEN KOMMT, SIEHT NIEMANDEN (11.09.2026).
Filipe: "egal welche rolle oder person hinzugefuegt wird soll immer
nur das sehen wass ich erlaube."
GEFUNDEN, NICHT UEBERLEGT. Eine Zeile in pruef-treff behauptete
"Personen: nichts" und wurde rot. Nachgemessen bekam ein Gast:
{"personen":[{"id":1,"name":"Filipe","rolle":"admin", ...},
{"id":5,"name":"Kessi","rolle":"gast", ...}]}
Also DogFathers Namen und seine Rolle. Nicht dramatisch -- ein
Streamer ist kein Geheimnis -- aber es war eine Auskunft, die
niemand erlaubt hatte, und genau das ist der Punkt. Die Liste kam
zustande, weil `sichtbarePersonenIdsRoh` fuer jede Rolle DogFather
mitgibt (er ist das Gegenueber von allem) und die vier Umhuellungen
danach nichts daran aendern.
`nurEigene` steht deshalb GANZ AUSSEN -- nach allen anderen. Wer
innen ansetzt, muss darauf vertrauen, dass keine spaetere Huelle
wieder etwas hinzufuegt; aussen gilt es unabhaengig davon, was
innen passiert. Und es gilt fuer alle vier Listen gleich, statt
viermal einzeln.
`null` heisst bei diesen Funktionen "alle" -- deshalb wird es hier
ausdruecklich zu einer Liste mit genau einem Eintrag. Ein `null`
durchzureichen hiesse fuer eine Aussenrolle das Gegenteil von dem,
was hier steht. */
function nurEigene(ids, person) {
if (!AUSSEN_ROLLEN.has(person?.rolle)) return ids;
return person?.id ? [person.id] : [];
}
export const sichtbareCreatorIds = (person) => nurEigene(nurCreatorIds(nurHaus(ohneVerborgene(sichtbareCreatorIdsRoh(person), person), person)), person);
export const sichtbarePersonenIds = (person) => nurEigene(nurHaus(ohneVerborgene(sichtbarePersonenIdsRoh(person), person), person), person);
export const betreuerIds = (person) => nurEigene(nurHaus(ohneVerborgene(betreuerIdsRoh(person), person), person), person);
export const einladbareIds = (person) => nurEigene(nurHaus(ohneVerborgene(einladbareIdsRoh(person), person), person), person);
/* SICH SELBST NICHT -- UND ZWAR AUCH DANN NICHT, WENN DIE LISTE
AUSGESCHRIEBEN WIRD (10.09.2026).