Der Treff sagt jetzt, ob gerade gestreamt wird

Filipe zu dieser Seite: "mach was anderes daraus oder mach es viel
krass und geiler ... erstell was was mich von den socken schmeisst. es
soll wirklich sogar ein wow effekt fuer die community ergeben." Dazu
die eine Auflage: die Kachel bleibt.

WAS SIE WAR: zwei Karten mit zwei Adressen. Sauber gebaut, richtig
beschriftet -- und niemand kam ein zweites Mal wieder. Wer im Treff
ist, kennt die Website. Den Rabattcode merkt man sich beim ersten Mal.

DIE FRAGE, DIE WIRKLICH JEMAND HAT, IST EINE ANDERE: Laeuft er
gerade? Und wenn nicht -- wie lange noch? Die Antwort kennt das Haus
seit dem 20.08.: postfach.dogfather-universe.com/live-status, derselbe
Dienst, von dem der oeffentliche Streamplan lebt. Im Treff gab es sie
bisher nicht.

Jetzt steht oben ein Puls: ein Ring, der sich ueber den Tag bis zur
naechsten Sendezeit fuellt, darin der Countdown auf die Sekunde. Sendet
er, wird die Karte rot, atmet im Ruhepuls und traegt den Titel des
Streams plus einen Weg hin. Darunter die drei Accounts und -- wenn die
Verwaltung eines eintraegt -- das besondere Event mit eigener Uhr.

DREI AUSGAENGE, NICHT ZWEI. Das ist der eigentliche Unterschied zur
oeffentlichen Seite: streamplan.js faengt dort jeden Fehler ab und
setzt `istLive = false`. Aus "wir konnten nicht fragen" wird "er sendet
nicht" -- und wer das liest, macht die Seite zu und verpasst den
Stream. Der Dienst liefert `autoHealthy` sogar mit; gelesen wird es
draussen nicht. Hier schon: Bei einer Stoerung steht "Konnten wir
gerade nicht nachsehen", dazu der Hinweis, dass das NICHT heisst, dass
kein Stream laeuft. Der Countdown laeuft dabei weiter -- er braucht
kein Netz.

NICHTS IST ABGESCHRIEBEN. Die drei Accounts kommen ueber
/workspace/api/draussen aus derselben KANAELE-Liste, an der die
Aufgabenverteilung haengt; die Adressen baut der Steckbrief. Die
Sendezeit steht im Server, und pruef-draussen haelt sie gegen
FIX_STUNDE in streamplan.js -- verschiebt jemand den Termin und aendert
nur einen Ort, wird die Pruefung rot.

Gemessen, bevor gebaut wurde: der Dienst antwortet und meldet sich
gesund, CORS steht auf *, und `connect-src` der Treff-Seiten fuehrt
postfach bereits. Ohne das Letzte waere der Abruf still blockiert
worden und haette ausgesehen wie ein toter Dienst.

Die Kachel heisst jetzt "Draussen" statt "Unsere Seiten" -- drei Dinge
statt zwei Adressen. Ziel und Datei bleiben gleich.
pruef-wege-nach-draussen schreibt den Namen nicht mehr ab, sondern
verlangt, dass Kachel und Seitenueberschrift uebereinstimmen. Zwei
Namen fuer denselben Ort waren schon einmal ein echter Fehler.

Nebenbefund, dort gleich behoben: Dieselbe Pruefung verlangte, dass
die Community-Kacheln restlos in Dreierreihen aufgehen -- obwohl ihr
eigener Kommentar sagt, eine freie Zelle am Ende sei erlaubt. Sie war
rot bei einwandfreiem Raster. Gemessen wird jetzt, was wirklich
schiefgeht: eine doppelt breite Kachel, die in der letzten Spalte
beginnt und die Zelle davor leer laesst. Mit Gegenprobe.

pruef-draussen: 39 Pruefungen, alle vier Lagen im echten Browser --
die Stoerung wird dafuer absichtlich hergestellt.
pruef-wege-nach-draussen: 67 statt 66, keine Fehler mehr.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-21 12:06:23 +02:00
co-authored by Claude Opus 5
parent 9a71f3a242
commit eca9aed279
44 changed files with 2141 additions and 729 deletions
+370
View File
@@ -425,3 +425,373 @@
.aussen:hover,
.aussen:focus-within { transform: none; }
}
/* =====================================================================
DER PULS, DAS EVENT UND DIE KANAELE (21.09.2026)
Filipe wollte aus dieser Seite etwas machen, das "von den Socken
haut". Der Weg dahin war NICHT mehr Effekt, sondern eine Frage, die
vorher niemand beantwortet hat: Laeuft er gerade?
DESHALB LIEGT DER GANZE AUFWAND OBEN. Der Puls ist das Erste und
Groesste auf der Seite, und er ist das Einzige, was sich bewegt --
und zwar nur dann, wenn die Bewegung etwas bedeutet (er sendet).
Bewegung, die immer laeuft, ist Tapete; Bewegung, die genau einmal
im Tag anspringt, ist ein Signal.
AUGENSCHONEND WIE ALLES IM HAUS: Der Ton liegt im Verlauf, im Rand
und im Ring -- nie als leuchtende Flaeche hinter Text. Das Rot des
Live-Zustands ist ein gedecktes Korallrot (#e8736b), kein
Signalrot: Auf einem dunklen Bildschirm ist reines Rot nach zwei
Minuten unangenehm, und diese Seite soll man waehrend des Streams
offen lassen koennen.
`prefers-reduced-motion` schaltet jede Bewegung ab. Der Zustand
bleibt trotzdem erkennbar -- er steht in Farbe UND in Worten, nicht
nur im Puls. Eine Auskunft, die ausschliesslich an einer Animation
haengt, kommt bei einem Teil der Leute gar nicht an.
===================================================================== */
.puls {
position: relative;
isolation: isolate;
overflow: hidden;
display: flex;
align-items: center;
gap: 1.25rem;
margin: 1.5rem 0 0;
padding: 1.35rem 1.4rem;
border-radius: 1.1rem;
border: 1px solid color-mix(in srgb, var(--puls-ton) 42%, transparent);
background:
radial-gradient(120% 95% at 6% -5%,
color-mix(in srgb, var(--puls-ton) 26%, transparent), transparent 58%),
radial-gradient(90% 80% at 105% 110%,
color-mix(in srgb, var(--puls-ton) 15%, transparent), transparent 62%),
linear-gradient(158deg,
color-mix(in srgb, var(--puls-ton) 13%, #0c1420),
#090f1a 72%);
box-shadow:
0 18px 38px -20px color-mix(in srgb, var(--puls-ton) 85%, transparent),
inset 0 1px 0 color-mix(in srgb, var(--puls-ton) 22%, transparent);
transition: border-color .4s ease, box-shadow .4s ease;
}
/* DIE VIER LAGEN. Jede hat ihren Ton, und der Ton ist nie die einzige
Auskunft -- daneben steht immer ein Satz. Wer Farben nicht
unterscheidet, liest trotzdem, was los ist. */
.puls { --puls-ton: #7ec8f2; } /* laedt / wartet */
.puls[data-lage="live"] { --puls-ton: #e8736b; } /* er sendet */
.puls[data-lage="unklar"] { --puls-ton: #d9a441; } /* wir wissen es nicht */
/* Die Aura liegt UNTER allem und traegt die einzige Bewegung der
Seite. Sie ist nur im Live-Zustand sichtbar. */
.puls__aura {
position: absolute;
inset: -40%;
z-index: 0;
pointer-events: none;
opacity: 0;
background: radial-gradient(closest-side,
color-mix(in srgb, var(--puls-ton) 30%, transparent), transparent 70%);
transition: opacity .6s ease;
}
.puls[data-lage="live"] .puls__aura {
opacity: 1;
animation: puls-atmen 3.4s ease-in-out infinite;
}
/* 3,4 SEKUNDEN, NICHT EINE. Ein schneller Puls wirkt wie eine Warnung
und macht nervoes; dieser hier atmet ungefaehr im Ruhepuls eines
Menschen. Die Deckkraft schwankt zwischen 55 und 100 Prozent --
sichtbar, aber nicht blinkend. */
@keyframes puls-atmen {
0%, 100% { opacity: .55; transform: scale(1); }
50% { opacity: 1; transform: scale(1.06); }
}
.puls__ring-feld {
position: relative;
z-index: 1;
flex: 0 0 auto;
width: 5.6rem;
height: 5.6rem;
display: grid;
place-items: center;
}
.puls__ring {
width: 100%;
height: 100%;
/* Oben anfangen statt rechts -- so liest sich der Bogen wie eine
Uhr und nicht wie ein Tachometer. */
transform: rotate(-90deg);
}
.puls__ring-bahn {
fill: none;
stroke: color-mix(in srgb, var(--puls-ton) 18%, transparent);
stroke-width: 6;
}
.puls__ring-fortschritt {
fill: none;
stroke: var(--puls-ton);
stroke-width: 6;
stroke-linecap: round;
/* Der Umfang wird im Skript gesetzt -- aus dem Radius gerechnet,
damit eine Aenderung am SVG nicht hier nachgezogen werden muss. */
transition: stroke-dashoffset .6s ease;
}
.puls__punkt {
position: absolute;
width: .85rem;
height: .85rem;
border-radius: 50%;
background: var(--puls-ton);
opacity: .9;
}
.puls[data-lage="live"] .puls__punkt {
animation: puls-punkt 3.4s ease-in-out infinite;
}
@keyframes puls-punkt {
0%, 100% { transform: scale(1); opacity: .75; }
50% { transform: scale(1.35); opacity: 1; }
}
.puls__text {
position: relative;
z-index: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: .2rem;
}
.puls__marke {
margin: 0;
font-size: .78rem;
letter-spacing: .08em;
text-transform: uppercase;
color: color-mix(in srgb, var(--puls-ton) 70%, #cfe0f0);
}
.puls__lage {
margin: 0;
font-size: 1.05rem;
font-weight: 600;
color: #eaf3fb;
}
/* DIE ZAHL IST DAS GROESSTE AUF DER SEITE -- sie ist der Grund, warum
man wiederkommt. `tabular-nums` verhindert das Zappeln, das sonst
jede Sekunde entsteht, wenn eine 1 schmaler ist als eine 8. */
.puls__zahl {
margin: .1rem 0 0;
font-size: clamp(1.8rem, 6vw, 2.6rem);
font-weight: 700;
line-height: 1.1;
font-variant-numeric: tabular-nums;
letter-spacing: .01em;
color: #f4f9ff;
overflow-wrap: anywhere;
}
/* Im Live-Zustand steht dort der Titel des Streams, kein Countdown --
ein Satz in Countdown-Groesse wuerde umbrechen und die Karte
aufreissen. */
.puls[data-lage="live"] .puls__zahl {
font-size: clamp(1.1rem, 3.4vw, 1.45rem);
font-weight: 600;
}
.puls__klein {
margin: .25rem 0 0;
font-size: .82rem;
line-height: 1.45;
color: #9fb4c8;
}
.puls__knopf {
position: relative;
z-index: 1;
/* Rechts anschlagen, solange die Karte eine Zeile ist. Bricht sie um,
hebt `margin-left: 0` unten die Regel wieder auf. */
margin-left: auto;
flex: 0 0 auto;
/* 44 px ist die Grenze im Haus -- am Daumen, nicht an der Maus
gemessen. */
min-height: 44px;
display: inline-flex;
align-items: center;
padding: .55rem 1.05rem;
border-radius: .7rem;
border: 1px solid color-mix(in srgb, var(--puls-ton) 55%, transparent);
background: color-mix(in srgb, var(--puls-ton) 18%, transparent);
color: #f4f9ff;
font-weight: 600;
text-decoration: none;
transition: background .2s ease, border-color .2s ease, transform .2s ease;
}
.puls__knopf:hover {
background: color-mix(in srgb, var(--puls-ton) 28%, transparent);
border-color: color-mix(in srgb, var(--puls-ton) 75%, transparent);
}
.puls__knopf:focus-visible {
outline: 2px solid var(--puls-ton);
outline-offset: 2px;
}
/* AM HANDY UNTEREINANDER. Keine Schwelle in Pixeln, sondern die
Frage, ob der Text neben dem Ring noch lesbar ist: Unter 34rem
bleiben fuer ihn weniger als 20 Zeichen je Zeile. */
@media (max-width: 34rem) {
.puls { flex-direction: column; align-items: flex-start; gap: .9rem; }
.puls__ring-feld { width: 4.6rem; height: 4.6rem; }
/* In einer Spalte gibt es kein "rechts" -- der Knopf stellt sich
unter den Text und nimmt die volle Breite, damit er am Daumen
nicht gesucht werden muss. */
.puls__knopf { margin-left: 0; width: 100%; justify-content: center; }
}
/* Der Textblock nimmt den Platz, der zwischen Ring und Knopf uebrig
bleibt -- sonst draengt sich alles links zusammen. */
.puls__text { flex: 1 1 auto; }
/* ---- DAS BESONDERE EVENT ------------------------------------------- */
.event {
position: relative;
display: flex;
align-items: center;
gap: 1rem;
margin: .9rem 0 0;
padding: 1rem 1.2rem;
border-radius: 1rem;
--event-ton: #d49ae8;
border: 1px solid color-mix(in srgb, var(--event-ton) 45%, transparent);
background: linear-gradient(152deg,
color-mix(in srgb, var(--event-ton) 16%, #0c1420), #090f1a 78%);
}
.event[hidden] { display: none; }
.event__zeichen {
flex: 0 0 auto;
font-size: 1.5rem;
color: var(--event-ton);
line-height: 1;
}
.event__text { min-width: 0; }
.event__marke {
margin: 0;
font-size: .74rem;
letter-spacing: .08em;
text-transform: uppercase;
color: color-mix(in srgb, var(--event-ton) 75%, #cfe0f0);
}
.event__titel {
margin: .15rem 0 0;
font-size: 1.05rem;
font-weight: 600;
color: #eaf3fb;
}
.event__zahl {
margin: .2rem 0 0;
font-variant-numeric: tabular-nums;
font-size: .92rem;
color: #b9c9da;
}
.event[data-jetzt="ja"] .event__zahl {
color: #f4f9ff;
font-weight: 700;
}
.event__knopf {
margin-left: auto;
min-height: 44px;
display: inline-flex;
align-items: center;
padding: .5rem 1rem;
border-radius: .7rem;
border: 1px solid color-mix(in srgb, var(--event-ton) 55%, transparent);
background: color-mix(in srgb, var(--event-ton) 16%, transparent);
color: #f4f9ff;
text-decoration: none;
font-weight: 600;
}
.event__knopf:focus-visible { outline: 2px solid var(--event-ton); outline-offset: 2px; }
@media (max-width: 34rem) {
.event { flex-wrap: wrap; }
.event__knopf { margin-left: 0; width: 100%; justify-content: center; }
}
/* ---- DIE KANAELE ---------------------------------------------------- */
.kanaele { margin: 1.6rem 0 0; }
.kanaele[hidden] { display: none; }
.kanaele__titel,
.seiten-titel {
margin: 0 0 .7rem;
font-size: 1.05rem;
font-weight: 600;
color: #cfe0f0;
}
.seiten-titel { margin-top: 1.8rem; }
.kanaele__reihe {
display: grid;
gap: .8rem;
/* Dieselbe Regel wie beim Kartengitter darunter: eine Regel statt
einer Zahl. Ein vierter Kanal ordnet sich von selbst ein. */
grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
}
.kanal {
--kanal-ton: #7ec8f2;
display: flex;
flex-direction: column;
gap: .15rem;
padding: .85rem 1rem;
border-radius: .9rem;
border: 1px solid color-mix(in srgb, var(--kanal-ton) 30%, transparent);
background: linear-gradient(150deg,
color-mix(in srgb, var(--kanal-ton) 11%, #0c1420), #090f1a 80%);
text-decoration: none;
transition: transform .2s ease, border-color .2s ease, background .2s ease;
}
.kanal:hover {
transform: translateY(-2px);
border-color: color-mix(in srgb, var(--kanal-ton) 58%, transparent);
}
.kanal:focus-visible { outline: 2px solid var(--kanal-ton); outline-offset: 2px; }
.kanal__name { font-weight: 600; color: #eaf3fb; }
.kanal__handle {
font-size: .85rem;
color: color-mix(in srgb, var(--kanal-ton) 78%, #cfe0f0);
overflow-wrap: anywhere;
}
.kanal__satz { margin-top: .2rem; font-size: .84rem; line-height: 1.45; color: #9fb4c8; }
.kanaele__fehler {
margin: 0;
padding: .8rem 1rem;
border-radius: .8rem;
border: 1px solid rgba(217, 164, 65, .35);
background: rgba(217, 164, 65, .08);
font-size: .88rem;
line-height: 1.5;
color: #d8c49a;
}
.kanaele__fehler[hidden] { display: none; }
/* ---- WER BEWEGUNG NICHT MOECHTE ------------------------------------- */
/* Der Zustand bleibt vollstaendig lesbar: Farbe, Ring und Text stehen
weiter da. Es hoert nur das Atmen auf. */
@media (prefers-reduced-motion: reduce) {
.puls[data-lage="live"] .puls__aura,
.puls[data-lage="live"] .puls__punkt { animation: none; }
.puls__ring-fortschritt { transition: none; }
.kanal { transition: none; }
.kanal:hover { transform: none; }
}
+294
View File
@@ -141,4 +141,298 @@
}, 4000);
});
}
/* ==================================================================
3. DER PULS -- laeuft er gerade? (21.09.2026)
==================================================================
Die Auskunft kommt von postfach.dogfather-universe.com/live-status,
demselben Dienst, von dem der oeffentliche Streamplan lebt. Er wird
direkt vom Browser gefragt; nachgemessen vor dem Bauen:
"access-control-allow-origin: *", und `connect-src` der
Treff-Seiten fuehrt postfach bereits -- sonst haette die
Inhaltsrichtlinie den Abruf still abgewiesen, und das haette
ausgesehen wie ein toter Dienst.
------------------------------------------------------------------
DREI AUSGAENGE, NICHT ZWEI -- UND DAS IST HIER KEINE FEINHEIT
`streamplan.js` macht an derselben Stelle etwas anderes: Sein
`.catch()` setzt `istLive = false` und zeigt den Countdown. Aus
"wir konnten nicht fragen" wird damit "er sendet nicht". Das ist
genau der Fehler, den das Haus seit dem 03.09. kennt -- eine
Pruefung ohne dritten Ausgang -- nur hier mit einer Folge, die
jemand merkt: Wer die Seite waehrend einer Stoerung aufmacht,
verpasst den Stream, weil eine Anzeige so getan hat, als haette
sie nachgesehen.
Der Dienst SAGT sogar, ob er nachsehen konnte: `autoHealthy` steht
in derselben Antwort und wird draussen nicht gelesen. Hier schon.
In der Lage "unklar" laeuft der Countdown trotzdem weiter -- er
braucht kein Netz. Eine Stoerung nimmt der Seite nur die eine
Auskunft, die wirklich an ihr haengt. */
const POSTFACH = 'https://postfach.dogfather-universe.com';
const puls = document.getElementById('puls');
if (puls) {
const eLage = document.getElementById('puls-lage');
const eZahl = document.getElementById('puls-zahl');
const eKlein = document.getElementById('puls-klein');
const eKnopf = document.getElementById('puls-knopf');
const eBogen = document.getElementById('puls-bogen');
/* DIE SENDEZEIT KOMMT VOM SERVER, DAMIT ES SIE NUR EINMAL GIBT.
Sie steht auch in assets/js/streamplan.js der oeffentlichen
Seite; zwei Zahlen, die dasselbe bedeuten, laufen irgendwann
auseinander. Bis die Antwort da ist, gilt derselbe Wert --
sonst stuende beim Laden gar nichts. Eine Pruefung haelt beide
Orte gegeneinander. */
let sendeStunde = 20;
let lage = 'laedt';
let uhrLaeuft = 0;
/* Der Ring: Umfang aus dem Radius gerechnet, nicht abgemessen.
Wird r im HTML geaendert, stimmt er weiter. */
const UMFANG = 2 * Math.PI * Number(eBogen.getAttribute('r') || 52);
eBogen.style.strokeDasharray = String(UMFANG);
const zwei = (n) => String(n).padStart(2, '0');
function naechsteSendezeit() {
const jetzt = new Date();
const ziel = new Date(jetzt);
ziel.setHours(sendeStunde, 0, 0, 0);
if (ziel <= jetzt) ziel.setDate(ziel.getDate() + 1);
return ziel;
}
function countdownZeichnen() {
const restMs = Math.max(0, naechsteSendezeit() - new Date());
const sek = Math.floor(restMs / 1000);
eZahl.textContent =
zwei(Math.floor(sek / 3600)) + ':'
+ zwei(Math.floor((sek % 3600) / 60)) + ':'
+ zwei(sek % 60);
/* Voll = gleich. Der Ring sagt dasselbe wie die Zahl, nur ohne
Lesen -- fuer den Blick im Vorbeigehen. */
const anteil = 1 - restMs / 86400000;
eBogen.style.strokeDashoffset = String(UMFANG * (1 - anteil));
}
/* Die Sekundenuhr laeuft NUR, solange jemand hinsieht. Ein
Zeitgeber, der im Hintergrund weiterlaeuft, kostet auf dem Handy
Akku, ohne etwas anzuzeigen. */
function uhrAn() {
if (uhrLaeuft || lage === 'live') return;
countdownZeichnen();
uhrLaeuft = setInterval(countdownZeichnen, 1000);
}
function uhrAus() {
if (!uhrLaeuft) return;
clearInterval(uhrLaeuft);
uhrLaeuft = 0;
}
function setzeLage(neueLage, titel) {
lage = neueLage;
puls.dataset.lage = neueLage;
if (neueLage === 'live') {
uhrAus();
eLage.textContent = 'Er ist JETZT live';
eZahl.textContent = titel || 'Der Stream läuft';
eKlein.textContent = 'Läuft gerade auf TikTok.';
eKnopf.hidden = false;
eKnopf.textContent = 'Jetzt zuschauen →';
eBogen.style.strokeDashoffset = '0';
return;
}
eKnopf.hidden = true;
if (neueLage === 'wartet') {
eLage.textContent = 'Gerade nicht live';
eKlein.textContent = 'Der nächste feste Termin ist '
+ sendeStunde + ':00 Uhr — in deiner Ortszeit.';
} else if (neueLage === 'unklar') {
/* DER SATZ SAGT, WAS ER WEISS, UND WAS NICHT. "Nicht live"
waere hier eine Behauptung, die niemand geprueft hat. */
eLage.textContent = 'Konnten wir gerade nicht nachsehen';
eKlein.textContent = 'Der Live-Dienst antwortet nicht. Das heißt NICHT, '
+ 'dass kein Stream läuft — schau im Zweifel direkt auf TikTok.';
eKnopf.hidden = false;
eKnopf.textContent = 'Auf TikTok nachsehen →';
} else {
eLage.textContent = 'Wir sehen gerade nach …';
eKlein.textContent = 'Alle Zeiten in deiner Ortszeit.';
}
uhrAn();
}
async function lageHolen() {
try {
const antwort = await fetch(POSTFACH + '/live-status', { cache: 'no-store' });
if (!antwort.ok) throw new Error('HTTP ' + antwort.status);
const d = await antwort.json();
if (!d || d.ok !== true) throw new Error('keine Auskunft');
if (d.live) { setzeLage('live', d.title); return; }
/* HIER STEHT DER UNTERSCHIED ZUR OEFFENTLICHEN SEITE. Der
Dienst schaut automatisch nach; klappt das gerade nicht,
sagt er es selbst. Ein "live: false" aus einem gestoerten
Pruefer ist keine Aussage ueber den Stream, sondern eine
ueber den Pruefer.
Nur wenn die Quelle "automatisch" ist -- hat jemand den
Status von Hand gesetzt, gilt der, und die Gesundheit des
automatischen Pruefers spielt keine Rolle. */
if (d.source === 'automatisch' && d.autoHealthy === false) {
setzeLage('unklar');
return;
}
setzeLage('wartet');
} catch {
setzeLage('unklar');
}
}
setzeLage('laedt');
lageHolen();
setInterval(lageHolen, 30000);
document.addEventListener('visibilitychange', () => {
if (document.hidden) { uhrAus(); return; }
uhrAn();
lageHolen();
});
/* ---- DAS BESONDERE EVENT -------------------------------------- */
const eventKasten = document.getElementById('event');
if (eventKasten) {
const evTitel = document.getElementById('event-titel');
const evZahl = document.getElementById('event-zahl');
const evKnopf = document.getElementById('event-knopf');
let evZiel = null;
let evUhr = 0;
const evZeichnen = () => {
if (!evZiel) return;
const restMs = evZiel - new Date();
if (restMs <= 0) {
evZahl.textContent = 'Es geht los!';
eventKasten.dataset.jetzt = 'ja';
clearInterval(evUhr);
evUhr = 0;
return;
}
const sek = Math.floor(restMs / 1000);
const tage = Math.floor(sek / 86400);
const rest = zwei(Math.floor((sek % 86400) / 3600)) + ':'
+ zwei(Math.floor((sek % 3600) / 60)) + ':' + zwei(sek % 60);
evZahl.textContent = tage > 0
? 'noch ' + tage + ' Tage · ' + rest
: 'noch ' + rest;
};
const eventHolen = async () => {
try {
const antwort = await fetch(POSTFACH + '/special-event', { cache: 'no-store' });
if (!antwort.ok) throw new Error('HTTP ' + antwort.status);
const d = await antwort.json();
const e = d && d.ok ? d.event : null;
if (evUhr) { clearInterval(evUhr); evUhr = 0; }
if (!e || !e.zieldatum) { eventKasten.hidden = true; evZiel = null; return; }
evZiel = new Date(e.zieldatum);
if (Number.isNaN(evZiel.getTime())) {
eventKasten.hidden = true; evZiel = null; return;
}
/* Der Titel kommt mehrsprachig; auf dieser Seite wird
deutsch gesprochen. `textContent`, nicht `innerHTML` --
der Text stammt aus der Verwaltung und ist damit
Eingabe, auch wenn sie aus dem eigenen Haus kommt. */
evTitel.textContent =
(e.titel && (e.titel.de || e.titel)) || 'Besonderes Event';
eventKasten.hidden = false;
delete eventKasten.dataset.jetzt;
evZeichnen();
evUhr = setInterval(evZeichnen, 1000);
if (e.link) { evKnopf.href = e.link; evKnopf.hidden = false; }
else evKnopf.hidden = true;
} catch {
/* HIER IST VERSTECKEN RICHTIG -- anders als beim Puls oben.
Der Puls beantwortet die Frage, wegen der man die Seite
oeffnet; schweigt er, muss das dastehen. Ein besonderes
Event gibt es an den meisten Tagen gar nicht. "Wir
konnten nicht nachsehen, ob es ein Event gibt, das es
vermutlich nicht gibt" ist keine Auskunft, sondern
Rauschen -- und Rauschen bringt Menschen dazu, auch die
echten Hinweise zu ueberlesen. */
eventKasten.hidden = true;
}
};
eventHolen();
setInterval(eventHolen, 60000);
}
/* ---- DIE KANAELE ---------------------------------------------- */
const kanaeleKasten = document.getElementById('kanaele');
if (kanaeleKasten) {
const reihe = document.getElementById('kanaele-reihe');
const fehlerText = document.getElementById('kanaele-fehler');
(async () => {
try {
const antwort = await fetch('/workspace/api/draussen',
{ credentials: 'same-origin' });
if (!antwort.ok) throw new Error('HTTP ' + antwort.status);
const d = await antwort.json();
const liste = Array.isArray(d.kanaele) ? d.kanaele : [];
/* DIE ZAHL GEHOERT IN DIE BEDINGUNG. Eine leere Antwort
wuerde sonst eine leere Reihe ergeben, die aussieht, als
gaebe es keine Kanaele -- statt zu sagen, dass etwas
schiefging. */
if (!liste.length) throw new Error('leere Liste');
if (Number.isFinite(d.sendeStunde)) {
sendeStunde = d.sendeStunde;
if (lage !== 'live') setzeLage(lage);
}
for (const k of liste) {
const a = document.createElement('a');
a.className = 'kanal';
a.href = k.adresse;
a.target = '_blank';
a.rel = 'noopener noreferrer';
const name = document.createElement('span');
name.className = 'kanal__name';
name.textContent = k.name;
const handle = document.createElement('span');
handle.className = 'kanal__handle';
handle.textContent = k.handle;
const satz = document.createElement('span');
satz.className = 'kanal__satz';
satz.textContent = k.satz || '';
a.append(name, handle, satz);
reihe.append(a);
}
kanaeleKasten.hidden = false;
} catch {
fehlerText.hidden = false;
kanaeleKasten.hidden = false;
}
})();
}
}
})();