Fehlende Kanäle und Rollen aus dem Panel anlegen
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

Die Status-Seite listet Module, denen ein Kanal oder eine Rolle fehlt.
Bisher hieß das: rüber zu Discord, Kanal anlegen, Rechte setzen, zurück ins
Panel, auswählen, speichern. Jetzt steht daneben ein Knopf, der genau das
in einem Schritt macht — inklusive Eintragen der Einstellung.

Die Vorlagen hängen bei den Modulen (modules.js): Name, Thema und ob der
Kanal privat sein soll. Private Kanäle bekommen @everyone entzogen und
Bot plus alle Rollen mit Moderationsrecht eingetragen — sonst legt man
einen Modmail-Kanal an, den das Team nicht sieht.

Gibt es Kanal oder Rolle schon (bei Rollen unabhängig von Groß- und
Kleinschreibung), werden sie verknüpft statt doppelt angelegt. Beim
Temp-Voice-Hub entsteht ein Sprachkanal statt eines Textkanals.

Fehlen dem Bot die Rechte, steht das im Panel im Klartext („Dem Bot fehlt
das Recht Kanäle verwalten") statt einer Discord-Fehlernummer.

Bewusst nur Anlegen: kein Löschen, kein Umbenennen. Ein Fehlklick beim
Anlegen kostet einen überflüssigen Kanal, einer beim Löschen dessen
Verlauf. Umbenennen wäre zusätzlich eine Falle, weil Discord es auf zwei
Änderungen pro zehn Minuten und Kanal begrenzt.

moduleMissing liefert dafür Objekte statt Beschriftungen, damit das Panel
weiß, was sich anlegen lässt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 18:29:54 +02:00
co-authored by Claude Fable 5
parent 0240225165
commit 684573b88c
6 changed files with 197 additions and 23 deletions
+11 -2
View File
@@ -140,8 +140,17 @@ Der aktive Bereich steht in der Adresse (`/settings#texte`), Links und Neuladen
landen also wieder dort. landen also wieder dort.
Der Status-Bereich ist der Einstieg: Kennzahlen plus **„Noch einzurichten"** — Der Status-Bereich ist der Einstieg: Kennzahlen plus **„Noch einzurichten"** —
alle eingeschalteten Module, denen noch ein Kanal oder eine Rolle fehlt, mit alle eingeschalteten Module, denen noch ein Kanal oder eine Rolle fehlt. Daneben
Sprung direkt zur richtigen Stelle. steht **„Kanal anlegen"** bzw. **„Rolle anlegen"**: der Bot legt sie in Discord
an, setzt bei privaten Kanälen die Rechte (nur Team sieht sie) und trägt die
Einstellung gleich ein. Gibt es Kanal oder Rolle schon, werden sie verknüpft
statt doppelt angelegt.
Dafür braucht der Bot *Kanäle verwalten* bzw. *Rollen verwalten* — fehlt das
Recht, sagt das Panel genau das. Die Vorlagen (Name, Thema, privat/öffentlich)
stehen bei den Modulen in [`src/modules.js`](src/modules.js). Gelöscht oder
umbenannt wird bewusst nichts: ein Fehlklick beim Anlegen kostet einen
überflüssigen Kanal, einer beim Löschen dessen Verlauf.
Team-Mitglieder sehen nur die Bereiche ihrer Rechte. Alles zur Laufzeit änderbar — Team-Mitglieder sehen nur die Bereiche ihrer Rechte. Alles zur Laufzeit änderbar —
gespeichert in SQLite, Env-Variablen sind nur Fallback, kein Redeploy nötig: gespeichert in SQLite, Env-Variablen sind nur Fallback, kein Redeploy nötig:
+37 -2
View File
@@ -304,6 +304,7 @@ export default function Settings({ me }) {
const [modules, setModules] = useState([]); const [modules, setModules] = useState([]);
const [moduleGroups, setModuleGroups] = useState([]); const [moduleGroups, setModuleGroups] = useState([]);
const [modFilter, setModFilter] = useState('alle'); const [modFilter, setModFilter] = useState('alle');
const [creating, setCreating] = useState(null); // Einstellung, die gerade angelegt wird
// Text-Vorlagen (was der Bot nach außen schreibt) // Text-Vorlagen (was der Bot nach außen schreibt)
const [templates, setTemplates] = useState([]); const [templates, setTemplates] = useState([]);
const [templateGroups, setTemplateGroups] = useState([]); const [templateGroups, setTemplateGroups] = useState([]);
@@ -501,6 +502,26 @@ export default function Settings({ me }) {
} }
} }
/** Fehlenden Kanal / fehlende Rolle vom Bot anlegen lassen */
async function createMissing(entry) {
setCreating(entry.setting);
try {
const res = await apiPost('/api/setup/create', { setting: entry.setting });
setModules(res.modules);
// Die neue Auswahl steht sofort im Formular — und zählt nicht als
// ungespeicherte Änderung, weil der Vergleichsstand mitwandert.
setForm(res.settings);
setData((d) => (d ? { ...d, settings: res.settings } : d));
flash(res.wiederverwendet
? `✓ Vorhandenen ${res.art === 'rolle' ? 'Rolle' : 'Kanal'}${res.name}" verknüpft`
: `${res.art === 'rolle' ? 'Rolle' : 'Kanal'}${res.name}" angelegt`);
} catch (error) {
flash(`${error.body?.error ?? 'Anlegen fehlgeschlagen'}`);
} finally {
setCreating(null);
}
}
/** Alle geänderten Stellwerte auf einmal speichern */ /** Alle geänderten Stellwerte auf einmal speichern */
async function saveTuning() { async function saveTuning() {
const changed = Object.fromEntries( const changed = Object.fromEntries(
@@ -1023,8 +1044,22 @@ export default function Settings({ me }) {
<div className="key-row" key={m.id}> <div className="key-row" key={m.id}>
<span className="key-name">{m.name}</span> <span className="key-name">{m.name}</span>
<span className="todo-missing with-icon"> <span className="todo-missing with-icon">
<IconWarning size={13} /> {m.missing.join(', ')} <IconWarning size={13} /> {m.missing.map((x) => x.label).join(', ')}
</span> </span>
{/* Was der Bot selbst anlegen kann, bekommt einen Knopf dafür */}
{m.missing.filter((x) => x.create).map((x) => (
<button
key={x.setting}
className="btn-mini btn-create"
disabled={creating === x.setting}
title={`#${x.create.name} anlegen und eintragen`}
onClick={() => createMissing(x)}
>
{creating === x.setting
? '…'
: `${x.create.rolle ? 'Rolle' : 'Kanal'} anlegen`}
</button>
))}
{m.tab && ( {m.tab && (
<button className="btn-mini" onClick={() => setTab(m.tab)}> <button className="btn-mini" onClick={() => setTab(m.tab)}>
Einrichten Einrichten
@@ -2262,7 +2297,7 @@ export default function Settings({ me }) {
<div className="mod-foot"> <div className="mod-foot">
{m.enabled && m.missing.length > 0 && ( {m.enabled && m.missing.length > 0 && (
<span className="mod-missing with-icon"> <span className="mod-missing with-icon">
<IconWarning size={13} /> Fehlt: {m.missing.join(', ')} <IconWarning size={13} /> Fehlt: {m.missing.map((x) => x.label).join(', ')}
</span> </span>
)} )}
{m.enabled && m.missing.length === 0 && ( {m.enabled && m.missing.length === 0 && (
+5
View File
@@ -720,6 +720,11 @@ a.sha:hover { border-color: var(--neon); background: rgba(245, 197, 24, .07); }
color: var(--neon2); flex: 1; color: var(--neon2); flex: 1;
} }
.todo-done { color: var(--success); } .todo-done { color: var(--success); }
/* Hebt sich vom „Einrichten"-Knopf daneben ab: der eine tut etwas, der andere
führt nur hin */
.btn-create { color: var(--neon); border-color: rgba(245, 197, 24, .45); }
.btn-create:hover:not(:disabled) { background: rgba(245, 197, 24, .1); }
.btn-create:disabled { opacity: .4; cursor: default; }
.settings-nav-item { .settings-nav-item {
display: flex; align-items: center; gap: .7rem; display: flex; align-items: center; gap: .7rem;
font-family: var(--mono); font-family: var(--mono);
+90
View File
@@ -0,0 +1,90 @@
// Fehlende Kanäle und Rollen direkt aus dem Panel anlegen.
//
// Anlass: Die Status-Seite listet Module, denen ein Kanal oder eine Rolle
// fehlt. Bisher hieß das: rüber zu Discord, Kanal anlegen, Rechte setzen,
// zurück ins Panel, auswählen, speichern. Das hier macht daraus einen Klick.
//
// Bewusst nur Anlegen, kein Löschen und kein Umbenennen: ein Fehlklick beim
// Anlegen kostet einen überflüssigen Kanal, ein Fehlklick beim Löschen kostet
// dessen Verlauf.
import { ChannelType, PermissionFlagsBits } from 'discord.js';
import { setSetting } from '../db.js';
import { creationPlan } from '../modules.js';
import { discordGuildId } from '../runtime-settings.js';
/** Server, auf dem gearbeitet wird — konfigurierter, sonst der erste */
function targetGuild(client) {
const id = discordGuildId();
return (id ? client.guilds.cache.get(id) : null) ?? [...client.guilds.cache.values()][0] ?? null;
}
/**
* Kanal oder Rolle für eine Einstellung anlegen und diese gleich eintragen.
*
* @param {import('discord.js').Client} client
* @param {string} setting — z. B. 'devlog_channel_id'
* @returns {Promise<{ id: string, name: string, art: 'kanal'|'rolle' }>}
* @throws {Error} mit einer Meldung, die im Panel angezeigt werden kann
*/
export async function createForSetting(client, setting) {
const plan = creationPlan(setting);
if (!plan) throw new Error('Dafür gibt es keine Vorlage — bitte von Hand anlegen.');
const guild = targetGuild(client);
if (!guild) throw new Error('Kein Discord-Server gefunden.');
const me = guild.members.me ?? await guild.members.fetchMe().catch(() => null);
if (!me) throw new Error('Der Bot findet sich selbst nicht auf dem Server.');
if (plan.rolle) {
if (!me.permissions.has(PermissionFlagsBits.ManageRoles)) {
throw new Error('Dem Bot fehlt das Recht „Rollen verwalten".');
}
// Vorhandene gleichnamige Rolle wiederverwenden statt ein Duplikat anlegen
const vorhanden = [...guild.roles.cache.values()]
.find((r) => r.name.toLowerCase() === plan.name.toLowerCase());
const role = vorhanden ?? await guild.roles.create({
name: plan.name,
color: plan.farbe ?? undefined,
mentionable: false,
reason: 'Über das d4rkbot-Panel angelegt',
});
setSetting(setting, role.id);
return { id: role.id, name: role.name, art: 'rolle', wiederverwendet: Boolean(vorhanden) };
}
if (!me.permissions.has(PermissionFlagsBits.ManageChannels)) {
throw new Error('Dem Bot fehlt das Recht „Kanäle verwalten".');
}
const gewuenschterTyp = plan.sprache ? ChannelType.GuildVoice : ChannelType.GuildText;
const vorhanden = [...guild.channels.cache.values()]
.find((c) => c.name === plan.name && c.type === gewuenschterTyp);
if (vorhanden) {
setSetting(setting, vorhanden.id);
return { id: vorhanden.id, name: vorhanden.name, art: 'kanal', wiederverwendet: true };
}
// Private Kanäle: @everyone raus, Bot und Team-Rollen mit Moderationsrecht rein
const overwrites = [];
if (plan.privat) {
overwrites.push({ id: guild.roles.everyone.id, deny: [PermissionFlagsBits.ViewChannel] });
overwrites.push({ id: me.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages] });
for (const role of guild.roles.cache.values()) {
if (role.permissions.has(PermissionFlagsBits.ManageMessages) && !role.managed) {
overwrites.push({ id: role.id, allow: [PermissionFlagsBits.ViewChannel] });
}
}
}
const channel = await guild.channels.create({
name: plan.name,
type: gewuenschterTyp,
topic: plan.sprache ? undefined : plan.topic,
permissionOverwrites: overwrites.length ? overwrites : undefined,
reason: 'Über das d4rkbot-Panel angelegt',
});
setSetting(setting, channel.id);
return { id: channel.id, name: channel.name, art: 'kanal', wiederverwendet: false };
}
+33 -19
View File
@@ -23,25 +23,25 @@ export const MODULES = [
id: 'devlogs', group: 'inhalte', name: 'Devlogs', id: 'devlogs', group: 'inhalte', name: 'Devlogs',
desc: 'Entwicklungs-Berichte als gebrandetes Embed posten und durchsuchbar archivieren.', desc: 'Entwicklungs-Berichte als gebrandetes Embed posten und durchsuchbar archivieren.',
tab: 'feeds', default: true, tab: 'feeds', default: true,
requires: [{ setting: 'devlog_channel_id', label: 'Devlog-Kanal' }], requires: [{ setting: 'devlog_channel_id', label: 'Devlog-Kanal', create: { name: 'dev-log', topic: 'Entwicklungs-Updates vom Bot' } }],
}, },
{ {
id: 'commit_feed', group: 'inhalte', name: 'Commit-Feed', id: 'commit_feed', group: 'inhalte', name: 'Commit-Feed',
desc: 'Pushes aus Gitea landen als Embed im Kanal. Archiviert wird unabhängig davon immer.', desc: 'Pushes aus Gitea landen als Embed im Kanal. Archiviert wird unabhängig davon immer.',
tab: 'feeds', setting: 'commit_feed_enabled', defaultOn: true, tab: 'feeds', setting: 'commit_feed_enabled', defaultOn: true,
requires: [{ setting: 'commit_channel_id', label: 'Commit-Kanal' }], requires: [{ setting: 'commit_channel_id', label: 'Commit-Kanal', create: { name: 'commits', privat: true, topic: 'Pushes aus Gitea' } }],
}, },
{ {
id: 'releases', group: 'inhalte', name: 'Release-Ankündigungen', id: 'releases', group: 'inhalte', name: 'Release-Ankündigungen',
desc: 'Neue Releases werden angekündigt und landen im öffentlichen Changelog.', desc: 'Neue Releases werden angekündigt und landen im öffentlichen Changelog.',
tab: 'feeds', default: true, tab: 'feeds', default: true,
requires: [{ setting: 'release_channel_id', label: 'Release-Kanal' }], requires: [{ setting: 'release_channel_id', label: 'Release-Kanal', create: { name: 'releases', topic: 'Neue Versionen' } }],
}, },
{ {
id: 'weekly_recap', group: 'inhalte', name: 'Wochen-Rückblick', id: 'weekly_recap', group: 'inhalte', name: 'Wochen-Rückblick',
desc: 'Sonntags um 20 Uhr eine Zusammenfassung der Woche.', desc: 'Sonntags um 20 Uhr eine Zusammenfassung der Woche.',
tab: 'feeds', setting: 'weekly_recap_enabled', defaultOn: true, tab: 'feeds', setting: 'weekly_recap_enabled', defaultOn: true,
requires: [{ setting: 'devlog_channel_id', label: 'Devlog-Kanal' }], 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', id: 'devlog_threads', group: 'inhalte', name: 'Devlog-Threads',
@@ -52,7 +52,7 @@ export const MODULES = [
id: 'social', group: 'inhalte', name: 'Twitch & YouTube', id: 'social', group: 'inhalte', name: 'Twitch & YouTube',
desc: 'Meldet Livestreams und neue Videos.', desc: 'Meldet Livestreams und neue Videos.',
tab: 'system', default: true, tab: 'system', default: true,
requires: [{ setting: 'social_announce_channel_id', label: 'Social-Kanal' }], requires: [{ setting: 'social_announce_channel_id', label: 'Social-Kanal', create: { name: 'streams', topic: 'Twitch- und YouTube-Meldungen' } }],
}, },
/* ── Community ────────────────────────────────── */ /* ── Community ────────────────────────────────── */
@@ -65,19 +65,19 @@ export const MODULES = [
id: 'starboard', group: 'community', name: 'Starboard', id: 'starboard', group: 'community', name: 'Starboard',
desc: 'Nachrichten mit genug Sternen landen in einem Best-of-Kanal.', desc: 'Nachrichten mit genug Sternen landen in einem Best-of-Kanal.',
tab: 'community', default: true, tab: 'community', default: true,
requires: [{ setting: 'starboard_channel_id', label: 'Starboard-Kanal' }], requires: [{ setting: 'starboard_channel_id', label: 'Starboard-Kanal', create: { name: 'best-of', topic: 'Nachrichten mit genug Sternen' } }],
}, },
{ {
id: 'gallery', group: 'community', name: 'Screenshot-Galerie', id: 'gallery', group: 'community', name: 'Screenshot-Galerie',
desc: 'Bilder aus einem Kanal erscheinen in der öffentlichen Galerie.', desc: 'Bilder aus einem Kanal erscheinen in der öffentlichen Galerie.',
tab: 'community', default: true, tab: 'community', default: true,
requires: [{ setting: 'screenshot_channel_id', label: 'Screenshot-Kanal' }], 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', id: 'voting', group: 'community', name: 'Feature-Wünsche',
desc: 'Wünsche einreichen und abstimmen — im Discord und auf der Roadmap.', desc: 'Wünsche einreichen und abstimmen — im Discord und auf der Roadmap.',
tab: 'community', default: true, tab: 'community', default: true,
requires: [{ setting: 'voting_channel_id', label: 'Voting-Kanal' }], requires: [{ setting: 'voting_channel_id', label: 'Voting-Kanal', create: { name: 'ideen', topic: 'Wuensche und Abstimmungen' } }],
}, },
{ {
id: 'giveaways', group: 'community', name: 'Verlosungen', id: 'giveaways', group: 'community', name: 'Verlosungen',
@@ -88,19 +88,19 @@ export const MODULES = [
id: 'birthdays', group: 'community', name: 'Geburtstage', id: 'birthdays', group: 'community', name: 'Geburtstage',
desc: 'Gratulation am großen Tag, optional mit Tages-Rolle.', desc: 'Gratulation am großen Tag, optional mit Tages-Rolle.',
tab: 'community', default: true, tab: 'community', default: true,
requires: [{ setting: 'birthday_channel_id', label: 'Geburtstags-Kanal' }], requires: [{ setting: 'birthday_channel_id', label: 'Geburtstags-Kanal', create: { name: 'geburtstage', topic: 'Der Bot gratuliert hier' } }],
}, },
{ {
id: 'events', group: 'community', name: 'Event-Ankündigungen', id: 'events', group: 'community', name: 'Event-Ankündigungen',
desc: 'Neue Discord-Events werden angekündigt und erscheinen auf der Events-Seite.', desc: 'Neue Discord-Events werden angekündigt und erscheinen auf der Events-Seite.',
tab: 'system', default: true, tab: 'system', default: true,
requires: [{ setting: 'events_announce_channel_id', label: 'Event-Kanal' }], requires: [{ setting: 'events_announce_channel_id', label: 'Event-Kanal', create: { name: 'events', topic: 'Playtests, Streams, Runden' } }],
}, },
{ {
id: 'playtester', group: 'community', name: 'Playtester-Programm', id: 'playtester', group: 'community', name: 'Playtester-Programm',
desc: 'Bewerbungs-Knopf, Rolle und Teilnehmerliste.', desc: 'Bewerbungs-Knopf, Rolle und Teilnehmerliste.',
tab: 'community', default: true, tab: 'community', default: true,
requires: [{ setting: 'playtester_role_id', label: 'Playtester-Rolle' }], requires: [{ setting: 'playtester_role_id', label: 'Playtester-Rolle', create: { rolle: true, name: 'Playtester', farbe: '#23a55a' } }],
}, },
{ {
id: 'alpha_keys', group: 'community', name: 'Alpha-Keys', id: 'alpha_keys', group: 'community', name: 'Alpha-Keys',
@@ -128,31 +128,31 @@ export const MODULES = [
id: 'modlog', group: 'moderation', name: 'Protokoll', id: 'modlog', group: 'moderation', name: 'Protokoll',
desc: 'Gelöschte und bearbeitete Nachrichten, Beitritte, Namens- und Rollenwechsel.', desc: 'Gelöschte und bearbeitete Nachrichten, Beitritte, Namens- und Rollenwechsel.',
tab: 'support', default: true, tab: 'support', default: true,
requires: [{ setting: 'modlog_channel_id', label: 'Protokoll-Kanal' }], requires: [{ setting: 'modlog_channel_id', label: 'Protokoll-Kanal', create: { name: 'protokoll', privat: true, topic: 'Bearbeitet, geloescht, beigetreten' } }],
}, },
{ {
id: 'modmail', group: 'moderation', name: 'Modmail', id: 'modmail', group: 'moderation', name: 'Modmail',
desc: 'Direktnachrichten an den Bot landen als Thread beim Team.', desc: 'Direktnachrichten an den Bot landen als Thread beim Team.',
tab: 'support', default: true, tab: 'support', default: true,
requires: [{ setting: 'modmail_channel_id', label: 'Modmail-Kanal' }], requires: [{ setting: 'modmail_channel_id', label: 'Modmail-Kanal', create: { name: 'modmail', privat: true, topic: 'Direktnachrichten an den Bot' } }],
}, },
{ {
id: 'tickets', group: 'moderation', name: 'Tickets', id: 'tickets', group: 'moderation', name: 'Tickets',
desc: 'Private Threads per Knopf, beim Schließen ein Gesprächsprotokoll.', desc: 'Private Threads per Knopf, beim Schließen ein Gesprächsprotokoll.',
tab: 'support', default: true, tab: 'support', default: true,
requires: [{ setting: 'ticket_channel_id', label: 'Ticket-Kanal' }], requires: [{ setting: 'ticket_channel_id', label: 'Ticket-Kanal', create: { name: 'support', topic: 'Tickets per Knopf' } }],
}, },
{ {
id: 'welcome', group: 'moderation', name: 'Willkommens-Karten', id: 'welcome', group: 'moderation', name: 'Willkommens-Karten',
desc: 'Begrüßung neuer Mitglieder mit gerendertem Bild.', desc: 'Begrüßung neuer Mitglieder mit gerendertem Bild.',
tab: 'support', default: true, tab: 'support', default: true,
requires: [{ setting: 'welcome_channel_id', label: 'Willkommens-Kanal' }], requires: [{ setting: 'welcome_channel_id', label: 'Willkommens-Kanal', create: { name: 'willkommen', topic: 'Begruessung neuer Mitglieder' } }],
}, },
{ {
id: 'autorole', group: 'moderation', name: 'Auto-Rolle', id: 'autorole', group: 'moderation', name: 'Auto-Rolle',
desc: 'Neue Mitglieder bekommen automatisch eine Startrolle.', desc: 'Neue Mitglieder bekommen automatisch eine Startrolle.',
tab: 'community', default: true, tab: 'community', default: true,
requires: [{ setting: 'autorole_id', label: 'Auto-Rolle' }], requires: [{ setting: 'autorole_id', label: 'Auto-Rolle', create: { rolle: true, name: 'Member' } }],
}, },
{ {
id: 'sticky_roles', group: 'moderation', name: 'Rollen merken', id: 'sticky_roles', group: 'moderation', name: 'Rollen merken',
@@ -182,7 +182,7 @@ export const MODULES = [
id: 'temp_voice', group: 'server', name: 'Temporäre Sprachkanäle', id: 'temp_voice', group: 'server', name: 'Temporäre Sprachkanäle',
desc: 'Beitritt zum Hub erzeugt einen eigenen Kanal mit Bedienfeld.', desc: 'Beitritt zum Hub erzeugt einen eigenen Kanal mit Bedienfeld.',
tab: 'system', default: true, tab: 'system', default: true,
requires: [{ setting: 'tempvoice_channel_id', label: 'Hub-Kanal' }], requires: [{ setting: 'tempvoice_channel_id', label: 'Hub-Kanal', create: { name: 'Eigenen Kanal erstellen', sprache: true } }],
}, },
{ {
id: 'triggers', group: 'server', name: 'Auto-Antworten', id: 'triggers', group: 'server', name: 'Auto-Antworten',
@@ -244,11 +244,25 @@ export function setModuleEnabled(id, on) {
return true; return true;
} }
/** Was fehlt diesem Modul noch? (leere Liste = einsatzbereit) */ /**
* 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) { export function moduleMissing(mod) {
return (mod.requires ?? []) return (mod.requires ?? [])
.filter((r) => !String(getSetting(r.setting) ?? '').trim()) .filter((r) => !String(getSetting(r.setting) ?? '').trim())
.map((r) => r.label); .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;
} }
/** Alle Module mit Zustand — für das Webinterface */ /** Alle Module mit Zustand — für das Webinterface */
+21
View File
@@ -205,6 +205,27 @@ export function registerApiRoutes(app, client) {
return { ok: true, modules: moduleStates() }; return { ok: true, modules: moduleStates() };
}); });
// Fehlenden Kanal / fehlende Rolle direkt anlegen (Status → Noch einzurichten)
app.post('/api/setup/create', async (request, reply) => {
if (requireScope(request, reply, 'settings')) return;
const setting = String(request.body?.setting ?? '');
try {
const { createForSetting } = await import('../bot/server-setup.js');
const res = await createForSetting(client, setting);
logAudit(
getSessionUser(request),
res.wiederverwendet ? 'vorhandenen kanal verknüpft' : `${res.art} angelegt`,
`${setting}${res.name}`
);
request.log.info(`${res.art} ${res.name} für ${setting} (${res.wiederverwendet ? 'verknüpft' : 'neu'})`);
return { ok: true, ...res, modules: moduleStates(), settings: currentSettings() };
} catch (error) {
request.log.error({ err: error }, `Anlegen für ${setting} fehlgeschlagen`);
// Discord-Meldungen sind für Menschen unbrauchbar — eigene durchreichen
return reply.code(400).send({ error: error.message || 'Anlegen fehlgeschlagen' });
}
});
// --- Stellwerte: Zahlen, die vorher fest im Code standen --- // --- Stellwerte: Zahlen, die vorher fest im Code standen ---
app.get('/api/tuning', async (request, reply) => { app.get('/api/tuning', async (request, reply) => {