/* ===================================================================== VERTRAULICH MELDEN (19.09.2026) Eine Seite, zwei Rollen: Ein Mitglied sieht seine eigenen Meldungen, DogFather und die rechte Hand sehen alle. WELCHE das sind, entscheidet der Server — diese Datei zeigt nur, was sie bekommt. DIE REGEL "IMMER ABWECHSELND" STEHT NICHT HIER. Sie steht im Server (`ich_bin_dran`), und diese Seite fragt danach. Zwei Stellen mit derselben Regel wären zwei Stellen, die auseinanderlaufen — und die im Browser wäre die, an der man sie umgehen kann. KEIN innerHTML mit fremdem Text. Alles, was jemand geschrieben hat, geht durch `textContent`. Hier landen Texte aus einem Streit — ausgerechnet die will man nicht als HTML ausführen. ===================================================================== */ (() => { 'use strict'; const $ = (id) => document.getElementById(id); const el = (was, klasse, text) => { const e = document.createElement(was); if (klasse) e.className = klasse; if (text !== undefined) e.textContent = text; return e; }; let leitung = false; let offeneEigene = 0; let offenMax = 3; /* ---- Wörter für die Zustände ---------------------------------------- Sie stehen hier als Anzeige-Texte, nicht als Regel. Der Server schickt den Schlüssel; was daraus auf dem Bildschirm wird, ist eine Frage der Sprache und darf sich ändern, ohne dass jemand die Logik anfasst. */ const STAND_WORT = { wartet_auf_antwort: { kurz: 'wartet auf Antwort', wer: 'leitung' }, antwort_da: { kurz: 'Antwort da', wer: 'melder' }, geschlossen: { kurz: 'abgeschlossen', wer: 'niemand' }, }; function melde(text) { const f = $('fehler'); if (!f) return; f.textContent = text || ''; f.hidden = !text; } async function hole(pfad, einstellungen) { const a = await fetch(pfad, { credentials: 'same-origin', ...einstellungen }); if (a.status === 401) { location.href = 'index.html'; throw new Error('weg'); } return a; } function datum(roh) { if (!roh) return ''; const d = new Date(roh); if (Number.isNaN(d.getTime())) return ''; return d.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); } /* ===================================================================== DIE LISTE ===================================================================== */ async function listeLaden() { const ziel = $('liste'); try { const a = await hole('/workspace/api/hilfe'); if (!a.ok) { melde('Die Liste ließ sich nicht laden.'); return; } const d = await a.json(); leitung = !!d.leitung; offeneEigene = d.offen_eigene ?? 0; offenMax = d.offen_max ?? 3; /* Die Seite heißt für beide anders — und das ist kein Schmuck: "Deine Meldungen" wäre für DogFather schlicht falsch. */ $('seiten-titel').textContent = leitung ? 'Vertrauliche Meldungen' : 'Vertraulich melden'; $('listen-titel').textContent = leitung ? 'Alle Meldungen' : 'Deine Meldungen'; $('neu-block').hidden = leitung; if (leitung) { $('seiten-unter').textContent = 'Was die Community vertraulich gemeldet hat. Du siehst den Namen; sonst niemand.'; $('vertrauen-text').textContent = 'Diese Meldungen sehen nur du und die rechte Hand. Weder Modis noch ' + 'andere Mitglieder haben Zugriff — auch nicht auf die Namen.'; } const faelle = d.faelle || []; $('listen-zahl').textContent = faelle.length ? `${faelle.length}` : ''; ziel.textContent = ''; ziel.removeAttribute('aria-busy'); if (!faelle.length) { const leer = el('p', 'hilfe-leer', leitung ? 'Nichts gemeldet. Das ist die beste Nachricht auf dieser Seite.' : 'Du hast noch nichts gemeldet. Gut so – und wenn doch mal etwas ist, ' + 'ist der Weg hier.'); ziel.append(leer); knopfStand(); return; } for (const f of faelle) ziel.append(zeileBauen(f)); knopfStand(); } catch (fehler) { if (String(fehler.message) !== 'weg') melde('Keine Verbindung.'); } } function zeileBauen(f) { const k = el('article', 'fall-zeile'); k.dataset.stand = f.stand; k.tabIndex = 0; k.setAttribute('role', 'button'); const oben = el('div', 'fall-zeile__oben'); const marke = el('span', 'fall-marke', STAND_WORT[f.stand]?.kurz || f.stand); marke.dataset.stand = f.stand; oben.append(marke); oben.append(el('h3', 'fall-zeile__titel', f.betreff)); k.append(oben); const unten = el('p', 'fall-zeile__unten'); /* Der Name nur, wenn der Server ihn geschickt hat — er kommt gar nicht erst mit, wenn jemand anderes fragt. */ if (f.melder) unten.append(el('span', 'fall-zeile__wer', f.melder)); unten.append(el('span', 'fall-zeile__zeit', datum(f.geaendert))); unten.append(el('span', 'fall-zeile__anzahl', f.nachrichten === 1 ? '1 Nachricht' : `${f.nachrichten} Nachrichten`)); k.append(unten); const auf = () => fallOeffnen(f.id); k.addEventListener('click', auf); k.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); auf(); } }); return k; } /** Darf noch eine Meldung aufgemacht werden? */ function knopfStand() { const knopf = $('neu-auf'); if (!knopf || leitung) return; const voll = offeneEigene >= offenMax; knopf.disabled = voll; knopf.title = voll ? `Du hast ${offeneEigene} offene Meldungen. Warte auf eine Antwort, ` + 'bevor du die nächste aufmachst.' : ''; } /* ===================================================================== EIN FALL ===================================================================== */ async function fallOeffnen(id) { try { const a = await hole('/workspace/api/hilfe/' + id); if (a.status === 404) { melde('Diese Meldung gibt es nicht (mehr).'); return; } if (!a.ok) { melde('Der Fall ließ sich nicht öffnen.'); return; } const d = await a.json(); fallZeigen(d); /* Die Adresse merkt sich den Fall — damit eine Benachrichtigung direkt hierher führen kann und ein Neuladen nicht zur Liste zurückwirft. */ history.replaceState(null, '', 'hilfe.html?fall=' + id); } catch (fehler) { if (String(fehler.message) !== 'weg') melde('Keine Verbindung.'); } } function fallZeigen(d) { const f = d.fall; $('fall-block').hidden = false; $('fall-titel').textContent = f.betreff; const kopf = $('fall-kopf'); kopf.textContent = ''; const marke = el('span', 'fall-marke', STAND_WORT[f.stand]?.kurz || f.stand); marke.dataset.stand = f.stand; kopf.append(marke); if (f.melder) kopf.append(el('span', 'fall-kopf__wer', 'von ' + f.melder)); kopf.append(el('span', 'fall-kopf__zeit', 'seit ' + datum(f.erstellt))); const ziel = $('verlauf'); ziel.textContent = ''; for (const n of d.nachrichten || []) { const b = el('div', 'blase'); b.dataset.seite = n.von_leitung ? 'leitung' : 'melder'; b.append(el('p', 'blase__wer', n.von_leitung ? 'DogFather / rechte Hand' : (f.melder || 'Du'))); /* Zeilenumbrüche erhalten, ohne HTML zuzulassen: white-space im CSS, Text über textContent. */ b.append(el('p', 'blase__text', n.text)); const fuss = el('p', 'blase__fuss', datum(n.erstellt)); if (!n.von_leitung && n.gelesen) fuss.textContent += ' · gelesen'; b.append(fuss); ziel.append(b); } const form = $('antwort-form'); const warten = $('fall-warten'); const zuKnopf = $('fall-schliessen'); zuKnopf.hidden = !(d.leitung && f.stand !== 'geschlossen'); zuKnopf.onclick = () => fallSchliessen(f.id); if (f.stand === 'geschlossen') { form.hidden = true; warten.hidden = false; warten.textContent = 'Dieser Fall ist abgeschlossen' + (f.geschlossen_am ? ' (' + datum(f.geschlossen_am) + ')' : '') + '.'; } else if (d.ich_bin_dran) { form.hidden = false; warten.hidden = true; $('antwort-schild').textContent = d.leitung ? 'Deine Antwort' : 'Deine Antwort'; form.onsubmit = (e) => { e.preventDefault(); antwortSenden(f.id); }; } else { form.hidden = true; warten.hidden = false; /* DER SATZ SAGT, WER DRAN IST. "Bitte warten" allein lässt offen, worauf — und ob überhaupt noch etwas kommt. */ warten.textContent = d.leitung ? 'Die andere Seite ist am Zug. Sobald sie antwortet, kannst du wieder schreiben.' : 'Deine Meldung ist angekommen. Sobald eine Antwort da ist, siehst du sie hier ' + '– und bekommst eine Benachrichtigung, wenn du sie erlaubt hast.'; } $('fall-block').scrollIntoView({ behavior: 'smooth', block: 'start' }); } async function antwortSenden(id) { const feld = $('antwort-text'); const text = feld.value.trim(); if (text.length < 2) { $('antwort-stand').textContent = 'Da fehlt noch etwas.'; return; } $('antwort-senden').disabled = true; $('antwort-stand').textContent = 'wird gesendet …'; try { const a = await hole('/workspace/api/hilfe/' + id + '/antwort', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }), }); const j = await a.json().catch(() => ({})); if (!a.ok) { $('antwort-stand').textContent = j.fehler === 'nicht_dran' ? 'Die andere Seite ist gerade am Zug.' : j.fehler === 'geschlossen' ? 'Dieser Fall ist schon abgeschlossen.' : 'Ging nicht.'; $('antwort-senden').disabled = false; return; } feld.value = ''; $('antwort-stand').textContent = 'gesendet'; await fallOeffnen(id); await listeLaden(); } catch { $('antwort-stand').textContent = 'Keine Verbindung.'; } $('antwort-senden').disabled = false; } async function fallSchliessen(id) { const wort = prompt('Ein Schlusswort? (Kann auch leer bleiben)'); if (wort === null) return; try { const a = await hole('/workspace/api/hilfe/' + id + '/schliessen', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: wort }), }); if (!a.ok) { melde('Ließ sich nicht abschließen.'); return; } await fallOeffnen(id); await listeLaden(); } catch { melde('Keine Verbindung.'); } } /* ===================================================================== NEUE MELDUNG ===================================================================== */ $('neu-auf')?.addEventListener('click', () => { $('neu-form').hidden = false; $('neu-auf').hidden = true; $('neu-betreff').focus(); }); $('neu-zu')?.addEventListener('click', () => { $('neu-form').hidden = true; $('neu-auf').hidden = false; $('neu-stand').textContent = ''; }); $('neu-form')?.addEventListener('submit', async (e) => { e.preventDefault(); const betreff = $('neu-betreff').value.trim(); const text = $('neu-text').value.trim(); const stand = $('neu-stand'); if (betreff.length < 3) { stand.textContent = 'Eine kurze Überschrift fehlt noch.'; return; } if (text.length < 10) { stand.textContent = 'Schreib bitte ein paar Sätze mehr.'; return; } $('neu-senden').disabled = true; stand.textContent = 'wird gesendet …'; try { const a = await hole('/workspace/api/hilfe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ betreff, text }), }); const j = await a.json().catch(() => ({})); if (!a.ok) { stand.textContent = j.fehler === 'zu_viele_offen' ? `Du hast schon ${j.offen} offene Meldungen. Warte auf eine Antwort.` : j.fehler === 'text_zu_kurz' ? 'Schreib bitte ein paar Sätze mehr.' : 'Ging nicht.'; $('neu-senden').disabled = false; return; } $('neu-betreff').value = ''; $('neu-text').value = ''; $('neu-form').hidden = true; $('neu-auf').hidden = false; stand.textContent = ''; await listeLaden(); if (j.id) await fallOeffnen(j.id); } catch { stand.textContent = 'Keine Verbindung.'; } $('neu-senden').disabled = false; }); $('fall-zurueck')?.addEventListener('click', () => { $('fall-block').hidden = true; history.replaceState(null, '', 'hilfe.html'); document.querySelector('.hilfe-block')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }); /* ---- Los ------------------------------------------------------------ */ (async () => { await listeLaden(); /* Kommt jemand über eine Benachrichtigung, steht der Fall in der Adresse — dann direkt aufmachen, statt ihn suchen zu lassen. */ const fall = new URLSearchParams(location.search).get('fall'); if (fall && /^\d+$/.test(fall)) await fallOeffnen(fall); })(); })();