/* ===================================================================== UNTERSTÜTZEN (24.09.2026) ===================================================================== Filipe: „jeder soll diese kachel sehen aber nur dogfather soll sie verändern können oder vieles mehr sehen. alle anderen, rechte und linke hand, modis und community sollen supporten können." --------------------------------------------------------------------- DREI DINGE, DIE HIER ANDERS SIND ALS AUF DEN ÜBRIGEN SEITEN 1. DIE OBERFLÄCHE ENTSCHEIDET NICHTS. `darf_aendern` kommt vom Server und wird hier nur GELESEN. Wer das Feld im Browser auf „true" setzt, bekommt trotzdem 404 zurück — die Regel steht in workspace-unterstuetzung.js. Ein verstecktes Feld ist eine Bitte, keine Sperre. 2. DER ZÄHLER DARF DEN WEG NIE AUFHALTEN. Ein Klick auf „Bei PayPal öffnen" muss PayPal öffnen, auch wenn der Server gerade nicht antwortet. Deshalb `sendBeacon`: Er schickt den Strich los und wartet auf nichts. Ein `await fetch()` vor der Weiterleitung wäre ein Zähler, der im Zweifel eine Zahlung verhindert. 3. EINE ADRESSE WIRD NICHT AUS TEXT ZUSAMMENGESETZT. Das Ziel geht über `href` in ein ``, nie über `innerHTML` — und jeder Weg nach draußen bekommt `rel="noopener noreferrer"`. Ohne `noopener` kann die geöffnete Seite über `window.opener` auf diese hier zurückgreifen. ===================================================================== */ (() => { const $ = (id) => document.getElementById(id); /* DER SATZ ZU EINER SERVER-KENNUNG -- mit Rueckfall (25.09.2026). Ueber `window` und nicht als blanker Aufruf: Wuerde diese Datei einmal ohne meldung.js geladen, waere `sagWas(...)` ein ReferenceError -- und der traete ausgerechnet dann auf, wenn ohnehin schon etwas schiefgegangen ist. Eine Fehlerbehandlung, die selbst abstuerzt, verschluckt den eigentlichen Fehler. Dieselbe Bauart steht in vorlagenbrett.js und ist dort aus demselben Grund entstanden. */ const sagWas = (fehler, ersatz) => (typeof window.sagWas === 'function' ? window.sagWas(fehler, ersatz) : (ersatz || 'Das hat nicht geklappt.')); const el = (t, k, text) => { const n = document.createElement(t); if (k) n.className = k; if (text !== undefined) n.textContent = text; return n; }; const hole = (pfad, optionen) => window.holen(pfad, optionen); const melde = (text, gut) => { const f = $('fehler'); if (!f) return; f.textContent = text || ''; f.hidden = !text; f.dataset.gut = gut ? 'ja' : 'nein'; }; let stand = null; // die Antwort des Servers let aendern = false; // steht der Änderungsmodus an? /* ===================================================================== DIE ZEICHEN Eines je Weg, mit einem Rückfall je Art. Ein Weg, den DogFather neu anlegt, hat keinen eigenen Schlüssel in dieser Liste — und bekommt trotzdem ein Zeichen, statt ein Loch zu lassen. `voll` markiert die Teile, die Fläche sind statt Linie. Auf 24 Pixeln verschwindet eine reine Strichzeichnung, und ein ganz gefülltes Zeichen sieht aus wie ein Klecks — dieselbe Aufteilung wie bei den Kachelzeichen in bereiche.js. ===================================================================== */ const ZEICHEN = { paypal: [ ['M3.6 8.2h16.8a1.4 1.4 0 0 1 1.4 1.4v8.2a1.4 1.4 0 0 1-1.4 1.4H3.6a1.4 1.4 0 0 1-1.4-1.4V9.6a1.4 1.4 0 0 1 1.4-1.4z'], ['M2.2 12.4h19.6'], ['M5.6 8.2 7.2 5a1.4 1.4 0 0 1 1.9-.6l6.9 3.8', 'voll-nein'], ], amazon: [ ['M4 11.2h16v7.2a1.4 1.4 0 0 1-1.4 1.4H5.4A1.4 1.4 0 0 1 4 18.4z'], ['M3 8.1h18v3.1H3z'], ['M12 8.1v11.7'], ['M12 8.1S10.9 4.2 8.8 4.2a2 2 0 0 0 0 3.9z'], ['M12 8.1s1.1-3.9 3.2-3.9a2 2 0 0 1 0 3.9z'], ], dabei: [ ['M9.2 11.4a3.1 3.1 0 1 0 0-6.2 3.1 3.1 0 0 0 0 6.2z'], ['M2.8 19.6c0-3.3 2.9-5.6 6.4-5.6s6.4 2.3 6.4 5.6'], ['M16.4 6a3 3 0 0 1 0 5.6'], ['M17.6 14.6c2.2.6 3.6 2.4 3.6 5'], ], weitersagen: [ ['M17.8 8.4a2.6 2.6 0 1 0 0-5.2 2.6 2.6 0 0 0 0 5.2z'], ['M5.6 14.6a2.6 2.6 0 1 0 0-5.2 2.6 2.6 0 0 0 0 5.2z'], ['M17.8 20.8a2.6 2.6 0 1 0 0-5.2 2.6 2.6 0 0 0 0 5.2z'], ['M7.9 10.7 15.5 7', 'M7.9 13.3l7.6 3.7'], ], draussen: [ ['M11 4.8H5.4A1.4 1.4 0 0 0 4 6.2v12.4a1.4 1.4 0 0 0 1.4 1.4h12.4a1.4 1.4 0 0 0 1.4-1.4V13'], ['M13.8 4H20v6.2'], ['M19.4 4.6 12 12'], ], /* Rückfall je Art. */ geld: [['M3.6 8.2h16.8a1.4 1.4 0 0 1 1.4 1.4v8.2a1.4 1.4 0 0 1-1.4 1.4H3.6a1.4 1.4 0 0 1-1.4-1.4V9.6a1.4 1.4 0 0 1 1.4-1.4z'], ['M2.2 12.4h19.6']], geschenk: [['M4 11.2h16v7.2a1.4 1.4 0 0 1-1.4 1.4H5.4A1.4 1.4 0 0 1 4 18.4z'], ['M3 8.1h18v3.1H3z'], ['M12 8.1v11.7']], frei: [['M12 20.2 4.4 12.6a4.6 4.6 0 0 1 6.3-6.7l1.3 1.2 1.3-1.2a4.6 4.6 0 0 1 6.3 6.7z']], }; function zeichenBauen(w) { const pfade = ZEICHEN[w.schluessel] || ZEICHEN[w.art] || ZEICHEN.frei; const NS = 'http://www.w3.org/2000/svg'; const svg = document.createElementNS(NS, 'svg'); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('aria-hidden', 'true'); for (const gruppe of pfade) { for (const d of gruppe) { if (!d.startsWith('M')) continue; const p = document.createElementNS(NS, 'path'); p.setAttribute('d', d); svg.append(p); } } return svg; } const PFEIL_RAUS = ['M13.8 4H20v6.2', 'M19.4 4.6 12 12', 'M11 4.8H5.4A1.4 1.4 0 0 0 4 6.2v12.4a1.4 1.4 0 0 0 1.4 1.4h12.4a1.4 1.4 0 0 0 1.4-1.4V13']; function pfeilBauen() { const NS = 'http://www.w3.org/2000/svg'; const svg = document.createElementNS(NS, 'svg'); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('aria-hidden', 'true'); for (const d of PFEIL_RAUS) { const p = document.createElementNS(NS, 'path'); p.setAttribute('d', d); svg.append(p); } return svg; } /* ===================================================================== EINE KARTE ===================================================================== */ /** Führt die Adresse aus dem Haus hinaus? * * Entscheidet über `target` und `rel` — und darüber, ob überhaupt * ein `rel` nötig ist. Eine Seite dieses Hauses öffnet im selben * Fenster: Ein neuer Tab für „Draußen" wäre ein Fenster mehr, das * niemand zumacht. */ const nachDraussen = (ziel) => /^https:\/\//i.test(ziel); function karte(w) { const k = el('article', 'u-weg'); k.dataset.art = w.art; k.dataset.schluessel = w.schluessel; if (!w.sichtbar) k.dataset.aus = 'ja'; const kopf = el('div', 'u-weg__kopf'); const zeichen = el('span', 'u-weg__zeichen'); zeichen.setAttribute('aria-hidden', 'true'); zeichen.append(zeichenBauen(w)); kopf.append(zeichen); const namen = el('div', 'u-weg__namen'); namen.append(el('h3', 'u-weg__titel', w.titel)); namen.append(el('span', 'u-weg__marke', stand.arten[w.art] || w.art)); kopf.append(namen); k.append(kopf); if (w.text) k.append(el('p', 'u-weg__text', w.text)); if (aendern && stand.darf_aendern) { k.append(formular(w)); return k; } /* NUR EIN KNOPF, WENN ES EINEN WEG GIBT. „Einfach da sein" hat keinen — und ein Knopf, der nichts tut, lässt die Seite kaputt aussehen statt den Weg selbsterklärend. */ if (w.ziel && w.knopf) { const a = el('a', 'u-weg__knopf'); a.href = w.ziel; a.append(document.createTextNode(w.knopf)); if (nachDraussen(w.ziel)) { a.target = '_blank'; /* `noopener` ist die Sicherung: Ohne ihn kann die geöffnete Seite über `window.opener` auf diese hier zugreifen. */ a.rel = 'noopener noreferrer'; a.append(pfeilBauen()); } a.addEventListener('click', () => strich(w.id)); k.append(a); } /* WAS NUR DOGFATHER SIEHT. Ohne Änderungsmodus, damit er im Alltag dieselbe Seite sieht wie alle anderen — und nur dann mehr, wenn er danach fragt. */ if (stand.darf_aendern) { if (!w.sichtbar) k.append(el('span', 'u-weg__aus', 'steht für niemanden da')); const z = stand.zahlen?.[w.id]; k.append(el('span', 'u-weg__zahl', z ? `${z.gesamt}× geöffnet · ${z.tage30} in 30 Tagen · ${z.tage7} in 7 Tagen` : 'noch nicht geöffnet')); } return k; } /* ===================================================================== DER STRICH Kein Name, keine Nummer — der Server zählt Weg und Tag (siehe unterstuetzung-tabellen.js). `sendBeacon` und nicht `fetch`: Der Browser folgt dem Link im selben Augenblick, und eine laufende Anfrage würde dabei abgebrochen. Ein Strich, der nur gezählt wird, wenn die Seite noch offen ist, zählt ausgerechnet die erfolgreichen Klicks nicht. ===================================================================== */ function strich(id) { const weg = `/workspace/api/unterstuetzung/${id}/strich`; try { if (navigator.sendBeacon && navigator.sendBeacon(weg, new Blob())) return; } catch { /* dann eben der Rückfall darunter */ } /* RÜCKFALL, und er darf ebenfalls nichts aufhalten: `keepalive` lässt die Anfrage weiterlaufen, während die Seite wechselt. */ try { fetch(weg, { method: 'POST', credentials: 'same-origin', keepalive: true }) .catch(() => {}); } catch { /* der Weg nach draußen zählt, nicht der Strich */ } } /* ===================================================================== DAS FORMULAR — nur im Änderungsmodus ===================================================================== */ function feld(art, name, wert, hinweis) { const l = el('label'); l.append(el('span', null, hinweis)); const i = document.createElement(art === 'lang' ? 'textarea' : 'input'); if (art !== 'lang') i.type = 'text'; i.value = wert ?? ''; i.dataset.feld = name; l.append(i); return l; } function formular(w) { const f = el('div', 'u-form'); f.append(feld('kurz', 'titel', w.titel, 'Name')); f.append(feld('lang', 'text', w.text, 'Was dahintersteht')); const reihe = el('div', 'u-form__reihe'); const artFeld = el('label'); artFeld.append(el('span', null, 'Art')); const auswahl = document.createElement('select'); auswahl.dataset.feld = 'art'; for (const [wert, name] of Object.entries(stand.arten)) { const o = document.createElement('option'); o.value = wert; o.textContent = name; if (wert === w.art) o.selected = true; auswahl.append(o); } artFeld.append(auswahl); reihe.append(artFeld); const an = el('label', 'u-form__an'); const haken = document.createElement('input'); haken.type = 'checkbox'; haken.dataset.feld = 'sichtbar'; haken.checked = !!w.sichtbar; an.append(haken, document.createTextNode('steht für alle da')); reihe.append(an); f.append(reihe); f.append(feld('kurz', 'knopf', w.knopf, 'Aufschrift des Knopfes (leer = kein Knopf)')); const zielFeld = feld('kurz', 'ziel', w.ziel, 'Adresse (https://… oder seite.html)'); f.append(zielFeld); const knoepfe = el('div', 'u-form__knoepfe'); const sichern = el('button', 'knopf knopf--klein', 'Speichern'); sichern.type = 'button'; sichern.addEventListener('click', () => sichere(w.id, f)); knoepfe.append(sichern); const weg = el('button', 'abmelden u-form__weg', 'Entfernen'); weg.type = 'button'; weg.addEventListener('click', () => entferne(w)); knoepfe.append(weg); f.append(knoepfe); return f; } const lies = (f, name) => { const e = f.querySelector(`[data-feld="${name}"]`); if (!e) return undefined; return e.type === 'checkbox' ? e.checked : e.value; }; async function sichere(id, f) { melde(''); const koerper = { titel: lies(f, 'titel'), text: lies(f, 'text'), knopf: lies(f, 'knopf'), ziel: lies(f, 'ziel'), art: lies(f, 'art'), sichtbar: lies(f, 'sichtbar'), }; try { const a = await hole(`/workspace/api/unterstuetzung/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(koerper), }); if (!a.ok) { const r = await a.json().catch(() => ({})); /* SATZ STATT MASCHINENWORT (25.09.2026). Hier stand `r.fehler || …` -- also wurde die Kennung des Servers ungefiltert angezeigt. Wer "nicht_gefunden" liest, weiss weder, was los ist, noch was er tun soll. `sagWas` macht daraus den Satz aus meldung.js und faellt auf den Text dahinter zurueck, wenn es die Kennung dort nicht gibt. */ melde(sagWas(r.fehler, 'Das ließ sich nicht speichern.')); return; } melde('Gespeichert.', true); await laden(); } catch { melde('Das ließ sich nicht speichern.'); } } async function entferne(w) { /* GEFRAGT WIRD ÜBER DIE HAUS-NACHFRAGE und nicht über `confirm`. Sie sieht aus wie der Rest des Hauses und lässt sich mit der Tastatur bedienen — `confirm` sperrt die ganze Seite. */ const ja = window.bestaetige ? await window.bestaetige({ titel: `„${w.titel}“ entfernen?`, text: 'Der Weg verschwindet für alle, und die Striche der letzten ' + 'Monate gehen mit. Das lässt sich nicht zurückholen.', ja: 'Entfernen', nein: 'Behalten', }) : true; if (!ja) return; try { const a = await hole(`/workspace/api/unterstuetzung/${w.id}`, { method: 'DELETE' }); if (!a.ok) { melde('Das ließ sich nicht entfernen.'); return; } melde('Entfernt.', true); await laden(); } catch { melde('Das ließ sich nicht entfernen.'); } } async function neuerWeg() { melde(''); try { const a = await hole('/workspace/api/unterstuetzung', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ titel: 'Neuer Weg', art: 'frei', text: '' }), }); if (!a.ok) { const r = await a.json().catch(() => ({})); melde(sagWas(r.fehler, 'Das ließ sich nicht anlegen.')); return; } await laden(); } catch { melde('Das ließ sich nicht anlegen.'); } } /* ===================================================================== ZEICHNEN ===================================================================== */ function gruppe(titel, satz, wege) { if (!wege.length) return null; const s = el('section', 'u-gruppe'); const kopf = el('div', 'u-gruppe__kopf'); kopf.append(el('h2', 'u-gruppe__titel', titel)); if (satz) kopf.append(el('p', 'u-gruppe__satz', satz)); s.append(kopf); const gitter = el('div', 'u-gitter'); for (const w of wege) gitter.append(karte(w)); s.append(gitter); return s; } function zeichnen() { const ziel = $('wege'); ziel.textContent = ''; const geben = stand.wege.filter((w) => w.art !== 'frei'); const frei = stand.wege.filter((w) => w.art === 'frei'); /* ZWEI GRUPPEN, UND DIE ZWEITE IST NICHT DIE ABTEILUNG FÜR ARME. Sie trägt dieselbe Kartenform und dieselbe Größe; nur die Überschrift sortiert. Ohne Überschriften stünden fünf Karten nebeneinander und man müsste jede lesen, um zu wissen, was Geld kostet. */ const a = gruppe('Direkt etwas geben', 'Geht an Dogi selbst – ohne Umweg über TikTok.', geben); const b = gruppe('Kostet nichts', 'Genauso viel wert. Wirklich.', frei); if (a) ziel.append(a); if (b) ziel.append(b); if (aendern && stand.darf_aendern) { const neu = el('button', 'u-neu'); neu.type = 'button'; neu.append(el('span', 'u-neu__plus', '+')); neu.append(el('span', null, 'Noch einen Weg dazunehmen')); neu.addEventListener('click', neuerWeg); const gitter = el('div', 'u-gitter'); gitter.append(neu); const s = el('section', 'u-gruppe'); s.append(gitter); ziel.append(s); } if (!stand.wege.length) { ziel.append(el('p', 'u-laedt', stand.darf_aendern ? 'Noch kein Weg eingetragen. Leg oben einen an.' : 'Hier steht gerade nichts. Schau später noch einmal vorbei.')); } } function leitungZeichnen() { const block = $('leitung'); if (!stand.darf_aendern) { block.hidden = true; return; } block.hidden = false; /* DER HINWEIS AUF DEN FEHLENDEN PAYPAL-LINK. Er steht hier und nicht als Platzhalter in der Datenbank: Eine geratene Zahlungsadresse wäre der gefährlichste Fehler dieser ganzen Seite — Geld an einen Fremden, und niemand merkt es. Also steht dort nichts, der Weg ist unsichtbar, und genau EINE Person erfährt, dass er fehlt. */ const ohne = stand.wege.filter((w) => w.knopf === '' && w.ziel === '' && w.art !== 'frei'); const unsichtbar = stand.wege.filter((w) => !w.sichtbar); const satz = $('leitung-satz'); if (ohne.length || unsichtbar.length) { const namen = [...new Set([...ohne, ...unsichtbar].map((w) => w.titel))].join(', '); satz.textContent = `Noch ohne Adresse und deshalb für niemanden sichtbar: ${namen}. ` + 'Drück auf „Ändern“, trag die Adresse ein und setz den Haken bei ' + '„steht für alle da“.'; satz.dataset.warnung = 'ja'; } else { satz.textContent = 'Du kannst hier jeden Weg umschreiben, ausblenden oder ' + 'einen neuen dazunehmen. Alle anderen sehen nur, was sichtbar ist.'; delete satz.dataset.warnung; } /* DIE SUMME ÜBER ALLE WEGE. Eine Zahl je Karte steht unten an der Karte; hier oben steht, ob die Seite überhaupt benutzt wird. */ const kasten = $('zahlen'); kasten.textContent = ''; const z = Object.values(stand.zahlen || {}); if (!z.length) { kasten.hidden = true; return; } kasten.hidden = false; const summe = (feld) => z.reduce((n, x) => n + (x[feld] || 0), 0); for (const [wert, text] of [ [summe('tage7'), 'geöffnet in 7 Tagen'], [summe('tage30'), 'in 30 Tagen'], [summe('gesamt'), 'insgesamt'], ]) { const kachel = el('div', 'u-zahl'); kachel.append(el('span', 'u-zahl__n', String(wert))); kachel.append(el('span', 'u-zahl__text', text)); kasten.append(kachel); } /* KEIN NAME, NIRGENDS. Die Tabelle hat gar keine Spalte dafür — das steht so im Schema und ist nicht nur eine Abfrage, die jemand später ändern könnte. */ } async function laden() { try { const a = await hole('/workspace/api/unterstuetzung'); if (!a.ok) { melde('Die Seite ließ sich nicht laden.'); return; } stand = await a.json(); $('laedt')?.remove(); leitungZeichnen(); zeichnen(); } catch { window.ladefehler?.(); } } $('aendern-an')?.addEventListener('click', () => { aendern = !aendern; $('aendern-an').textContent = aendern ? 'Fertig' : 'Ändern'; melde(''); zeichnen(); }); laden(); })();