Files
dogfather-universe/server-internal/routes/site-texts.js
T
DogFatherGitandClaude Sonnet 5 81f55d67db Team-Verwaltung Runde 2: bestehende Mitglieder importieren + Kategorien/Seitentitel editierbar
Nutzer-Wunsch 21.08.2026: "wenn ich eine änderung mache ist es auch für
jeden für die die schon da sind und die neuen" + "ich will ich titel
und namen von kategorien und seiten ändern können in der verwaltung".

Backend (braucht Filipes manuellen Deploy):
- Migration 0008: team_members bekommt intro/bioHtml/extraCta-Spalten
  -- volle Feld-Parität mit den von Hand gepflegten Profilen (Diene/
  Patrick/Bananenstift/Marina nutzen diese Felder).
- routes/team.js: neue importLegacyMember()-Funktion -- übernimmt ein
  bestehendes Profil 1:1 in die Datenbank, OHNE erneut zu übersetzen
  (die vorhandenen, von Hand geschriebenen Übersetzungen bleiben
  erhalten). Idempotent: mehrfacher Import erzeugt keine Duplikate.
- routes/site-texts.js (neu): admin-editierbare Kategorie-Namen und
  Seitentitel, generischer key->{de,...}-Override über app_settings
  (wie "Event des Jahres"), fester Schlüssel-Katalog aus
  Sicherheitsgründen. Leeres Feld setzt auf den Standardtext zurück.
- 27 Tests in isolierter Testumgebung geprüft (kein better-sqlite3
  lokal kompilierbar).

Frontend:
- window.dogiTeamZusammenfuehren() (main.js): admin-Mitglieder
  ÜBERSCHREIBEN jetzt gleichnamige statische Einträge (per slug) statt
  sie zu duplizieren -- eine Bearbeitung wirkt dadurch für alle.
- window.dogiSiteTexteLaden() + applyTranslations() erweitert um
  data-site-text-key -- Kategorie-Seitentitel (team-modis.html/
  team-scouts.html/creator.html/manager.html) sind jetzt live editierbar.
- Verwaltung: "📥 Bestehende Mitglieder importieren"-Knopf (holt VanVan/
  Diene/Funny/Miss/Marina/Ghost/Patrick/Bananenstift aus den data-*.js-
  Dateien, VanVan bewusst ausgenommen -- eigene Sonderkarte + Seite),
  erweitertes Formular (Intro/ausführliche Vorstellung/zweiter Button,
  eingeklappt unter "Erweitert"), neue Sektion "Kategorien &
  Seitentitel" (4 Karten, sofort wirksam auf Website UND Verwaltung).

Beim Testen einen echten UX-Bug gefunden und gefixt: die "Gespeichert"-
Meldung nach dem Speichern eines Mitglieds wurde von der direkt
anschließenden Formular-Zurücksetzung sofort wieder überschrieben und
war nie sichtbar.

Cache-Busting-Version auf 20260821k erhöht.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 03:47:51 +02:00

96 lines
4.3 KiB
JavaScript

/* =====================================================================
routes/site-texts.js — admin-editierbare Kategorie-Namen & Seitentitel
(21.08.2026, Nutzer-Wunsch: "ich will ich titel und namen von
kategorien und seiten ändern können in der verwaltung").
Bewusst als generischer key->{de,"de-CH",en,fr,pt}-Override gebaut
(wiederverwendet die vorhandene app_settings-Tabelle, key-Präfix
"site_text_"), NICHT als eigene Tabelle -- dieselbe Technik wie
"Event des Jahres". Ein fester, serverseitiger Katalog erlaubter
Keys (ERLAUBTE_KEYS) verhindert, dass beliebige/kaputte Keys
gespeichert werden können. Aktuell nur die vier Team-Kategorien +
ihre Seitentitel belegt -- das Muster ist aber bewusst so gebaut,
dass in einer künftigen Runde weitere Seiten/Überschriften einfach
per neuem Key ergänzt werden können (siehe main.js:
applyTranslations() prüft data-site-text-key genau wie data-i18n).
Öffentlich lesbar (jede Seite braucht das ohne Login), Schreiben nur
mit TEAM_MANAGE (dieselbe Berechtigung wie die Team-Verwaltung,
bewusst keine eigene -- beides gehört zusammen). ===================================================================== */
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";
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);
}
// key -> { label, maxLen } -- label ist nur für Log-/Fehlermeldungen.
const ERLAUBTE_KEYS = {
kategorie_modi: { label: "Kategorie-Name: Modi", maxLen: 40 },
kategorie_scout: { label: "Kategorie-Name: Scout", maxLen: 40 },
kategorie_creator: { label: "Kategorie-Name: Creator", maxLen: 40 },
kategorie_manager: { label: "Kategorie-Name: Manager", maxLen: 40 },
seitentitel_modi: { label: "Seitentitel: Modi-Team", maxLen: 100 },
seitentitel_scout: { label: "Seitentitel: Scout-Team", maxLen: 100 },
seitentitel_creator: { label: "Seitentitel: Creator", maxLen: 100 },
seitentitel_manager: { label: "Seitentitel: Manager", maxLen: 100 },
};
function settingsKey(key) {
return `site_text_${key}`;
}
/** Öffentlich, keine Session: liefert alle aktuell GESETZTEN Overrides
als { key: {de,"de-CH",en,fr,pt} }. Keys ohne eigenen Override
fehlen einfach im Ergebnis -- die aufrufende Seite behält dann ihren
eingebauten Standardtext (siehe main.js). */
export async function getSiteTextsPublic(req, res) {
const rows = db
.prepare(`SELECT key, value FROM app_settings WHERE key LIKE 'site_text_%'`)
.all();
const texte = {};
for (const row of rows) {
const kurzKey = row.key.replace(/^site_text_/, "");
try {
texte[kurzKey] = JSON.parse(row.value);
} catch {
/* kaputter Eintrag -> einfach überspringen, kein Fehler für alle anderen Keys */
}
}
return json(res, { ok: true, texte });
}
export async function saveSiteText(req, res, session) {
if (!can(session, "TEAM_MANAGE")) return forbidden(res);
const body = req.body || {};
const key = String(body.key || "").trim();
const eintrag = ERLAUBTE_KEYS[key];
if (!eintrag) return json(res, { ok: false, error: "Unbekannter Text-Schlüssel." }, 400);
const textDe = String(body.text || "").trim().slice(0, eintrag.maxLen);
if (!textDe) {
// Leeres Feld = Override entfernen -> Seite fällt zurück auf ihren
// eingebauten Standardtext (Dogi kann so jederzeit "zurücksetzen").
db.prepare(`DELETE FROM app_settings WHERE key = ?`).run(settingsKey(key));
logAction(session.actor, "site_text.reset", key, null);
return json(res, { ok: true, text: null });
}
const text = await uebersetzeAlleSprachen(textDe);
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(settingsKey(key), JSON.stringify(text), new Date().toISOString());
logAction(session.actor, "site_text.saved", key, { textDe });
return json(res, { ok: true, text });
}