diff --git a/README.md b/README.md
index 10d335f..e2bf816 100644
--- a/README.md
+++ b/README.md
@@ -85,6 +85,17 @@ Share Tech Mono, selbst gehostet).
### Module
+Jede Funktion hat **ihre eigene Seite**: ein Klick auf ihre Karte zeigt Schalter,
+Kanäle und Rollen, ihre Texte, ihre Werte und den Weg zum passenden Werkzeug —
+statt alles über vier Bereiche verteilt. Für „Willkommen" also Kanal, Karte,
+Farben, Vorschau und Begrüßungstext an einem Ort. Die Adresse merkt sich die
+Seite (`/settings#module/welcome`).
+
+Gebaut wird sie aus den Registern: [`src/modules.js`](src/modules.js) kennt die
+Felder, [`src/templates.js`](src/templates.js) die Texte, [`src/tuning.js`](src/tuning.js)
+die Werte — jeweils über `module` verknüpft. Eine neue Funktion bekommt ihre
+Seite dadurch geschenkt.
+
Alle **35 Funktionen** lassen sich im Tab *Module* einzeln ein- und ausschalten —
gruppiert nach Inhalte, Community, Moderation und Server. Jede Karte zeigt, ob das
Modul einsatzbereit ist oder noch etwas fehlt (z. B. ein Kanal), und verlinkt direkt
diff --git a/frontend/src/components/ModulePage.jsx b/frontend/src/components/ModulePage.jsx
new file mode 100644
index 0000000..de9bc9a
--- /dev/null
+++ b/frontend/src/components/ModulePage.jsx
@@ -0,0 +1,342 @@
+// Die Seite einer einzelnen Funktion: Schalter, ihre Kanäle und Felder, ihre
+// Texte, ihre Werte — alles an einem Ort statt über vier Bereiche verteilt.
+//
+// Vorbild ist die Plugin-Seite von MEE6. Die Felder werden aus den Registern
+// gerendert (modules.js, templates.js, tuning.js), nicht von Hand gebaut —
+// eine neue Funktion bekommt ihre Seite dadurch geschenkt.
+import { useEffect, useState } from 'react';
+import { apiGet, apiPut, apiPost } from '../api.js';
+import { moduleIcon } from '../module-icons.jsx';
+import { IconArrowLeft, IconWarning, IconCheck, IconRefresh, IconChevronDown } from '../icons.jsx';
+
+/** Ein Feld aus dem Register — Art entscheidet über die Darstellung */
+function Feld({ feld, value, onChange, PickSelect, channelItems, roleItems }) {
+ const gemeinsam = { id: `feld-${feld.setting}` };
+
+ if (feld.art === 'kanal' || feld.art === 'rolle') {
+ return (
+
Diese Funktion konnte nicht geladen werden.
; + if (!detail) returnLade …
; + + const Icon = moduleIcon(detail.id); + + async function textSpeichern(textId, wert) { + try { + const res = await apiPut(`/api/texts/${encodeURIComponent(textId)}`, { text: wert }); + setDetail((d) => ({ ...d, texte: res.templates.filter((t) => t.module === d.id) })); + setTextEntwurf(({ [textId]: _weg, ...rest }) => rest); + flash(wert.trim() ? '✓ Text gespeichert' : '✓ Auf den Standard zurückgesetzt'); + } catch { + flash('✗ Speichern fehlgeschlagen'); + } + } + + async function werteSpeichern() { + const geaendert = Object.fromEntries( + Object.entries(werteEntwurf).filter(([wid, v]) => { + const jetzt = detail.werte.find((w) => w.id === wid); + return jetzt && String(v) !== String(jetzt.value); + }) + ); + if (Object.keys(geaendert).length === 0) return; + try { + const res = await apiPut('/api/tuning', geaendert); + setDetail((d) => ({ ...d, werte: res.values.filter((w) => w.module === d.id) })); + setWerteEntwurf({}); + flash('✓ Werte gespeichert'); + } catch (error) { + flash(`✗ ${error.body?.error ?? 'Speichern fehlgeschlagen'}`); + } + } + + async function anlegen(setting) { + try { + const res = await apiPost('/api/setup/create', { setting }); + setForm(res.settings); + laden(); + flash(res.wiederverwendet ? `✓ „${res.name}" verknüpft` : `✓ „${res.name}" angelegt`); + } catch (error) { + flash(`✗ ${error.body?.error ?? 'Anlegen fehlgeschlagen'}`); + } + } + + const werteOffen = detail.werte.some( + (w) => werteEntwurf[w.id] !== undefined && String(werteEntwurf[w.id]) !== String(w.value) + ); + + return ( +{detail.desc}
+
+
+
+ Das Eigentliche wird in einem eigenen Bereich gepflegt — dort ist mehr Platz dafür. +
+ +@@ -2387,11 +2411,17 @@ export default function Settings({ me }) { return (
- Alles, was der Bot nach außen schreibt. Zeile anklicken zum Bearbeiten — + Alle Texte am Stück — wer nur die einer Funktion sucht, findet sie + auch auf deren Modul-Seite. Zeile anklicken zum Bearbeiten — die Bausteine in geschweiften Klammern setzt der Bot beim Senden ein, ein Klick darauf fügt sie an der Cursor-Position ein. Ein leeres Feld bedeutet: wieder der Standardtext. @@ -2541,7 +2565,8 @@ export default function Settings({ me }) {
- Zahlen, die das Verhalten steuern — wie viel XP eine Nachricht bringt, + Alle Werte am Stück; jeder steht zusätzlich auf der Seite seiner + Funktion. Zahlen, die das Verhalten steuern — wie viel XP eine Nachricht bringt, wie oft geprüft wird, wann gepostet wird. Geänderte Intervalle greifen ab dem nächsten Durchlauf, ohne Neustart. Neben jedem Feld steht der erlaubte Bereich; angepasste Werte lassen sich einzeln zurücksetzen. diff --git a/frontend/src/style.css b/frontend/src/style.css index 95838e1..00f7593 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -726,6 +726,55 @@ a.sha:hover { border-color: var(--neon); background: rgba(245, 197, 24, .07); } .btn-create:hover:not(:disabled) { background: rgba(245, 197, 24, .1); } .btn-create:disabled { opacity: .4; cursor: default; } +/* ── MODUL-SEITE ───────────────────────────────────── */ +/* Name und Symbol auf der Karte öffnen die Seite der Funktion */ +.mod-open { + display: flex; align-items: flex-start; gap: .8rem; + flex: 1; min-width: 0; text-align: left; + background: none; border: 0; padding: 0; cursor: pointer; +} +.mod-open:hover .mod-name { color: var(--neon); } + +.modpage-back { + background: none; border: 0; padding: 0; margin-bottom: 1.2rem; + font-family: var(--mono); font-size: .62rem; + letter-spacing: .15em; text-transform: uppercase; + color: var(--muted2); cursor: pointer; + transition: color .15s; +} +.modpage-back:hover { color: var(--neon); } + +.modpage-head { + display: flex; align-items: flex-start; gap: 1rem; + padding-bottom: 1.2rem; margin-bottom: 1.2rem; + border-bottom: 1px solid var(--border); +} +.modpage-icon { + flex: none; display: inline-flex; align-items: center; justify-content: center; + width: 3rem; height: 3rem; border-radius: 11px; + background: var(--bg3); border: 1px solid rgba(245, 197, 24, .3); + color: var(--neon); +} +.modpage-titles { flex: 1; min-width: 0; } +.modpage-name { + font-family: var(--display); font-size: 1.7rem; + letter-spacing: .04em; color: var(--text); margin: 0 0 .2rem; +} +.modpage-desc { color: var(--muted); font-weight: 300; line-height: 1.5; margin: 0; } + +/* Abschnitte innerhalb der Seite — flacher als die großen Bereichs-Karten */ +.modpage-block { margin-bottom: 1.8rem; } +.modpage-block:last-child { margin-bottom: 0; } +.modpage-block > .settings-title { + font-size: .66rem; color: var(--muted); + margin-bottom: .9rem; +} +.modpage-block > .settings-title::after { + background: linear-gradient(90deg, rgba(255, 255, 255, .12), transparent); +} +/* Die Seite ist schmaler als das zweispaltige Raster — Formulare lesen sich so besser */ +.modpage { max-width: 60rem; } + /* Grundgerüst-Assistent: Vorschau und Ergebnis */ .setup-plan { border: 1px solid var(--border); border-radius: 12px; diff --git a/src/modules.js b/src/modules.js index 23f59bd..1085524 100644 --- a/src/modules.js +++ b/src/modules.js @@ -25,12 +25,19 @@ export const MODULES = [ desc: 'Entwicklungs-Berichte als gebrandetes Embed posten und durchsuchbar archivieren.', tab: 'feeds', default: true, requires: [{ setting: 'devlog_channel_id', label: 'Devlog-Kanal', create: { name: 'dev-log', topic: 'Entwicklungs-Updates vom Bot' } }], + fields: [ + { setting: 'devlog_ping_role_id', label: 'Ping-Rolle', art: 'rolle', hint: 'wird bei jedem Devlog erwähnt; leer = kein Ping' }, + ], }, { id: 'commit_feed', group: 'inhalte', name: 'Commit-Feed', desc: 'Pushes aus Gitea landen als Embed im Kanal. Archiviert wird unabhängig davon immer.', tab: 'feeds', setting: 'commit_feed_enabled', defaultOn: true, requires: [{ setting: 'commit_channel_id', label: 'Commit-Kanal', create: { name: 'commits', privat: true, topic: 'Pushes aus Gitea' } }], + fields: [ + { setting: 'commit_branch_filter', label: 'Branch-Filter', art: 'text', hint: 'kommagetrennt, leer = alle' }, + { setting: 'ignored_repos', label: 'Ignorierte Repos', art: 'text', hint: 'kommagetrennt, volle Namen' }, + ], }, { id: 'releases', group: 'inhalte', name: 'Release-Ankündigungen', @@ -54,6 +61,10 @@ export const MODULES = [ desc: 'Meldet Livestreams und neue Videos.', tab: 'system', default: true, requires: [{ setting: 'social_announce_channel_id', label: 'Social-Kanal', create: { name: 'streams', topic: 'Twitch- und YouTube-Meldungen' } }], + fields: [ + { setting: 'twitch_channel', label: 'Twitch-Kanal', art: 'text', hint: 'Login-Name, z. B. d4rkst3r' }, + { setting: 'youtube_channel_id', label: 'YouTube-Kanal', art: 'text', hint: 'Kanal-ID (UC…)' }, + ], }, /* ── Community ────────────────────────────────── */ @@ -61,12 +72,19 @@ export const MODULES = [ id: 'levels', group: 'community', name: 'Level-System', desc: 'XP fürs Mitreden, Rollen-Belohnungen und öffentliche Bestenliste.', tab: 'community', setting: 'levels_enabled', defaultOn: false, + fields: [ + { setting: 'levels_announce', label: 'Level-Up im Kanal feiern', art: 'schalter' }, + { setting: 'level_rewards', label: 'Rollen-Belohnungen', art: 'text', hint: 'Level=Rollen-ID, kommagetrennt — z. B. 5=1234, 10=5678' }, + ], }, { id: 'starboard', group: 'community', name: 'Starboard', desc: 'Nachrichten mit genug Sternen landen in einem Best-of-Kanal.', tab: 'community', default: true, requires: [{ setting: 'starboard_channel_id', label: 'Starboard-Kanal', create: { name: 'best-of', topic: 'Nachrichten mit genug Sternen' } }], + fields: [ + { setting: 'starboard_threshold', label: 'Schwellwert', art: 'zahl', hint: 'so viele ⭐ braucht eine Nachricht' }, + ], }, { id: 'gallery', group: 'community', name: 'Screenshot-Galerie', @@ -90,6 +108,9 @@ export const MODULES = [ desc: 'Gratulation am großen Tag, optional mit Tages-Rolle.', tab: 'community', default: true, requires: [{ setting: 'birthday_channel_id', label: 'Geburtstags-Kanal', create: { name: 'geburtstage', topic: 'Der Bot gratuliert hier' } }], + fields: [ + { setting: 'birthday_role_id', label: 'Tages-Rolle', art: 'rolle', hint: 'wird am nächsten Morgen wieder entfernt' }, + ], }, { id: 'events', group: 'community', name: 'Event-Ankündigungen', @@ -102,21 +123,25 @@ export const MODULES = [ desc: 'Bewerbungs-Knopf, Rolle und Teilnehmerliste.', tab: 'community', default: true, requires: [{ setting: 'playtester_role_id', label: 'Playtester-Rolle', create: { rolle: true, name: 'Playtester', farbe: '#23a55a' } }], + tool: { tab: 'community', label: 'Playtester-Liste' }, }, { id: 'alpha_keys', group: 'community', name: 'Alpha-Keys', desc: 'Schlüssel-Pool mit Verteilung per Direktnachricht.', tab: 'community', default: true, + tool: { tab: 'community', label: 'Schlüssel-Pool' }, }, { id: 'role_menus', group: 'community', name: 'Rollen-Menüs', desc: 'Selbstbedienungs-Rollen per Knopf — im Discord und im Profil.', tab: 'rollen', default: true, + tool: { tab: 'rollen', label: 'Rollen-Menüs bauen' }, }, { id: 'applications', group: 'community', name: 'Bewerbungen', desc: 'Formulare als Eingabefenster, Prüfung mit Annehmen/Ablehnen.', tab: 'bewerbungen', default: true, + tool: { tab: 'bewerbungen', label: 'Formulare bearbeiten' }, }, /* ── Moderation & Support ─────────────────────── */ @@ -124,6 +149,7 @@ export const MODULES = [ id: 'moderation', group: 'moderation', name: 'Moderation', desc: 'Verwarnen, Auszeiten, Aufräumen — mit Verlauf und Protokoll.', tab: 'support', default: true, + tool: { tab: 'support', label: 'Moderation' }, }, { id: 'modlog', group: 'moderation', name: 'Protokoll', @@ -148,6 +174,18 @@ export const MODULES = [ desc: 'Begrüßung neuer Mitglieder mit gerendertem Bild.', tab: 'support', default: true, requires: [{ setting: 'welcome_channel_id', label: 'Willkommens-Kanal', create: { name: 'willkommen', topic: 'Begruessung neuer Mitglieder' } }], + fields: [ + { setting: 'welcome_card_enabled', label: 'Karte rendern', art: 'schalter', hint: 'aus = nur das Text-Embed' }, + { setting: 'welcome_card_kicker', label: 'Kleine Zeile', art: 'text', hint: 'über dem Namen — leer = weglassen' }, + { setting: 'welcome_card_sub', label: 'Zeile darunter', art: 'text', hint: '{count} wird zur Mitglieds-Nummer' }, + { setting: 'welcome_card_image', label: 'Hintergrundbild', art: 'text', hint: 'https-Adresse, 900×300 passt genau' }, + { setting: 'welcome_card_bg', label: 'Hintergrund', art: 'farbe' }, + { setting: 'welcome_card_accent', label: 'Akzent 1', art: 'farbe' }, + { setting: 'welcome_card_accent2', label: 'Akzent 2', art: 'farbe' }, + { setting: 'welcome_card_text', label: 'Name', art: 'farbe' }, + { setting: 'welcome_card_watermark', label: 'Markenname im Hintergrund', art: 'schalter' }, + ], + vorschau: 'welcome', }, { id: 'autorole', group: 'moderation', name: 'Auto-Rolle', @@ -167,6 +205,9 @@ export const MODULES = [ // System-Bereich — sonst landet „Einrichten" an der falschen Stelle. tab: 'brand', default: true, requires: [{ setting: 'gitea_api_token', label: 'Gitea-Token' }], + fields: [ + { setting: 'bug_report_repo', label: 'Ziel-Repository', art: 'text', hint: 'Besitzer/Name, z. B. D4rkst3r/EcoGame' }, + ], }, /* ── Server & Technik ─────────────────────────── */ @@ -174,12 +215,20 @@ export const MODULES = [ id: 'server_monitor', group: 'server', name: 'Server-Monitor', desc: 'Live-Status der Game-Server mit Verlauf und Ausfall-Alarm.', tab: 'server', default: true, + fields: [ + { setting: 'status_channel_id', label: 'Status-Kanal', art: 'kanal', hint: 'hier steht das Live-Embed' }, + { setting: 'server_alert_channel_id', label: 'Alarm-Kanal', art: 'kanal', hint: '🚨 down / ✅ wieder online' }, + ], + tool: { tab: 'server', label: 'Game-Server verwalten' }, }, { id: 'watchdog', group: 'server', name: 'Erreichbarkeits-Wächter', desc: 'Prüft hinterlegte Adressen und meldet Ausfälle per Direktnachricht.', tab: 'system', default: true, requires: [{ setting: 'watchdog_urls', label: 'Adressen' }], + fields: [ + { setting: 'watchdog_urls', label: 'Zu prüfende Adressen', art: 'text', hint: 'kommagetrennt, mit http(s)' }, + ], }, { id: 'temp_voice', group: 'server', name: 'Temporäre Sprachkanäle', @@ -191,6 +240,7 @@ export const MODULES = [ id: 'triggers', group: 'server', name: 'Auto-Antworten', desc: 'Der Bot antwortet auf hinterlegte Schlüsselwörter.', tab: 'composer', default: true, + tool: { tab: 'composer', label: 'Auto-Antworten pflegen' }, }, { id: 'reminders', group: 'server', name: 'Erinnerungen', @@ -201,16 +251,21 @@ export const MODULES = [ id: 'scheduled_posts', group: 'server', name: 'Geplante Beiträge', desc: 'Nachrichten zu festen Zeiten, einmalig oder wiederkehrend.', tab: 'composer', default: true, + tool: { tab: 'composer', label: 'Geplante Beiträge' }, }, { id: 'tags', group: 'server', name: 'Textbausteine', desc: 'Gespeicherte Texte per Befehl abrufen.', tab: 'composer', default: true, + tool: { tab: 'composer', label: 'Textbausteine' }, }, { id: 'backups', group: 'server', name: 'Datenbank-Sicherung', desc: 'Nächtliche Sicherung mit Aufbewahrung über 14 Tage.', tab: 'system', setting: 'backup_enabled', defaultOn: true, + fields: [ + { setting: 'backup_channel_id', label: 'Kanal für die Sicherung', art: 'kanal', hint: 'leer = nur lokal' }, + ], }, { id: 'repo_backups', group: 'server', name: 'Repository-Sicherung', @@ -221,6 +276,9 @@ export const MODULES = [ id: 'member_gate', group: 'server', name: 'Login nur für Mitglieder', desc: 'Nur wer auf dem Discord-Server ist, kann sich im Web anmelden.', tab: 'system', setting: 'member_gate_enabled', defaultOn: true, + fields: [ + { setting: 'discord_invite_url', label: 'Einladungs-Link', art: 'text', hint: 'bekommt, wer nicht auf dem Server ist' }, + ], }, ]; @@ -270,6 +328,38 @@ export function creationPlan(setting) { return null; } +/** + * Alles, was zu einem Modul gehört — für seine eigene Seite im Panel. + * Bringt Kanäle/Rollen, Zusatzfelder und den Verweis auf ein Werkzeug + * zusammen; Texte und Werte holt die API aus ihren eigenen Registern. + */ +export function moduleDetail(id) { + const mod = MODULES.find((m) => m.id === id); + if (!mod) return null; + return { + id: mod.id, + name: mod.name, + desc: mod.desc, + group: mod.group, + tab: mod.tab ?? null, + enabled: moduleEnabled(mod.id), + missing: moduleMissing(mod), + // Voraussetzungen zuerst — ohne sie tut das Modul nichts + felder: [ + ...(mod.requires ?? []).map((r) => ({ + setting: r.setting, + label: r.label, + art: r.create?.rolle ? 'rolle' : 'kanal', + hint: null, + pflicht: true, + })), + ...(mod.fields ?? []).map((f) => ({ ...f, hint: f.hint ?? null, pflicht: false })), + ], + tool: mod.tool ?? null, + vorschau: mod.vorschau ?? null, + }; +} + /** Alle Module mit Zustand — für das Webinterface */ export function moduleStates() { return MODULES.map((mod) => ({ diff --git a/src/tuning.js b/src/tuning.js index f9523fa..8588d43 100644 --- a/src/tuning.js +++ b/src/tuning.js @@ -1,6 +1,9 @@ // Stellwerte: alle Zahlen, die vorher fest im Code standen — XP-Beträge, // Wartezeiten, Prüf-Intervalle, Obergrenzen. // +// `module` verbindet einen Wert mit seiner Funktion — die Modul-Seite zeigt ihn +// dann dort, wo er hingehört, statt nur in der flachen Werte-Liste. +// // Wie bei den Modulen und Vorlagen lebt der Standard im Code und die // Datenbank enthält nur Abweichungen. `min`/`max` sind keine Kosmetik: sie // verhindern, dass ein Vertipper den Bot in eine Prüfschleife im @@ -17,94 +20,94 @@ export const TUNING_GROUPS = [ export const TUNING = [ /* ── Level & Aktivität ────────────────────────────── */ { - id: 'xp_min', group: 'level', label: 'XP pro Nachricht — mindestens', + id: 'xp_min', module: 'levels', group: 'level', label: 'XP pro Nachricht — mindestens', unit: 'XP', default: 15, min: 1, max: 500, }, { - id: 'xp_max', group: 'level', label: 'XP pro Nachricht — höchstens', + id: 'xp_max', module: 'levels', group: 'level', label: 'XP pro Nachricht — höchstens', hint: 'Der Bot würfelt zwischen beiden Werten.', unit: 'XP', default: 25, min: 1, max: 500, }, { - id: 'xp_cooldown', group: 'level', label: 'Wartezeit zwischen zwei XP-Gutschriften', + id: 'xp_cooldown', module: 'levels', group: 'level', label: 'Wartezeit zwischen zwei XP-Gutschriften', hint: 'Verhindert, dass Vielschreiber das Level-System leerlaufen lassen.', unit: 'Sekunden', default: 60, min: 5, max: 3600, }, { - id: 'trigger_cooldown', group: 'level', label: 'Wartezeit für Auto-Antworten', + id: 'trigger_cooldown', module: 'triggers', group: 'level', label: 'Wartezeit für Auto-Antworten', hint: 'Pro Schlüsselwort und Kanal — sonst antwortet der Bot sich in Grund und Boden.', unit: 'Sekunden', default: 30, min: 5, max: 3600, }, /* ── Wann der Bot postet ──────────────────────────── */ { - id: 'birthday_hour', group: 'zeiten', label: 'Geburtstags-Gratulation ab', + id: 'birthday_hour', module: 'birthdays', group: 'zeiten', label: 'Geburtstags-Gratulation ab', hint: 'Uhrzeit in Europe/Berlin. Gratuliert wird einmal am Tag.', unit: 'Uhr', default: 9, min: 0, max: 23, }, { - id: 'recap_weekday', group: 'zeiten', label: 'Wochen-Rückblick am', + id: 'recap_weekday', module: 'weekly_recap', group: 'zeiten', label: 'Wochen-Rückblick am', hint: '0 = Sonntag, 1 = Montag … 6 = Samstag.', unit: 'Wochentag', default: 0, min: 0, max: 6, }, { - id: 'recap_hour', group: 'zeiten', label: 'Wochen-Rückblick ab', + id: 'recap_hour', module: 'weekly_recap', group: 'zeiten', label: 'Wochen-Rückblick ab', unit: 'Uhr', default: 20, min: 0, max: 23, }, /* ── Prüf-Intervalle ──────────────────────────────── */ { - id: 'monitor_interval', group: 'pruefung', label: 'Game-Server prüfen alle', + id: 'monitor_interval', module: 'server_monitor', group: 'pruefung', label: 'Game-Server prüfen alle', unit: 'Minuten', default: 2, min: 1, max: 120, }, { - id: 'monitor_timeout', group: 'pruefung', label: 'Game-Server — Antwort abwarten', + id: 'monitor_timeout', module: 'server_monitor', group: 'pruefung', label: 'Game-Server — Antwort abwarten', unit: 'Sekunden', default: 8, min: 2, max: 60, }, { - id: 'monitor_fails', group: 'pruefung', label: 'Game-Server — Alarm nach', + id: 'monitor_fails', module: 'server_monitor', group: 'pruefung', label: 'Game-Server — Alarm nach', hint: 'Fehlversuchen in Folge. 1 meldet jeden Aussetzer, höher meldet nur echte Ausfälle.', unit: 'Fehlversuchen', default: 2, min: 1, max: 10, }, { - id: 'watchdog_interval', group: 'pruefung', label: 'Adressen prüfen alle', + id: 'watchdog_interval', module: 'watchdog', group: 'pruefung', label: 'Adressen prüfen alle', unit: 'Minuten', default: 2, min: 1, max: 120, }, { - id: 'watchdog_timeout', group: 'pruefung', label: 'Adressen — Antwort abwarten', + id: 'watchdog_timeout', module: 'watchdog', group: 'pruefung', label: 'Adressen — Antwort abwarten', unit: 'Sekunden', default: 10, min: 2, max: 60, }, { - id: 'watchdog_fails', group: 'pruefung', label: 'Adressen — Alarm nach', + id: 'watchdog_fails', module: 'watchdog', group: 'pruefung', label: 'Adressen — Alarm nach', unit: 'Fehlversuchen', default: 2, min: 1, max: 10, }, { - id: 'social_interval', group: 'pruefung', label: 'Twitch & YouTube prüfen alle', + id: 'social_interval', module: 'social', group: 'pruefung', label: 'Twitch & YouTube prüfen alle', hint: 'Zu kurz bringt nichts — YouTube-Feeds aktualisieren sich ohnehin nur alle paar Minuten.', unit: 'Minuten', default: 5, min: 1, max: 180, }, { - id: 'birthday_interval', group: 'pruefung', label: 'Geburtstage prüfen alle', + id: 'birthday_interval', module: 'birthdays', group: 'pruefung', label: 'Geburtstage prüfen alle', unit: 'Minuten', default: 15, min: 1, max: 120, }, /* ── Obergrenzen ──────────────────────────────────── */ { - id: 'ticket_transcript_max', group: 'grenzen', label: 'Nachrichten im Ticket-Protokoll', + id: 'ticket_transcript_max', module: 'tickets', group: 'grenzen', label: 'Nachrichten im Ticket-Protokoll', hint: 'Ältere fallen weg. Hoch gesetzt dauert das Schließen länger.', unit: 'Nachrichten', default: 500, min: 50, max: 2000, }, { - id: 'gallery_max_images', group: 'grenzen', label: 'Bilder je Galerie-Beitrag', + id: 'gallery_max_images', module: 'gallery', group: 'grenzen', label: 'Bilder je Galerie-Beitrag', unit: 'Bilder', default: 4, min: 1, max: 10, }, { - id: 'commits_shown', group: 'grenzen', label: 'Commits je Push-Embed', + id: 'commits_shown', module: 'commit_feed', group: 'grenzen', label: 'Commits je Push-Embed', hint: 'Der Rest wird als „… und N weitere" zusammengefasst.', unit: 'Commits', default: 10, min: 1, max: 20, }, { - id: 'thread_archive_days', group: 'grenzen', label: 'Threads archivieren nach', + id: 'thread_archive_days', module: 'tickets', group: 'grenzen', label: 'Threads archivieren nach', hint: 'Für Ticket- und Modmail-Threads. Discord erlaubt 1, 3 oder 7 Tage.', unit: 'Tagen', default: 7, min: 1, max: 7, choices: [1, 3, 7], }, @@ -167,7 +170,8 @@ export function tuningStates() { return TUNING.map((t) => { const custom = getSetting(`tune_${t.id}`); return { - id: t.id, label: t.label, hint: t.hint ?? null, group: t.group, + id: t.id, label: t.label, hint: t.hint ?? null, + group: t.group, module: t.module ?? null, unit: t.unit, min: t.min, max: t.max, choices: t.choices ?? null, default: t.default, diff --git a/src/web/api.js b/src/web/api.js index 839f52d..ee8cdbc 100644 --- a/src/web/api.js +++ b/src/web/api.js @@ -21,7 +21,7 @@ import { savePage, getPage, listPages, listPublishedPages, deletePage, } from '../db.js'; import crypto from 'node:crypto'; -import { moduleStates, setModuleEnabled, MODULE_GROUPS, MODULES } from '../modules.js'; +import { moduleStates, moduleDetail, setModuleEnabled, MODULE_GROUPS, MODULES } from '../modules.js'; import { templateStates, setTemplate, TEMPLATE_GROUPS } from '../templates.js'; import { tuningStates, setTuning, TUNING_GROUPS } from '../tuning.js'; import { lastResults, GAME_ICONS } from '../bot/server-monitor.js'; @@ -193,6 +193,19 @@ export function registerApiRoutes(app, client) { return { modules: moduleStates(), groups: MODULE_GROUPS }; }); + // Alles zu einer Funktion an einem Ort: ihre Kanäle und Felder, ihre Texte, + // ihre Werte. Genau das zeigt die Modul-Seite. + app.get('/api/modules/:id', async (request, reply) => { + if (requireAnyScope(request, reply)) return; + const detail = moduleDetail(String(request.params.id)); + if (!detail) return reply.code(404).send({ error: 'Modul unbekannt' }); + return { + ...detail, + texte: templateStates().filter((t) => t.module === detail.id), + werte: tuningStates().filter((w) => w.module === detail.id), + }; + }); + app.put('/api/modules/:id', async (request, reply) => { if (requireScope(request, reply, 'settings')) return; const id = String(request.params.id);