+ {/* Was der Bot selbst anlegen kann, bekommt einen Knopf dafür */}
+ {m.missing.filter((x) => x.create).map((x) => (
+
+ ))}
{m.tab && (
setTab(m.tab)}>
Einrichten
@@ -2262,7 +2297,7 @@ export default function Settings({ me }) {
{m.enabled && m.missing.length > 0 && (
- Fehlt: {m.missing.join(', ')}
+ Fehlt: {m.missing.map((x) => x.label).join(', ')}
)}
{m.enabled && m.missing.length === 0 && (
diff --git a/frontend/src/style.css b/frontend/src/style.css
index 54813f2..3b85e61 100644
--- a/frontend/src/style.css
+++ b/frontend/src/style.css
@@ -720,6 +720,11 @@ a.sha:hover { border-color: var(--neon); background: rgba(245, 197, 24, .07); }
color: var(--neon2); flex: 1;
}
.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 {
display: flex; align-items: center; gap: .7rem;
font-family: var(--mono);
diff --git a/src/bot/server-setup.js b/src/bot/server-setup.js
new file mode 100644
index 0000000..0139031
--- /dev/null
+++ b/src/bot/server-setup.js
@@ -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 };
+}
diff --git a/src/modules.js b/src/modules.js
index bf87992..424dd9d 100644
--- a/src/modules.js
+++ b/src/modules.js
@@ -23,25 +23,25 @@ export const MODULES = [
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' }],
+ 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',
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' }],
+ 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',
desc: 'Neue Releases werden angekündigt und landen im öffentlichen Changelog.',
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',
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' }],
+ 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',
@@ -52,7 +52,7 @@ export const MODULES = [
id: 'social', group: 'inhalte', name: 'Twitch & YouTube',
desc: 'Meldet Livestreams und neue Videos.',
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 ────────────────────────────────── */
@@ -65,19 +65,19 @@ export const MODULES = [
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' }],
+ requires: [{ setting: 'starboard_channel_id', label: 'Starboard-Kanal', create: { name: 'best-of', topic: 'Nachrichten mit genug Sternen' } }],
},
{
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' }],
+ 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' }],
+ requires: [{ setting: 'voting_channel_id', label: 'Voting-Kanal', create: { name: 'ideen', topic: 'Wuensche und Abstimmungen' } }],
},
{
id: 'giveaways', group: 'community', name: 'Verlosungen',
@@ -88,19 +88,19 @@ export const MODULES = [
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' }],
+ requires: [{ setting: 'birthday_channel_id', label: 'Geburtstags-Kanal', create: { name: 'geburtstage', topic: 'Der Bot gratuliert hier' } }],
},
{
id: 'events', group: 'community', name: 'Event-Ankündigungen',
desc: 'Neue Discord-Events werden angekündigt und erscheinen auf der Events-Seite.',
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',
desc: 'Bewerbungs-Knopf, Rolle und Teilnehmerliste.',
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',
@@ -128,31 +128,31 @@ export const MODULES = [
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' }],
+ 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' }],
+ 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' }],
+ 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: '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',
desc: 'Neue Mitglieder bekommen automatisch eine Startrolle.',
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',
@@ -182,7 +182,7 @@ export const MODULES = [
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' }],
+ requires: [{ setting: 'tempvoice_channel_id', label: 'Hub-Kanal', create: { name: 'Eigenen Kanal erstellen', sprache: true } }],
},
{
id: 'triggers', group: 'server', name: 'Auto-Antworten',
@@ -244,11 +244,25 @@ export function setModuleEnabled(id, on) {
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) {
return (mod.requires ?? [])
.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 */
diff --git a/src/web/api.js b/src/web/api.js
index 0772b28..0622a35 100644
--- a/src/web/api.js
+++ b/src/web/api.js
@@ -205,6 +205,27 @@ export function registerApiRoutes(app, client) {
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 ---
app.get('/api/tuning', async (request, reply) => {