Modul-System: alle 35 Funktionen im Panel ein- und ausschaltbar
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

Bisher waren die Schalter über die Oberfläche verstreut: fünf Funktionen
hatten einen Haken, 22 weitere gingen nur über den Umweg „Kanal leeren".
Für jeden außer dem Owner war das nicht auffindbar.

Jetzt steht in src/modules.js ein zentrales Register aller Funktionen mit
Beschreibung, Standard-Zustand und den Einstellungen, die sie brauchen.
Daraus entsteht ein neuer Setup-Tab mit Karten: Schiebeschalter, kurze
Erklärung, Hinweis was noch fehlt ("Fehlt noch: Release-Kanal") und ein
Sprung zu den Einstellungen.

Damit die Schalter keine Attrappen sind, prüfen die Bot-Module jetzt an
18 Stellen zentral, ob sie laufen dürfen — Starboard, Galerie, Modmail,
Willkommen, Protokoll, Auto-Antworten, Sprachkanäle, Erinnerungen,
Events, Twitch/YouTube, Monitor, Wächter, Releases, Rückblick,
Geburtstage, Verlosungen und geplante Beiträge.

Funktionen mit vorhandenem Schalter (Level-System, Backups, Member-Gate …)
nutzen weiterhin dieselbe Einstellung, damit keine zweite Wahrheit
entsteht und bestehende Konfigurationen unverändert weiterlaufen.

Nebenbei gefunden und behoben: Beim Anlegen eines Modmail-Threads wurde
`member.client` benutzt, obwohl es an der Stelle kein `member` gibt — der
erste Modmail-Thread wäre mit einem Fehler abgebrochen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 15:53:44 +02:00
co-authored by Claude Fable 5
parent bdbd35883c
commit 59aad27534
16 changed files with 451 additions and 1 deletions
+77
View File
@@ -5,6 +5,7 @@ import {
IconStatus, IconBrand, IconFeeds, IconCommunity, IconRoles, IconSupport,
IconApplications, IconComposer, IconServer, IconSystem, IconApi, IconTeam,
IconLock, IconBot, IconUpload, IconChevronDown, IconKey, IconPages, IconX,
IconHome as IconLayers, IconWarning, IconCheck,
} from '../icons.jsx';
import { Markdown } from '../markdown.jsx';
@@ -17,6 +18,7 @@ const API_SCOPES = [
const TABS = [
{ id: 'status', Icon: IconStatus, label: 'Status' },
{ id: 'module', Icon: IconLayers, label: 'Module' },
{ id: 'brand', Icon: IconBrand, label: 'Brand' },
{ id: 'feeds', Icon: IconFeeds, label: 'Feeds' },
{ id: 'community', Icon: IconCommunity, label: 'Community' },
@@ -34,6 +36,7 @@ const TABS = [
// Welcher Team-Scope schaltet welchen Tab frei (null = nur Owner)
const TAB_SCOPES = {
status: 'any',
module: 'settings',
brand: null,
feeds: 'settings',
community: 'community',
@@ -169,6 +172,9 @@ export default function Settings({ me }) {
const [botDesc, setBotDesc] = useState('');
const [team, setTeam] = useState([]);
const [audit, setAudit] = useState([]);
// Module (An/Aus für alle Funktionen)
const [modules, setModules] = useState([]);
const [moduleGroups, setModuleGroups] = useState([]);
// Eigene Seiten (Regeln, Über uns, …)
const emptyPage = { slug: '', title: '', content: '', published: false, in_menu: true, sort: 0, isNew: true };
const [pages, setPages] = useState([]);
@@ -226,6 +232,10 @@ export default function Settings({ me }) {
apiGet('/api/ssoapps').then((d) => setSsoApps(d.apps)).catch(() => {});
apiGet('/api/services').then((d) => setServices(d.services)).catch(() => {});
apiGet('/api/pages/all/list').then((d) => setPages(d.pages)).catch(() => {});
apiGet('/api/modules').then((d) => {
setModules(d.modules);
setModuleGroups(d.groups);
}).catch(() => {});
}, [me.admin, me.scopes?.length]);
useEffect(() => {
@@ -259,6 +269,18 @@ export default function Settings({ me }) {
if (res) setTeam(res.admins);
}
async function toggleModule(id, enabled) {
// Sofort umschalten, damit sich der Schalter nicht träge anfühlt
setModules((old) => old.map((m) => (m.id === id ? { ...m, enabled } : m)));
try {
const res = await apiPut(`/api/modules/${id}`, { enabled });
setModules(res.modules);
} catch {
setModules((old) => old.map((m) => (m.id === id ? { ...m, enabled: !enabled } : m)));
flash('✗ Umschalten fehlgeschlagen');
}
}
/** Titel → Adress-Kürzel (Umlaute mit auflösen) */
function slugify(value) {
return String(value).toLowerCase()
@@ -1680,6 +1702,60 @@ export default function Settings({ me }) {
</>
);
const tabModule = (
<div className="settings-section">
<h2 className="settings-title">// Module</h2>
<p className="section-intro">
Jede Funktion des Bots lässt sich hier ein- und ausschalten. Ausgeschaltete
Module reagieren auf nichts mehr keine Posts, keine Befehle, keine
Hintergrund-Prüfungen. Was noch eingerichtet werden muss, steht direkt an
der Karte.
</p>
{moduleGroups.map((group) => {
const mods = modules.filter((m) => m.group === group.id);
if (mods.length === 0) return null;
return (
<div key={group.id} style={{ marginBottom: '1.8rem' }}>
<h3 className="settings-title" style={{ fontSize: '.66rem', marginBottom: '.8rem' }}>
{group.label}
</h3>
<div className="mod-grid">
{mods.map((m) => (
<div className={`card mod-card${m.enabled ? ' on' : ''}`} key={m.id}>
<div className="mod-head">
<span className="mod-name">{m.name}</span>
<button
className={`mod-switch${m.enabled ? ' on' : ''}`}
title={m.enabled ? 'Ausschalten' : 'Einschalten'}
onClick={() => toggleModule(m.id, !m.enabled)}
>
<span className="mod-knob" />
</button>
</div>
<p className="mod-desc">{m.desc}</p>
{m.enabled && m.missing.length > 0 && (
<p className="mod-missing with-icon">
<IconWarning size={13} /> Fehlt noch: {m.missing.join(', ')}
</p>
)}
{m.enabled && m.missing.length === 0 && (
<p className="mod-ready with-icon"><IconCheck size={13} /> Einsatzbereit</p>
)}
{m.tab && (
<button className="mod-link" onClick={() => setTab(m.tab)}>
Einstellungen
</button>
)}
</div>
))}
</div>
</div>
);
})}
</div>
);
const tabSeiten = (
<div className="settings-section">
<h2 className="settings-title">// Eigene Seiten</h2>
@@ -1866,6 +1942,7 @@ export default function Settings({ me }) {
const content = {
status: tabStatus,
module: tabModule,
brand: tabBrand,
feeds: tabFeeds,
community: tabCommunity,