Der Feed eines LS-Servers gibt viel mehr her als die Spielerzahl, die der Monitor bisher davon benutzt hat: in denselben 49 KB stehen 163 Parzellen mit Besitzer und Hektar, 130 Felder, jedes Fahrzeug samt Ladung und die Position jedes Spielers. Und das Luftbild liefert der Server auch, in jeder Groesse bis 4096. Also eine Karte: gekaufte Parzellen nach Hof eingefaerbt, Kreisflaeche gleich Hektar, Fahrzeuge und Spieler live, dazu eine Hof-Uebersicht mit Flaeche und Wert. Wechselt Land den Besitzer, kommt eine Meldung — beim allerersten Durchlauf bewusst nicht, sonst haette der Botstart eine Meldung pro bereits gekaufter Parzelle abgesetzt. Dazu die Preiskurven aus der economy.xml: je Fruchtart, was sie in welcher der zwoelf Perioden bringt, als Balkenzeile mit "jetzt verkaufen" oder "+43 Prozent im Hochwinter". Was NICHT geht, und zwar grundsaetzlich: welche Frucht auf welchem Feld steht und wie weit sie ist. Das liegt in einer binaeren Density-Map, die kein Endpunkt herausgibt. Ich habe 18 Dateinamen durchprobiert, es gibt genau drei. Die Mods, die das anzeigen, sind reine HUD-Overlays ohne Ausgang. Und keiner der fuenf anderen LS-Bots kann es, obwohl einer davon Geld kostet. Das steht mit allen Belegen in docs/ls-feed.md, damit die Frage nicht dreimal kommt. Beim Bauen aufgefallen: - istLsServer() gab den Zugangs-Code zurueck statt true — die &&-Kette liefert den letzten wahren Wert. Beim ersten Log-Aufruf haette der Code im Klartext im Protokoll gestanden. - Die Karte als PNG war 2,5 MB, als JPEG 268 KB. Es ist ein Luftbild. - Nach Preisschwankung sortiert gewinnt Spargel: 26 bis 7540 Euro, und Weizen taucht gar nicht mehr auf. Die Liste arbeitet jetzt mit einer Auswahl. - Die Jahreszeit steht in keinem Endpunkt. Schaetzbar waere sie, aber nur solange niemand die Zeitskala aendert — also eine Einstellung. Geprueft gegen echte Serverdaten: Zerlegen der Statusabfrage inklusive Vollstaendigkeit aller 163 Parzellen, Ladung ohne Diesel und leere Slots, Hof-Uebersicht ohne verlorene Parzellen, Preiskurve mit vollem und leerem Balken an der richtigen Stelle, flache Kurve ohne Division durch Null, Besitzwechsel inklusive Erstlauf und neu hinzugekommener Parzelle, Hof-Namen aus der Einstellung, SVG ohne NaN und mit maskierten Namen, und der Merker in der Datenbank inklusive kaputtem JSON. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
447 lines
22 KiB
JavaScript
447 lines
22 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: 'ls_farm', group: 'server', name: 'LS — Höfe & Preise',
|
||
desc: 'Landwirtschafts-Simulator: Besitzkarte mit Höfen, Fahrzeugen und Spielern, '
|
||
+ 'dazu die Preiskurven übers Jahr. Braucht einen LS-Server mit Zugangs-Code unter „Game-Server".',
|
||
tab: 'server', setting: 'ls_farm_enabled',
|
||
requires: [{ setting: 'ls_channel_id', label: 'Karten-Kanal', create: { name: 'ls-hoefe', topic: 'Besitzkarte und Preise vom LS-Server' } }],
|
||
fields: [
|
||
{ setting: 'ls_event_channel_id', label: 'Ereignis-Kanal', art: 'kanal', hint: 'Land gekauft oder verkauft; leer = in den Karten-Kanal' },
|
||
{ setting: 'ls_farm_names', label: 'Hof-Namen', art: 'text', hint: 'der Feed kennt nur Nummern — z. B. „1=Alexander, 2=Timo, 3=Kitty"' },
|
||
{ setting: 'ls_period', label: 'Aktuelle Jahreszeit', art: 'text', hint: 'z. B. MID_SUMMER — steht nicht im Feed. Leer lassen heißt: Kurve ohne „jetzt verkaufen"-Rat.' },
|
||
{ setting: 'ls_crops', label: 'Sorten in der Preisliste', art: 'text', hint: 'kommagetrennt, leer = die üblichen. Ohne Auswahl verdrängt Spargel jede echte Feldfrucht.' },
|
||
],
|
||
},
|
||
{
|
||
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),
|
||
}));
|
||
}
|