Files
DogFatherGitandClaude Opus 5 5d0ea11570 Arbeit der parallelen Session in git gesichert (war nur auf dem Server)
Diese Änderungen liefen bereits live auf dem Server, lagen dort aber
ausschließlich als nicht eingecheckte Arbeitskopie — bei jedem Deploy
(stash/pull/pop) und bei jedem Serverproblem wären sie verloren gewesen.
Deshalb hier unverändert in git übernommen, bevor darauf aufgebaut wird.

Enthalten (nicht von mir gebaut, nur gesichert):
- Supporter: eigener fester Zugangscode statt Einmalcode-Login
  (Migration 0009, lib/crypto.js scrypt-Hash, routes/supporter.js,
  abonnieren.html, supporter.html, i18n-abonnieren/-supporter)
- Stimmen: Profilbilder (Migration 0010, routes/testimonials.js)
- Event-Bild-Upload: voller Pfad statt relativem (routes/events.js)
- Neue/überarbeitete Hintergrundbilder für viele Seiten
- Teilen-Funktion (streamplan.js, i18n-index/-streamplan, main.css)

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-21 17:35:14 +02:00

252 lines
11 KiB
JavaScript

/* =====================================================================
streamplan.js — "Live-Radar" NUR für streamplan.html (20.08.2026,
Nutzer-Wunsch: "soll viel geiler und spezieller sein, überrasch mich",
ergänzt um eine zweite Uhr für ein "spezielles Live-Event": "sehr
wichtig ist dass ich auch spezielle live events da auch eintragen kann
in der verwaltungsseite so dass da eine zweite uhr erscheint wo die
zeit dann für dieses spezielle event läuft").
Zwei 24-Stunden-Ziffernblätter (reines SVG, keine Bild-Assets nötig):
- Uhr 1 (immer sichtbar): der feste 20-Uhr-Termin leuchtet dauerhaft als
Fixpunkt, ein "Jetzt"-Zeiger bewegt sich mit der echten Uhrzeit der
besuchenden Person mit. Live laut /live-status -> Rot/Puls +
"Jetzt anschauen".
- Uhr 2 (nur wenn in der Verwaltung aktiviert): zählt zu einem exakten
Zieldatum+Uhrzeit runter (Tage möglich, nicht nur Stunden), Magenta/
Violett statt Cyan/Gold, damit beide Uhren auf einen Blick
unterscheidbar sind ("bissl Farbe"). ===================================================================== */
(function () {
const API_BASE_URL = "https://postfach.dogfather-universe.com";
const SVG_NS = "http://www.w3.org/2000/svg";
const CX = 150, CY = 150, R_OUT = 130, R_TICK_IN = 116, R_TICK_MAJOR_IN = 106, R_HAND = 100;
function polar(r, stundenAnteil) {
const winkel = (stundenAnteil / 24) * 360 - 90; // 0 Uhr oben, im Uhrzeigersinn
const rad = (winkel * Math.PI) / 180;
return { x: CX + r * Math.cos(rad), y: CY + r * Math.sin(rad) };
}
/** Zeichnet die 24 Stundenstriche + einen Fixpunkt-Leuchtpunkt bei
"fixStunde" (kann eine Dezimalzahl sein, z.B. 19.5 = 19:30) in die
übergebenen <g>-Container. Läuft einmalig pro Uhr. */
function zeichneZiffernblatt(ticksHost, fixHost, fixStunde) {
ticksHost.innerHTML = "";
for (let h = 0; h < 24; h++) {
const major = h % 6 === 0;
const p1 = polar(R_OUT, h);
const p2 = polar(major ? R_TICK_MAJOR_IN : R_TICK_IN, h);
const line = document.createElementNS(SVG_NS, "line");
line.setAttribute("x1", p1.x); line.setAttribute("y1", p1.y);
line.setAttribute("x2", p2.x); line.setAttribute("y2", p2.y);
line.setAttribute("class", major ? "radar-tick radar-tick-major" : "radar-tick");
ticksHost.appendChild(line);
}
fixHost.innerHTML = "";
if (fixStunde != null) {
const fixPos = polar(R_OUT + 11, fixStunde);
const fixDot = document.createElementNS(SVG_NS, "circle");
fixDot.setAttribute("cx", fixPos.x); fixDot.setAttribute("cy", fixPos.y);
fixDot.setAttribute("r", 6.5);
fixDot.setAttribute("class", "radar-fixmarker-dot");
fixHost.appendChild(fixDot);
}
}
function zeigerAktualisieren(handEl, dotEl, stundenAnteil) {
const spitze = polar(R_HAND, stundenAnteil);
handEl.setAttribute("x2", spitze.x); handEl.setAttribute("y2", spitze.y);
dotEl.setAttribute("cx", spitze.x); dotEl.setAttribute("cy", spitze.y);
}
function zweistellig(n) { return String(n).padStart(2, "0"); }
/* ===================================================================
Uhr 1: täglicher 20-Uhr-Fixtermin (bereits bestehend, unverändert im
Verhalten) ================================================================= */
const radar = document.getElementById("stream-radar");
if (radar) {
const FIX_STUNDE = 20;
zeichneZiffernblatt(document.getElementById("radar-ticks"), document.getElementById("radar-fixmarker"), FIX_STUNDE);
const nowHand = document.getElementById("radar-now-hand");
const nowDot = document.getElementById("radar-now-dot");
function zeigerTick() {
const jetzt = new Date();
zeigerAktualisieren(nowHand, nowDot, jetzt.getHours() + jetzt.getMinutes() / 60 + jetzt.getSeconds() / 3600);
}
zeigerTick();
setInterval(zeigerTick, 15000);
function naechsterFixTermin() {
const jetzt = new Date();
const ziel = new Date(jetzt);
ziel.setHours(FIX_STUNDE, 0, 0, 0);
if (ziel <= jetzt) ziel.setDate(ziel.getDate() + 1);
return ziel;
}
const countdownEl = document.getElementById("radar-countdown");
const statusText = document.getElementById("radar-status-text");
const statusSub = document.getElementById("radar-status-sub");
const watchBtn = document.getElementById("radar-watch-btn");
let istLive = false;
let letzterTitel = null;
function countdownTick() {
if (istLive) return;
const diffMs = naechsterFixTermin() - new Date();
const gesamtSek = Math.max(0, Math.floor(diffMs / 1000));
const std = Math.floor(gesamtSek / 3600);
const min = Math.floor((gesamtSek % 3600) / 60);
const sek = gesamtSek % 60;
countdownEl.textContent = `${zweistellig(std)}:${zweistellig(min)}:${zweistellig(sek)}`;
}
setInterval(countdownTick, 1000);
function texteNeuSetzen() {
const t = window.dogiUebersetzen || ((k) => k);
if (istLive) {
statusText.textContent = t("sp_radar_live");
countdownEl.textContent = letzterTitel || t("sp_radar_live_generic");
statusSub.textContent = "";
watchBtn.hidden = false;
} else {
statusText.textContent = t("sp_radar_offline");
statusSub.textContent = t("sp_radar_countdown_label");
watchBtn.hidden = true;
countdownTick();
}
}
function liveStatusAnwenden(data) {
istLive = !!(data && data.live);
letzterTitel = data && data.title ? data.title : null;
radar.classList.toggle("ist-live", istLive);
texteNeuSetzen();
}
function liveStatusLaden() {
return fetch(`${API_BASE_URL}/live-status`, { cache: "no-store" })
.then((res) => res.json())
.then(liveStatusAnwenden)
.catch(() => {
istLive = false;
radar.classList.remove("ist-live");
texteNeuSetzen();
});
}
liveStatusLaden();
setInterval(liveStatusLaden, 30000);
document.addEventListener("visibilitychange", () => { if (!document.hidden) liveStatusLaden(); });
document.addEventListener("dogi-sprache-geaendert", texteNeuSetzen);
}
/* ===================================================================
Uhr 2: "spezielles Live-Event" aus der Verwaltung (20.08.2026, neu).
Nur sichtbar, wenn /special-event ein aktives, noch bevorstehendes
Event liefert. Zieluhrzeit kann Tage entfernt sein -> Countdown zeigt
bei Bedarf "Xd HH:MM:SS" statt nur HH:MM:SS. ================================================================= */
const seWrap = document.getElementById("se-radar-wrap");
if (seWrap) {
const seRadar = document.getElementById("se-radar");
const seNowHand = document.getElementById("se-radar-now-hand");
const seNowDot = document.getElementById("se-radar-now-dot");
const seCountdown = document.getElementById("se-radar-countdown");
const seTitel = document.getElementById("se-radar-titel");
const seLink = document.getElementById("se-radar-link");
const seTeilenBtn = document.getElementById("se-radar-teilen");
let zielDatum = null;
let aktuellesEvent = null; // für dogiTeilen() beim Klick (21.08.2026)
let countdownIntervalId = null;
let zeigerIntervalId = null;
function seCountdownTick() {
if (!zielDatum) return;
const diffMs = zielDatum - new Date();
if (diffMs <= 0) {
seRadar.classList.add("ist-live");
seCountdown.textContent = window.dogiUebersetzen ? window.dogiUebersetzen("sp_radar_live_generic") : "Live!";
clearInterval(countdownIntervalId);
return;
}
const gesamtSek = Math.floor(diffMs / 1000);
const tage = Math.floor(gesamtSek / 86400);
const std = Math.floor((gesamtSek % 86400) / 3600);
const min = Math.floor((gesamtSek % 3600) / 60);
const sek = gesamtSek % 60;
seCountdown.textContent = tage > 0
? `${tage}T ${zweistellig(std)}:${zweistellig(min)}:${zweistellig(sek)}`
: `${zweistellig(std)}:${zweistellig(min)}:${zweistellig(sek)}`;
}
// Der "Jetzt"-Zeiger zeigt wie bei Uhr 1 die ECHTE aktuelle Uhrzeit --
// der magentafarbene Fixpunkt markiert dagegen die Tageszeit, zu der
// das spezielle Event stattfindet. So bleibt die Bildsprache zwischen
// beiden Uhren konsistent (nur der Fixpunkt unterscheidet sich).
function seZeigerTick() {
const jetzt = new Date();
zeigerAktualisieren(seNowHand, seNowDot, jetzt.getHours() + jetzt.getMinutes() / 60 + jetzt.getSeconds() / 3600);
}
function specialEventLaden() {
return fetch(`${API_BASE_URL}/special-event`, { cache: "no-store" })
.then((res) => res.json())
.then((data) => {
const event = data && data.ok ? data.event : null;
if (countdownIntervalId) clearInterval(countdownIntervalId);
if (zeigerIntervalId) clearInterval(zeigerIntervalId);
if (!event) {
seWrap.hidden = true;
zielDatum = null;
return;
}
zielDatum = new Date(event.zieldatum);
aktuellesEvent = event;
seWrap.hidden = false;
seRadar.classList.remove("ist-live");
seTitel.textContent = window.dogiFeld ? window.dogiFeld(event.titel) : event.titel?.de || "";
zeichneZiffernblatt(document.getElementById("se-radar-ticks"), document.getElementById("se-radar-fixmarker"), zielDatum.getHours() + zielDatum.getMinutes() / 60);
seZeigerTick();
seCountdownTick();
zeigerIntervalId = setInterval(seZeigerTick, 15000);
countdownIntervalId = setInterval(seCountdownTick, 1000);
if (event.link) {
seLink.href = event.link;
seLink.hidden = false;
} else {
seLink.hidden = true;
}
})
.catch(() => {
seWrap.hidden = true;
});
}
specialEventLaden();
setInterval(specialEventLaden, 60000); // reicht -- Aenderungen in der Verwaltung muessen nicht sekundengenau ankommen
document.addEventListener("dogi-sprache-geaendert", () => {
if (zielDatum) specialEventLaden();
});
// Teilen (21.08.2026, Nutzer-Wunsch: "... speziellen live events soll man
// auch teilen können") -- teilt die Streamplan-Seite selbst (dort steht
// ja bereits automatisch das aktuell aktive Event), zusammen mit Titel
// im Text, damit klar ist, worum es geht. Nur EIN spezielles Event kann
// je aktiv sein, deshalb reicht ein einziger Knopf ohne Slot-Bezug.
seTeilenBtn?.addEventListener("click", () => {
if (!aktuellesEvent) return;
const titel = window.dogiFeld ? window.dogiFeld(aktuellesEvent.titel) : aktuellesEvent.titel?.de || "";
window.dogiTeilen?.(
{
title: titel,
text: `🔴 ${titel} — spezielles Live-Event bei DogFather!`,
url: `${location.origin}/streamplan.html`,
},
seTeilenBtn
);
});
}
})();