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]>
@@ -46,6 +46,68 @@
|
||||
}
|
||||
.eintrag-knoepfe { display: flex; gap: 6px; margin-left: auto; flex-wrap: wrap; }
|
||||
|
||||
/* =====================================================================
|
||||
DER TREFF (11.09.2026)
|
||||
|
||||
Drei Zusaetze, und nur auf den sieben Brettern der Community.
|
||||
|
||||
SIE STEHEN IN EINER EIGENEN ZEILE UNTER DEM FUSS und nicht darin.
|
||||
Der Fuss ist eine Auskunft (Datum, wer, Stand); hier stehen Dinge,
|
||||
die man TUT. Zusammen in einer Reihe waere "von Frida" so breit wie
|
||||
"Will ich auch" -- und auf einem Handy laege beides ineinander.
|
||||
===================================================================== */
|
||||
.treff-knoepfe {
|
||||
display: flex; gap: 7px; flex-wrap: wrap; align-items: center;
|
||||
margin-top: 10px; padding-top: 10px;
|
||||
border-top: 1px solid var(--rand);
|
||||
}
|
||||
|
||||
/* Der Rollenname am Beitrag. Leise, aber nicht versteckt: Er soll
|
||||
beim Lesen auffallen, ohne den Beitrag zu ueberschreiben. Deshalb
|
||||
dieselbe Groesse wie der Fuss und nur ein kraeftigerer Ton. */
|
||||
.treff-rolle {
|
||||
color: var(--text-leise);
|
||||
font-weight: 600;
|
||||
letter-spacing: .01em;
|
||||
}
|
||||
|
||||
/* "Will ich auch". Der einzige Knopf auf diesen Brettern, der keine
|
||||
Verwaltung ist, sondern eine Meinung -- er darf deshalb als
|
||||
einziger einen Ton tragen.
|
||||
|
||||
GEDRUECKT WIRD ER RUHIGER, NICHT LAUTER. Der naheliegende Entwurf
|
||||
waere ein kraeftiges Leuchten fuer "ich bin dabei". Nur sieht man
|
||||
auf einer Wunschliste meist ZEHN Karten gleichzeitig, und wer bei
|
||||
der Haelfte mitgestimmt hat, saehe zehn leuchtende Flaechen
|
||||
untereinander. Das ist genau das, was diese Seite nicht sein soll
|
||||
(Hausregel: augenschonend). Der gedrueckte Zustand bekommt deshalb
|
||||
eine gefuellte Flaeche in gedecktem Ton, der ungedrueckte bleibt
|
||||
ein Umriss -- unterscheidbar, ohne zu blenden. */
|
||||
.treff-will {
|
||||
font: inherit; font-size: .8rem; font-weight: 600;
|
||||
padding: 6px 12px; border-radius: 999px; cursor: pointer;
|
||||
color: var(--text-leise);
|
||||
background: transparent;
|
||||
border: 1px solid var(--rand-hell);
|
||||
transition: background .16s ease, color .16s ease, border-color .16s ease;
|
||||
}
|
||||
.treff-will:hover:not(:disabled) {
|
||||
color: var(--text);
|
||||
border-color: color-mix(in srgb, var(--akzent) 45%, transparent);
|
||||
}
|
||||
.treff-will[aria-pressed="true"] {
|
||||
color: var(--text);
|
||||
background: color-mix(in srgb, var(--akzent) 14%, transparent);
|
||||
border-color: color-mix(in srgb, var(--akzent) 42%, transparent);
|
||||
}
|
||||
.treff-will:disabled { opacity: .55; cursor: default; }
|
||||
|
||||
/* Dieselbe Ruecksicht wie ueberall: Wer Bewegung abbestellt hat,
|
||||
bekommt keine. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.treff-will { transition: none; }
|
||||
}
|
||||
|
||||
.filter { display: flex; gap: 8px; margin: 0 0 20px; flex-wrap: wrap; }
|
||||
.filter .schritt[aria-pressed="true"] {
|
||||
/* Gedrueckt = der "voll"-Auftritt des Knopf-Systems. Vorher stand
|
||||
|
||||
@@ -1030,6 +1030,68 @@ body.start > main { position: relative; z-index: 1; }
|
||||
[data-ton="26"] { --ton: #7fd4a8; } /* Entwicklung */
|
||||
[data-ton="27"] { --ton: #8f7bd6; } /* Talente */
|
||||
|
||||
/* =====================================================================
|
||||
DIE SIEBEN TOENE DES TREFFS (11.09.2026)
|
||||
|
||||
Bei 27 Kacheln war der Farbkreis voll -- das stand seit dem 11.09. als
|
||||
Notiz im Code. Sieben weitere von Hand zu waehlen haette Toene
|
||||
ergeben, die sich nur noch der Messung nach unterscheiden.
|
||||
|
||||
Sie sind deshalb GERECHNET: ein Gitter aus Farbton, Saettigung und
|
||||
Helligkeit, umgerechnet in den Lab-Raum (dort entspricht Abstand dem,
|
||||
was das Auge als Unterschied sieht), und daraus gierig die sieben mit
|
||||
dem groessten kleinsten Abstand zu allen 27 vorhandenen UND
|
||||
zueinander.
|
||||
|
||||
Zwei Bereiche sind ausgeschlossen, und beide aus einem Grund:
|
||||
* ROT (unter 22 und ueber 338 Grad) gehoert Spicy Media. Eine
|
||||
Community-Kachel in deren Farbe waere ein falsches Signal.
|
||||
* NEON (Buntheit ueber 62) verstoesst gegen die Hausregel
|
||||
"augenschonend". Dasselbe gilt nach unten: unter 24 wird es grau,
|
||||
und grau liest sich als "abgeschaltet".
|
||||
|
||||
Ergebnis: schwaechster Abstand 24,9 -- zum Vergleich lag eine von
|
||||
Hand gewaehlte Farbfamilie im selben Blau bei 12,6. Die Rechnung war
|
||||
also nicht Genauigkeitsspielerei, sondern der Unterschied zwischen
|
||||
unterscheidbar und nicht. */
|
||||
[data-ton="28"] { --ton: #8e784f; } /* Regeln & Hilfe */
|
||||
[data-ton="29"] { --ton: #8e5b77; } /* Highlights */
|
||||
[data-ton="30"] { --ton: #b2bc78; } /* Wunschliste */
|
||||
[data-ton="31"] { --ton: #656a9e; } /* Was ansteht */
|
||||
[data-ton="32"] { --ton: #668e6e; } /* Mitmachen */
|
||||
[data-ton="33"] { --ton: #cc9451; } /* Anschlagbrett */
|
||||
[data-ton="34"] { --ton: #cc92c6; } /* Der Treff */
|
||||
|
||||
/* DER 35. TON (11.09.2026) — für „Wer sieht was".
|
||||
|
||||
GEMESSEN, NICHT GEWÄHLT. Gesucht war der Punkt im Farbraum, der von
|
||||
ALLEN 34 vorhandenen Tönen am weitesten entfernt liegt — Abstand in
|
||||
Lab, nicht in HSL, weil zwei Töne mit gleichem HSL-Abstand
|
||||
unterschiedlich verschieden AUSSEHEN können.
|
||||
|
||||
Zwei Bereiche waren dabei ausgeschlossen, und beide aus einem Grund
|
||||
und nicht aus Geschmack:
|
||||
* Rot (Farbton unter 22° / über 338°) gehört Spicy Media.
|
||||
* Neon (Sättigung über 52 %) verstößt gegen die Hausregel
|
||||
„augenschonend" — die gilt hier wie überall.
|
||||
|
||||
Ergebnis: #2f2f7f, Abstand 31,4 zum nächsten Nachbarn (#656a9e, „Was
|
||||
ansteht"). Zum Vergleich: Das schwächste Paar unter den vorhandenen
|
||||
34 liegt bei 24,9. Der neue Ton ist also unterscheidbarer als das,
|
||||
was ohnehin nebeneinander steht — und die beiden nächsten Nachbarn
|
||||
stehen in verschiedenen Gruppen, treffen sich auf dem Bildschirm
|
||||
also gar nicht.
|
||||
|
||||
WARUM ÜBERHAUPT EIN NEUER: Die 35. Kachel hätte sich sonst einen Ton
|
||||
mit einer anderen geteilt. Zwei Dinge in einer Farbe heißt, dass die
|
||||
Farbe nichts mehr sagt — und pruef-start-ansicht zählt genau das
|
||||
nach ("34 Farben auf 35 Kacheln" war ihre Meldung). */
|
||||
[data-ton="35"] { --ton: #2f2f7f; } /* Wer sieht was */
|
||||
/* Der 36., nach derselben Rechnung: #7c2771, Abstand 29,7 zu den beiden
|
||||
nächsten (#8e5b77 „Highlights", #2f2f7f „Wer sieht was"). Auch das
|
||||
liegt über dem schwächsten Paar der vorhandenen Töne (24,9). */
|
||||
[data-ton="36"] { --ton: #7c2771; } /* Meldungen & Maßnahmen */
|
||||
|
||||
/* Rückfallwert für alles, was einen Ton braucht, aber keine Nummer hat.
|
||||
Steht bewusst in :where() -- damit zählt die Zeile beim Wettstreit der
|
||||
Regeln als NICHT vorhanden und die Nummer oben gewinnt immer. Ohne
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
/* =====================================================================
|
||||
Der Treff — die beiden Textseiten (11.09.2026)
|
||||
|
||||
Regeln & Hilfe (für die Community) und „Wer sieht was" (für
|
||||
DogFather). Beides sind LESESEITEN: langer, zusammenhängender Text
|
||||
und eine Tabelle, keine Kartenwand.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WARUM SIE KEINE MODULKARTEN SIND
|
||||
|
||||
Die naheliegende Wahl wäre `.eintrag-karte` oder ein neuer Eintrag in
|
||||
der MODULLISTE gewesen — dann sähen die Abschnitte aus wie alles
|
||||
andere im Haus: geschliffene Fase, Kantenlicht, Sternenfeld.
|
||||
|
||||
Dagegen sprechen zwei Dinge, und das zweite wiegt schwerer:
|
||||
|
||||
1. Die MODULLISTE steht siebenmal in module.css und wird von
|
||||
pruef-css-klassen Zeichen für Zeichen verglichen. Eine achte Klasse
|
||||
hätte an sieben Stellen dasselbe ergänzen müssen — machbar, aber
|
||||
jede dieser sieben ist eine Gelegenheit für einen Unterschied.
|
||||
|
||||
2. Karten zerteilen. Ein Regeltext ist ein Text, den man von oben
|
||||
nach unten liest; sieben Karten daraus zu machen heißt, sieben
|
||||
Anfänge zu machen. Genau das ist bei Regeln der Fehler: Wer nach
|
||||
der dritten Karte aufhört, hat vier Regeln nicht gelesen und
|
||||
glaubt trotzdem, er kenne sie.
|
||||
|
||||
Deshalb hier nur so viel Trennung, dass man die Abschnitte findet —
|
||||
eine Linie und Abstand, keine Kante.
|
||||
===================================================================== */
|
||||
|
||||
.treff-abschnitt {
|
||||
margin: 0 0 26px;
|
||||
padding: 0 0 22px;
|
||||
border-bottom: 1px solid var(--rand);
|
||||
}
|
||||
.treff-abschnitt:last-of-type { border-bottom: 0; }
|
||||
|
||||
.treff-abschnitt__titel {
|
||||
margin: 0 0 10px;
|
||||
font-size: 1.06rem;
|
||||
font-weight: 650;
|
||||
letter-spacing: .005em;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Der Lesefluss ist hier die eigentliche Aufgabe.
|
||||
|
||||
1,72 statt der üblichen 1,55: Auf einem Handy stehen in einer Zeile
|
||||
etwa 42 Zeichen; bei engem Zeilenabstand verliert man beim Sprung an
|
||||
den Zeilenanfang die Zeile. Das merkt niemand als „schlechter
|
||||
Zeilenabstand" — man liest den Absatz einfach zweimal und hält den
|
||||
Text für kompliziert.
|
||||
|
||||
Und eine Breitengrenze: Über etwa 70 Zeichen je Zeile wird derselbe
|
||||
Sprung wieder schwierig, nur von der anderen Seite. Auf einem breiten
|
||||
Bildschirm liefe der Regeltext sonst über 1100 Pixel. */
|
||||
.treff-abschnitt__inhalt { max-width: 68ch; }
|
||||
.treff-abschnitt__inhalt p {
|
||||
margin: 0 0 12px;
|
||||
font-size: .92rem;
|
||||
line-height: 1.72;
|
||||
color: var(--text-leise);
|
||||
}
|
||||
.treff-abschnitt__inhalt p:last-child { margin-bottom: 0; }
|
||||
.treff-abschnitt__inhalt strong { color: var(--text); font-weight: 640; }
|
||||
|
||||
/* Die Aufzählungen. Die Zahl bzw. der Punkt steht AUSSERHALB des
|
||||
Textblocks — so beginnt jede Zeile einer mehrzeiligen Regel an
|
||||
derselben Stelle, und die Liste bleibt als Liste lesbar. */
|
||||
.regel-liste {
|
||||
margin: 0 0 12px;
|
||||
padding-left: 1.35em;
|
||||
font-size: .92rem;
|
||||
line-height: 1.72;
|
||||
color: var(--text-leise);
|
||||
}
|
||||
.regel-liste li { margin: 0 0 9px; padding-left: .2em; }
|
||||
.regel-liste li:last-child { margin-bottom: 0; }
|
||||
.regel-liste li::marker { color: var(--text-still); }
|
||||
.regel-liste strong { color: var(--text); font-weight: 640; }
|
||||
|
||||
/* =====================================================================
|
||||
„WER SIEHT WAS" — die Rechtetafel als Tabelle
|
||||
|
||||
Sie wird aus der echten Tafel gerechnet (rechte.js), nicht gepflegt.
|
||||
Was sie leisten muss: Man soll eine LÜCKE sehen, ohne zu suchen.
|
||||
===================================================================== */
|
||||
|
||||
.rechte-tafel { overflow-x: auto; margin: 0 0 26px; }
|
||||
|
||||
.rechte-tafel table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
font-size: .82rem;
|
||||
}
|
||||
|
||||
/* Die Kopfzeile bleibt stehen. Bei 22 Zeilen und acht Spalten hat man
|
||||
sonst nach dem halben Scrollen vergessen, welche Spalte welche Rolle
|
||||
ist — und rät. Bei einer Rechteübersicht ist Raten das Gegenteil des
|
||||
Zwecks. */
|
||||
.rechte-tafel thead th {
|
||||
position: sticky; top: 0; z-index: 1;
|
||||
background: var(--flaeche);
|
||||
padding: 9px 8px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: var(--text-leise);
|
||||
border-bottom: 1px solid var(--rand-hell);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.rechte-tafel thead th:first-child { text-align: left; }
|
||||
|
||||
.rechte-tafel td {
|
||||
padding: 7px 8px;
|
||||
border-bottom: 1px solid var(--rand);
|
||||
text-align: center;
|
||||
color: var(--text-still);
|
||||
}
|
||||
.rechte-tafel td:first-child {
|
||||
text-align: left;
|
||||
color: var(--text-leise);
|
||||
font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
|
||||
font-size: .79rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.rechte-tafel tbody tr:hover td { background: rgba(255, 255, 255, .025); }
|
||||
|
||||
/* JA UND NEIN SIND NICHT GRÜN UND ROT.
|
||||
|
||||
Das wäre die naheliegende Wahl und hier die falsche: In dieser
|
||||
Tabelle ist ein „nein" nichts Schlechtes — es ist der Normalfall und
|
||||
genau das, was Filipe wollte. Grün/Rot würde eine Tafel mit 140
|
||||
roten Feldern erzeugen, die aussieht wie ein Schadensbericht.
|
||||
|
||||
Stattdessen: sichtbar gegen leise. Ein Punkt, wo es erlaubt ist,
|
||||
nichts, wo nicht. Man sieht die Zeilen mit vielen Punkten sofort —
|
||||
und das ist die Frage, die man an diese Tabelle hat. */
|
||||
.rechte-ja {
|
||||
display: inline-block; width: 7px; height: 7px; border-radius: 50%;
|
||||
background: var(--akzent);
|
||||
box-shadow: 0 0 8px -2px var(--akzent);
|
||||
}
|
||||
.rechte-nein { color: var(--text-still); opacity: .3; }
|
||||
|
||||
/* Die Zählzeile über der Tabelle: „Community — 3 von 22 Seiten". Das
|
||||
ist die eine Zahl, die die ganze Frage beantwortet. */
|
||||
.rechte-zahlen {
|
||||
display: flex; flex-wrap: wrap; gap: 8px;
|
||||
margin: 0 0 18px; padding: 0; list-style: none;
|
||||
}
|
||||
.rechte-zahlen li {
|
||||
padding: 6px 11px;
|
||||
border: 1px solid var(--rand-hell);
|
||||
border-radius: 999px;
|
||||
font-size: .8rem;
|
||||
color: var(--text-leise);
|
||||
}
|
||||
.rechte-zahlen li b { color: var(--text); font-weight: 650; }
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.treff-abschnitt__inhalt p, .regel-liste { font-size: .95rem; }
|
||||
.rechte-tafel table { font-size: .78rem; }
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
DER WEG ZU DEN REGELN — auf jedem Brett des Treffs
|
||||
|
||||
Er steht direkt unter der Unterzeile und sieht aus wie ein Link,
|
||||
nicht wie ein Knopf. Das ist Absicht: Ein Knopf verspricht, dass
|
||||
etwas PASSIERT; hier wechselt man nur die Seite. Wer auf jedem Brett
|
||||
einen zusätzlichen Knopf sieht, zählt ihn nach zwei Tagen zum
|
||||
Hintergrund.
|
||||
===================================================================== */
|
||||
.treff-regelweg {
|
||||
display: inline-block;
|
||||
margin-top: 6px;
|
||||
font-size: .82rem;
|
||||
color: var(--text-leise);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid var(--rand-hell);
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
.treff-regelweg:hover, .treff-regelweg:focus-visible {
|
||||
color: var(--text);
|
||||
border-bottom-color: color-mix(in srgb, var(--akzent) 55%, transparent);
|
||||
}
|
||||
|
||||
/* Links im Fließtext der Regelseite. Unterstrichen, weil Farbe allein
|
||||
kein Unterschied ist, den jeder sieht (WCAG 1.4.1). */
|
||||
.treff-abschnitt__inhalt a {
|
||||
color: var(--text);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
text-decoration-color: color-mix(in srgb, var(--akzent) 55%, transparent);
|
||||
}
|
||||
.treff-abschnitt__inhalt a:hover {
|
||||
text-decoration-color: var(--akzent);
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
MELDUNGEN & MASSNAHMEN — ein Fall je Karte
|
||||
|
||||
Karten sind hier richtig, wo sie beim Regeltext falsch waren: Es sind
|
||||
EINZELNE Vorgänge, die man einzeln entscheidet. Jeder Anfang ist ein
|
||||
neuer Fall, und das soll man sehen.
|
||||
===================================================================== */
|
||||
.treff-fall {
|
||||
margin: 0 0 12px;
|
||||
padding: 13px 15px;
|
||||
border: 1px solid var(--rand);
|
||||
border-left: 2px solid color-mix(in srgb, var(--akzent) 45%, transparent);
|
||||
background: rgba(255, 255, 255, .018);
|
||||
}
|
||||
|
||||
/* Was schon entschieden ist, tritt zurück -- aber es verschwindet
|
||||
nicht. Ausblenden wäre bequemer und wäre falsch: Man muss nachsehen
|
||||
können, wie ein ähnlicher Fall damals entschieden wurde. */
|
||||
.treff-fall[data-vorbei="ja"] {
|
||||
opacity: .62;
|
||||
border-left-color: var(--rand);
|
||||
}
|
||||
|
||||
.treff-fall__titel {
|
||||
margin: 0 0 8px;
|
||||
font-size: .95rem;
|
||||
font-weight: 640;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.treff-fall__zeile {
|
||||
margin: 0 0 5px;
|
||||
font-size: .85rem;
|
||||
line-height: 1.6;
|
||||
color: var(--text-leise);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.treff-fall__zeile:last-of-type { margin-bottom: 0; }
|
||||
|
||||
/* Das Schild steht in fester Breite links. Ohne sie beginnt jede Zeile
|
||||
woanders, und man liest die Liste als Fließtext statt als Angaben --
|
||||
bei fünf Zeilen je Fall ist das der Unterschied zwischen Überfliegen
|
||||
und Lesenmüssen. */
|
||||
.treff-fall__schild {
|
||||
flex: 0 0 7.5em;
|
||||
color: var(--text-still);
|
||||
font-size: .78rem;
|
||||
text-transform: lowercase;
|
||||
padding-top: .1em;
|
||||
}
|
||||
|
||||
.treff-fall__knoepfe {
|
||||
display: flex; gap: 7px; flex-wrap: wrap;
|
||||
margin-top: 11px; padding-top: 10px;
|
||||
border-top: 1px solid var(--rand);
|
||||
}
|
||||
/* Der Link "zum Brett" sieht aus wie die Knöpfe daneben -- er tut ja
|
||||
dasselbe: Er bringt einen woandershin. Ein Link, der zwischen zwei
|
||||
Knöpfen anders aussieht, wird für Text gehalten. */
|
||||
.treff-fall__knoepfe a.schritt { text-decoration: none; }
|
||||
|
||||
@media (max-width: 620px) {
|
||||
/* Untereinander statt nebeneinander: Bei 7,5em Schild und 42 Zeichen
|
||||
Zeilenbreite blieben für den Wert noch etwa 20 Zeichen -- ein Grund
|
||||
in zwei Wörtern je Zeile. */
|
||||
.treff-fall__zeile { display: block; }
|
||||
.treff-fall__schild { display: block; padding: 0 0 1px; }
|
||||
}
|
||||
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 92 KiB |
@@ -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)) {
|
||||
|
||||
@@ -76,6 +76,31 @@
|
||||
und zwar ohne Fehlermeldung im Protokoll. */
|
||||
const ohneKacheln = rollen.length === 0;
|
||||
|
||||
/* UND SEIT DEM 11.09.2026 GIBT ES SIE WIEDER -- die Wand des Treffs.
|
||||
|
||||
Dort ist nur EINE Sorte Zugang vorgesehen, und eine Kachelreihe
|
||||
mit genau einem Eintrag waere keine Auswahl, sondern eine Huerde:
|
||||
ein Knopf, den man druecken muss, damit der naechste Knopf
|
||||
funktioniert.
|
||||
|
||||
DER SERVER BRAUCHT DIE ROLLE TROTZDEM. Er vergleicht sie gegen den
|
||||
Kachelsatz seiner Adresse; kaeme "" an, waere die Antwort dieselbe
|
||||
wie bei einem falschen Code -- 401, ohne dass irgendwo etwas
|
||||
kaputt ist. Genau so eine Stelle sucht man lange, denn sie sieht
|
||||
aus wie ein Tippfehler des Nutzers.
|
||||
|
||||
WARUM DAS AM <body> STEHT UND NICHT IM SKRIPT: gate.js wird an
|
||||
ALLE drei Waende ausgeliefert. Eine Fallunterscheidung hier
|
||||
("wenn Treff, dann gast") waere eine vierte Stelle, an der die
|
||||
Adressen aufgezaehlt werden -- und die erste, die beim naechsten
|
||||
Umbau vergessen wird. Die Wand sagt selbst, wofuer sie da ist.
|
||||
|
||||
EIN LECK IST DAS NICHT: Auf dieser Wand gibt es genau diese eine
|
||||
Rolle. Der Name verraet nichts, was der Seitenkopf nicht ohnehin
|
||||
sagt -- die beiden anderen Waende nennen ihre Rollen seit jeher
|
||||
offen im Quelltext. */
|
||||
const VORGABE = document.body.dataset.rolle || "";
|
||||
|
||||
/* ---------- Rolle wählen ------------------------------------------ */
|
||||
|
||||
function waehle(rolle, fokus = false) {
|
||||
@@ -161,7 +186,7 @@
|
||||
braucht ihn dort nicht: Er prueft den verborgenen Zugang
|
||||
VOR dem gewohnten Weg, und ein leerer Name faellt danach in
|
||||
dieselbe Antwort wie ein erfundener. */
|
||||
body: JSON.stringify({ rolle: gewaehlt || '', code: wert }),
|
||||
body: JSON.stringify({ rolle: gewaehlt || VORGABE, code: wert }),
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/* =====================================================================
|
||||
„Wer sieht was" — die Rechtetafel, gezeichnet (11.09.2026)
|
||||
|
||||
Dieses Skript ENTSCHEIDET nichts. Es holt die Tafel vom Server und
|
||||
zeichnet sie. Jede Zahl und jeder Punkt darin kommt aus rechte.js —
|
||||
derselben Datei, aus der auch die Schranke ihre Antwort holt.
|
||||
|
||||
WARUM DAS WICHTIG IST: Eine Übersicht, die im Browser gebaut wird
|
||||
(„Rolle X darf wohl Y, weil …"), wäre eine zweite Meinung über die
|
||||
Rechte — und ausgerechnet die, der man glaubt, weil sie wie eine
|
||||
Auskunft aussieht. Sie würde altern, ohne dass man es ihr ansieht.
|
||||
===================================================================== */
|
||||
(() => {
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const el = (t, k, text) => {
|
||||
const n = document.createElement(t);
|
||||
if (k) n.className = k;
|
||||
if (text !== undefined) n.textContent = text;
|
||||
return n;
|
||||
};
|
||||
|
||||
const melde = (t) => {
|
||||
const f = $('fehler');
|
||||
if (f) { f.textContent = t; f.hidden = !t; }
|
||||
};
|
||||
|
||||
(async () => {
|
||||
let r;
|
||||
try {
|
||||
const a = await fetch('/workspace/api/rechte/tafel', { credentials: 'same-origin' });
|
||||
if (a.status === 401) { location.assign('/workspace/'); return; }
|
||||
/* 404 heißt hier: diese Seite gibt es für dich nicht. Die
|
||||
Schranke hätte schon vorher umgeleitet; kommt es trotzdem
|
||||
hierher, ist die Startseite die richtige Antwort — und nicht
|
||||
eine Fehlermeldung, die verrät, dass es die Auskunft gibt. */
|
||||
if (a.status === 404) { location.assign('/workspace/start.html'); return; }
|
||||
if (!a.ok) { melde('Die Tafel konnte nicht geladen werden.'); return; }
|
||||
r = await a.json();
|
||||
} catch {
|
||||
melde('Die Tafel konnte nicht geladen werden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const gesamt = r.seiten.length;
|
||||
|
||||
/* ---- Die Zahlen je Rolle ---------------------------------------
|
||||
Sortiert nach Anzahl, absteigend. Eine alphabetische Reihenfolge
|
||||
hätte dieselben Zahlen gezeigt und die Frage nicht beantwortet:
|
||||
Man sieht hier auf einen Blick, wer viel sieht und wer wenig —
|
||||
und ob irgendwo eine Rolle steht, die mehr sieht, als sie sollte. */
|
||||
const zahlen = $('zahlen');
|
||||
if (zahlen) {
|
||||
zahlen.textContent = '';
|
||||
for (const rolle of [...r.rollen].sort((a, b) => b.seiten - a.seiten)) {
|
||||
const li = document.createElement('li');
|
||||
li.append(rolle.name + ' ');
|
||||
const b = document.createElement('b');
|
||||
b.textContent = `${rolle.seiten} von ${gesamt}`;
|
||||
li.append(b);
|
||||
zahlen.append(li);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Die Tafel --------------------------------------------------- */
|
||||
const ziel = $('tafel');
|
||||
if (ziel) {
|
||||
ziel.textContent = '';
|
||||
const tab = document.createElement('table');
|
||||
|
||||
const kopf = document.createElement('thead');
|
||||
const kz = document.createElement('tr');
|
||||
kz.append(el('th', null, 'Seite'));
|
||||
for (const rolle of r.rollen) {
|
||||
const th = el('th', null, rolle.name);
|
||||
/* Der Schlüssel als Titel: Im Alltag liest man „Community",
|
||||
im Code steht „gast". Wer die beiden nicht zusammenbringen
|
||||
kann, sucht im Quelltext an der falschen Stelle. */
|
||||
th.title = rolle.schluessel;
|
||||
kz.append(th);
|
||||
}
|
||||
kopf.append(kz);
|
||||
tab.append(kopf);
|
||||
|
||||
const koerper = document.createElement('tbody');
|
||||
for (const seite of r.seiten) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.append(el('td', null, seite.datei));
|
||||
for (const rolle of r.rollen) {
|
||||
const td = document.createElement('td');
|
||||
if (seite.rollen.includes(rolle.schluessel)) {
|
||||
const punkt = el('span', 'rechte-ja');
|
||||
punkt.setAttribute('role', 'img');
|
||||
punkt.setAttribute('aria-label', 'darf');
|
||||
td.append(punkt);
|
||||
} else {
|
||||
/* Ein Zeichen und nicht ein leeres Feld: Ein Bildschirmleser
|
||||
liest eine leere Zelle als nichts vor — man hört dann
|
||||
nicht, dass die Antwort „nein" ist, sondern gar nichts. */
|
||||
const nein = el('span', 'rechte-nein', '–');
|
||||
nein.setAttribute('aria-label', 'darf nicht');
|
||||
td.append(nein);
|
||||
}
|
||||
tr.append(td);
|
||||
}
|
||||
koerper.append(tr);
|
||||
}
|
||||
tab.append(koerper);
|
||||
ziel.append(tab);
|
||||
ziel.setAttribute('aria-busy', 'false');
|
||||
}
|
||||
|
||||
/* ---- Ohne Anmeldung --------------------------------------------- */
|
||||
const offen = $('offen');
|
||||
if (offen) {
|
||||
offen.textContent = '';
|
||||
for (const pfad of r.ohne_anmeldung) {
|
||||
offen.append(el('li', null, pfad));
|
||||
}
|
||||
}
|
||||
})();
|
||||
})();
|
||||
@@ -0,0 +1,312 @@
|
||||
/* =====================================================================
|
||||
Meldungen & Maßnahmen im Treff (11.09.2026)
|
||||
|
||||
Drei Listen und ein Knopf. Die Regeln stehen alle auf dem Server —
|
||||
wer eine Maßnahme verhängen darf, wie lange ein Modi sperren kann,
|
||||
ob eine Meldung schon entschieden ist. Diese Datei zeichnet nur und
|
||||
fragt nach.
|
||||
|
||||
WARUM HIER KEINE RECHTEPRÜFUNG STEHT: Sie stünde dann zweimal, und
|
||||
die Fassung im Browser kann jeder ändern. Was hier passiert, ist
|
||||
höchstens ein Knopf, der gar nicht erst erscheint — das ist
|
||||
Bequemlichkeit, keine Sicherung. Die Sicherung antwortet mit 403.
|
||||
===================================================================== */
|
||||
(() => {
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const el = (t, k, text) => {
|
||||
const n = document.createElement(t);
|
||||
if (k) n.className = k;
|
||||
if (text !== undefined) n.textContent = text;
|
||||
return n;
|
||||
};
|
||||
const melde = (t) => { const f = $('fehler'); if (f) { f.textContent = t; f.hidden = !t; } };
|
||||
|
||||
const datum = (iso) => {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
return Number.isNaN(d.getTime()) ? '' : d.toLocaleDateString('de-DE',
|
||||
{ day: 'numeric', month: 'short', year: 'numeric' });
|
||||
};
|
||||
|
||||
async function hole(pfad, optionen = {}) {
|
||||
const a = await fetch(pfad, { credentials: 'same-origin', ...optionen });
|
||||
if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); }
|
||||
if (a.status === 404) { location.assign('/workspace/start.html'); throw new Error('unbekannt'); }
|
||||
return a;
|
||||
}
|
||||
|
||||
function knopf(text, tu) {
|
||||
const b = el('button', 'schritt', text);
|
||||
b.type = 'button';
|
||||
b.addEventListener('click', async () => {
|
||||
b.disabled = true; melde('');
|
||||
try { await tu(); } catch { /* umgeleitet */ } finally { b.disabled = false; }
|
||||
});
|
||||
return b;
|
||||
}
|
||||
|
||||
/* Eine Karte für alle drei Listen. Derselbe Aufbau, damit man die
|
||||
Seite nicht dreimal neu lesen lernen muss. */
|
||||
function karte(kopfText, zeilen, knoepfe = []) {
|
||||
const k = el('article', 'treff-fall');
|
||||
k.append(el('h3', 'treff-fall__titel', kopfText));
|
||||
for (const [schild, wert] of zeilen) {
|
||||
if (!wert) continue;
|
||||
const p = el('p', 'treff-fall__zeile');
|
||||
p.append(el('span', 'treff-fall__schild', schild));
|
||||
p.append(el('span', null, wert));
|
||||
k.append(p);
|
||||
}
|
||||
if (knoepfe.length) {
|
||||
const reihe = el('div', 'treff-fall__knoepfe');
|
||||
for (const b of knoepfe) reihe.append(b);
|
||||
k.append(reihe);
|
||||
}
|
||||
return k;
|
||||
}
|
||||
|
||||
/* ---- Meldungen ---------------------------------------------------- */
|
||||
async function meldungen() {
|
||||
const ziel = $('meldungen');
|
||||
const a = await hole('/workspace/api/treff/meldungen');
|
||||
if (!a.ok) { ziel.textContent = ''; ziel.append(el('p', 'leise', 'Ging nicht.')); return; }
|
||||
const liste = (await a.json()).meldungen || [];
|
||||
ziel.textContent = '';
|
||||
ziel.setAttribute('aria-busy', 'false');
|
||||
|
||||
const offen = liste.filter((m) => m.status === 'offen');
|
||||
const erledigt = liste.filter((m) => m.status !== 'offen');
|
||||
|
||||
if (!offen.length) {
|
||||
ziel.append(el('p', 'leise', 'Nichts offen. Das ist die gute Nachricht.'));
|
||||
}
|
||||
for (const m of offen) {
|
||||
const wer = m.melder_name
|
||||
? m.melder_name + (m.melder_rolle_name && m.melder_rolle !== 'gast'
|
||||
? ' · ' + m.melder_rolle_name : '')
|
||||
: 'jemand';
|
||||
const autor = m.autor_name
|
||||
? m.autor_name + (m.autor_rolle_name && m.autor_rolle !== 'gast'
|
||||
? ' · ' + m.autor_rolle_name : '')
|
||||
: 'unbekannt';
|
||||
const k = karte(m.titel || '(Beitrag gelöscht)', [
|
||||
['Grund', m.grund],
|
||||
['gemeldet von', wer + ' am ' + datum(m.zeitpunkt)],
|
||||
['Beitrag von', autor],
|
||||
['Brett', m.bereich || '–'],
|
||||
['Text', m.text],
|
||||
], [
|
||||
knopf('erledigt', () => entscheiden(m.id, 'erledigt')),
|
||||
knopf('nichts dran', () => entscheiden(m.id, 'abgelehnt')),
|
||||
/* Der Weg zum Beitrag selbst. Ohne ihn müsste man die Meldung
|
||||
lesen, sich das Brett merken und dort suchen -- und genau
|
||||
dabei entscheidet man nach Erinnerung statt nach dem, was
|
||||
wirklich dasteht. */
|
||||
...(m.bereich ? [(() => {
|
||||
const a2 = el('a', 'schritt', 'zum Brett');
|
||||
a2.href = 'bereich.html?b=' + encodeURIComponent(m.bereich);
|
||||
return a2;
|
||||
})()] : []),
|
||||
]);
|
||||
ziel.append(k);
|
||||
}
|
||||
|
||||
if (erledigt.length) {
|
||||
const h = el('p', 'leise', `${erledigt.length} schon entschieden:`);
|
||||
ziel.append(h);
|
||||
for (const m of erledigt.slice(0, 20)) {
|
||||
ziel.append(karte(m.titel || '(Beitrag gelöscht)', [
|
||||
['Grund', m.grund],
|
||||
['Entscheidung', (m.status === 'erledigt' ? 'erledigt' : 'nichts dran')
|
||||
+ (m.entscheid ? ' – ' + m.entscheid : '')],
|
||||
['von', (m.entschieden_name || '') + ' am ' + datum(m.entschieden_am)],
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function entscheiden(id, status) {
|
||||
const entscheid = prompt(status === 'erledigt'
|
||||
? 'Was hast du getan?\n\nEin Satz genügt – er wird zur Meldung festgehalten.'
|
||||
: 'Warum ist daran nichts dran?\n\nEin Satz genügt – er wird festgehalten.');
|
||||
if (entscheid === null) return;
|
||||
const a = await hole('/workspace/api/treff/meldung/' + id, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status, entscheid }),
|
||||
});
|
||||
if (!a.ok) {
|
||||
melde((await a.json().catch(() => ({}))).fehler || 'Ging nicht.');
|
||||
return;
|
||||
}
|
||||
await meldungen();
|
||||
}
|
||||
|
||||
/* ---- Entfernt ----------------------------------------------------- */
|
||||
async function entfernt() {
|
||||
const ziel = $('entfernt');
|
||||
const a = await hole('/workspace/api/treff/entfernt');
|
||||
if (!a.ok) { ziel.textContent = ''; return; }
|
||||
const liste = (await a.json()).entfernt || [];
|
||||
ziel.textContent = '';
|
||||
ziel.setAttribute('aria-busy', 'false');
|
||||
if (!liste.length) { ziel.append(el('p', 'leise', 'Noch nichts entfernt.')); return; }
|
||||
for (const e of liste.slice(0, 50)) {
|
||||
ziel.append(karte(e.titel, [
|
||||
['Grund', e.grund],
|
||||
['war von', e.autor_name || 'unbekannt'],
|
||||
['entfernt von', (e.von_name || '')
|
||||
+ (e.von_rolle_name ? ' · ' + e.von_rolle_name : '') + ' am ' + datum(e.zeitpunkt)],
|
||||
['Brett', e.bereich],
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Mitglieder ---------------------------------------------------- */
|
||||
|
||||
/* DIE STUFE STEHT NUR HIER. Im Treff selbst taucht sie nirgends auf
|
||||
-- weder neben dem Namen noch an einem Beitrag. Das ist eine
|
||||
bewusste Entscheidung aus dem Plan (Teil H): Eine sichtbare Stufe
|
||||
macht aus einem Schutz eine Rangordnung, und aus „Neu" wird ein
|
||||
Abzeichen, das man los sein will. Für die Moderation ist sie
|
||||
dagegen die wichtigste Angabe überhaupt. */
|
||||
async function mitglieder() {
|
||||
const ziel = $('mitglieder');
|
||||
const a = await hole('/workspace/api/treff/mitglieder');
|
||||
if (!a.ok) { ziel.textContent = ''; return; }
|
||||
const { mitglieder: liste, darf } = await a.json();
|
||||
ziel.textContent = '';
|
||||
ziel.setAttribute('aria-busy', 'false');
|
||||
if (!liste.length) {
|
||||
ziel.append(el('p', 'leise',
|
||||
'Noch niemand. Zugänge legst du in „Personen & Zugänge" an, Rolle Community.'));
|
||||
return;
|
||||
}
|
||||
const STUFE = { neu: 'Neu', dabei: 'Dabei', stamm: 'Stamm' };
|
||||
for (const m of liste) {
|
||||
const knoepfe = [];
|
||||
if (!m.sperre) {
|
||||
if (darf?.pause) knoepfe.push(knopf('Pause', () => massnahme(m, 'pause', darf)));
|
||||
if (darf?.ausschluss_befristet) {
|
||||
knoepfe.push(knopf('Ausschluss', () => massnahme(m, 'ausschluss', darf)));
|
||||
}
|
||||
knoepfe.push(knopf('Hinweis', () => massnahme(m, 'hinweis', darf)));
|
||||
}
|
||||
const k = karte(m.name, [
|
||||
['Stufe', STUFE[m.stufe] || m.stufe],
|
||||
['dabei seit', datum(m.seit)],
|
||||
['Beiträge', String(m.beitraege)],
|
||||
['zuletzt da', m.zuletzt ? datum(m.zuletzt) : 'noch nie'],
|
||||
['gesperrt', m.sperre
|
||||
? `${m.sperre.art === 'pause' ? 'Pause' : 'Ausschluss'} `
|
||||
+ (m.sperre.bis ? 'bis ' + datum(m.sperre.bis) : 'dauerhaft')
|
||||
+ ' – ' + m.sperre.grund
|
||||
: ''],
|
||||
], knoepfe);
|
||||
if (m.sperre) k.dataset.vorbei = 'ja';
|
||||
ziel.append(k);
|
||||
}
|
||||
}
|
||||
|
||||
/* EIN VORGANG, ZWEI FRAGEN — und die Reihenfolge ist wichtig.
|
||||
|
||||
Zuerst der GRUND, dann die Dauer. Umgekehrt wäre bequemer zu
|
||||
tippen und falsch herum gedacht: Wer zuerst „wie lange?" gefragt
|
||||
wird, hat die Strafe schon festgelegt, bevor er sie begründet hat.
|
||||
So steht am Anfang die Frage, was eigentlich passiert ist. */
|
||||
async function massnahme(m, art, darf) {
|
||||
const grund = prompt(art === 'hinweis'
|
||||
? `Hinweis an ${m.name} – worum geht es?`
|
||||
: `${art === 'pause' ? 'Pause' : 'Ausschluss'} für ${m.name} – warum?\n\n`
|
||||
+ 'Der Grund wird festgehalten, und die Person hat ein Recht darauf.');
|
||||
if (grund === null) return;
|
||||
|
||||
let tage;
|
||||
if (art !== 'hinweis') {
|
||||
const hinweis = darf?.frist_tage
|
||||
? `Du kannst bis zu ${darf.frist_tage} Tage geben.`
|
||||
: 'Leer lassen = dauerhaft (nur bei Ausschluss).';
|
||||
const ein = prompt(`Für wie viele Tage?\n\n${hinweis}`,
|
||||
darf?.frist_tage ? String(darf.frist_tage) : '7');
|
||||
if (ein === null) return;
|
||||
tage = ein.trim() === '' ? null : Number(ein);
|
||||
if (tage !== null && !(tage > 0)) { melde('Das ist keine Anzahl Tage.'); return; }
|
||||
if (tage === null && art === 'pause') { melde('Eine Pause braucht eine Dauer.'); return; }
|
||||
}
|
||||
|
||||
const a = await hole('/workspace/api/treff/massnahme', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ person_id: m.id, art, grund, tage }),
|
||||
});
|
||||
if (!a.ok) {
|
||||
melde((await a.json().catch(() => ({}))).fehler || 'Ging nicht.');
|
||||
return;
|
||||
}
|
||||
await mitglieder();
|
||||
await massnahmen();
|
||||
}
|
||||
|
||||
/* ---- Maßnahmen ---------------------------------------------------- */
|
||||
async function massnahmen() {
|
||||
const ziel = $('massnahmen');
|
||||
const a = await hole('/workspace/api/treff/massnahmen');
|
||||
if (!a.ok) { ziel.textContent = ''; return; }
|
||||
const liste = (await a.json()).massnahmen || [];
|
||||
ziel.textContent = '';
|
||||
ziel.setAttribute('aria-busy', 'false');
|
||||
if (!liste.length) { ziel.append(el('p', 'leise', 'Noch keine Maßnahme.')); return; }
|
||||
|
||||
const NAME = { hinweis: 'Hinweis', pause: 'Pause', ausschluss: 'Ausschluss' };
|
||||
for (const m of liste.slice(0, 50)) {
|
||||
const laeuft = !m.aufgehoben_am
|
||||
&& (!m.bis || Date.parse(m.bis) > Date.now());
|
||||
const k = karte(`${NAME[m.art] || m.art} · ${m.person_name || 'unbekannt'}`, [
|
||||
['Grund', m.grund],
|
||||
['gilt', m.aufgehoben_am ? 'aufgehoben am ' + datum(m.aufgehoben_am)
|
||||
: m.bis ? 'bis ' + datum(m.bis) : 'dauerhaft'],
|
||||
['von', (m.von_name || '') + (m.von_rolle_name ? ' · ' + m.von_rolle_name : '')
|
||||
+ ' am ' + datum(m.zeitpunkt)],
|
||||
], laeuft && m.art !== 'hinweis' ? [
|
||||
knopf('aufheben', async () => {
|
||||
if (!confirm(`Maßnahme gegen ${m.person_name} aufheben?`)) return;
|
||||
const r = await hole('/workspace/api/treff/massnahme/' + m.id, { method: 'DELETE' });
|
||||
if (!r.ok) {
|
||||
melde((await r.json().catch(() => ({}))).fehler || 'Ging nicht.');
|
||||
return;
|
||||
}
|
||||
await massnahmen();
|
||||
}),
|
||||
] : []);
|
||||
if (!laeuft) k.dataset.vorbei = 'ja';
|
||||
ziel.append(k);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Die Leiter in Worten ------------------------------------------
|
||||
Die Zahlen kommen vom Server, aus demselben Grund wie im Regeltext:
|
||||
Eine Frist, die in der Oberfläche anders steht als im Code, ist
|
||||
schlimmer als gar keine Angabe. */
|
||||
async function leiter() {
|
||||
try {
|
||||
const a = await fetch('/workspace/api/treff/regeln', { credentials: 'same-origin' });
|
||||
if (!a.ok) return;
|
||||
const r = await a.json();
|
||||
$('leiter').textContent =
|
||||
`Hinweis, dann Pause, dann Ausschluss – in dieser Reihenfolge. Ein Modi kann `
|
||||
+ `bis zu ${r.modi_frist_tage} Tage geben; alles darüber und jeder dauerhafte `
|
||||
+ `Ausschluss ist DogFathers Entscheidung. Wer eine dauerhafte Maßnahme nicht `
|
||||
+ `verhängen darf, kann sie auch nicht aufheben.`;
|
||||
} catch { /* dann bleibt "…" stehen, und das sieht man */ }
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await leiter();
|
||||
await meldungen();
|
||||
await entfernt();
|
||||
await mitglieder();
|
||||
await massnahmen();
|
||||
} catch { /* umgeleitet */ }
|
||||
})();
|
||||
})();
|
||||
@@ -0,0 +1,82 @@
|
||||
/* =====================================================================
|
||||
Die Zahlen im Regeltext (11.09.2026)
|
||||
|
||||
Dieses Skript schreibt nichts dazu -- es setzt nur die Zahlen ein,
|
||||
die im Text stehen muessen: Fristen, Stufen, Mindestalter, wie lange
|
||||
ein Modi eine Pause geben darf.
|
||||
|
||||
WARUM UEBERHAUPT EIN SKRIPT FUER FUENF ZAHLEN
|
||||
|
||||
Weil sie sonst zweimal existieren wuerden: einmal als Regel im Code
|
||||
(DABEI_TAGE, STAMM_TAGE, MINDESTALTER ...) und einmal als Wort in
|
||||
der HTML-Datei. Beim naechsten Nachjustieren waere eine von beiden
|
||||
geaendert worden, und der Regeltext haette etwas anderes versprochen
|
||||
als der Server tut. Das faellt niemandem auf -- und wer es bemerkt,
|
||||
glaubt dem Text, nicht dem Verhalten.
|
||||
|
||||
Ein Regeltext, der sich selbst widerspricht, ist ausserdem rechtlich
|
||||
das Gegenteil dessen, wofuer er da ist.
|
||||
|
||||
WENN DIE ANTWORT AUSBLEIBT, bleibt "…" stehen. Das ist Absicht: Eine
|
||||
fehlende Zahl sieht man, eine falsche nicht.
|
||||
===================================================================== */
|
||||
(() => {
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
const melde = (t) => {
|
||||
const f = $('fehler');
|
||||
if (f) { f.textContent = t; f.hidden = !t; }
|
||||
};
|
||||
|
||||
(async () => {
|
||||
let r;
|
||||
try {
|
||||
const a = await fetch('/workspace/api/treff/regeln', { credentials: 'same-origin' });
|
||||
if (a.status === 401) { location.assign('/workspace/'); return; }
|
||||
if (!a.ok) { melde('Die Zahlen konnten nicht geladen werden. Der Text gilt trotzdem.'); return; }
|
||||
r = await a.json();
|
||||
} catch {
|
||||
melde('Die Zahlen konnten nicht geladen werden. Der Text gilt trotzdem.');
|
||||
return;
|
||||
}
|
||||
|
||||
/* Die drei Stufen -- aus der Antwort gebaut, nicht aus drei festen
|
||||
Zeilen. Wer eine vierte Stufe einbaut, bekommt sie hier von
|
||||
selbst; eine feste Liste haette sie unterschlagen, ohne dass
|
||||
irgendwo etwas rot wird. */
|
||||
const liste = $('stufen');
|
||||
if (liste && Array.isArray(r.stufen)) {
|
||||
const was = {
|
||||
neu: 'Lesen und bei Wünschen mitstimmen.',
|
||||
dabei: `Ab ${r.dabei_tage} Tagen: Du kannst im Treff schreiben, `
|
||||
+ 'Wünsche einstellen und dich bei „Mitmachen“ melden.',
|
||||
stamm: `Ab ${r.stamm_tage} Tagen und ${r.stamm_beitraege} eigenen Beiträgen: `
|
||||
+ 'Du kannst Clips und Bilder einstellen – sie erscheinen, sobald '
|
||||
+ 'jemand aus dem Team sie freigegeben hat.',
|
||||
};
|
||||
liste.textContent = '';
|
||||
for (const s of r.stufen) {
|
||||
const li = document.createElement('li');
|
||||
const stark = document.createElement('strong');
|
||||
stark.textContent = s.name;
|
||||
li.append(stark, ' – ', was[s.schluessel] || '');
|
||||
liste.append(li);
|
||||
}
|
||||
}
|
||||
|
||||
if ($('alter')) {
|
||||
$('alter').textContent =
|
||||
`Mindestalter: ${r.mindestalter} Jahre. Wer jünger ist, hat hier nichts `
|
||||
+ 'verloren – nicht weil wir es kompliziert machen wollen, sondern weil '
|
||||
+ 'der Treff zu einem Stream für Erwachsene gehört.';
|
||||
}
|
||||
|
||||
if ($('ausschluss')) {
|
||||
$('ausschluss').textContent =
|
||||
`Ein Modi kann eine Pause von bis zu ${r.modi_frist_tage} Tagen geben. `
|
||||
+ 'Alles, was länger gilt, und jeder endgültige Ausschluss ist DogFathers '
|
||||
+ 'Entscheidung. Jede dieser Entscheidungen wird mit Grund festgehalten – '
|
||||
+ 'auch, damit man sie zurücknehmen kann.';
|
||||
}
|
||||
})();
|
||||
})();
|
||||