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:
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user