/* ===================================================================== DIE SPENDENTAFEL ALS BROWSER-QUELLE (28.09.2026) Hört am Bühnenstrom und lässt jede bestätigte Spende als Karte laufen. Sonst tut diese Seite nichts. --------------------------------------------------------------------- SIE MUSS TAGE LAUFEN, OHNE DASS JEMAND HINSIEHT Das ist der ganze Unterschied zu einer Seite im Browser: Wer eine Seite offen hat, merkt, wenn sie hängt, und lädt neu. Eine Quelle in OBS läuft im Hintergrund, und wenn sie einmal stehenbleibt, fällt es erst auf, wenn die erste Spende nicht erscheint — mitten in der Sendung, vor Publikum. Deshalb drei Dinge: Der Browser verbindet einen abgerissenen Ereignisstrom von selbst neu (`retry:` kommt vom Server). Darauf allein zu bauen reicht aber nicht: Schläft der Rechner, kann der Strom in einem Zustand stehenbleiben, aus dem der Browser nicht von selbst herausfindet. Also eine eigene WACHE: Kommt 70 Sekunden lang gar nichts — auch kein Lebenszeichen, das alle 25 Sekunden gesendet wird —, wird die Verbindung weggeworfen und neu aufgebaut. Und ein FEHLER WIRD ANGEZEIGT, aber nur der, der etwas bedeutet: ein falscher Schlüssel. Alles andere bleibt still, weil jede Fläche hier im Stream zu sehen wäre. ===================================================================== */ (() => { 'use strict'; const tafel = document.getElementById('tafel'); /* ==== GROESSE UND LAGE STEHEN IN DER ADRESSE ==================== Wer in OBS eine Browser-Quelle einrichtet, hat die Adresse ohnehin vor sich. Ein Wert, den man stattdessen im Regiepult suchen muesste, waere ein Fensterwechsel mitten im Einrichten -- und beim Probieren aendert man zehnmal hintereinander etwas. DIE VORGABE IST 1,4 UND NICHT 1. Im Saal steht die Karte neben einem Chat und darf nichts zudecken; auf der Buehne deckt sie nichts zu, was man lesen muesste, und darf deutlich groesser sein. Eine Vorgabe, bei der man immer nachstellen muss, ist die falsche. */ { const p = new URLSearchParams(location.search); const g = Number(String(p.get('g') || '').replace(',', '.')); /* Zwischen 0,5 und 4. Darunter ist nichts mehr zu lesen, darueber passt keine Karte mehr ins Bild -- beides waere eine Quelle, die aussieht, als sei sie kaputt. */ const faktor = Number.isFinite(g) && g >= 0.5 && g <= 4 ? g : 1.4; document.documentElement.style.fontSize = `${Math.round(16 * faktor)}px`; /* ==== WO DIE KARTE STEHT — EINE ANTWORT, EINE AUSNAHME ======= (geändert 08.10.2026) Seit heute entscheidet das Regiepult, ob die Karte in der MITTE des Bildes steht oder am RAND; die Karte bringt den Platz mit („Gestaltung → Wo die Karte steht"), und derselbe Wert gilt im Saal und hier. Das ist die eine Antwort — wer umstellt, stellt beide Orte zugleich um und muss nicht daran denken, in OBS eine Adresse nachzuziehen. `pos` IN DER ADRESSE BLEIBT TROTZDEM, UND ZWAR ALS AUSNAHME. Im Stream liegt oft noch etwas anderes im Bild — ein Kamerakasten, ein Alert, ein Rahmen. Dafür braucht es eine Stelle, an der man die Karte für DIESE Szene wegschiebt, ohne den Saal mitzuverstellen. UND SIE GILT NUR, WENN SIE AUSDRÜCKLICH DASTEHT. Vorher stand hier `p.get('pos') || 'ul'` — damit hatte JEDE Adresse eine Lage, auch die, in der nie jemand eine angegeben hat, und die Hauseinstellung wäre an dieser Seite wirkungslos verpufft. Das ist der Unterschied zwischen einer Ausnahme und einer zweiten Regel. Die Werte heißen `align-content`/`justify-items` und nicht `justify-content`/`align-items`: Die Tafel ist seit dem 08.10. ein Raster mit einer Spalte, und dort sind das die Eigenschaften, die wirken. */ const lagen = { ul: ['end', 'start'], ur: ['end', 'end'], ol: ['start', 'start'], or: ['start', 'end'], mitte: ['center', 'center'], }; const gewuenscht = String(p.get('pos') || ''); if (gewuenscht && lagen[gewuenscht]) { const [senkrecht, waagerecht] = lagen[gewuenscht]; tafel.style.alignContent = senkrecht; tafel.style.justifyItems = waagerecht; /* Die FORM muss zur Lage passen: In der Mitte steht die Karte als Aufsteller (Münze oben, Betrag darunter), an jeder anderen Lage als Zeile. Ohne diese Zeile stünde mitten im Bild die flache Randfassung. */ tafel.dataset.platz = gewuenscht === 'mitte' ? 'mitte' : 'rand'; tafel.dataset.posFest = 'ja'; } } const schluessel = new URLSearchParams(location.search).get('s') || ''; function sagFehler(text) { let k = document.querySelector('.obs-fehler'); if (!k) { k = document.createElement('div'); k.className = 'obs-fehler'; document.body.append(k); } k.textContent = text; } if (!schluessel) { sagFehler('Diese Adresse ist unvollständig — der Schlüssel fehlt. ' + 'Hol sie dir im Regiepult unter „Bühne“.'); return; } let strom = null; let letztes = Date.now(); function verbinden() { try { if (strom) strom.close(); } catch { /* war schon zu */ } strom = new EventSource( `/workspace/api/buehne/strom?s=${encodeURIComponent(schluessel)}`); strom.addEventListener('open', () => { letztes = Date.now(); const k = document.querySelector('.obs-fehler'); if (k) k.remove(); }); /* JEDE NACHRICHT ZAEHLT ALS LEBENSZEICHEN -- auch die Doppelpunkt- Zeile, die der Server alle 25 Sekunden schickt. Sie kommt hier als „message" ohne Inhalt an. */ strom.addEventListener('message', () => { letztes = Date.now(); }); strom.addEventListener('spende', (e) => { letztes = Date.now(); try { const d = JSON.parse(e.data); if (d.was === 'zeigen' && window.Spendenkarte) { window.Spendenkarte.zeigen(tafel, d.karte); } } catch { /* eine kaputte Meldung darf die Quelle nicht reissen */ } }); strom.addEventListener('error', () => { /* KEINE MELDUNG INS BILD. Ein kurzer Abriss ist normal, und der Browser verbindet von selbst neu. Eine rote Fläche dafür stünde im Stream. */ if (strom.readyState === EventSource.CLOSED) { setTimeout(verbinden, 3000); } }); } /* Einmal fragen, ob der Schlüssel überhaupt stimmt. Eine Quelle, die still bleibt, weil die Adresse falsch ist, sieht genauso aus wie eine, bei der nur niemand gespendet hat -- und das ist die eine Verwechslung, die hier wirklich schadet. */ fetch(`/workspace/api/buehne/lage?s=${encodeURIComponent(schluessel)}`) .then((a) => { if (a.status === 404) { sagFehler('Der Schlüssel stimmt nicht mehr. ' + 'Hol dir die Adresse neu im Regiepult unter „Bühne“.'); return; } verbinden(); }) .catch(() => verbinden()); /* DIE WACHE. Kommt 70 Sekunden nichts -- nicht einmal ein Lebenszeichen --, ist die Verbindung tot, auch wenn der Browser sie fuer offen haelt. */ setInterval(() => { if (Date.now() - letztes > 70_000) { letztes = Date.now(); verbinden(); } }, 10_000); /* WACHT DER RECHNER AUF, WIRD SOFORT NEU VERBUNDEN. Nach dem Ruhezustand ist ein Ereignisstrom fast immer tot, und bis die Wache greift, vergeht eine Minute. */ document.addEventListener('visibilitychange', () => { if (!document.hidden && Date.now() - letztes > 30_000) verbinden(); }); })();