Deutsch oeffnet mit „ und schliesst mit “. An 199 Stellen, die ein
Mensch liest, stand als Schlusszeichen ein GERADES " -- „Nächstes"
statt „Nächstes“. Auf sechzehn oeffentlichen Seiten, in vierzig
Dateien des Workspace und in zwoelf Servermodulen, die Texte
verschicken.
Auf dem Bildschirm sieht man den Unterschied sofort. Beim Schreiben
nicht: Das gerade " liegt auf der Tastatur, die anderen nicht.
WARUM EIN ERSTER ANLAUF ZURUECKGENOMMEN WURDE
Ein gerades " ist an vielen Stellen SYNTAX und kein Schriftzeichen --
Grenze einer Zeichenkette, Grenze eines HTML-Attributs, Zeichen in
einem regulaeren Ausdruck. Wer stumpf ersetzt, macht aus
„<a href="https://… ein kaputtes Attribut
/^["'„»\s]+|["'“«.\s]+$/ einen kaputten Ausdruck
"… nichts „mal " + "eben …" eine kaputte Zeichenkette
DIE UNTERSCHEIDUNG LAEUFT AN MERKMALEN, NICHT AN EINER LISTE
< > = dazwischen -> HTML-Marke oder Attribut
endet auf Leerzeichen -> die Zeichenkette hoert hier auf, der
Satz geht in der naechsten Zeile weiter.
Ein deutsches Schlusszeichen steht NIE
hinter einem Leerzeichen.
Rueckstrich mittendrin -> regulaerer Ausdruck
${ ohne } -> mitten in einem Ausdruck
Eine Liste erlaubter Ausnahmen waere die naechste, die niemand
pflegt. Zwoelf Stellen bleiben dadurch stehen, alle zwoelf einzeln
angesehen und alle zwoelf zu Recht -- dort steht das richtige
Schlusszeichen ohnehin weiter unten im Satz.
Fuenf davon waren allerdings ECHTE Fehler HINTER dem Link
(`…>HasiDog</a>".`) -- die erste Regel hatte nur das Attribut
gesehen, nicht den Satz danach. Gezielt nachgezogen.
Ein maskiertes `\"` in workspace-vorlagen.js (28 Hooks) wird zu “ --
ohne Rueckstrich, denn “ begrenzt nichts.
KOMMENTARE BLEIBEN, WIE SIE SIND. Dort liest es niemand ausser mir;
eine Wache, die auch Kosmetik anmahnt, wird weggeklickt. Beim ersten
Messen fielen ausserdem acht Stellen aus buehne.html faelschlich an,
weil `/* */` in HTML (in <style> und <script>) nicht ausgeblendet
war -- jetzt schon.
DIE WACHE DAZU
pruef-struktur prueft es ab sofort mit derselben Regel: 322 Dateien
mit sichtbarem Text, 0 Funde, und die zwoelf bewussten Ausnahmen
werden GEZAEHLT und genannt (erlaubt: 12). Eine Ausnahme, die niemand
sieht, waechst -- und irgendwann steht der echte Fall darin.
GEPRUEFT
pruef-struktur 59 -> 68 Pruefungen, 0 Fehler
node --check auf allen geaenderten JS-Dateien
nachgemessen: 199 geaendert, 12 mit Grund stehen geblieben
gruen geblieben: bewerbung-aufgaben 163, nachwuchs 262,
reaktion 421, support 63, content 45, terminregel 35, treff 85
Und nachgesehen, ob eine Pruefung noch die alte Schreibweise
ERWARTET: 13 Fundstellen, alle dreizehn nur Text in ihrer eigenen
Ausgabe, keine einzige ein Vergleich mit dem Seitentext.
GEGENPROBE: In reaktion.html ein Schlusszeichen zurueckgedreht ->
„workspace/reaktion.html:546 „Nächstes"", mit Datei und Zeile. Und
die Erkennung einzeln gegen HTML-Attribut, fortgesetzte
Zeichenkette, regulaeren Ausdruck und eingesetzten Wert geprueft.
Stempel gesetzt: workspace 670 Verweise in 45 Dateien, oeffentlich
554 in 48 Seiten.
Co-Authored-By: Claude Opus 5 <[email protected]>
440 lines
18 KiB
JavaScript
440 lines
18 KiB
JavaScript
/* =====================================================================
|
||
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 das Team 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'); }
|
||
/* EIN 404 IST KEIN GRUND, WORTLOS ZU VERSCHWINDEN (20.09.2026).
|
||
Hier wurde sofort auf die Startseite gesprungen -- ohne ein
|
||
Wort. Wer das erlebt, weiss nicht, ob er etwas falsch gemacht
|
||
hat, ob die Seite kaputt ist oder ob ihm gerade ein Recht
|
||
entzogen wurde. Jetzt steht ein Satz da, und er entscheidet
|
||
selbst, wann er geht. */
|
||
if (a.status === 404) {
|
||
window.ladefehler?.({
|
||
satz: 'Dieser Teil der Moderation steht dir gerade nicht offen.',
|
||
grund: 'Vielleicht hat sich etwas an deinen Rechten geändert. '
|
||
+ 'Frag im Chat nach, wenn das nicht stimmt.',
|
||
});
|
||
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 ---------------------------------------------------- */
|
||
/* WAS HEREINKOMMT -- alle Community-Bretter in einer Liste.
|
||
Nicht der Inhalt, nur: was, wo, von wem, wann. Wer mehr will,
|
||
klickt auf das Brett. Eine Vorschau des Textes hier waere eine
|
||
zweite Stelle, an der derselbe Beitrag steht. */
|
||
const BRETT_NAME = {
|
||
anschlag: 'Anschlagbrett', ansteht: 'Was ansteht', treff: 'Das Rudel',
|
||
wunsch: 'Wunschliste', highlight: 'Highlights', regeln: 'Regeln & Hilfe',
|
||
mitmachen: 'Mitmachen',
|
||
};
|
||
|
||
async function zulaufLaden() {
|
||
const ziel = document.getElementById('zulauf');
|
||
if (!ziel) return;
|
||
const a = await hole('/workspace/api/treff/zulauf');
|
||
if (!a.ok) { ziel.textContent = ''; ziel.append(el('p', 'leise', 'Ging nicht.')); return; }
|
||
const { zulauf } = await a.json();
|
||
ziel.textContent = '';
|
||
ziel.setAttribute('aria-busy', 'false');
|
||
if (!zulauf.length) {
|
||
/* Ein Satz statt einer leeren Flaeche -- und einer, der nicht wie
|
||
ein Fehler klingt. */
|
||
ziel.append(el('p', 'leer-hinweis', 'Noch nichts hereingekommen.'));
|
||
return;
|
||
}
|
||
const liste = el('ul', 'zulauf');
|
||
for (const e of zulauf) {
|
||
const z = el('li', 'zulauf__zeile');
|
||
const brett = el('span', 'zulauf__brett', BRETT_NAME[e.bereich] || e.bereich);
|
||
brett.dataset.brett = e.bereich;
|
||
z.append(brett);
|
||
const a2 = el('a', 'zulauf__titel', e.titel || '(ohne Titel)');
|
||
a2.href = `/workspace/bereich.html?b=${encodeURIComponent(e.bereich)}#e${e.id}`;
|
||
z.append(a2);
|
||
z.append(el('span', 'zulauf__wer',
|
||
(e.von_name || 'unbekannt') + (e.von_rolle_name ? ' · ' + e.von_rolle_name : '')));
|
||
z.append(el('span', 'zulauf__wann', zeitKurz(e.erstellt)));
|
||
liste.append(z);
|
||
}
|
||
ziel.append(liste);
|
||
}
|
||
|
||
/* "vor 3 Min" statt eines Zeitstempels: Bei einer Moderationsliste
|
||
ist die Frage "wie frisch", nicht "wann genau". */
|
||
function zeitKurz(iso) {
|
||
const ms = Date.now() - Date.parse(iso);
|
||
if (!Number.isFinite(ms)) return '';
|
||
const min = Math.floor(ms / 60000);
|
||
if (min < 1) return 'gerade eben';
|
||
if (min < 60) return `vor ${min} Min`;
|
||
const std = Math.floor(min / 60);
|
||
if (std < 24) return `vor ${std} Std`;
|
||
const tage = Math.floor(std / 24);
|
||
return tage === 1 ? 'gestern' : `vor ${tage} Tagen`;
|
||
}
|
||
|
||
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 ae = await frageNach({
|
||
titel: status === 'erledigt' ? 'Meldung als erledigt schließen'
|
||
: 'Meldung als unbegründet schließen',
|
||
was: status === 'erledigt'
|
||
? 'Die Meldung gilt danach als bearbeitet.'
|
||
: 'Die Meldung gilt danach als geprüft, ohne Folgen für die Person.',
|
||
bleibt: 'Dein Satz wird zur Meldung festgehalten und bleibt im Verlauf '
|
||
+ 'lesbar. Der Beitrag selbst ändert sich dadurch nicht.',
|
||
ja: 'Schließen',
|
||
grund: status === 'erledigt' ? 'Was hast du getan?'
|
||
: 'Warum ist daran nichts dran?',
|
||
grundPflicht: true,
|
||
platzhalter: status === 'erledigt'
|
||
? 'z. B. Beitrag entfernt und die Person angeschrieben.'
|
||
: 'z. B. Der Beitrag verstößt gegen keine Regel.',
|
||
});
|
||
if (!ae) return;
|
||
const entscheid = ae.grund;
|
||
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(sagWas((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) {
|
||
/* NUR NENNEN, WAS DIESER MENSCH AUCH OEFFNEN DARF (20.09.2026).
|
||
----------------------------------------------------------
|
||
Hier stand: „Zugaenge legst du in ‚Personen & Zugaenge' an."
|
||
Ein Modi darf `personen.html` NICHT oeffnen (rechte.js) --
|
||
und diese Seite ist seine Kernseite. Am ersten Tag, wenn die
|
||
Community noch leer ist, war das der einzige Satz im
|
||
Abschnitt „Die Mitglieder": Er sucht die genannte Kachel auf
|
||
seiner Startseite, findet sie nicht, und haelt die Seite fuer
|
||
kaputt.
|
||
|
||
`ich.seiten` sagt, welche Seiten offenstehen -- dieselbe
|
||
Auskunft, nach der auch die Kacheln gebaut werden. Wer die
|
||
Seite nicht hat, bekommt den Weg genannt, den er wirklich
|
||
gehen kann. */
|
||
const darfPersonen = Array.isArray(window.__ich?.seiten)
|
||
&& window.__ich.seiten.includes('/workspace/personen.html');
|
||
ziel.append(el('p', 'leise', darfPersonen
|
||
? 'Noch niemand. Zugänge legst du in „Personen & Zugänge“ an, Rolle Community.'
|
||
: 'Noch niemand. Zugänge für die Community legt DogFather an – '
|
||
+ 'sag ihm im Chat Bescheid, wenn jemand dazukommen soll.'));
|
||
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 istHinweis = art === 'hinweis';
|
||
const wieLang = istHinweis ? null : (darf?.frist_tage || 0);
|
||
const a0 = await frageNach({
|
||
titel: istHinweis
|
||
? `Hinweis an ${m.name}`
|
||
: `${art === 'pause' ? 'Pause' : 'Ausschluss'} für ${m.name}`,
|
||
was: istHinweis
|
||
? 'Die Person bekommt deinen Text zu lesen. Sie darf weiter mitmachen.'
|
||
: (art === 'pause'
|
||
? 'Die Person kann für die eingetragene Zeit nichts schreiben.'
|
||
: 'Die Person kann nichts mehr schreiben.'),
|
||
bleibt: 'Der Grund wird festgehalten und der Person mitgeteilt – sie hat '
|
||
+ 'ein Recht darauf. Aufheben geht jederzeit.',
|
||
ja: istHinweis ? 'Hinweis schicken' : 'Eintragen',
|
||
grund: istHinweis ? 'Worum geht es?' : 'Warum?', grundPflicht: true,
|
||
platzhalter: istHinweis
|
||
? 'z. B. Bitte nicht mehrfach dasselbe posten.'
|
||
: 'z. B. Wiederholte Beleidigungen trotz Hinweis.',
|
||
zahl: istHinweis ? '' : 'Für wie viele Tage?',
|
||
zahlWert: wieLang ? String(wieLang) : '7',
|
||
zahlMax: wieLang || 0,
|
||
/* Leer = dauerhaft, aber NUR beim Ausschluss -- eine Pause ohne
|
||
Ende waere ein Ausschluss unter falschem Namen. */
|
||
zahlLeer: art === 'ausschluss' && !wieLang,
|
||
zahlHinweis: wieLang ? `Höchstens ${wieLang} Tage.`
|
||
: (art === 'ausschluss' ? 'Leer lassen = dauerhaft.' : ''),
|
||
});
|
||
if (!a0) return;
|
||
const grund = a0.grund;
|
||
const tage = istHinweis ? undefined : a0.zahl;
|
||
|
||
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(sagWas((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 (!await frageNach({
|
||
titel: `Maßnahme gegen ${m.person_name} aufheben?`,
|
||
was: 'Die Person darf danach sofort wieder mitmachen.',
|
||
bleibt: 'Die Maßnahme bleibt im Verlauf sichtbar – sie wird nur '
|
||
+ 'beendet, nicht gelöscht.',
|
||
ja: 'Aufheben',
|
||
})) return;
|
||
const r = await hole('/workspace/api/treff/massnahme/' + m.id, { method: 'DELETE' });
|
||
if (!r.ok) {
|
||
melde(sagWas((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 ${r.rolle_name} kann `
|
||
+ `bis zu ${r.pause_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();
|
||
/* ZUERST DER ZULAUF: Er steht oben auf der Seite, und eine Seite,
|
||
die von unten nach oben fuellt, wackelt beim Aufbau. */
|
||
await zulaufLaden();
|
||
await meldungen();
|
||
await entfernt();
|
||
await mitglieder();
|
||
await massnahmen();
|
||
} catch {
|
||
/* NICHT MEHR STUMM (20.09.2026). Scheitert hier etwas am Netz,
|
||
blieben die Ladeplatzhalter fuer immer stehen -- gemessen auf
|
||
13 von 21 Seiten. Umgeleitet wird nur bei 401; alles andere
|
||
ist ein Ladefehler und gehoert gesagt. */
|
||
window.ladefehler?.();
|
||
}
|
||
})();
|
||
})();
|