Nutzer-Wunsch 20.08.2026: "wuensch mir nur noch bissl farbe und 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 fuer dieses spezielle event laeuft." - Neue Verwaltungs-Sektion "Spezielles Live-Event": Titel (Deutsch, wird automatisch uebersetzt), echtes Datum+Uhrzeit, optionaler Link, Aktiv- Schalter -- nur mit Titel + Zukunftsdatum aktivierbar. - Zweite Uhr auf streamplan.html (Magenta/Violett statt Cyan/Gold, direkt neben der bestehenden), nur sichtbar wenn ein Event aktiviert ist und das Zieldatum noch nicht vorbei ist. Echter Countdown inkl. Tage (z.B. "2T 03:14:59"), "Jetzt"-Zeiger zeigt wie bei Uhr 1 die tatsaechliche Uhrzeit, der magentafarbene Fixpunkt markiert die Tageszeit des Events. Zieldatum wird als UTC gespeichert -- jede besuchende Person sieht den exakt richtigen Countdown in ihrer eigenen Zeitzone. - "Bissl Farbe": bunter Farbverlauf (Cyan/Violett/Gold) auf dem aeusseren Ring statt reinem Grauton, kraeftigerer Sweep-Farbschein. - Backend: neue routes/special-event.js (getSpecialEventPublic nur bei aktiv+zukuenftig, getSpecialEventAdmin fuer Entwuerfe, saveSpecialEvent mit Validierung), 13 automatisierte Tests gegen eine Fake-DB bestanden. - Bugfix unterwegs gefunden (Playwright-Screenshot, wiederholtes Muster auf dieser Seite): .stream-radar-wrap blieb trotz [hidden]-Attribut sichtbar (display:block ueberschreibt die eingebaute [hidden]-Regel bei gleicher Spezifitaet) -- explizite Regel ergaenzt, per Playwright erneut verifiziert (display:none bestaetigt). Backend-Teil (server-internal/) noch ohne Deploy-Zugriff -- Dogi muss ihn manuell auf dogiintern ausrollen, sonst bleibt die zweite Uhr unsichtbar. Co-Authored-By: Claude Opus 5 <[email protected]>
231 lines
9.9 KiB
JavaScript
231 lines
9.9 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");
|
|
let zielDatum = null;
|
|
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);
|
|
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();
|
|
});
|
|
}
|
|
})();
|