From 4fcc111def144b75bd40f66be4a840ac62e16cdb Mon Sep 17 00:00:00 2001 From: Alexander Zielonka Date: Wed, 7 Jan 2026 18:29:13 +0100 Subject: [PATCH] Add multi-guild Discord bot support with auto-setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bot creates category and channels automatically when joining a server - Channel structure: info, status, alerts, updates, diskussion, requests (forum) - Add guild_settings database table for per-server configuration - Add Discord bot invite button to Dashboard - Add display settings API functions - Add comprehensive Discord bot documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- docs/discord-bot.md | 197 ++++++++++ gsm-backend/services/discordBot.js | 521 +++++++++++++++++++++++++++ gsm-frontend/src/api.js | 24 ++ gsm-frontend/src/pages/Dashboard.jsx | 28 ++ 4 files changed, 770 insertions(+) create mode 100644 docs/discord-bot.md create mode 100644 gsm-backend/services/discordBot.js diff --git a/docs/discord-bot.md b/docs/discord-bot.md new file mode 100644 index 0000000..8939d90 --- /dev/null +++ b/docs/discord-bot.md @@ -0,0 +1,197 @@ +# Discord Bot + +Der GSM Discord Bot bietet Live-Status-Updates für alle Gameserver direkt in Discord. Der Bot kann auf mehreren Discord-Servern gleichzeitig laufen. + +## Features + +- **Live-Status**: Automatisch aktualisiertes Embed mit Server-Status, Spielerzahlen und Metriken +- **Alerts**: Benachrichtigungen wenn Server online/offline gehen oder Spieler joinen/leaven +- **Multi-Guild**: Kann auf beliebig vielen Discord-Servern eingesetzt werden +- **Auto-Setup**: Erstellt automatisch alle nötigen Channels beim Beitreten + +## Bot einladen + +### Invite-Link + +``` +https://discord.com/oauth2/authorize?client_id=1458251194806833306&permissions=34359831568&integration_type=0&scope=bot+applications.commands +``` + +Der Link ist auch im GSM Dashboard unter den Server-Cards verfügbar. + +### Benötigte Permissions + +| Permission | Verwendung | +|------------|------------| +| View Channels | Channels sehen | +| Manage Channels | Channels erstellen | +| Send Messages | Nachrichten senden | +| Manage Messages | Eigene Nachrichten bearbeiten | +| Embed Links | Rich Embeds für Status | +| Read Message History | Alte Nachrichten lesen | +| Create Public Threads | Forum-Threads erstellen | + +## Automatisch erstellte Channel-Struktur + +Wenn der Bot einem Server beitritt, erstellt er automatisch folgende Struktur: + +``` +🎮 Gameserver (Kategorie) +├── ℹ️│info - Informationen zum GSM System +├── 📊│status - Live-Status aller Gameserver (Auto-Update) +├── 📢│alerts - Server-Events und Spieler-Benachrichtigungen +├── 📰│updates - Ankündigungen zu neuen Gameservern +├── 💬│diskussion - Diskussions-Channel (User können schreiben) +└── 💡│requests - Forum für Gameserver-Vorschläge +``` + +### Channel-Permissions + +| Channel | @everyone | Bot | +|---------|-----------|-----| +| Kategorie | Lesen | Schreiben | +| info | Lesen | Schreiben | +| status | Lesen | Schreiben | +| alerts | Lesen | Schreiben | +| updates | Lesen | Schreiben | +| diskussion | Lesen + Schreiben | Schreiben | +| requests | Lesen + Threads erstellen | Schreiben | + +## Datenbank + +### guild_settings Tabelle + +Speichert die Channel-IDs für jeden Discord-Server: + +```sql +CREATE TABLE guild_settings ( + guild_id TEXT PRIMARY KEY, + category_id TEXT, + info_channel_id TEXT, + status_channel_id TEXT, + status_message_id TEXT, + alerts_channel_id TEXT, + updates_channel_id TEXT, + discussion_channel_id TEXT, + requests_channel_id TEXT, + requests_info_thread_id TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +``` + +### DB-Funktionen + +In `backend/db/init.js`: + +```javascript +initGuildSettings() // Tabelle erstellen +getGuildSettings(guildId) // Settings für einen Server +getAllGuildSettings() // Alle Server-Settings +setGuildSettings(guildId, {}) // Settings speichern +deleteGuildSettings(guildId) // Settings löschen (bei Bot-Kick) +``` + +## Bot-Events + +### guildCreate + +Wird ausgelöst wenn der Bot einem neuen Server beitritt: + +1. Erstellt Kategorie und alle Channels +2. Postet Info-Nachricht im `ℹ️│info` Channel +3. Erstellt Info-Thread im `💡│requests` Forum +4. Speichert alle Channel-IDs in der Datenbank +5. Sendet erste Status-Nachricht + +### guildDelete + +Wird ausgelöst wenn der Bot von einem Server entfernt wird: + +1. Löscht alle Settings aus der Datenbank + +## Status-Updates + +Der Bot aktualisiert die Status-Nachricht in allen registrierten Guilds alle 30 Sekunden: + +```javascript +async function updateAllGuildStatus() { + const guilds = getAllGuildSettings(); + for (const guild of guilds) { + await updateGuildStatus(guild); + } +} +``` + +Jeder Server bekommt ein eigenes Embed mit: +- Server-Name und Status (Online/Offline) +- Aktuelle Spielerzahl +- CPU und RAM Auslastung +- Verbindungsadresse + +## Konfiguration + +### Umgebungsvariablen (.env) + +```env +DISCORD_CLIENT_ID=1458251194806833306 +DISCORD_CLIENT_SECRET=xxx +DISCORD_BOT_TOKEN=xxx +DISCORD_GUILD_ID=729865854329815051 # Haupt-Server für Login +DISCORD_ADMIN_ROLE_ID=1024693717434650736 +DISCORD_MOD_ROLE_ID=1024693170958766141 +``` + +**Hinweis**: `DISCORD_GUILD_ID` wird nur für den Discord OAuth Login verwendet, nicht für den Bot selbst. + +### Developer Portal Einstellungen + +1. Gehe zu https://discord.com/developers/applications +2. Wähle die Bot-Application +3. **Bot** Tab: + - "Public Bot" aktivieren (damit andere einladen können) + - Privileged Gateway Intents: + - Server Members Intent: Optional + - Message Content Intent: Nicht benötigt + +## Dateien + +| Datei | Beschreibung | +|-------|--------------| +| `backend/services/discordBot.js` | Bot-Logik und Event-Handler | +| `backend/db/init.js` | Guild-Settings DB-Funktionen | +| `frontend/src/pages/Dashboard.jsx` | Invite-Button im Dashboard | + +## Troubleshooting + +### Bot erstellt keine Channels + +- Prüfen ob Bot "Manage Channels" Permission hat +- Prüfen ob Bot-Rolle hoch genug in der Rollen-Hierarchie ist + +### Status-Nachricht wird nicht aktualisiert + +```bash +pm2 logs gameserver-backend --lines 50 +``` + +Suche nach `[DiscordBot]` Log-Einträgen. + +### Bot aus Datenbank entfernen + +```bash +sqlite3 /opt/gameserver-monitor/backend/users.sqlite +DELETE FROM guild_settings WHERE guild_id = 'xxx'; +``` + +## Login vs. Bot + +| Feature | Login (OAuth) | Bot | +|---------|--------------|-----| +| Erfordert Mitgliedschaft | Haupt-Discord | Nein | +| Server-Steuerung | Ja (je nach Rolle) | Nein | +| Status sehen | Ja | Ja | +| Alerts erhalten | Nein | Ja | +| Verfügbar für | Haupt-Discord Mitglieder | Alle mit Bot | + +Der Login zur Webapp erfordert Mitgliedschaft im Haupt-Discord-Server (DISCORD_GUILD_ID). Der Bot ist davon unabhängig und zeigt nur passive Status-Updates. diff --git a/gsm-backend/services/discordBot.js b/gsm-backend/services/discordBot.js new file mode 100644 index 0000000..4a28670 --- /dev/null +++ b/gsm-backend/services/discordBot.js @@ -0,0 +1,521 @@ +import { Client, GatewayIntentBits, EmbedBuilder, ChannelType, PermissionFlagsBits } from 'discord.js'; +import { readFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; +import { getServerStatus } from './ssh.js'; +import { getPlayers, getPlayerList } from './rcon.js'; +import { initGuildSettings, getGuildSettings, getAllGuildSettings, setGuildSettings, deleteGuildSettings } from '../db/init.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +let client = null; + +// State tracking for alerts (per guild) +const previousServerState = new Map(); +const previousPlayerLists = new Map(); + +// Server display config +const serverDisplay = { + minecraft: { name: 'Minecraft ATM10', icon: '⛏️', color: 0x7B5E3C, address: 'minecraft.zeasy.dev' }, + factorio: { name: 'Factorio', icon: '⚙️', color: 0xF97316, address: 'factorio.zeasy.dev' }, + zomboid: { name: 'Project Zomboid', icon: '🧟', color: 0x4ADE80, address: 'pz.zeasy.dev:16261' }, + vrising: { name: 'V Rising', icon: '🧛', color: 0xDC2626, address: 'vrising.zeasy.dev' }, + palworld: { name: 'Palworld', icon: '🦎', color: 0x00D4AA, address: 'palworld.zeasy.dev:8211' } +}; + +function loadConfig() { + return JSON.parse(readFileSync(join(__dirname, '..', 'config.json'), 'utf-8')); +} + +// ============================================ +// Channel Setup Functions +// ============================================ + +async function setupGuildChannels(guild) { + console.log('[DiscordBot] Setting up channels for guild: ' + guild.name); + + try { + // Create category + const category = await guild.channels.create({ + name: '🎮 Gameserver', + type: ChannelType.GuildCategory, + permissionOverwrites: [ + { + id: guild.id, // @everyone + deny: [PermissionFlagsBits.SendMessages], + allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ReadMessageHistory] + }, + { + id: client.user.id, // Bot + allow: [PermissionFlagsBits.SendMessages, PermissionFlagsBits.ManageMessages, PermissionFlagsBits.EmbedLinks] + } + ] + }); + + // Create info channel + const infoChannel = await guild.channels.create({ + name: 'ℹ️│info', + type: ChannelType.GuildText, + parent: category.id, + topic: 'Informationen zum Gameserver Management System' + }); + + // Create status channel + const statusChannel = await guild.channels.create({ + name: '📊│status', + type: ChannelType.GuildText, + parent: category.id, + topic: 'Live-Status aller Gameserver' + }); + + // Create alerts channel + const alertsChannel = await guild.channels.create({ + name: '📢│alerts', + type: ChannelType.GuildText, + parent: category.id, + topic: 'Benachrichtigungen über Server-Events und Spieler' + }); + + // Create updates channel + const updatesChannel = await guild.channels.create({ + name: '📰│updates', + type: ChannelType.GuildText, + parent: category.id, + topic: 'Ankündigungen zu neuen Gameservern' + }); + + // Create discussion channel (users can write) + const discussionChannel = await guild.channels.create({ + name: '💬│diskussion', + type: ChannelType.GuildText, + parent: category.id, + topic: 'Diskutiert hier über die Gameserver', + permissionOverwrites: [ + { + id: guild.id, + allow: [PermissionFlagsBits.SendMessages, PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ReadMessageHistory] + } + ] + }); + + // Create requests forum channel + const requestsChannel = await guild.channels.create({ + name: '💡│requests', + type: ChannelType.GuildForum, + parent: category.id, + topic: 'Schlage neue Gameserver vor', + defaultAutoArchiveDuration: 10080, // 7 days + permissionOverwrites: [ + { + id: guild.id, + allow: [PermissionFlagsBits.SendMessages, PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ReadMessageHistory, PermissionFlagsBits.CreatePublicThreads] + } + ] + }); + + // Create info thread in requests forum + const infoThread = await requestsChannel.threads.create({ + name: '📌 Wie funktioniert dieser Kanal?', + message: { + embeds: [new EmbedBuilder() + .setTitle('💡 Gameserver Vorschläge') + .setDescription( + 'In diesem Kanal kannst du neue Gameserver vorschlagen!\n\n' + + '**So funktioniert\'s:**\n' + + '1. Erstelle einen neuen Beitrag mit dem Namen des Spiels\n' + + '2. Beschreibe warum dieser Server cool wäre\n' + + '3. Andere können deinen Vorschlag diskutieren und liken\n\n' + + '**Was wir berücksichtigen:**\n' + + '• Interesse der Community (Likes & Diskussion)\n' + + '• Technische Machbarkeit\n' + + '• Serverkosten\n\n' + + 'Beliebte Vorschläge werden von uns geprüft!' + ) + .setColor(0x5865F2) + .setFooter({ text: 'Zeasy Gameserver' }) + ] + } + }); + + // Pin the info thread + await infoThread.pin(); + + // Create info message + const infoEmbed = new EmbedBuilder() + .setTitle('🎮 Zeasy Gameserver Management') + .setDescription( + 'Verwalte und überwache unsere Gameserver bequem über das Web-Interface.\n\n' + + '**Features:**\n' + + '• Server starten, stoppen & neustarten\n' + + '• Live Server-Status & Spielerlisten\n' + + '• Server-Konsole & RCON-Befehle\n' + + '• CPU, RAM & Uptime Metriken\n' + + '• Welten-Verwaltung (Factorio)\n' + + '• Config-Editor (Palworld, Zomboid)\n\n' + + '**Zugang:**\n' + + 'Melde dich mit deinem Discord-Account an.' + ) + .setColor(0x5865F2) + .addFields({ + name: '🔗 Web-Interface', + value: '[server.zeasy.dev](https://server.zeasy.dev)', + inline: false + }) + .setFooter({ text: 'Zeasy Software' }) + .setTimestamp(); + + await infoChannel.send({ embeds: [infoEmbed] }); + + // Create initial status message + const statusMsg = await statusChannel.send({ embeds: [createLoadingEmbed()] }); + + // Save settings to database + const settings = { + category_id: category.id, + info_channel_id: infoChannel.id, + status_channel_id: statusChannel.id, + status_message_id: statusMsg.id, + alerts_channel_id: alertsChannel.id, + updates_channel_id: updatesChannel.id, + discussion_channel_id: discussionChannel.id, + requests_channel_id: requestsChannel.id, + requests_info_thread_id: infoThread.id + }; + + setGuildSettings(guild.id, settings); + + console.log('[DiscordBot] Setup complete for guild: ' + guild.name); + + // Send welcome message in alerts + const welcomeEmbed = new EmbedBuilder() + .setTitle('🎉 Gameserver Bot eingerichtet!') + .setDescription( + 'Die Gameserver-Kanäle wurden erfolgreich erstellt.\n\n' + + '**Kanäle:**\n' + + '• <#' + infoChannel.id + '> - Allgemeine Infos\n' + + '• <#' + statusChannel.id + '> - Live Server-Status\n' + + '• <#' + alertsChannel.id + '> - Benachrichtigungen\n' + + '• <#' + updatesChannel.id + '> - Server-Ankündigungen\n' + + '• <#' + discussionChannel.id + '> - Diskussion\n' + + '• <#' + requestsChannel.id + '> - Server-Vorschläge' + ) + .setColor(0x22C55E) + .setTimestamp(); + + await alertsChannel.send({ embeds: [welcomeEmbed] }); + + return settings; + + } catch (err) { + console.error('[DiscordBot] Error setting up guild channels:', err); + throw err; + } +} + +// ============================================ +// Alert Functions +// ============================================ + +async function sendAlertToAllGuilds(embed) { + const allSettings = getAllGuildSettings(); + + for (const settings of allSettings) { + if (!settings.alerts_channel_id) continue; + + try { + const channel = await client.channels.fetch(settings.alerts_channel_id); + if (channel) { + await channel.send({ embeds: [embed] }); + } + } catch (err) { + console.error('[DiscordBot] Error sending alert to guild ' + settings.guild_id + ':', err.message); + } + } +} + +async function checkAndSendAlerts(serverStatuses) { + for (const server of serverStatuses) { + const display = serverDisplay[server.id] || { name: server.name, icon: '🖥️', color: 0x6B7280 }; + const prevState = previousServerState.get(server.id); + const prevPlayers = previousPlayerLists.get(server.id) || []; + + // Check server status changes + if (prevState !== undefined && prevState !== server.status) { + let embed; + + if (server.status === 'online' && prevState !== 'online') { + embed = new EmbedBuilder() + .setTitle(display.icon + ' Server gestartet') + .setDescription('**' + display.name + '** ist jetzt online') + .setColor(0x22C55E) + .setTimestamp(); + } else if (server.status === 'offline' && prevState === 'online') { + embed = new EmbedBuilder() + .setTitle(display.icon + ' Server gestoppt') + .setDescription('**' + display.name + '** ist jetzt offline') + .setColor(0xEF4444) + .setTimestamp(); + } + + if (embed) { + await sendAlertToAllGuilds(embed); + } + } + + // Check player changes (only if server is online) + if (server.status === 'online' && server.playerList) { + const currentPlayers = server.playerList; + + for (const player of currentPlayers) { + if (!prevPlayers.includes(player)) { + const embed = new EmbedBuilder() + .setTitle('➡️ Spieler beigetreten') + .setDescription('**' + player + '** hat **' + display.name + '** betreten') + .setColor(0x22C55E) + .setTimestamp(); + await sendAlertToAllGuilds(embed); + } + } + + for (const player of prevPlayers) { + if (!currentPlayers.includes(player)) { + const embed = new EmbedBuilder() + .setTitle('⬅️ Spieler verlassen') + .setDescription('**' + player + '** hat **' + display.name + '** verlassen') + .setColor(0xF59E0B) + .setTimestamp(); + await sendAlertToAllGuilds(embed); + } + } + + previousPlayerLists.set(server.id, [...currentPlayers]); + } else { + previousPlayerLists.set(server.id, []); + } + + previousServerState.set(server.id, server.status); + } +} + +// ============================================ +// Status Update Functions +// ============================================ + +function createLoadingEmbed() { + return new EmbedBuilder() + .setTitle('🎮 Gameserver Status') + .setDescription('Lade Server-Status...') + .setColor(0x6B7280) + .setTimestamp(); +} + +function createStatusEmbeds(serverStatuses) { + const embeds = []; + + const onlineCount = serverStatuses.filter(s => s.running).length; + const totalPlayers = serverStatuses.reduce((sum, s) => sum + s.players, 0); + + const headerEmbed = new EmbedBuilder() + .setTitle('🎮 Gameserver Status') + .setDescription('**' + onlineCount + '/' + serverStatuses.length + '** Server online • **' + totalPlayers + '** Spieler') + .setColor(onlineCount > 0 ? 0x22C55E : 0xEF4444) + .setTimestamp() + .setFooter({ text: 'Aktualisiert alle 60 Sekunden' }); + + embeds.push(headerEmbed); + + for (const server of serverStatuses) { + const display = serverDisplay[server.id] || { name: server.name, icon: '🖥️', color: 0x6B7280, address: '' }; + + const serverEmbed = new EmbedBuilder() + .setTitle(display.icon + ' ' + display.name) + .setColor(server.running ? display.color : 0x4B5563); + + if (server.running) { + let description = '✅ **Online**\n'; + + if (display.address) { + description += '```' + display.address + '```'; + } + + description += '👥 **Spieler:** ' + server.players; + if (server.maxPlayers) { + description += '/' + server.maxPlayers; + } + + if (server.playerList && server.playerList.length > 0) { + const names = server.playerList.slice(0, 15).join(', '); + description += '\n' + names; + if (server.playerList.length > 15) { + description += ' *+' + (server.playerList.length - 15) + ' mehr*'; + } + } + + serverEmbed.setDescription(description); + } else { + serverEmbed.setDescription('❌ **Offline**'); + } + + embeds.push(serverEmbed); + } + + return embeds; +} + +async function fetchServerStatuses() { + const config = loadConfig(); + + return await Promise.all(config.servers.map(async (server) => { + try { + const status = await getServerStatus(server); + const running = status === 'online'; + + let players = { online: 0, max: null }; + let playerList = { players: [] }; + + if (running && server.rconPassword) { + try { + players = await getPlayers(server); + playerList = await getPlayerList(server); + } catch (e) { + // RCON might fail + } + } + + return { + id: server.id, + name: server.name, + type: server.type, + status: running ? 'online' : 'offline', + running, + players: players.online || 0, + maxPlayers: players.max, + playerList: playerList.players || [] + }; + } catch (err) { + return { + id: server.id, + name: server.name, + type: server.type, + status: 'unreachable', + running: false, + players: 0, + maxPlayers: null, + playerList: [] + }; + } + })); +} + +async function updateAllStatusMessages(skipAlerts = false) { + if (!client) return; + + try { + const serverStatuses = await fetchServerStatuses(); + const embeds = createStatusEmbeds(serverStatuses); + + // Send alerts + if (!skipAlerts) { + await checkAndSendAlerts(serverStatuses); + } else { + // Just populate initial state + for (const server of serverStatuses) { + previousServerState.set(server.id, server.status); + previousPlayerLists.set(server.id, server.playerList || []); + } + } + + // Update status message in all guilds + const allSettings = getAllGuildSettings(); + + for (const settings of allSettings) { + if (!settings.status_channel_id || !settings.status_message_id) continue; + + try { + const channel = await client.channels.fetch(settings.status_channel_id); + const message = await channel.messages.fetch(settings.status_message_id); + await message.edit({ embeds }); + } catch (err) { + console.error('[DiscordBot] Error updating status for guild ' + settings.guild_id + ':', err.message); + } + } + + } catch (err) { + console.error('[DiscordBot] Error updating status messages:', err.message); + } +} + +// ============================================ +// Public Functions +// ============================================ + +export async function sendUpdateToAllGuilds(embed) { + const allSettings = getAllGuildSettings(); + + for (const settings of allSettings) { + if (!settings.updates_channel_id) continue; + + try { + const channel = await client.channels.fetch(settings.updates_channel_id); + if (channel) { + await channel.send({ embeds: [embed] }); + } + } catch (err) { + console.error('[DiscordBot] Error sending update to guild ' + settings.guild_id + ':', err.message); + } + } +} + +export async function initDiscordBot() { + const token = process.env.DISCORD_BOT_TOKEN; + + if (!token) { + console.log('[DiscordBot] Missing DISCORD_BOT_TOKEN, bot disabled'); + return; + } + + // Initialize database table + initGuildSettings(); + + client = new Client({ + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages] + }); + + // Bot joined a new server + client.on('guildCreate', async (guild) => { + console.log('[DiscordBot] Joined new guild: ' + guild.name + ' (' + guild.id + ')'); + + try { + await setupGuildChannels(guild); + } catch (err) { + console.error('[DiscordBot] Failed to setup guild:', err.message); + } + }); + + // Bot was removed from a server + client.on('guildDelete', (guild) => { + console.log('[DiscordBot] Left guild: ' + guild.name + ' (' + guild.id + ')'); + deleteGuildSettings(guild.id); + }); + + client.once('ready', async () => { + console.log('[DiscordBot] Logged in as ' + client.user.tag); + + // First run - populate state without alerts + await updateAllStatusMessages(true); + + // Regular updates every 60 seconds + setInterval(() => updateAllStatusMessages(false), 60000); + }); + + client.login(token).catch(err => { + console.error('[DiscordBot] Failed to login:', err.message); + }); +} + +export function getDiscordClient() { + return client; +} + +// Export setup function for manual setup via API +export { setupGuildChannels }; diff --git a/gsm-frontend/src/api.js b/gsm-frontend/src/api.js index fa78a87..7d48674 100644 --- a/gsm-frontend/src/api.js +++ b/gsm-frontend/src/api.js @@ -249,3 +249,27 @@ export async function getActivityLog(token, limit = 100) { headers: { Authorization: `Bearer ${token}` }, }) } + +// Display Settings +export async function getAllDisplaySettings(token) { + return fetchAPI('/servers/display-settings', { + headers: token ? { Authorization: `Bearer ${token}` } : {}, + }) +} + +export async function getDisplaySettings(token, serverId) { + return fetchAPI(`/servers/${serverId}/display-settings`, { + headers: { Authorization: `Bearer ${token}` }, + }) +} + +export async function saveDisplaySettings(token, serverId, address, hint) { + return fetchAPI(`/servers/${serverId}/display-settings`, { + method: 'PUT', + headers: { Authorization: `Bearer ${token}` }, + body: JSON.stringify({ address, hint }), + }) +} + +// Alias for backwards compatibility +export const setDisplaySettings = saveDisplaySettings diff --git a/gsm-frontend/src/pages/Dashboard.jsx b/gsm-frontend/src/pages/Dashboard.jsx index acebbff..c48bc17 100644 --- a/gsm-frontend/src/pages/Dashboard.jsx +++ b/gsm-frontend/src/pages/Dashboard.jsx @@ -230,6 +230,34 @@ export default function Dashboard({ onLogin, onLogout }) { ))} )} + + {/* Discord Bot Invite Section */} +
+
+
+ + + +
+

Discord Bot

+

+ Willst du Server-Status Updates auch auf deinem Discord? +

+
+
+ + + + + Bot einladen + +
+
{/* Modals */}