diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx
index b0b625c..d54ccf9 100644
--- a/frontend/src/pages/Settings.jsx
+++ b/frontend/src/pages/Settings.jsx
@@ -152,6 +152,23 @@ export default function Settings({ me }) {
+ {/* Devlog */}
+
+
// Devlog
+
+
+
+
+
+
{/* Commit-Feed */}
// Commit-Feed
diff --git a/src/bot/client.js b/src/bot/client.js
index e8f2fe7..bd2aae8 100644
--- a/src/bot/client.js
+++ b/src/bot/client.js
@@ -1,7 +1,7 @@
// Discord-Client: Commands laden, registrieren und Interactions verarbeiten
-import { Client, Collection, Events, GatewayIntentBits, Partials, REST, Routes } from 'discord.js';
+import { Client, Collection, Events, GatewayIntentBits, MessageFlags, Partials, REST, Routes } from 'discord.js';
import { config } from '../config.js';
-import { devlogChannelId } from '../runtime-settings.js';
+import { devlogChannelId, devlogPingRoleId } from '../runtime-settings.js';
import { archiveDevlogMessage, removeDevlog } from './devlog-archive.js';
import * as ping from './commands/ping.js';
import * as devlogBackfill from './commands/devlog-backfill.js';
@@ -58,6 +58,38 @@ export async function startBot() {
});
client.on(Events.InteractionCreate, async (interaction) => {
+ // 🔔-Button unterm Devlog: Ping-Rolle selbst an-/abmelden
+ if (interaction.isButton() && interaction.customId === 'devlog_ping_toggle') {
+ const roleId = devlogPingRoleId();
+ try {
+ if (!roleId || !interaction.inGuild()) {
+ throw new Error('Keine Ping-Rolle konfiguriert');
+ }
+ const member = interaction.member;
+ const hasRole = member.roles.cache.has(roleId);
+ if (hasRole) {
+ await member.roles.remove(roleId);
+ } else {
+ await member.roles.add(roleId);
+ }
+ await interaction.reply({
+ content: hasRole
+ ? '🔕 Devlog-Benachrichtigungen **abbestellt**.'
+ : '🔔 Devlog-Benachrichtigungen **abonniert** — du wirst ab jetzt gepingt.',
+ flags: MessageFlags.Ephemeral,
+ });
+ } catch (error) {
+ console.error('[ping-rolle] Toggle fehlgeschlagen:', error);
+ await interaction
+ .reply({
+ content: '❌ Konnte die Rolle nicht ändern — dem Bot fehlt vermutlich „Rollen verwalten" oder die Rolle steht über seiner.',
+ flags: MessageFlags.Ephemeral,
+ })
+ .catch(() => {});
+ }
+ return;
+ }
+
if (!interaction.isChatInputCommand()) return;
const command = client.commands.get(interaction.commandName);
diff --git a/src/runtime-settings.js b/src/runtime-settings.js
index c8f762f..0e0ceac 100644
--- a/src/runtime-settings.js
+++ b/src/runtime-settings.js
@@ -16,6 +16,11 @@ export function releaseChannelId() {
return getSetting('release_channel_id') || null;
}
+/** Rolle, die bei Devlog-Posts gepingt wird — leer = kein Ping */
+export function devlogPingRoleId() {
+ return getSetting('devlog_ping_role_id') || null;
+}
+
/** Commit-Feed global an/aus (Default: an). Aus = weiter archivieren, nur nicht posten. */
export function commitFeedEnabled() {
return getSetting('commit_feed_enabled') !== '0';
diff --git a/src/web/api.js b/src/web/api.js
index ddcb2e4..e376871 100644
--- a/src/web/api.js
+++ b/src/web/api.js
@@ -3,7 +3,7 @@ import { EmbedBuilder } from 'discord.js';
import { listDevlogs, searchDevlogs, listCommits, listReleases, archiveStats, getSetting, setSetting } from '../db.js';
import { config } from '../config.js';
import { removeDevlog } from '../bot/devlog-archive.js';
-import { commitChannelId, devlogChannelId, releaseChannelId } from '../runtime-settings.js';
+import { commitChannelId, devlogChannelId, releaseChannelId, devlogPingRoleId } from '../runtime-settings.js';
import { getSessionUser, isAdmin } from './auth.js';
const PAGE_SIZE = 20;
@@ -116,11 +116,25 @@ ${rssItems}
return channels.sort((a, b) => a.name.localeCompare(b.name));
}
+ /** Rollen, die der Bot vergeben kann (unter seiner höchsten Rolle, nicht managed) */
+ function listRoles() {
+ const roles = [];
+ for (const guild of client.guilds.cache.values()) {
+ for (const role of guild.roles?.cache?.values() ?? []) {
+ if (role.id === guild.id) continue; // @everyone
+ if (role.managed) continue; // Bot-/Integrations-Rollen
+ roles.push({ id: role.id, name: role.name, guild: guild.name });
+ }
+ }
+ return roles.sort((a, b) => a.name.localeCompare(b.name));
+ }
+
function currentSettings() {
return {
commit_channel_id: commitChannelId(),
devlog_channel_id: devlogChannelId(),
release_channel_id: releaseChannelId() ?? '',
+ devlog_ping_role_id: devlogPingRoleId() ?? '',
commit_feed_enabled: getSetting('commit_feed_enabled') !== '0',
commit_branch_filter: getSetting('commit_branch_filter') ?? '',
ignored_repos: getSetting('ignored_repos') ?? '',
@@ -133,6 +147,7 @@ ${rssItems}
const stats = archiveStats();
return {
channels: listChannels(),
+ roles: listRoles(),
settings: currentSettings(),
status: {
botTag: client.user?.tag ?? null,
@@ -162,6 +177,19 @@ ${rssItems}
}
setSetting(key, value);
}
+ // Ping-Rolle: muss existieren ('' = kein Ping)
+ if (body.devlog_ping_role_id !== undefined) {
+ const value = String(body.devlog_ping_role_id);
+ if (value !== '') {
+ const exists = [...client.guilds.cache.values()].some((g) =>
+ g.roles?.cache?.has?.(value)
+ );
+ if (!exists) {
+ return reply.code(400).send({ error: 'devlog_ping_role_id: Rolle nicht gefunden' });
+ }
+ }
+ setSetting('devlog_ping_role_id', value);
+ }
if (body.commit_feed_enabled !== undefined) {
setSetting('commit_feed_enabled', body.commit_feed_enabled ? '1' : '0');
}
diff --git a/src/web/devlog-endpoint.js b/src/web/devlog-endpoint.js
index bcb691e..8dc4ee4 100644
--- a/src/web/devlog-endpoint.js
+++ b/src/web/devlog-endpoint.js
@@ -10,7 +10,7 @@ import { join } from 'node:path';
import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js';
import { config } from '../config.js';
import { saveDevlog } from '../db.js';
-import { devlogChannelId } from '../runtime-settings.js';
+import { devlogChannelId, devlogPingRoleId } from '../runtime-settings.js';
import { imagesDir } from '../bot/devlog-archive.js';
const BRAND_YELLOW = 0xf5c518;
@@ -123,7 +123,7 @@ export function registerDevlogEndpoint(app, client) {
}
});
- // Link-Buttons unterm Embed: Archiv-Seite + Repo
+ // Link-Buttons unterm Embed: Archiv-Seite + Repo + Abo-Button für die Ping-Rolle
const buttons = new ActionRowBuilder().addComponents(
new ButtonBuilder().setStyle(ButtonStyle.Link).setLabel('Devlog-Archiv').setEmoji('🗂️').setURL(groupUrl)
);
@@ -133,7 +133,26 @@ export function registerDevlogEndpoint(app, client) {
);
}
- const message = await channel.send({ embeds, files: attachments, components: [buttons] });
+ // Ping-Rolle: Rolle erwähnen + Abo-Button anbieten (Setup-Seite)
+ const pingRole = devlogPingRoleId();
+ if (pingRole) {
+ buttons.addComponents(
+ new ButtonBuilder()
+ .setStyle(ButtonStyle.Secondary)
+ .setLabel('Benachrichtigungen')
+ .setEmoji('🔔')
+ .setCustomId('devlog_ping_toggle')
+ );
+ }
+
+ const message = await channel.send({
+ embeds,
+ files: attachments,
+ components: [buttons],
+ ...(pingRole
+ ? { content: `<@&${pingRole}>`, allowedMentions: { roles: [pingRole] } }
+ : {}),
+ });
// Direkt archivieren (der Live-Listener ignoriert eigene Bot-Posts bewusst)
const imageFiles = [];