/* ===================================================================== routes/special-event.js — "Spezielles Live-Event" für streamplan.html (20.08.2026, Nutzer-Wunsch: "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"). Anders als "Event des Jahres" (2 feste Slots) gibt es hier GENAU EIN Event mit einem exakten Zieldatum+Uhrzeit statt nur einem freien Datumstext — daraus lässt sich ein echter Countdown berechnen. Läuft auf derselben app_settings-Tabelle (key/value), kein Schema-Umbau nötig. Gleiche Rechte-/Freigabe-Logik wie "Event des Jahres" (EVENTS_MANAGE, Owner immer erlaubt, öffentlich lesbar, aber nur wenn aktiviert UND das Zieldatum noch in der Zukunft liegt). ===================================================================== */ import { db } from "../db.js"; import { hasPermission } from "../lib/permissions.js"; import { json } from "../lib/http.js"; import { logAction } from "../lib/audit.js"; import { uebersetzeAlleSprachen } from "../lib/translate.js"; const SETTINGS_KEY = "special_live_event"; function can(session, key) { if (!session) return false; if (session.isOwner) return true; return hasPermission(session.permissions, session.overrides, key); } function forbidden(res) { return json(res, { ok: false, error: "Keine Berechtigung." }, 403); } function laden() { const row = db.prepare(`SELECT value FROM app_settings WHERE key = ?`).get(SETTINGS_KEY); if (!row) return null; try { return JSON.parse(row.value); } catch { return null; } } /** Öffentlich, keine Session: nur wenn aktiviert UND das Zieldatum noch nicht vorbei ist (ein abgelaufenes Event soll nicht ewig als "Countdown bei 00:00:00" hängen bleiben, sondern einfach verschwinden). */ export async function getSpecialEventPublic(req, res) { const eintrag = laden(); const istGueltig = eintrag && eintrag.aktiv && new Date(eintrag.zieldatum) > new Date(); return json(res, { ok: true, event: istGueltig ? eintrag : null }); } /** Für die Verwaltung: liefert den rohen Stand (auch inaktiv/abgelaufen), damit das Formular beim Öffnen alles vorausfüllen kann. */ export async function getSpecialEventAdmin(req, res, session) { if (!can(session, "EVENTS_MANAGE")) return forbidden(res); return json(res, { ok: true, event: laden() }); } export async function saveSpecialEvent(req, res, session) { if (!can(session, "EVENTS_MANAGE")) return forbidden(res); const body = req.body || {}; const titelDe = String(body.titel || "").trim().slice(0, 120); const zieldatumRoh = String(body.zieldatum || "").trim(); const link = String(body.link || "").trim().slice(0, 300) || null; const aktiv = !!body.aktiv; if (!titelDe) return json(res, { ok: false, error: "Titel ist Pflicht." }, 400); const zieldatum = new Date(zieldatumRoh); if (!zieldatumRoh || Number.isNaN(zieldatum.getTime())) { return json(res, { ok: false, error: "Bitte ein gültiges Datum + Uhrzeit angeben." }, 400); } if (link && !/^https?:\/\//i.test(link)) { return json(res, { ok: false, error: "Link muss mit http:// oder https:// beginnen." }, 400); } if (aktiv && zieldatum <= new Date()) { return json(res, { ok: false, error: "Das Zieldatum liegt in der Vergangenheit — zum Aktivieren muss es in der Zukunft liegen." }, 400); } const titel = await uebersetzeAlleSprachen(titelDe); const eintrag = { titel, zieldatum: zieldatum.toISOString(), link, aktiv, aktualisiertVon: session.actor, aktualisiertAm: new Date().toISOString(), }; db.prepare( `INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at` ).run(SETTINGS_KEY, JSON.stringify(eintrag), eintrag.aktualisiertAm); logAction(session.actor, "special_event.saved", null, { titel: titelDe, zieldatum: eintrag.zieldatum, aktiv }); return json(res, { ok: true, event: eintrag }); }