Playtester und Alpha-Keys standen als zwei Kaesten mitten im Community-Bereich zwischen Geburtstagen und Starboard. Die Liste war reine Anzeige: Namen und Datum, sonst nichts. Wer einem Einzelnen einen Schluessel geben wollte, konnte nur "an alle verteilen" druecken, und wer jemanden wieder rausnehmen wollte, musste an die Datenbank. Jetzt ist es ein eigener Bereich mit drei Zahlen oben und einer Liste, in der neben jedem Namen sein Schluessel steht. Daneben, je nach Zustand, "Schluessel geben" oder "Zurueckziehen", und ein ✕ zum Entfernen — die Rolle geht dabei mit, denn nur aus der Liste zu streichen und die Rolle stehen zu lassen waere eine Halbwahrheit. Der Vorrat ist jetzt sichtbar, nicht nur seine Groesse: einen vertippten Schluessel findet man sonst nie wieder. Nachlegen und "wer bekommt einen?" laufen ueber Dialoge, damit der Bereich nicht zumuellt — dafuer gibt es jetzt eine kleine Dialog-Komponente, gebaut wie die Lightbox (Portal, Escape, Hintergrund festhalten). Zurueckziehen legt den Schluessel zurueck in den Vorrat. Die Person hat ihn per DM natuerlich weiterhin — das ist eine Buchhaltung, keine Sperre. Loeschen geht absichtlich nur bei freien Schluesseln. ## Module dorthin, wo man sie sucht Ein paar Module standen im falschen Bereich, teils seit Langem: - Willkommens-Karte: Support → Brand. Sie ist Aussehen, kein Support-Fall, und im Brand-Bereich steht der Rest vom Auftritt. Der Block ist woertlich umgezogen, nur in eine eigene Konstante — so kann sich beim Umzug nichts am Inhalt geaendert haben. - Twitch & YouTube: System → Feeds. Es ist ein Feed. - Events: System → Community. - Auto-Rolle und Rollen merken: Ihre Felder standen laengst im Rollen-Bereich, nur das Register sagte "Community" — dadurch sprang "Einrichten" woanders hin als die Einstellung liegt. - Playtester und Alpha-Keys: Community → eigener Bereich. Damit ist System kein Sammelbecken mehr, sondern nur noch Technik: Waechter, Sprachkanaele, Erinnerungen, Sicherungen, Member-Gate. Geprueft: Routen (alle vier neuen bewacht), SQL gegen das Schema, und im Browser beide Dialoge (Escape schliesst, Hintergrund wird wieder freigegeben, Hinzufuegen bleibt bei leerem Feld aus), die Liste in allen drei Zustaenden, und die Suche — "willkommen" fuehrt jetzt nach Brand, "twitch" zu Feeds, "sticky" zu Rollen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
434 lines
20 KiB
JavaScript
434 lines
20 KiB
JavaScript
// Modul-Register: eine Stelle, an der alle Funktionen des Bots stehen.
|
||
//
|
||
// Jedes Modul hat einen Schalter im Webinterface. Damit nichts kaputtgeht,
|
||
// nutzen Funktionen mit einem bereits vorhandenen Schalter (etwa das
|
||
// Level-System) weiterhin denselben Einstellungs-Namen — es entsteht also
|
||
// keine zweite Wahrheit.
|
||
//
|
||
// `requires` beschreibt, was gesetzt sein muss, damit ein Modul wirklich
|
||
// arbeiten kann. Fehlt etwas, zeigt das Panel „noch nicht einsatzbereit"
|
||
// statt das Modul stillschweigend nichts tun zu lassen.
|
||
import { getSetting, setSetting } from './db.js';
|
||
import { effectiveSetting } from './runtime-settings.js';
|
||
|
||
export const MODULE_GROUPS = [
|
||
{ id: 'inhalte', label: 'Inhalte & Feeds' },
|
||
{ id: 'community', label: 'Community' },
|
||
{ id: 'moderation', label: 'Moderation & Support' },
|
||
{ id: 'server', label: 'Server & Technik' },
|
||
];
|
||
|
||
export const MODULES = [
|
||
/* ── Inhalte & Feeds ──────────────────────────── */
|
||
{
|
||
id: 'devlogs', group: 'inhalte', name: 'Devlogs',
|
||
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',
|
||
desc: 'Neue Releases werden angekündigt und landen im öffentlichen Changelog.',
|
||
tab: 'feeds', default: true,
|
||
requires: [{ setting: 'release_channel_id', label: 'Release-Kanal', create: { name: 'releases', topic: 'Neue Versionen' } }],
|
||
},
|
||
{
|
||
id: 'weekly_recap', group: 'inhalte', name: 'Wochen-Rückblick',
|
||
desc: 'Sonntags um 20 Uhr eine Zusammenfassung der Woche.',
|
||
tab: 'feeds', setting: 'weekly_recap_enabled', defaultOn: true,
|
||
requires: [{ setting: 'devlog_channel_id', label: 'Devlog-Kanal', create: { name: 'dev-log', topic: 'Entwicklungs-Updates vom Bot' } }],
|
||
},
|
||
{
|
||
id: 'devlog_threads', group: 'inhalte', name: 'Devlog-Threads',
|
||
desc: 'Unter jedem Devlog entsteht ein Diskussions-Thread.',
|
||
tab: 'feeds', setting: 'devlog_threads_enabled', defaultOn: true,
|
||
},
|
||
{
|
||
id: 'social', group: 'inhalte', name: 'Twitch & YouTube',
|
||
desc: 'Meldet Livestreams und neue Videos.',
|
||
tab: 'feeds', 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 ────────────────────────────────── */
|
||
{
|
||
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',
|
||
desc: 'Bilder aus einem Kanal erscheinen in der öffentlichen Galerie.',
|
||
tab: 'community', default: true,
|
||
requires: [{ setting: 'screenshot_channel_id', label: 'Screenshot-Kanal', create: { name: 'screenshots', topic: 'Bilder landen in der Galerie' } }],
|
||
},
|
||
{
|
||
id: 'voting', group: 'community', name: 'Feature-Wünsche',
|
||
desc: 'Wünsche einreichen und abstimmen — im Discord und auf der Roadmap.',
|
||
tab: 'community', default: true,
|
||
requires: [{ setting: 'voting_channel_id', label: 'Voting-Kanal', create: { name: 'ideen', topic: 'Wuensche und Abstimmungen' } }],
|
||
},
|
||
{
|
||
id: 'polls', group: 'community', name: 'Umfragen',
|
||
desc: 'Schnelle Abstimmungen als native Discord-Umfrage — /umfrage.',
|
||
tab: 'community', default: true,
|
||
},
|
||
{
|
||
id: 'giveaways', group: 'community', name: 'Verlosungen',
|
||
desc: 'Teilnahme per Knopf, automatische Ziehung, Neuauslosung möglich.',
|
||
tab: 'community', default: true,
|
||
},
|
||
{
|
||
id: 'birthdays', group: 'community', name: 'Geburtstage',
|
||
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',
|
||
desc: 'Neue Discord-Events werden angekündigt und erscheinen auf der Events-Seite.',
|
||
tab: 'community', default: true,
|
||
requires: [{ setting: 'events_announce_channel_id', label: 'Event-Kanal', create: { name: 'events', topic: 'Playtests, Streams, Runden' } }],
|
||
},
|
||
{
|
||
id: 'playtester', group: 'community', name: 'Playtester-Programm',
|
||
desc: 'Bewerbungs-Knopf, Rolle und Teilnehmerliste.',
|
||
tab: 'playtester', default: true,
|
||
requires: [{ setting: 'playtester_role_id', label: 'Playtester-Rolle', create: { rolle: true, name: 'Playtester', farbe: '#23a55a' } }],
|
||
tool: { tab: 'playtester', label: 'Playtester-Liste' },
|
||
},
|
||
{
|
||
id: 'alpha_keys', group: 'community', name: 'Alpha-Keys',
|
||
desc: 'Schlüssel-Pool mit Verteilung per Direktnachricht.',
|
||
tab: 'playtester', default: true,
|
||
tool: { tab: 'playtester', label: 'Schlüssel-Vorrat' },
|
||
},
|
||
{
|
||
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 ─────────────────────── */
|
||
{
|
||
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: 'automod', group: 'moderation', name: 'AutoMod-Protokoll',
|
||
desc: 'Treffer von Discords eigenem Filter landen im Protokoll, Regeln sind hier schaltbar.',
|
||
tab: 'support', setting: 'automod_enabled', defaultOn: false,
|
||
requires: [{ setting: 'modlog_channel_id', label: 'Protokoll-Kanal', create: { name: 'protokoll', privat: true, topic: 'Bearbeitet, geloescht, beigetreten' } }],
|
||
},
|
||
{
|
||
id: 'anti_raid', group: 'moderation', name: 'Raid-Schutz',
|
||
desc: 'Erkennt Beitritts-Wellen und macht den Server vorübergehend dicht.',
|
||
tab: 'support', setting: 'anti_raid_enabled', defaultOn: false,
|
||
},
|
||
{
|
||
id: 'modlog', group: 'moderation', name: 'Protokoll',
|
||
desc: 'Gelöschte und bearbeitete Nachrichten, Beitritte, Namens- und Rollenwechsel.',
|
||
tab: 'support', default: true,
|
||
requires: [{ setting: 'modlog_channel_id', label: 'Protokoll-Kanal', create: { name: 'protokoll', privat: true, topic: 'Bearbeitet, geloescht, beigetreten' } }],
|
||
},
|
||
{
|
||
id: 'modmail', group: 'moderation', name: 'Modmail',
|
||
desc: 'Direktnachrichten an den Bot landen als Thread beim Team.',
|
||
tab: 'support', default: true,
|
||
requires: [{ setting: 'modmail_channel_id', label: 'Modmail-Kanal', create: { name: 'modmail', privat: true, topic: 'Direktnachrichten an den Bot' } }],
|
||
},
|
||
{
|
||
id: 'tickets', group: 'moderation', name: 'Tickets',
|
||
desc: 'Private Threads per Knopf, beim Schließen ein Gesprächsprotokoll.',
|
||
tab: 'support', default: true,
|
||
requires: [{ setting: 'ticket_channel_id', label: 'Ticket-Kanal', create: { name: 'support', topic: 'Tickets per Knopf' } }],
|
||
},
|
||
{
|
||
id: 'welcome', group: 'moderation', name: 'Willkommens-Karten',
|
||
desc: 'Begrüßung neuer Mitglieder mit gerendertem Bild.',
|
||
tab: 'brand', 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: 'Logo im Hintergrund', art: 'schalter' },
|
||
],
|
||
vorschau: 'welcome',
|
||
},
|
||
{
|
||
id: 'autorole', group: 'moderation', name: 'Auto-Rolle',
|
||
desc: 'Neue Mitglieder bekommen automatisch eine Startrolle.',
|
||
tab: 'rollen', default: true,
|
||
requires: [{ setting: 'autorole_id', label: 'Auto-Rolle', create: { rolle: true, name: 'Member' } }],
|
||
},
|
||
{
|
||
id: 'linked_roles', group: 'community', name: 'Verknüpfte Rollen',
|
||
desc: 'Rollen, die an geprüfte Werte hängen — Level, Playtester, Dauer der Mitgliedschaft.',
|
||
tab: 'rollen', setting: 'linked_roles_enabled', defaultOn: false,
|
||
},
|
||
{
|
||
id: 'sticky_roles', group: 'moderation', name: 'Rollen merken',
|
||
desc: 'Wer den Server verlässt und zurückkommt, bekommt seine Rollen wieder.',
|
||
tab: 'rollen', setting: 'sticky_roles_enabled', defaultOn: false,
|
||
},
|
||
{
|
||
id: 'bug_reports', group: 'moderation', name: 'Fehlermeldungen',
|
||
desc: 'Meldungen werden zu Issues im Repository, mit Rückmeldung beim Schließen.',
|
||
// Der Token steht im Brand-Bereich („Aus der Env verlagert"), nicht im
|
||
// 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 ─────────────────────────── */
|
||
{
|
||
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',
|
||
desc: 'Beitritt zum Hub erzeugt einen eigenen Kanal mit Bedienfeld.',
|
||
tab: 'system', default: true,
|
||
requires: [{ setting: 'tempvoice_channel_id', label: 'Hub-Kanal', create: { name: 'Eigenen Kanal erstellen', sprache: true } }],
|
||
},
|
||
{
|
||
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',
|
||
desc: 'Mitglieder lassen sich per Direktnachricht erinnern.',
|
||
tab: 'system', default: true,
|
||
},
|
||
{
|
||
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',
|
||
desc: 'Alle Gitea-Repositories als Bundle sichern.',
|
||
tab: 'system', setting: 'repo_backup_enabled', defaultOn: false,
|
||
},
|
||
{
|
||
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' },
|
||
],
|
||
},
|
||
];
|
||
|
||
/**
|
||
* Funktionen, die in der Seitenleiste stehen — die paar, die man dauernd
|
||
* anfasst. Der Rest bleibt im Raster.
|
||
*
|
||
* Der Standard ist eine Vermutung, kein Gesetz: geändert wird er über den
|
||
* Stern auf der Modul-Karte. Deckel bei acht, damit die Leiste kurz bleibt —
|
||
* sonst ist der Gewinn gegenüber dem Raster wieder weg.
|
||
*/
|
||
export const DEFAULT_FAVORITES = ['devlogs', 'welcome', 'tickets', 'starboard', 'gallery'];
|
||
export const MAX_FAVORITES = 8;
|
||
|
||
/** Aktuelle Auswahl — nur existierende Module, in gespeicherter Reihenfolge */
|
||
export function moduleFavorites() {
|
||
const gespeichert = String(getSetting('module_favorites') ?? '').trim();
|
||
if (!gespeichert) return DEFAULT_FAVORITES.filter((id) => MODULES.some((m) => m.id === id));
|
||
// Ein einzelner Bindestrich heißt „bewusst keine" — sonst käme der Standard zurück
|
||
if (gespeichert === '-') return [];
|
||
return gespeichert
|
||
.split(',')
|
||
.map((s) => s.trim())
|
||
.filter((id) => MODULES.some((m) => m.id === id))
|
||
.slice(0, MAX_FAVORITES);
|
||
}
|
||
|
||
/**
|
||
* Auswahl setzen.
|
||
* @returns {{ ok: true, favorites: string[] } | { ok: false, error: string }}
|
||
*/
|
||
export function setModuleFavorites(ids) {
|
||
if (!Array.isArray(ids)) return { ok: false, error: 'Liste erwartet' };
|
||
const gefiltert = [...new Set(ids.map(String))].filter((id) => MODULES.some((m) => m.id === id));
|
||
if (gefiltert.length > MAX_FAVORITES) {
|
||
return { ok: false, error: `Höchstens ${MAX_FAVORITES} Funktionen in der Seitenleiste` };
|
||
}
|
||
setSetting('module_favorites', gefiltert.length ? gefiltert.join(',') : '-');
|
||
return { ok: true, favorites: gefiltert };
|
||
}
|
||
|
||
/** Einstellungs-Name des Schalters für ein Modul */
|
||
function toggleKey(mod) {
|
||
return mod.setting ?? `module_${mod.id}`;
|
||
}
|
||
|
||
/** Läuft dieses Modul? (unabhängig davon, ob alles Nötige konfiguriert ist) */
|
||
export function moduleEnabled(id) {
|
||
const mod = MODULES.find((m) => m.id === id);
|
||
if (!mod) return true; // unbekannt → nicht blockieren
|
||
const value = getSetting(toggleKey(mod));
|
||
if (value === '1') return true;
|
||
if (value === '0') return false;
|
||
return mod.setting ? Boolean(mod.defaultOn) : mod.default !== false;
|
||
}
|
||
|
||
/** Schalter setzen */
|
||
export function setModuleEnabled(id, on) {
|
||
const mod = MODULES.find((m) => m.id === id);
|
||
if (!mod) return false;
|
||
setSetting(toggleKey(mod), on ? '1' : '0');
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Was fehlt diesem Modul noch? (leere Liste = einsatzbereit)
|
||
* Liefert Objekte statt nur Beschriftungen, damit das Panel weiß, was es
|
||
* anlegen könnte: `{ setting, label, create }`.
|
||
*/
|
||
export function moduleMissing(mod) {
|
||
return (mod.requires ?? [])
|
||
// Effektiver Wert, nicht nur die Einstellung: was über die Umgebung
|
||
// gesetzt ist, fehlt nicht.
|
||
.filter((r) => !effectiveSetting(r.setting))
|
||
.map((r) => ({ setting: r.setting, label: r.label, create: r.create ?? null }));
|
||
}
|
||
|
||
/** Bauanleitung zu einer Einstellung — null, wenn sie sich nicht anlegen lässt */
|
||
export function creationPlan(setting) {
|
||
for (const mod of MODULES) {
|
||
for (const req of mod.requires ?? []) {
|
||
if (req.setting === setting && req.create) return { ...req.create, label: req.label };
|
||
}
|
||
}
|
||
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) => ({
|
||
id: mod.id,
|
||
name: mod.name,
|
||
desc: mod.desc,
|
||
group: mod.group,
|
||
tab: mod.tab ?? null,
|
||
enabled: moduleEnabled(mod.id),
|
||
missing: moduleMissing(mod),
|
||
}));
|
||
}
|