"Event des Jahres" jetzt in der Verwaltung pflegbar (Bild, Text, Link)

Nutzer-Wunsch 20.08.2026: "will ich in der admin seite das selbst
gestalten können, mit bild und text und am besten auch einen link." Auf
Rueckfrage entschieden: Titel/Text nur auf Deutsch eingeben, die anderen
4 Sprachen werden beim Speichern automatisch uebersetzt (MyMemory, 0€,
kein API-Key) -- bewusste Ausnahme von der sonst geltenden "immer echte
Uebersetzung"-Regel, klar dokumentiert und im Admin-UI selbst als Hinweis
sichtbar. de-CH bekommt denselben deutschen Text (Dialekt ist keine von
Uebersetzungs-APIs unterstuetzte Zielsprache).

Backend (server-internal):
- routes/events.js: getEventsPublic (oeffentlich, keine Session),
  saveEvent + uploadEventImage (beide hinter neuer EVENTS_MANAGE-
  Berechtigung, Owner immer erlaubt). Speichert in der bereits
  bestehenden app_settings-Tabelle (2 feste Slots) statt einer neuen
  Tabelle -- es gibt nie mehr als genau 2 Events.
- lib/translate.js: MyMemory-Anbindung mit "fail closed auf Deutsch"
  pro Sprache, falls der Dienst mal nicht antwortet.
- Bild-Upload per multer, zufaelliger Dateiname (crypto.randomUUID,
  verhindert Path-Traversal ueber den Originalnamen komplett), 5 MB
  Limit, nur jpeg/png/webp, Ablage unter /var/lib/dogfather-internal/
  uploads/events (NICHT im Git-Ordner -- uebersteht Deploys), oeffentlich
  ausgeliefert unter /uploads.
- Neue Berechtigung EVENTS_MANAGE im Katalog (Gruppe "Startseite").

Frontend:
- index.html: laedt /events-of-year beim Aufruf, ueberschreibt pro Slot
  Datum/Titel/Text/Bild/Link NUR wenn dort tatsaechlich etwas gespeichert
  ist -- bleibt der Abruf aus oder ist ein Slot leer, bleibt der bisherige
  fest eingebaute Standardinhalt (dieselben zwei echten Events) stehen.
  Kein Blocker, kein sichtbarer Fehler bei Ausfall.
- verwaltung.html: neue Sektion "🏆 Event des Jahres" (nur mit
  EVENTS_MANAGE sichtbar), 2 Karten mit Bild-Upload+Vorschau, Datum,
  Titel, Text, Link, eigenem Speichern-Knopf pro Event.

Ausfuehrlich getestet, weil server-internal wegen fehlender Visual-
Studio-Build-Tools auf dieser Windows-Maschine nicht lokal mit echtem
better-sqlite3 laufen kann: routes/events.js komplett isoliert gegen eine
Fake-DB getestet (11 Szenarien: oeffentlicher Abruf, fehlende Session,
Session ohne Recht, Owner, Rolle MIT EVENTS_MANAGE, alle Validierungen,
Bild-Upload inkl. falscher Dateityp, Abruf des hochgeladenen Bilds).
index.html per Playwright mit echtem Netzwerk-Mocking gegen zwei
Szenarien getestet (API nicht erreichbar -> Standardinhalt bleibt; API
liefert echte Daten -> nur der befuellte Slot wird ueberschrieben, der
leere bleibt Standard). Dabei einen echten CSS-Bug gefunden und behoben
(display:block auf .jahres-event-bild überschrieb die [hidden]-Regel des
Browsers, leeres Bild waere immer sichtbar gewesen). verwaltung.html per
Playwright mit gemocktem Login+API end-to-end getestet: Formular wird
korrekt vorbefuellt, Bild-Upload + Speichern senden die richtigen Daten.

WICHTIG: server-internal laeuft unter einem eigenen Systembenutzer
(dogiintern), auf den ich (claudian) bewusst KEINEN Zugriff habe -- diese
Aenderung kann ich anders als sonst nicht selbst bis auf den Server
bringen. Dogi muss den Deploy-Schritt fuer server-internal selbst
ausfuehren (git pull + npm install + Neustart des dogiintern-Dienstes).

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-20 15:09:54 +02:00
co-authored by Claude Opus 5
parent 3f3ba835a9
commit f86b96a22f
8 changed files with 423 additions and 2 deletions
+53 -2
View File
@@ -91,13 +91,15 @@
<span class="tag" data-i18n="idx_jahres_events_tag">🏆 Event des Jahres</span>
</div>
<div class="jahres-events-grid">
<a class="jahres-event-card" href="zeitreise.html">
<a class="jahres-event-card" href="zeitreise.html" id="jahres-event-1">
<img class="jahres-event-bild" alt="" hidden />
<span class="jahres-event-datum" data-i18n="idx_jahres_event1_datum">7. März 2026</span>
<h3 data-i18n="idx_jahres_event1_h3">Der 28-Stunden-Stream</h3>
<p data-i18n="idx_jahres_event1_p">DogFather stellt sich einer besonderen Herausforderung und streamt 28 Stunden am Stück.</p>
<span class="jahres-event-mehr" data-i18n="idx_jahres_events_mehr">Zur Zeitreise →</span>
</a>
<a class="jahres-event-card" href="zeitreise.html">
<a class="jahres-event-card" href="zeitreise.html" id="jahres-event-2">
<img class="jahres-event-bild" alt="" hidden />
<span class="jahres-event-datum" data-i18n="idx_jahres_event2_datum">27. Juli bis 2. August 2026</span>
<h3 data-i18n="idx_jahres_event2_h3">Der Creator Cup</h3>
<p data-i18n="idx_jahres_event2_p">Gemeinsam mit Cidem organisiert DogFather den Creator Cup für die Streamer von Spicy Media.</p>
@@ -355,6 +357,55 @@ document.addEventListener("DOMContentLoaded", () => {
});
}
/* "Event des Jahres": seit 20.08.2026 in der Verwaltung pflegbar (Bild,
Text, Link je Slot, siehe verwaltung.html + server-internal/routes/
events.js). Lädt beim Seitenaufruf, was Dogi/VanVan zuletzt gespeichert
haben — bleibt der Fetch aus (noch nie gespeichert, Dienst nicht
erreichbar) oder ist ein Slot noch leer, bleibt bewusst der fest
eingebaute Standardinhalt aus dem HTML stehen (dieselben zwei echten
Events wie bisher) statt einer kaputten/leeren Kachel. */
function wendeJahresEventAn(slot, daten) {
if (!daten) return; // kein gespeicherter Eintrag -> Standardinhalt im HTML bleibt
const karte = document.getElementById(`jahres-event-${slot}`);
if (!karte) return;
const feld = (x) => (window.dogiFeld ? window.dogiFeld(x) : x || "");
const bild = karte.querySelector(".jahres-event-bild");
if (daten.bild) {
bild.src = daten.bild;
bild.alt = feld(daten.titel);
bild.hidden = false;
}
karte.querySelector(".jahres-event-datum").textContent = daten.datum || "";
karte.querySelector("h3").textContent = feld(daten.titel);
karte.querySelector("p").textContent = feld(daten.text);
if (daten.link) karte.href = daten.link;
}
if (API_BASE_URL) {
let letzteJahresEvents = null;
const ladeJahresEvents = () =>
fetch(`${API_BASE_URL}/events-of-year`, { cache: "no-store" })
.then((res) => res.json())
.then((data) => {
if (!data?.ok) return;
letzteJahresEvents = data.events;
wendeJahresEventAn(1, data.events[0]);
wendeJahresEventAn(2, data.events[1]);
})
.catch(() => {
/* Standardinhalt aus dem HTML bleibt stehen -- kein sichtbarer Fehler nötig. */
});
ladeJahresEvents();
// Bei Sprachwechsel mit den zuletzt geladenen Daten neu rendern (kein
// erneuter Server-Aufruf nötig) — Titel/Text stehen ja bereits in allen
// Sprachen im geladenen Objekt.
document.addEventListener("dogi-sprache-geaendert", () => {
if (!letzteJahresEvents) return;
wendeJahresEventAn(1, letzteJahresEvents[0]);
wendeJahresEventAn(2, letzteJahresEvents[1]);
});
}
// Stats: Team-Größe aus den Datenobjekten berechnen
const teamCount = (window.MODIS || []).length + (window.SCOUTS || []).length;
const statTeam = document.getElementById("stat-team");