Zweite Live-Uhr fuer "spezielles Live-Event" + mehr Farbe im Radar

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]>
This commit is contained in:
2026-08-20 23:24:17 +02:00
co-authored by Claude Opus 5
parent 7fdceadb9e
commit 519e209bce
40 changed files with 763 additions and 369 deletions
+111 -6
View File
@@ -19,10 +19,10 @@
<link rel="apple-touch-icon" href="assets/img/apple-touch-icon-admin.png" />
<meta name="apple-mobile-web-app-title" content="DogiCrew-Verwaltung" />
<meta name="theme-color" content="#0e0a16" />
<link rel="stylesheet" href="assets/css/main.css?v=20260820i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260820i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260820i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260820i" />
<link rel="stylesheet" href="assets/css/main.css?v=20260820j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260820j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260820j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260820j" />
<style>
/* =====================================================================
verwaltung.html — Dogis privater Command-Center-Bereich.
@@ -803,6 +803,47 @@
</div>
</section>
<!-- ===== "Spezielles Live-Event" (Streamplan-Seite) — 20.08.2026,
Nutzer-Wunsch: "spezielle live events da auch eintragen ... so dass
da eine zweite uhr erscheint wo die zeit dann für dieses spezielle
event läuft." Anders als "Event des Jahres" hier ein echtes
Datum+Uhrzeit statt eines freien Textes -- daraus baut
streamplan.html einen echten Countdown. Nur sichtbar mit derselben
EVENTS_MANAGE-Berechtigung. -->
<section class="section-tight" id="special-event-root" style="display:none;padding-top:0;">
<div class="container">
<h2 class="text-center" style="margin-bottom:.3rem;">⚡ Spezielles Live-Event</h2>
<p class="small muted text-center" style="max-width:60ch;margin:0 auto 1.4rem;">
Erscheint als zweite Uhr auf der Streamplan-Seite, sobald aktiviert. Titel bitte auf
Deutsch eintippen, wird beim Speichern automatisch übersetzt.
</p>
<div class="card vw-card vw-shine-card vw-noise" style="max-width:520px;margin:0 auto;padding:1.5rem;">
<div class="field">
<label for="se-titel">Titel (Deutsch)</label>
<input type="text" id="se-titel" maxlength="120" placeholder="z. B. Großer Q&amp;A-Abend" />
</div>
<div class="field">
<label for="se-datum">Datum &amp; Uhrzeit</label>
<input type="datetime-local" id="se-datum" />
</div>
<div class="field">
<label for="se-link">Link (optional, muss mit https:// beginnen)</label>
<input type="url" id="se-link" placeholder="https://…" />
</div>
<label style="display:flex;align-items:center;gap:.6rem;margin:1rem 0;cursor:pointer;">
<input type="checkbox" id="se-aktiv" style="width:1.2rem;height:1.2rem;" />
<span>Aktiv — zweite Uhr auf der Streamplan-Seite sichtbar</span>
</label>
<p class="small muted" style="margin:-.4rem 0 1rem;">
Nur mit Titel + Datum in der Zukunft aktivierbar. Ist das Datum vorbei, verschwindet
die zweite Uhr automatisch wieder.
</p>
<button type="button" class="btn btn-primary" id="se-speichern" style="width:100%;">Speichern</button>
<p class="small" id="se-status" style="margin-top:.6rem;"></p>
</div>
</div>
</section>
<!-- ===== Supporter-Liste ===== -->
<section class="section-tight" id="supp-root" style="display:none;padding-top:0;">
<div class="container" id="liste-ansicht">
@@ -977,8 +1018,8 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/main.js?v=20260820i"></script>
<script src="assets/js/admin-auth.js?v=20260820i"></script>
<script src="assets/js/main.js?v=20260820j"></script>
<script src="assets/js/admin-auth.js?v=20260820j"></script>
<script>
(function () {
const API_BASE_URL = "https://postfach.dogfather-universe.com";
@@ -1051,6 +1092,8 @@
if (window.AdminAuth.sessionHasPermission(session, "EVENTS_MANAGE")) {
document.getElementById("events-root").style.display = "";
await ladeUndRenderEvents(session);
document.getElementById("special-event-root").style.display = "";
await ladeUndRenderSpecialEvent();
}
const pollsBox = document.getElementById("box-abstimmungen");
if (window.AdminAuth.sessionHasPermission(session, "POLLS_MANAGE")) {
@@ -1069,6 +1112,7 @@
document.getElementById("vw-stats-root").style.display = "none";
document.getElementById("supp-root").style.display = "none";
document.getElementById("events-root").style.display = "none";
document.getElementById("special-event-root").style.display = "none";
document.getElementById("box-abstimmungen").style.display = "none";
}
@@ -1372,6 +1416,67 @@
});
}
/* ===== "Spezielles Live-Event" — Verwaltung (20.08.2026) =====
Datum+Uhrzeit kommt vom Server als UTC-ISO-String, das <input
type="datetime-local"> braucht aber "YYYY-MM-DDTHH:mm" in der
LOKALEN Zeitzone der Person, die gerade in der Verwaltung sitzt —
deshalb hier von Hand umgerechnet statt toISOString() (das würde UTC
zurückgeben und beim erneuten Öffnen eine falsche Uhrzeit anzeigen). */
function isoZuLokalemDatetimeLocal(iso) {
const d = new Date(iso);
const pad = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
async function ladeUndRenderSpecialEvent() {
const statusEl = document.getElementById("se-status");
let eintrag = null;
try {
const res = await window.AdminAuth.apiCall(API_BASE_URL, "/admin/special-event", {});
if (res.ok) eintrag = res.event;
} catch {
/* Formular bleibt leer, Speichern funktioniert trotzdem. */
}
if (eintrag) {
document.getElementById("se-titel").value = eintrag.titel?.de || "";
document.getElementById("se-datum").value = isoZuLokalemDatetimeLocal(eintrag.zieldatum);
document.getElementById("se-link").value = eintrag.link || "";
document.getElementById("se-aktiv").checked = !!eintrag.aktiv;
}
document.getElementById("se-speichern").addEventListener("click", async () => {
statusEl.textContent = "";
const titel = document.getElementById("se-titel").value.trim();
const datumRoh = document.getElementById("se-datum").value; // "YYYY-MM-DDTHH:mm", lokale Zeit
if (!titel || !datumRoh) {
statusEl.textContent = "Titel und Datum sind Pflicht.";
statusEl.style.color = "#ff6b6b";
return;
}
statusEl.textContent = "Wird gespeichert und übersetzt …";
statusEl.style.color = "";
// new Date("YYYY-MM-DDTHH:mm") interpretiert das als lokale Zeit dieses
// Browsers -- .toISOString() macht daraus den korrekten UTC-Zeitpunkt,
// den jede besuchende Person später in IHRER eigenen Ortszeit sieht.
const zieldatum = new Date(datumRoh).toISOString();
const res = await window.AdminAuth.apiCall(API_BASE_URL, "/admin/special-event/save", {
titel,
zieldatum,
link: document.getElementById("se-link").value.trim(),
aktiv: document.getElementById("se-aktiv").checked,
});
if (!res.ok) {
statusEl.textContent = res.error || "Speichern fehlgeschlagen.";
statusEl.style.color = "#ff6b6b";
return;
}
statusEl.textContent = res.event.aktiv
? "✅ Gespeichert — zweite Uhr ist live auf der Streamplan-Seite."
: "✅ Als Entwurf gespeichert (noch nicht sichtbar).";
statusEl.style.color = "#baf7d6";
});
}
// Nutzer-Wunsch 04.08.2026 (6. Runde): zwei getrennte Kisten/Formulare
// (Dogi + VanVan) statt einem gemeinsamen Feld — beide rufen denselben
// Login-Endpunkt auf, die Fehlermeldung erscheint jeweils nur in der