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
+257 -4
View File
@@ -21,7 +21,31 @@
Notizbuch. Der Server setzt das ebenfalls durch (siehe
workspace-bereiche.js); hier geht es nur darum, ihm keine Knoepfe
hinzustellen, die ohnehin nur eine Absage bringen. */
const darfEintragen = () => ich && ich.rolle !== 'creator';
/* WER HIER ETWAS EINTRAGEN DARF.
Bis zum 11.09.2026 stand hier nur "alle außer dem Creator". Das
stimmte, solange jedes Brett jedem Angemeldeten offenstand. Mit den
sieben Brettern des Treffs stimmt es nicht mehr: Auf dem
Anschlagbrett und bei "Was ansteht" schreibt nur das Team, und wer
neu ist, darf die erste Woche überhaupt nicht schreiben.
OHNE DIESE ZEILE HÄTTE EIN MITGLIED DEN KNOPF GESEHEN und beim
Drücken eine Absage bekommen -- genau die Sorte Knopf, die in
diesem Haus als Fehler gilt: Er verspricht etwas, das der Server
ablehnt, und der Mensch davor hält sich selbst für den Fehler.
DIE ANTWORT KOMMT VOM SERVER (/api/treff/lage), nicht aus einer
zweiten Regel hier. Sonst gäbe es die Schreibregel zweimal, und die
Fassung im Browser kann man ändern. Faellt die Auskunft aus, bleibt
die Lage leer und es gilt wieder die alte Regel -- der Knopf ist
dann da und der Server sagt notfalls nein. Das ist die richtige
Richtung: Ein Knopf zu viel ist ärgerlich, ein fehlender Riegel
wäre ein Loch. */
const darfEintragen = () => {
if (!ich || ich.rolle === 'creator') return false;
if (treffBrett && lage && lage.schreiben) return lage.schreiben[bereich] === true;
return true;
};
const $ = (id) => document.getElementById(id);
const UNTERZEILE = {
@@ -40,6 +64,14 @@
let ich = null;
let bereich = null;
let einstellung = null;
/* Ist dies ein Brett des Treffs, und was darf ich dort? Beides sagt
der Server (daten.treff und /api/treff/lage) -- im Browser steht
keine Liste der Bretter und keine Rechteregel, sondern nur die
Antwort auf "was darf ICH hier gerade". Eine Regel, die im Browser
noch einmal steht, ist die, die auseinanderlaeuft; und die Fassung
im Browser kann man aendern. */
let treffBrett = false;
let lage = null;
let filter = '';
/* Nur beim ersten Laden springen -- sonst reisst es die Seite nach
jeder Bewertung erneut an dieselbe Stelle. */
@@ -214,6 +246,174 @@
if (e.erstellt_name) fuss.append(el('span', null, '· von ' + e.erstellt_name));
if (e.status === 'erledigt') fuss.append(el('span', null, '· erledigt'));
/* ---- Der Treff -------------------------------------------------
DER VOLLE ROLLENNAME AM BEITRAG (Filipe, 11.09.2026: "voller
Rollenname zeigen"). Er kommt fertig vom Server; hier steht
keine Zuordnung Rolle -> Name, die waere eine Liste aller Rollen
dieses Hauses auf jedem Rechner der Community.
WARUM DAS UEBERHAUPT DRANSTEHT: In einem Raum mit Zuschauern und
Moderation muss man sehen, wer spricht. "Frida" und "Frida ·
Modi" sind zwei verschiedene Saetze -- der zweite ist eine
Ansage, der erste eine Meinung.
BEI MITGLIEDERN STEHT NICHTS DA. "Kessi · Community" waere eine
Auszeichnung fuer das Normale; der Rollenname ist hier eine
Auskunft ueber Verantwortung, nicht ueber Zugehoerigkeit. */
if (treffBrett && e.erstellt_rolle_name && e.erstellt_rolle !== 'gast') {
fuss.append(el('span', 'treff-rolle', '· ' + e.erstellt_rolle_name));
}
if (treffBrett && lage) {
const zeile = el('div', 'treff-knoepfe');
/* WILL ICH AUCH. Die Zahl steht AM Knopf und nicht daneben: Sie
ist das Ergebnis des Knopfes, nicht eine Angabe ueber den
Eintrag. Und sie aendert sich sofort im Bild, bevor der Server
geantwortet hat -- bei einem Knopf, der eine Meinung abgibt,
ist eine halbe Sekunde Verzoegerung der Unterschied zwischen
"hat geklappt" und "hab ich das jetzt gedrueckt?". Antwortet
der Server anders, gilt seine Zahl. */
if (bereich === 'wunsch') {
const w = el('button', 'treff-will', '');
w.type = 'button';
let meine = !!e.meine_stimme;
let zahl = Number(e.stimmen) || 0;
const male = () => {
w.textContent = (meine ? '★ ' : '☆ ') + 'Will ich auch · ' + zahl;
w.setAttribute('aria-pressed', String(meine));
};
male();
w.addEventListener('click', async () => {
w.disabled = true;
meine = !meine; zahl += meine ? 1 : -1; male();
try {
/* EIN DELETE TRAEGT HIER KEINEN KOERPER (gemessen 11.09.2026).
Der naheliegende Aufbau waere gewesen, PUT und DELETE
gleich zu behandeln -- gleicher Kopf, gleicher leerer
Koerper. Das geht NICHT: Nodes HTTP-Parser weist ein
DELETE mit Koerper mit einem leeren 400 ab, noch bevor
Express die Anfrage sieht. Man sucht den Fehler dann in
der Route, und dort ist keiner.
Nachgemessen: PUT mit Koerper -> 200, DELETE ohne
Koerper -> 200, DELETE mit Koerper -> 400 mit genau
einem Kopffeld ("connection: close"). Das ist die
Handschrift des Parsers, nicht die einer Anwendung. */
const a = await hole('/workspace/api/treff/eintrag/' + e.id + '/willich',
meine
? { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: '{}' }
: { method: 'DELETE' });
if (a.ok) { const j = await a.json(); zahl = j.stimmen; meine = j.meine; male(); }
} catch { /* umgeleitet */ } finally { w.disabled = false; }
});
zeile.append(w);
}
/* MELDEN. Nur fuer die Community und nur an fremden Beitraegen --
den eigenen kann man loeschen, das ist der kuerzere Weg.
Der Grund ist Pflicht (DSA Art. 16); der Server prueft ihn
noch einmal, die Abfrage hier erspart nur den Fehlschlag. */
if (!lage.team && e.erstellt_von !== ich.id) {
zeile.append(tuKnopf('melden', async () => {
const grund = prompt('Was ist daran nicht in Ordnung?\n\n'
+ 'Bitte schreib es in einem Satz – sonst können wir es nicht prüfen.');
if (grund === null) return;
const a = await hole('/workspace/api/treff/eintrag/' + e.id + '/melden', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ grund }),
});
melde(a.ok ? 'Danke – wir sehen uns das an.'
: ((await a.json().catch(() => ({}))).fehler || 'Ging nicht.'));
}));
}
if (lage.team) {
/* ANHEFTEN. Hoechstens drei; die Zahl kommt vom Server
(lage.anschlag_max), damit sie nicht an zwei Stellen steht. */
if (bereich === 'anschlag') {
const dran = !!e.angeheftet;
zeile.append(tuKnopf(dran ? 'abnehmen' : 'anheften', async () => {
const a = await hole('/workspace/api/treff/eintrag/' + e.id + '/anheften',
dran
? { method: 'DELETE' }
: { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: '{}' });
if (!a.ok) {
melde((await a.json().catch(() => ({}))).fehler
|| ('Es hängen schon ' + lage.anschlag_max + ' Ansagen.'));
return;
}
await laden();
}));
}
/* FREIGEBEN. Bei Terminen heisst es "im Treff zeigen", bei
Highlights "freigeben" -- derselbe Vorgang, zwei Woerter,
weil es zwei verschiedene Entscheidungen sind. */
if (bereich === 'ansteht' || bereich === 'highlight') {
const frei = !!e.freigegeben;
const wort = bereich === 'ansteht'
? (frei ? 'nicht mehr zeigen' : 'im Treff zeigen')
: (frei ? 'zurückziehen' : 'freigeben');
if (!frei) {
const m = el('span', 'marke-art marke-art--nur', 'noch nicht sichtbar');
m.title = 'Die Community sieht das erst nach der Freigabe.';
zeile.append(m);
}
zeile.append(tuKnopf(wort, async () => {
const a = await hole('/workspace/api/treff/eintrag/' + e.id + '/freigeben',
frei
? { method: 'DELETE' }
: { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: '{}' });
if (!a.ok) { melde('Ging nicht.'); return; }
await laden();
}));
}
/* AUS "MITMACHEN" WIRD EIN TALENT. Ein ausdruecklicher Schritt
und keine Automatik: Wer sich meldet, ist ein Kandidat --
dass er einer wird, entscheidet ein Mensch. */
if (bereich === 'mitmachen' && e.erstellt_rolle === 'gast') {
zeile.append(tuKnopf('zu den Talenten', async () => {
if (!confirm('„' + e.titel + '“ zu den Talenten übernehmen?')) return;
const a = await hole('/workspace/api/treff/mitmachen/' + e.id + '/uebernehmen',
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
const j = await a.json().catch(() => ({}));
melde(a.ok ? 'Steht jetzt bei den Talenten.'
: (j.fehler === 'schon_uebernommen' ? 'Steht dort schon.'
: (j.fehler || 'Ging nicht.')));
}));
}
/* ENTFERNEN MIT GRUND (DSA Art. 17). Eigener Knopf neben dem
gewohnten "loeschen" -- denn "loeschen" gibt es weiterhin
fuer den eigenen Beitrag, und der braucht keine Begruendung. */
if (e.erstellt_von !== ich.id) {
zeile.append(tuKnopf('entfernen', async () => {
const grund = prompt('Warum wird „' + e.titel + '“ entfernt?\n\n'
+ 'Der Grund wird festgehalten – die Person hat ein Recht darauf.');
if (grund === null) return;
/* DER GRUND REIST IN DER ADRESSE, nicht im Koerper -- aus
demselben gemessenen Grund wie oben. Er ist kein
Geheimnis: Er wird der Person, die den Beitrag
geschrieben hat, ohnehin mitgeteilt, und er steht im
Protokoll. */
const a = await hole('/workspace/api/bereich/' + bereich + '/' + e.id
+ '?grund=' + encodeURIComponent(grund), { method: 'DELETE' });
if (!a.ok) {
melde((await a.json().catch(() => ({}))).fehler || 'Ging nicht.');
return;
}
await laden();
}));
}
}
if (zeile.childNodes.length) k.append(zeile);
}
if (!darfEintragen()) { k.append(fuss); return k; }
const knoepfe = el('div', 'eintrag-knoepfe');
@@ -263,6 +463,19 @@
}
}
/* Einmal je Seitenaufruf. Sie wird an ZWEI Stellen gebraucht -- beim
Aufbau des Formulars und beim Zeichnen der Karten -- und beide
brauchen sie, BEVOR sie etwas anzeigen. Ein zweiter Abruf wäre eine
zweite Gelegenheit für zwei verschiedene Antworten auf dem selben
Bildschirm. */
async function lageHolen() {
if (!treffBrett || lage) return;
try {
const l = await fetch('/workspace/api/treff/lage', { credentials: 'same-origin' });
if (l.ok) lage = await l.json();
} catch { /* dann gilt die alte Regel, siehe darfEintragen */ }
}
function filterBauen() {
const ziel = $('filter');
ziel.textContent = '';
@@ -284,6 +497,8 @@
if (!a.ok) { melde('Einträge konnten nicht geladen werden.'); return; }
const daten = await a.json();
einstellung = daten.einstellung;
treffBrett = !!daten.treff;
await lageHolen();
filterBauen();
const ziel = $('liste');
@@ -539,6 +754,36 @@
if (einstellung.ober && $('ober')) $('ober').textContent = einstellung.ober;
$('unterzeile').textContent = UNTERZEILE[bereich] || '';
/* VOR dem Aufbau des Formulars. Danach wäre zu spät: Der Knopf
stünde dann kurz da und verschwände wieder -- sichtbar, und
ausgerechnet an der Stelle, an der jemand gerade überlegt, ob er
schreiben darf. */
treffBrett = !!daten.treff;
await lageHolen();
/* DER WEG ZU DEN REGELN STEHT AUF JEDEM BRETT DES TREFFS
(11.09.2026).
Nicht nur auf dem Brett "Regeln & Hilfe". Wer die Regeln braucht,
braucht sie in dem Moment, in dem er etwas schreiben will -- und
das ist fast nie dort. Ein Mensch, der erst zurueck zur
Startseite und dann auf die richtige Kachel muss, liest sie
nicht; er schreibt einfach.
ALS LINK UND NICHT ALS EINGEBLENDETER TEXT: Die Regeln sind zu
lang, um sie ueber jedes Brett zu haengen, und ein Kasten, den
man jeden Tag wegklickt, wird nach drei Tagen nicht mehr
gesehen. */
if (daten.treff) {
const vorhanden = $('treff-regelweg');
if (!vorhanden) {
const a = el('a', 'treff-regelweg', 'Regeln & Hilfe');
a.id = 'treff-regelweg';
a.href = 'treff-regeln.html';
$('unterzeile').after(a);
}
}
/* Fuer den Creator verschwindet alles Schreibende -- und die
Unterzeile sagt, warum. Ein leerer Bereich ohne Erklaerung wirkt
kaputt; einer mit Erklaerung wirkt gedacht. */
@@ -551,10 +796,18 @@
"was zu dir gehört", laese sich eine vollstaendige Liste wie
ein Ausschnitt, und die naheliegende Frage waere: Was sehe ich
hier eigentlich nicht? */
/* IM TREFF STIMMT KEINER DER BEIDEN SÄTZE. Dort geht es weder um
die Agentur noch um Betreuung, sondern entweder um ein Brett,
das dem Team gehört, oder um eine Stufe, die noch nicht
erreicht ist. Und der Server weiß beides genau -- er schickt
den Satz mit, statt dass der Browser ihn errät. */
$('unterzeile').textContent = (UNTERZEILE[bereich] || '')
+ (einstellung.fuerAlle
? ' Eingetragen wird von der Agentur – du siehst alles, was hier steht.'
: ' Hier trägt deine Betreuung ein – du siehst alles, was zu dir gehört.');
+ (treffBrett
? ' ' + (lage?.warum?.[bereich]
|| 'Hier schreibt das Team – lesen kannst du alles.')
: einstellung.fuerAlle
? ' Eingetragen wird von der Agentur – du siehst alles, was hier steht.'
: ' Hier trägt deine Betreuung ein – du siehst alles, was zu dir gehört.');
}
for (const [wert, name] of Object.entries(einstellung.arten)) {