# Discord Bot Admin Panel - Implementation Guide **Version:** 1.0 **Date:** March 23, 2026 **Author:** Chronicler #40 (with architecture by Gemini/Google AI) **Audience:** Michael (setup) + Holly (usage) **Purpose:** Web-based admin panel for managing Discord bot role mappings --- ## 📋 TABLE OF CONTENTS 1. [Overview](#overview) 2. [Architecture](#architecture) 3. [Why We Built This](#why-we-built-this) 4. [Part 1: Prerequisites](#part-1-prerequisites) 5. [Part 2: Create Dedicated Bot User](#part-2-create-dedicated-bot-user) 6. [Part 3: Set Up Discord OAuth2 Application](#part-3-set-up-discord-oauth2-application) 7. [Part 4: Deploy Backend Code](#part-4-deploy-backend-code) 8. [Part 5: Deploy Frontend Code](#part-5-deploy-frontend-code) 9. [Part 6: Configure Nginx & SSL](#part-6-configure-nginx-ssl) 10. [Part 7: Holly's Usage Guide](#part-7-hollys-usage-guide) 11. [Testing & Verification](#testing-verification) 12. [Troubleshooting](#troubleshooting) 13. [Maintenance](#maintenance) --- ## 🎯 OVERVIEW ### What Is This? A secure web-based admin panel at `https://discord-bot.firefrostgaming.com/admin` where Holly can: - Log in using her Discord account (no passwords to remember) - Update Discord role mappings for all 10 subscription tiers - View bot status and recent webhook events - Save changes instantly (no SSH access needed) ### The Problem We Solved **Before:** - Discord bot role mappings were hardcoded in `bot.js` - Holly had to give Michael the role IDs via Discord/email - Michael had to SSH into Command Center to update the file - Michael had to restart the bot manually - Holly waited for Michael's availability **After:** - Holly logs into web panel with Discord OAuth - Holly updates role IDs in a simple form - Changes save instantly (in-memory update, no restart) - Config written atomically to disk with backup - Role IDs validated against Discord API before saving - Audit log posted to Discord channel **Result:** Holly is independent, Michael isn't the bottleneck. --- ## 🏗️ ARCHITECTURE ### Design Principles (Thanks to Gemini) This architecture was designed in consultation with Gemini (Google AI) and follows production best practices: **1. Security First** - Dedicated Linux user (`firefrost-bot`) - NOT root - Discord OAuth2 authentication (no password management) - Whitelist authorization (only Holly + Michael's Discord IDs) - HTTPS via Nginx + Let's Encrypt - CSRF protection on admin forms **2. Zero Downtime** - Configuration loaded into memory on startup - Updates modify in-memory config immediately - No bot restart required - Atomic disk writes (no corruption) - Backup of last-known-good config **3. Simplicity** - Single Node.js/Express app (webhook + admin in one) - JSON config file (no database needed for 10 key-value pairs) - Validation: Regex check + Discord API verification - In-memory webhook logs (last 50 events, rotating) **4. Auditability** - Discord `#bot-audit-logs` channel for config changes - Backup config file for disaster recovery - Systemd journal logs everything ### Tech Stack **Backend:** - Node.js 18+ - Express.js (web server) - discord.js (Discord API) - Passport.js (Discord OAuth2) - write-file-atomic (safe config saves) **Frontend:** - Simple HTML/CSS/JavaScript - Vanilla JS with `fetch()` API - No frameworks (keep it simple for Holly) - Fire/Frost branding **Infrastructure:** - Command Center (63.143.34.217) - Nginx reverse proxy - Let's Encrypt SSL - Systemd service --- ## 🤔 WHY WE BUILT THIS ### Holly's Perspective **Before:** - "I created Discord roles, now I need to wait for Michael to update the bot." - "I want to test if the role IDs work, but I can't update them myself." - "If I make a typo in the role ID I give Michael, we have to repeat the whole process." **After:** - "I created Discord roles, I'll paste the IDs into the admin panel." - "I can test immediately - if a role ID is wrong, the panel tells me before saving." - "I'm independent - I can iterate and test without waiting." ### Michael's Perspective **Before:** - "Holly needs role IDs updated. I have to stop what I'm doing, SSH in, edit the file, restart the bot." - "If I make a typo, Holly has to tell me, and I have to repeat the process." - "I'm the single point of failure for a 5-minute task." **After:** - "Holly handles her own role mappings. I only get involved if something breaks." - "The admin panel validates role IDs before saving, so typos get caught automatically." - "I set this up once, Holly uses it forever." --- ## ✅ PART 1: PREREQUISITES ### Before You Start **On Command Center (63.143.34.217):** - [ ] Discord bot already running (from Subscription Automation Guide Part 1) - [ ] Bot accessible at `https://webhook.firefrostgaming.com/webhook/paymenter` - [ ] Node.js 18+ installed - [ ] Nginx installed and configured - [ ] SSL certificate (Let's Encrypt) **Discord Requirements:** - [ ] Discord bot application exists - [ ] Bot is in Firefrost Gaming Discord server - [ ] You have bot token - [ ] You have Guild ID (server ID) - [ ] You have Holly's Discord user ID - [ ] You have Michael's Discord user ID **DNS Configuration:** - [ ] `discord-bot.firefrostgaming.com` A record → 63.143.34.217 - [ ] Cloudflare proxy: OFF (orange cloud = OFF) --- ## 🔧 PART 2: CREATE DEDICATED BOT USER **CRITICAL SECURITY FIX: Do NOT run the bot as root.** Running Node.js as root is a major security risk. If any npm package has a vulnerability, an attacker gets full control of Command Center. ### Step 1: Create System User SSH to Command Center: ```bash ssh root@63.143.34.217 ``` Create dedicated user: ```bash # Create system user (no login shell, no home directory login) sudo useradd -r -s /bin/false firefrost-bot # Verify user was created id firefrost-bot # Should show: uid=... gid=... groups=... ``` ### Step 2: Transfer Ownership Transfer bot directory to new user: ```bash # Change ownership of bot directory sudo chown -R firefrost-bot:firefrost-bot /opt/firefrost-discord-bot # Verify permissions ls -la /opt/firefrost-discord-bot # Should show: drwxr-xr-x ... firefrost-bot firefrost-bot ``` ### Step 3: Update Systemd Service Edit the service file: ```bash sudo nano /etc/systemd/system/firefrost-discord-bot.service ``` **Replace contents with this complete configuration:** ```ini [Unit] Description=Firefrost Discord Bot & Admin Panel After=network.target [Service] Type=simple User=firefrost-bot Group=firefrost-bot WorkingDirectory=/opt/firefrost-discord-bot ExecStart=/usr/bin/node bot.js Restart=on-failure RestartSec=10 Environment=NODE_ENV=production [Install] WantedBy=multi-user.target ``` **What this configuration does:** - **After=network.target:** Waits for network before starting - **User/Group=firefrost-bot:** Runs as dedicated user (NOT root) - **Restart=on-failure:** Auto-restarts if bot crashes - **RestartSec=10:** Waits 10 seconds before restart - **NODE_ENV=production:** Sets production environment Save and exit: `Ctrl+X`, `Y`, `Enter` Reload and restart: ```bash # Reload systemd sudo systemctl daemon-reload # Restart bot with new user sudo systemctl restart firefrost-discord-bot # Verify it's running sudo systemctl status firefrost-discord-bot # Should show: Active: active (running) # Check logs sudo journalctl -u firefrost-discord-bot -n 50 # Should show no permission errors ``` **If you see permission errors:** Fix file permissions and retry. --- ## 🔐 PART 3: SET UP DISCORD OAUTH2 APPLICATION ### Step 1: Discord Developer Portal 1. Go to: https://discord.com/developers/applications 2. Select your **Firefrost Subscription Manager** bot application 3. Click **OAuth2** in left sidebar ### Step 2: Add Redirect URLs Under **Redirects**, add: **For local testing (optional):** ``` http://localhost:3100/auth/discord/callback ``` **For production (required):** ``` https://discord-bot.firefrostgaming.com/auth/discord/callback ``` Click **Save Changes**. ### Step 3: Copy Credentials **Copy these values (you'll need them for .env file):** - **Client ID:** (18-digit number under OAuth2 General) - **Client Secret:** Click "Reset Secret" → Copy the new secret **⚠️ IMPORTANT:** The Client Secret only shows once. Copy it immediately and save to Vaultwarden. ### Step 4: Get Discord User IDs **Holly's Discord ID:** 1. In Discord, right-click Holly's username 2. Click "Copy User ID" 3. Paste somewhere safe (e.g., `123456789012345678`) **Michael's Discord ID:** 1. Same process for Michael's account 2. Paste somewhere safe **Note:** If "Copy User ID" doesn't appear, enable Developer Mode: - Discord Settings → Advanced → Developer Mode (toggle ON) --- ## 💻 PART 4: DEPLOY BACKEND CODE ### Overview Deploy the complete Discord bot backend with OAuth2 authentication, in-memory config management, atomic file writes, role validation, webhook logging, and Discord audit log embeds. **Architecture designed by:** Gemini (Google AI) **Implementation:** Production-ready Node.js/Express/Discord.js application --- ### Step 1: Install Dependencies SSH to Command Center: ```bash ssh root@63.143.34.217 cd /opt/firefrost-discord-bot ``` Install required npm packages: ```bash npm install express express-session passport passport-discord write-file-atomic dotenv discord.js ``` **What these packages do:** - **express:** Web server framework - **express-session:** Session management for OAuth - **passport:** Authentication framework - **passport-discord:** Discord OAuth2 strategy - **write-file-atomic:** Safe config file writes (prevents corruption) - **dotenv:** Environment variable management - **discord.js:** Discord API client library --- ### Step 2: Create Environment Variables File **Option A: Use the template (recommended)** Create `.env.template` first for reference: ```bash nano /opt/firefrost-discord-bot/.env.template ``` **Paste this complete template:** ```env # ========================================== # FIREFROST COMMAND CENTER - ENVIRONMENT VARIABLES # Copy this file to .env and fill in the values. # DO NOT commit the actual .env file to version control. # ========================================== # 1. Server Configuration # ------------------------------------------ # production or development NODE_ENV=production # The internal port the Node app runs on (Nginx proxies to this) PORT=3100 # A long, random string used to encrypt web session cookies SESSION_SECRET=YOUR_SUPER_SECRET_RANDOM_STRING_HERE # 2. Discord Bot Core # ------------------------------------------ # The actual token for the Firefrost Discord Bot (from Discord Developer Portal -> Bot) DISCORD_TOKEN=YOUR_BOT_TOKEN_HERE # The ID of the Firefrost Gaming Discord Server GUILD_ID=YOUR_SERVER_GUILD_ID_HERE # 3. Discord OAuth2 (Web Login) # ------------------------------------------ # From Discord Developer Portal -> OAuth2 -> General DISCORD_CLIENT_ID=YOUR_OAUTH_CLIENT_ID_HERE DISCORD_CLIENT_SECRET=YOUR_OAUTH_CLIENT_SECRET_HERE # The URL Discord will redirect to after login (Must match the one added in the Dev Portal exactly) CALLBACK_URL=https://discord-bot.firefrostgaming.com/auth/discord/callback # 4. Security & Auditing # ------------------------------------------ # Comma-separated list of Discord User IDs who are allowed to log in (Holly and Michael) # Example: 123456789,987654321 ALLOWED_ADMINS=HOLLYS_ID,MICHAELS_ID # The ID of the private #bot-audit-logs channel where config change embeds are sent AUDIT_CHANNEL_ID=YOUR_AUDIT_CHANNEL_ID_HERE ``` Save and exit: `Ctrl+X`, `Y`, `Enter` **Now create the actual .env file:** ```bash # Copy template to .env cp /opt/firefrost-discord-bot/.env.template /opt/firefrost-discord-bot/.env # Edit .env file nano /opt/firefrost-discord-bot/.env ``` **Option B: Create .env directly (alternative)** If you didn't create the template, create `.env` directly: ```bash nano /opt/firefrost-discord-bot/.env ``` **Paste the template above and fill in YOUR values below.** --- **How to fill in each value:** **DISCORD_TOKEN:** - Go to Discord Developer Portal → Your Bot → Bot → Token - Click "Reset Token" → Copy **DISCORD_CLIENT_ID:** - Discord Developer Portal → Your Bot → OAuth2 → Client ID - Copy the 18-digit number **DISCORD_CLIENT_SECRET:** - Discord Developer Portal → Your Bot → OAuth2 → Client Secret - Click "Reset Secret" → Copy (shows only once!) **GUILD_ID:** - In Discord, right-click your server icon → Copy ID - Requires Developer Mode enabled (User Settings → Advanced → Developer Mode) **CALLBACK_URL:** - Leave as: `https://discord-bot.firefrostgaming.com/auth/discord/callback` - Must match exactly what you added in Discord Developer Portal **SESSION_SECRET:** - Generate random string: `openssl rand -base64 48` - Copy output and paste here **ALLOWED_ADMINS:** - Holly's Discord ID: Right-click Holly's username → Copy ID - Michael's Discord ID: Right-click Michael's username → Copy ID - Format: `123456789012345678,987654321098765432` (comma-separated, no spaces) **AUDIT_CHANNEL_ID:** - Create `#bot-audit-logs` channel in Discord (private, restricted to Michael, Holly, bot) - Right-click channel → Copy ID **NODE_ENV:** - Leave as: `production` **PORT:** - Leave as: `3100` (or change if you need different port) Save and exit: `Ctrl+X`, `Y`, `Enter` --- ### Step 3: Set Environment File Permissions **CRITICAL:** `.env` file contains secrets and must be read-only for bot user: ```bash # Make .env readable only by firefrost-bot user chmod 600 /opt/firefrost-discord-bot/.env chown firefrost-bot:firefrost-bot /opt/firefrost-discord-bot/.env # Verify permissions ls -la /opt/firefrost-discord-bot/.env # Should show: -rw------- firefrost-bot firefrost-bot ``` --- ### Step 4: Deploy Complete bot.js **Back up existing bot.js (if it exists):** ```bash # Only if you have an existing bot.js cp /opt/firefrost-discord-bot/bot.js /opt/firefrost-discord-bot/bot.js.backup ``` Create new bot.js: ```bash nano /opt/firefrost-discord-bot/bot.js ``` **Paste the complete production-ready code below.** **⚠️ COPY THE ENTIRE FILE - ALL 8 SECTIONS:** ```javascript // ============================================================================ // FIREFROST GAMING - DISCORD BOT ADMIN PANEL // Command Center Server (63.143.34.217) // Architecture by: Gemini (Google AI) - March 23, 2026 // Implementation by: Chronicler #40 (Claude) + Michael // ============================================================================ // ============================================================================ // SECTION 1: IMPORTS AND ENVIRONMENT SETUP // ============================================================================ require('dotenv').config(); const express = require('express'); const session = require('express-session'); const passport = require('passport'); const DiscordStrategy = require('passport-discord').Strategy; const fs = require('fs'); const path = require('path'); const writeFileAtomic = require('write-file-atomic'); const { Client, GatewayIntentBits, EmbedBuilder } = require('discord.js'); const app = express(); const PORT = process.env.PORT || 3100; // ============================================================================ // SECTION 2: CONSTANTS AND IN-MEMORY STATE // ============================================================================ const CONFIG_PATH = path.join(__dirname, 'config.json'); const BACKUP_PATH = path.join(__dirname, 'config.json.backup'); const PRODUCT_NAMES = { '2': 'The Awakened', '3': 'Fire Elemental', '4': 'Frost Elemental', '5': 'Fire Knight', '6': 'Frost Knight', '7': 'Fire Master', '8': 'Frost Master', '9': 'Fire Legend', '10': 'Frost Legend', '11': 'Sovereign' }; let currentConfig = {}; let webhookLogs = []; // Circular buffer (max 50) // Load initial config try { if (fs.existsSync(CONFIG_PATH)) { currentConfig = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); } } catch (error) { console.error('Failed to load initial config:', error); } // ============================================================================ // SECTION 3: HELPER FUNCTIONS (FILE IO & DISCORD API) // ============================================================================ async function saveConfig(newConfig) { try { // Create backup of current config before overwriting if (fs.existsSync(CONFIG_PATH)) { fs.copyFileSync(CONFIG_PATH, BACKUP_PATH); } // Atomically write new config (prevents corruption) await writeFileAtomic(CONFIG_PATH, JSON.stringify(newConfig, null, 2)); // Update in-memory state only if write succeeds currentConfig = newConfig; return true; } catch (error) { console.error('Failed to save config:', error); return false; } } async function roleExists(client, guildId, roleId) { try { const guild = await client.guilds.fetch(guildId); const role = await guild.roles.fetch(roleId); return !!role; } catch (error) { return false; } } // ============================================================================ // SECTION 4: AUDIT LOG GENERATOR // ============================================================================ async function sendAuditLog(client, req, productId, oldRoleId, newRoleId) { try { const auditChannelId = process.env.AUDIT_CHANNEL_ID; if (!auditChannelId) return; const channel = await client.channels.fetch(auditChannelId); if (!channel) return console.error('Audit channel not found.'); const productName = PRODUCT_NAMES[productId] || 'Unknown Product'; const isFrost = productName.includes('Frost'); const embedColor = isFrost ? 0x4ECDC4 : 0xFF6B35; // Frost Blue or Fire Orange const userName = req.user ? req.user.username : 'Unknown Admin'; const userAvatar = req.user && req.user.avatar ? `https://cdn.discordapp.com/avatars/${req.user.id}/${req.user.avatar}.png` : null; const embed = new EmbedBuilder() .setColor(embedColor) .setAuthor({ name: `${userName} updated a role mapping`, iconURL: userAvatar }) .setTitle('📝 Configuration Changed') .addFields( { name: 'Product', value: `Product ${productId} (${productName})`, inline: false }, { name: 'Old Role ID', value: oldRoleId ? `\`${oldRoleId}\`` : '`None`', inline: true }, { name: 'New Role ID', value: `\`${newRoleId}\``, inline: true } ) .setTimestamp() .setFooter({ text: 'Firefrost Command Center' }); await channel.send({ embeds: [embed] }); } catch (error) { console.error('Failed to send audit log:', error); } } // ============================================================================ // SECTION 5: PASSPORT & MIDDLEWARE SETUP // ============================================================================ const allowedAdmins = (process.env.ALLOWED_ADMINS || '').split(','); passport.serializeUser((user, done) => done(null, user)); passport.deserializeUser((obj, done) => done(null, obj)); passport.use(new DiscordStrategy({ clientID: process.env.DISCORD_CLIENT_ID, clientSecret: process.env.DISCORD_CLIENT_SECRET, callbackURL: process.env.CALLBACK_URL, scope: ['identify'] }, (accessToken, refreshToken, profile, done) => { // Check if user is in whitelist if (allowedAdmins.includes(profile.id)) return done(null, profile); return done(null, false, { message: 'Access Denied' }); })); app.use(express.json()); app.use(express.static('public')); // Serve frontend files app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { secure: process.env.NODE_ENV === 'production' } })); app.use(passport.initialize()); app.use(passport.session()); function isAuthenticated(req, res, next) { if (req.isAuthenticated()) return next(); res.status(401).json({ error: 'Unauthorized' }); } // ============================================================================ // SECTION 6: AUTHENTICATION & UI ROUTES // ============================================================================ app.get('/auth/discord', passport.authenticate('discord')); app.get('/auth/discord/callback', passport.authenticate('discord', { failureRedirect: '/' }), (req, res) => res.redirect('/') ); app.get('/logout', (req, res) => { req.logout(() => res.redirect('/')); }); // ============================================================================ // SECTION 7: API ROUTES (CONFIG & LOGS) // ============================================================================ // Get current config app.get('/api/config', isAuthenticated, (req, res) => { res.json(currentConfig); }); // Get webhook logs app.get('/api/logs', isAuthenticated, (req, res) => { res.json(webhookLogs); }); // Update config (save role mapping) app.post('/api/config', isAuthenticated, async (req, res) => { const { productId, roleId } = req.body; // Validation: Regex check (18-19 digit snowflake) if (!/^\d{17,19}$/.test(roleId)) { return res.status(400).json({ error: 'Invalid Discord Role ID format.' }); } // Validation: Discord API check (role exists in guild) const isValid = await roleExists(client, process.env.GUILD_ID, roleId); if (!isValid) { return res.status(400).json({ error: 'Role does not exist in the Discord server.' }); } // Capture old role ID for audit log const oldRoleId = currentConfig[productId]; // Update config const newConfig = { ...currentConfig, [productId]: roleId }; const success = await saveConfig(newConfig); if (success) { // Send audit log to Discord (async, don't block response) sendAuditLog(client, req, productId, oldRoleId, roleId); res.json({ success: true, message: 'Configuration updated successfully.' }); } else { res.status(500).json({ error: 'Failed to save configuration.' }); } }); // ============================================================================ // SECTION 8: WEBHOOK RECEIVER & INITIALIZATION // ============================================================================ app.post('/webhook/paymenter', async (req, res) => { // TODO: Add your Paymenter webhook validation logic here // TODO: Add Discord role assignment logic here // Example logging (adapt to your actual webhook payload) const isSuccess = true; // Replace with actual success status const incomingProductId = req.body.productId || 'Unknown'; const incomingUserId = req.body.userId || 'Unknown'; // Log webhook event (circular buffer, max 50) webhookLogs.unshift({ timestamp: new Date().toISOString(), productId: incomingProductId, userId: incomingUserId, status: isSuccess ? 'Success' : 'Failed', success: isSuccess, error: isSuccess ? null : 'Assignment failed' }); // Keep only last 50 events if (webhookLogs.length > 50) webhookLogs.pop(); res.status(200).send('Webhook processed'); }); // Initialize Discord Client const client = new Client({ intents: [GatewayIntentBits.Guilds] }); client.once('ready', () => { console.log(`Bot logged in as ${client.user.tag}`); // Start Express server AFTER Discord client is ready app.listen(PORT, () => { console.log(`Firefrost Command Center running on port ${PORT}`); }); }); // Login to Discord client.login(process.env.DISCORD_TOKEN); ``` Save and exit: `Ctrl+X`, `Y`, `Enter` --- ### Step 5: Set File Permissions Ensure firefrost-bot user owns the bot.js file: ```bash chown firefrost-bot:firefrost-bot /opt/firefrost-discord-bot/bot.js chmod 644 /opt/firefrost-discord-bot/bot.js ``` --- ### Step 6: Create Discord Audit Log Channel Before starting the bot, create the audit log channel in Discord: 1. Open Discord server 2. Create new channel: `#bot-audit-logs` 3. Set channel to **Private** 4. Add members: Michael, Holly, Firefrost Subscription Manager (bot) 5. Right-click channel → Copy ID 6. Add channel ID to `.env` file (`AUDIT_CHANNEL_ID`) --- ### Step 7: Restart Bot Service Apply all changes: ```bash # Restart bot with new code sudo systemctl restart firefrost-discord-bot # Check status sudo systemctl status firefrost-discord-bot # Should show: Active: active (running) # View logs to verify startup sudo journalctl -u firefrost-discord-bot -n 50 ``` **Expected log output:** ``` Bot logged in as Firefrost Subscription Manager#1234 Firefrost Command Center running on port 3100 ``` If you see these messages, backend deployment is successful! ✅ --- ## 🎯 BACKEND CODE FEATURES ### Security - ✅ Runs as `firefrost-bot` user (not root) - ✅ Discord OAuth2 authentication - ✅ Whitelist authorization (Holly + Michael only) - ✅ Session management with secure cookies - ✅ Environment variables for secrets ### Configuration Management - ✅ In-memory config (zero downtime updates) - ✅ Atomic file writes (prevents corruption) - ✅ Automatic backups (config.json.backup) - ✅ Validation before save (regex + Discord API) ### Validation - ✅ Regex check (18-19 digit snowflake) - ✅ Discord API verification (role exists in guild) - ✅ Error messages returned to frontend ### Audit Logging - ✅ Discord embed posts to #bot-audit-logs - ✅ Dynamic Fire/Frost colors (based on product name) - ✅ Shows who changed what (username + avatar) - ✅ Shows old → new role ID - ✅ Timestamp + footer ### Webhook Logging - ✅ Circular buffer (last 50 events) - ✅ In-memory (no database needed) - ✅ Accessible via `/api/logs` endpoint - ✅ Shows in admin panel logs table --- **Backend deployment complete!** ✅ Next: Deploy Frontend Code (Part 5) --- **Code provided by:** Gemini (Google AI) - March 23, 2026 --- ## 🎨 PART 5: DEPLOY FRONTEND CODE ### Overview The frontend provides Holly with a clean, mobile-friendly interface to manage Discord role mappings. It features: - Discord OAuth login flow - 10 product → role ID input fields - Per-row save buttons with validation feedback - Bot status indicator - Recent webhook logs table with manual refresh - Fire/Frost branding (#FF6B35 / #4ECDC4) **Tech:** Vanilla HTML/CSS/JavaScript (no frameworks) **Design by:** Gemini (Google AI) --- ### Step 1: Create Public Directory SSH to Command Center: ```bash ssh root@63.143.34.217 cd /opt/firefrost-discord-bot # Create public directory mkdir -p public # Set ownership chown firefrost-bot:firefrost-bot public ``` --- ### Step 2: Enable Static File Serving Edit `bot.js` to serve static files: ```bash nano /opt/firefrost-discord-bot/bot.js ``` Add this line after `app.use(express.json());`: ```javascript app.use(express.static('public')); ``` **Full context in bot.js:** ```javascript const app = express(); app.use(express.json()); // For parsing application/json app.use(express.static('public')); // <-- ADD THIS LINE ``` Save and exit. --- ### Step 3: Create index.html Create the main HTML file: ```bash nano /opt/firefrost-discord-bot/public/index.html ``` **Paste this complete HTML:** ```html Firefrost Gaming - Command Center ``` Save and exit: `Ctrl+X`, `Y`, `Enter` --- ### Step 4: Create style.css Create the CSS stylesheet with Fire/Frost branding: ```bash nano /opt/firefrost-discord-bot/public/style.css ``` **Paste this complete CSS:** ```css :root { --fire: #FF6B35; --frost: #4ECDC4; --bg-dark: #121212; --bg-card: #1E1E1E; --text-main: #FFFFFF; --text-muted: #A0A0A0; --error: #FF4C4C; --success: #4CC9F0; } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, sans-serif; background-color: var(--bg-dark); color: var(--text-main); line-height: 1.6; } .hidden { display: none !important; } .view { min-height: 100vh; display: flex; flex-direction: column; } .container { max-width: 800px; margin: 0 auto; padding: 20px; width: 100%; } /* Cards & Nav */ .card { background: var(--bg-card); border-radius: 8px; padding: 30px; text-align: center; border-top: 4px solid var(--fire); } .login-card { max-width: 400px; margin: auto; } .login-card h1 { margin-bottom: 10px; } .login-card p { margin-bottom: 25px; color: var(--text-muted); } .navbar { display: flex; justify-content: space-between; align-items: center; padding: 15px 30px; background: var(--bg-card); border-bottom: 2px solid var(--frost); } .brand { font-size: 1.2em; font-weight: bold; } .nav-actions { display: flex; gap: 15px; align-items: center; } .status-badge { padding: 5px 10px; border-radius: 4px; font-size: 0.85em; background: #2A2A2A; } /* Mapping Section */ .mapping-section { margin-bottom: 40px; } .mapping-section h2 { margin-bottom: 5px; } .subtitle { color: var(--text-muted); margin-bottom: 20px; } /* Role Form Rows */ .role-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; background: var(--bg-card); padding: 15px; border-radius: 8px; margin-bottom: 15px; } .role-info { flex: 1; min-width: 200px; } .role-info strong { display: block; font-size: 1.1em; } .role-info span { font-size: 0.85em; color: var(--text-muted); } .role-input { padding: 10px; border: 1px solid #333; border-radius: 4px; background: #2A2A2A; color: white; width: 200px; font-family: monospace; } .role-input:focus { outline: none; border-color: var(--frost); } /* Buttons */ .btn { padding: 10px 15px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; text-decoration: none; display: inline-block; transition: opacity 0.2s; } .btn:hover { opacity: 0.8; } .btn:disabled { opacity: 0.5; cursor: not-allowed; } .save-btn { background: var(--fire); color: white; } .login-btn { background: #5865F2; color: white; width: 100%; } .logout-btn { background: #333; color: white; padding: 5px 10px; font-size: 0.9em; } .secondary-btn { background: var(--frost); color: #121212; } .error-text { color: var(--error); font-size: 0.85em; width: 100%; margin-top: 5px; } /* Logs Section */ .logs-section { margin-top: 40px; } .logs-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .table-container { background: var(--bg-card); border-radius: 8px; overflow-x: auto; } /* Table */ table { width: 100%; border-collapse: collapse; } th, td { padding: 12px; text-align: left; border-bottom: 1px solid #333; } th { background: #2A2A2A; font-weight: bold; } tbody tr:hover { background: #252525; } /* Mobile Responsive */ @media (max-width: 600px) { .role-row { flex-direction: column; align-items: stretch; } .role-input { width: 100%; } .navbar { flex-direction: column; gap: 10px; } .logs-header { flex-direction: column; align-items: stretch; gap: 10px; } } ``` Save and exit: `Ctrl+X`, `Y`, `Enter` --- ### Step 5: Create app.js Create the JavaScript application logic: ```bash nano /opt/firefrost-discord-bot/public/app.js ``` **Paste this complete JavaScript:** ```javascript // Product definitions for the UI const PRODUCTS = [ { id: '2', name: 'The Awakened', type: '$1 one-time' }, { id: '3', name: 'Fire Elemental', type: '$5/mo' }, { id: '4', name: 'Frost Elemental', type: '$5/mo' }, { id: '5', name: 'Fire Knight', type: '$10/mo' }, { id: '6', name: 'Frost Knight', type: '$10/mo' }, { id: '7', name: 'Fire Master', type: '$15/mo' }, { id: '8', name: 'Frost Master', type: '$15/mo' }, { id: '9', name: 'Fire Legend', type: '$20/mo' }, { id: '10', name: 'Frost Legend', type: '$20/mo' }, { id: '11', name: 'Sovereign', type: '$499 one-time' } ]; document.addEventListener('DOMContentLoaded', initApp); async function initApp() { try { // Try to fetch config. If we get a 401, they need to log in. const response = await fetch('/api/config'); if (response.status === 401) { document.getElementById('login-view').classList.remove('hidden'); return; } if (response.ok) { const config = await response.json(); document.getElementById('dashboard-view').classList.remove('hidden'); renderRoleRows(config); updateBotStatus('Online'); } } catch (error) { console.error('Failed to initialize app', error); document.getElementById('login-view').classList.remove('hidden'); } } function renderRoleRows(currentConfig) { const container = document.getElementById('roles-container'); container.innerHTML = ''; // Clear existing PRODUCTS.forEach(product => { const currentRoleId = currentConfig[product.id] || ''; const row = document.createElement('div'); row.className = 'role-row'; row.innerHTML = `
Product ${product.id}: ${product.name} ${product.type}
`; container.appendChild(row); }); } async function saveRole(productId) { const input = document.getElementById(`input-${productId}`); const btn = document.getElementById(`btn-${productId}`); const errorDiv = document.getElementById(`error-${productId}`); const roleId = input.value.trim(); // Reset UI errorDiv.classList.add('hidden'); btn.textContent = 'Saving...'; btn.disabled = true; try { const response = await fetch('/api/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ productId, roleId }) }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Failed to save'); } // Success UX btn.textContent = 'Saved!'; btn.style.backgroundColor = 'var(--success)'; setTimeout(() => { btn.textContent = 'Save'; btn.style.backgroundColor = 'var(--fire)'; btn.disabled = false; }, 2000); } catch (error) { // Error UX errorDiv.textContent = error.message; errorDiv.classList.remove('hidden'); btn.textContent = 'Save'; btn.disabled = false; } } function updateBotStatus(status) { const badge = document.getElementById('bot-status'); badge.textContent = `Bot Status: ${status}`; badge.style.color = status === 'Online' ? 'var(--success)' : 'var(--error)'; } // Webhook Logs Refresh document.getElementById('refresh-logs').addEventListener('click', async () => { const btn = document.getElementById('refresh-logs'); btn.textContent = 'Refreshing...'; btn.disabled = true; try { // Fetch logs from backend endpoint const response = await fetch('/api/logs'); if (response.ok) { const logs = await response.json(); renderLogs(logs); } } catch (error) { console.error('Failed to fetch logs', error); } finally { btn.textContent = 'Refresh Logs'; btn.disabled = false; } }); function renderLogs(logs) { const tbody = document.getElementById('logs-body'); tbody.innerHTML = ''; if (logs.length === 0) { tbody.innerHTML = 'No recent events.'; return; } // Show most recent first logs.reverse().forEach(log => { const tr = document.createElement('tr'); const statusColor = log.success ? 'var(--success)' : 'var(--error)'; const statusText = log.status || (log.success ? 'Success' : 'Failed'); tr.innerHTML = ` ${new Date(log.timestamp).toLocaleTimeString()} Product ${log.productId} ${statusText} `; tbody.appendChild(tr); }); } ``` Save and exit: `Ctrl+X`, `Y`, `Enter` --- ### Step 6: Set File Permissions Ensure firefrost-bot user owns all frontend files: ```bash chown -R firefrost-bot:firefrost-bot /opt/firefrost-discord-bot/public chmod 644 /opt/firefrost-discord-bot/public/* ``` --- ### Step 7: Add Webhook Logging Endpoint Edit `bot.js` to add the `/api/logs` endpoint: ```bash nano /opt/firefrost-discord-bot/bot.js ``` **Add this endpoint after the `/api/config` routes:** ```javascript // Webhook Logs Endpoint app.get('/api/logs', isAuthenticated, (req, res) => { res.json(webhookLogs); }); ``` **Also update your webhook handler to log events:** In your `POST /webhook/paymenter` handler, add logging: ```javascript app.post('/webhook/paymenter', async (req, res) => { try { const { productId, userId } = req.body; // Adjust based on actual Paymenter payload // Log the webhook event webhookLogs.push({ timestamp: new Date().toISOString(), productId: productId, userId: userId, success: true, status: 'Success' }); // Keep only last 50 logs (circular buffer) if (webhookLogs.length > 50) { webhookLogs.shift(); } // Your existing webhook logic here... res.json({ success: true }); } catch (error) { // Log failure webhookLogs.push({ timestamp: new Date().toISOString(), productId: req.body.productId || 'unknown', success: false, status: 'Failed', error: error.message }); res.status(500).json({ error: error.message }); } }); ``` Save and exit. --- ### Step 8: Restart Bot Apply all frontend changes: ```bash # Restart bot service sudo systemctl restart firefrost-discord-bot # Check status sudo systemctl status firefrost-discord-bot # View logs sudo journalctl -u firefrost-discord-bot -n 50 ``` Should show: `Active: active (running)` with no errors. --- ### Step 9: Test Frontend Access **Before OAuth is set up:** 1. Open browser 2. Go to: `http://localhost:3100` (from Command Center) 3. Should see login screen with "🔥 Firefrost Command ❄️" **Note:** Full testing requires OAuth setup (Part 3) and Nginx/SSL (Part 6). --- ## 🎨 FRONTEND FEATURES ### Login Screen - Clean card design with Fire/Frost branding - "Login with Discord" button - Redirects to Discord OAuth ### Dashboard - **Navbar:** Bot status indicator + logout button - **Role Mappings Section:** - 10 product rows (Awakened → Sovereign) - Each row: Product name, tier price, role ID input, Save button - Per-row save (instant feedback) - Inline error messages - **Webhook Logs Section:** - Table: Time, Product ID, Status - Manual refresh button - Last 50 events ### Mobile Responsive - Flexbox layout adapts to phone screens - Input fields stack vertically on mobile - Navbar collapses to single column - Touch-friendly button sizes --- ## 🎨 UI/UX DECISIONS (BY GEMINI) **Save Per Row (Not "Save All"):** - If one role ID is invalid, others aren't blocked - Instant, precise feedback on which field failed - Holly can save valid ones, fix invalid ones, retry **Validate on Save (Not on Blur):** - Prevents API spam while typing - Explicit user action required - Clear visual feedback (button changes) **Inline Errors:** - Error appears directly under failed field - Holly knows exactly what to fix - Color-coded: red = error, green = success **Manual Log Refresh:** - Prevents auto-refresh layout shifting - Lower browser memory usage - Holly controls when to check logs --- **Frontend deployment complete!** ✅ Next: Configure Nginx & SSL (Part 6) --- ## 🌐 PART 6: CONFIGURE NGINX & SSL ### Overview Configure Nginx reverse proxy to forward HTTPS traffic to the Node.js app, then secure with Let's Encrypt SSL certificate. **What this does:** - Nginx listens on port 80 (HTTP) and 443 (HTTPS) - Forwards traffic to Node.js app on localhost:3100 - Let's Encrypt provides free SSL certificate - Auto-renews certificate every 90 days --- ### Step 1: Create Nginx Configuration Create new site config: ```bash sudo nano /etc/nginx/sites-available/discord-bot.firefrostgaming.com ``` **Paste this complete configuration:** ```nginx server { listen 80; server_name discord-bot.firefrostgaming.com; location / { proxy_pass http://localhost:3100; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; } } ``` **What these headers do:** - **X-Real-IP:** Passes client's real IP to Node.js (not Nginx's IP) - **X-Forwarded-For:** Shows full proxy chain - **X-Forwarded-Proto:** Tells app if request was HTTP or HTTPS - **Upgrade/Connection:** Required for WebSocket support (future-proofing) Save and exit: `Ctrl+X`, `Y`, `Enter` --- ### Step 2: Enable Site Create symlink to enable the site: ```bash sudo ln -s /etc/nginx/sites-available/discord-bot.firefrostgaming.com /etc/nginx/sites-enabled/ ``` Test Nginx configuration for syntax errors: ```bash sudo nginx -t ``` **Expected output:** ``` nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful ``` If test passes, reload Nginx: ```bash sudo systemctl reload nginx ``` --- ### Step 3: Verify HTTP Access (Before SSL) **Test that Nginx is forwarding correctly:** 1. Open browser 2. Go to: `http://discord-bot.firefrostgaming.com` 3. Should see admin panel login screen **If you get an error:** - Check bot is running: `sudo systemctl status firefrost-discord-bot` - Check Nginx logs: `sudo tail -f /var/log/nginx/error.log` - Verify DNS: `dig discord-bot.firefrostgaming.com` (should show 63.143.34.217) --- ### Step 4: Install Certbot (If Not Already Installed) Check if Certbot is installed: ```bash certbot --version ``` **If not installed:** ```bash # Install Certbot and Nginx plugin sudo apt update sudo apt install certbot python3-certbot-nginx -y ``` --- ### Step 5: Obtain SSL Certificate Run Certbot with Nginx plugin: ```bash sudo certbot --nginx -d discord-bot.firefrostgaming.com ``` **Certbot will ask:** 1. **Email address:** (for renewal notices) - Enter Michael's email or devops@firefrostgaming.com 2. **Terms of Service:** (A)gree - Type `A` and press Enter 3. **Share email with EFF?** (Y)es or (N)o - Your choice (either is fine) **Certbot will automatically:** - ✅ Validate domain ownership (checks DNS points to this server) - ✅ Obtain SSL certificate from Let's Encrypt - ✅ Modify Nginx config to enable HTTPS (port 443) - ✅ Add HTTP → HTTPS redirect - ✅ Set up auto-renewal (certificate renews every 90 days) **Expected output:** ``` Successfully received certificate. Certificate is saved at: /etc/letsencrypt/live/discord-bot.firefrostgaming.com/fullchain.pem Key is saved at: /etc/letsencrypt/live/discord-bot.firefrostgaming.com/privkey.pem ... Congratulations! You have successfully enabled HTTPS on https://discord-bot.firefrostgaming.com ``` --- ### Step 6: Verify HTTPS Access Test SSL is working: 1. Open browser 2. Go to: `https://discord-bot.firefrostgaming.com` 3. Should see: - ✅ Green padlock icon (valid SSL) - ✅ Admin panel login screen - ✅ "Login with Discord" button **Test HTTP redirect:** 1. Go to: `http://discord-bot.firefrostgaming.com` (HTTP, not HTTPS) 2. Should automatically redirect to HTTPS version 3. URL bar should show `https://discord-bot.firefrostgaming.com` --- ### Step 7: Verify Auto-Renewal Certbot sets up automatic renewal via systemd timer. **Check renewal timer status:** ```bash sudo systemctl status certbot.timer ``` Should show: `Active: active (waiting)` **Test renewal (dry run, doesn't actually renew):** ```bash sudo certbot renew --dry-run ``` Should show: `Congratulations, all simulated renewals succeeded` **Certificate auto-renews:** Every 90 days, systemd timer runs `certbot renew` automatically. --- ### Step 8: View Final Nginx Configuration Certbot modified your Nginx config to add SSL. View the changes: ```bash cat /etc/nginx/sites-available/discord-bot.firefrostgaming.com ``` **You'll now see TWO server blocks:** 1. **HTTP (port 80):** Redirects to HTTPS 2. **HTTPS (port 443):** Proxies to Node.js with SSL **Example of Certbot's additions:** ```nginx server { listen 443 ssl; server_name discord-bot.firefrostgaming.com; ssl_certificate /etc/letsencrypt/live/discord-bot.firefrostgaming.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/discord-bot.firefrostgaming.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # ... your original location / block ... } server { listen 80; server_name discord-bot.firefrostgaming.com; return 301 https://$server_name$request_uri; } ``` --- ### Step 9: Security Headers (Optional but Recommended) Add security headers to HTTPS server block: ```bash sudo nano /etc/nginx/sites-available/discord-bot.firefrostgaming.com ``` **Add these lines inside the `server { listen 443 ssl; ... }` block:** ```nginx # Security Headers add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; ``` **What these do:** - **X-Frame-Options:** Prevents clickjacking (site can't be embedded in iframe) - **X-Content-Type-Options:** Prevents MIME-type sniffing attacks - **X-XSS-Protection:** Enables browser XSS filter - **Referrer-Policy:** Controls what info is sent in Referer header Save, test, reload: ```bash sudo nginx -t sudo systemctl reload nginx ``` --- ## ✅ NGINX & SSL COMPLETE **You now have:** - ✅ Nginx reverse proxy forwarding to Node.js - ✅ Valid SSL certificate from Let's Encrypt - ✅ HTTPS enforced (HTTP redirects to HTTPS) - ✅ Auto-renewal configured (every 90 days) - ✅ Security headers enabled - ✅ Admin panel accessible at `https://discord-bot.firefrostgaming.com` **Next:** Holly's Usage Guide (Part 7) --- **Configuration provided by:** Gemini (Google AI) - March 23, 2026 --- ## 👥 PART 7: HOLLY'S USAGE GUIDE **This section is for Holly - how to use the admin panel.** ### When You Need This Use the admin panel when: - ✅ You've created new Discord roles - ✅ You need to update role IDs in the bot - ✅ You want to test if role IDs are correct - ✅ You need to change a role mapping ### Step 1: Log In 1. Open browser (Chrome, Firefox, Edge) 2. Go to: `https://discord-bot.firefrostgaming.com/admin` 3. Click **"Login with Discord"** button 4. Discord OAuth page appears 5. Click **"Authorize"** 6. You're redirected back to admin panel **Note:** You only need to log in once - browser remembers you via session cookie. ### Step 2: View Current Mappings Admin panel shows current role mappings: ``` Product 2 (The Awakened): [123456789012345678] Product 3 (Fire Elemental): [234567890123456789] Product 4 (Frost Elemental): [345678901234567890] ... ``` These are the Discord role IDs currently in use. ### Step 3: Update Role IDs **When you create Discord roles:** 1. In Discord, go to Server Settings → Roles 2. Right-click a role → Copy ID 3. Paste into appropriate field in admin panel 4. Repeat for all 10 roles **Example:** You created "🔥 Fire Elemental" role in Discord: 1. Right-click role → Copy ID → `987654321098765432` 2. In admin panel, find "Product 3 (Fire Elemental)" field 3. Paste: `987654321098765432` ### Step 4: Save Changes 1. Click **"Save All Changes"** button 2. Panel validates each role ID: - ✅ Checks format (must be 18-19 digit number) - ✅ Verifies role exists in Discord server 3. If validation passes: - ✅ Config saved to disk - ✅ In-memory config updated (instant effect) - ✅ Audit log posted to Discord `#bot-audit-logs` - ✅ Success message appears: "All role mappings updated!" 4. If validation fails: - ❌ Error message shows which role ID is invalid - ❌ Config NOT saved (prevents bot from breaking) - ❌ Fix the invalid role ID and try again ### Step 5: Test Webhook After saving role mappings: 1. Go to Paymenter test page (ask Michael for URL) 2. Create test purchase for $5 Fire Elemental tier 3. Check Discord - does bot assign "🔥 Fire Elemental" role? 4. If yes: ✅ Role mapping works! 5. If no: ❌ Check bot logs or ask Michael ### Step 6: Logout (Optional) Click **"Logout"** button when done. **Note:** You can stay logged in - session expires after 24 hours. --- ## ✅ TESTING & VERIFICATION ### Test Checklist After deployment, verify: #### 1. Bot User Running Correctly ```bash # Check service status sudo systemctl status firefrost-discord-bot # Should show: # - Active: active (running) # - User: firefrost-bot (NOT root) ``` #### 2. OAuth Login Works 1. Open `https://discord-bot.firefrostgaming.com/admin` 2. Click "Login with Discord" 3. Authorize 4. Should redirect to admin panel 5. Should see current role mappings #### 3. Role ID Validation Works **Test invalid role ID:** 1. Enter `123` (too short) in any field 2. Click "Save All Changes" 3. Should show error: "Invalid Discord Role ID format" **Test non-existent role ID:** 1. Enter `999999999999999999` (valid format, but role doesn't exist) 2. Click "Save All Changes" 3. Should show error: "Role does not exist in Discord server" **Test valid role ID:** 1. Create test role in Discord 2. Copy role ID 3. Paste in admin panel 4. Click "Save All Changes" 5. Should show: "All role mappings updated successfully!" #### 4. Config Persists After Restart ```bash # Restart bot sudo systemctl restart firefrost-discord-bot # Reload admin panel in browser # Should still show saved role mappings (loaded from config.json) ``` #### 5. Backup File Created ```bash # Check for backup ls -la /opt/firefrost-discord-bot/config.json.backup # Should exist after first save ``` #### 6. Audit Logs Appear in Discord 1. Make a config change in admin panel 2. Check Discord `#bot-audit-logs` channel 3. Should see embed with: - Author: Holly (or Michael) - Action: Updated Role Mappings - Changes: Product X: old_id → new_id --- ## 🔧 TROUBLESHOOTING ### Problem: "Unauthorized" Error When Accessing Admin Panel **Symptoms:** Can't access `/admin`, get 401 error. **Causes:** 1. Not logged in via Discord OAuth 2. Your Discord ID isn't in `ALLOWED_ADMINS` list **Solutions:** **Check if logged in:** - Clear browser cookies - Try logging in again via "Login with Discord" **Check whitelist:** ```bash # On Command Center cat /opt/firefrost-discord-bot/.env | grep ALLOWED_ADMINS ``` Should show Holly's and Michael's Discord IDs separated by comma. **If your ID is missing:** ```bash # Edit .env sudo nano /opt/firefrost-discord-bot/.env # Add your Discord ID to ALLOWED_ADMINS ALLOWED_ADMINS=HOLLYS_ID,MICHAELS_ID,YOUR_ID # Save and restart bot sudo systemctl restart firefrost-discord-bot ``` --- ### Problem: "Role does not exist in Discord server" Error **Symptoms:** Valid-looking role ID rejected during save. **Causes:** 1. Role ID is from wrong Discord server 2. Role was deleted after you copied ID 3. Bot doesn't have permission to see roles **Solutions:** **Verify role exists:** 1. Go to Discord Server Settings → Roles 2. Find the role 3. Right-click → Copy ID again 4. Paste fresh ID into admin panel **Check bot permissions:** 1. Discord Server Settings → Roles 2. Find "Firefrost Subscription Manager" bot role 3. Ensure it has "Manage Roles" permission 4. Ensure bot role is ABOVE the roles it needs to assign --- ### Problem: Admin Panel Shows Old Role Mappings **Symptoms:** You saved new IDs, but admin panel shows old ones after refresh. **Causes:** 1. Browser cache 2. Config file didn't save 3. In-memory config didn't update **Solutions:** **Hard refresh browser:** - Windows: `Ctrl + Shift + R` - Mac: `Cmd + Shift + R` **Check config file:** ```bash # On Command Center cat /opt/firefrost-discord-bot/config.json ``` Should show your latest role IDs. **If config.json is outdated:** ```bash # Restart bot sudo systemctl restart firefrost-discord-bot # Try saving again in admin panel ``` --- ### Problem: OAuth Login Redirects to "Cannot GET /auth/discord/callback" **Symptoms:** After clicking "Authorize" in Discord, get error page. **Causes:** 1. Callback URL mismatch in Discord Developer Portal 2. Backend route not set up correctly **Solutions:** **Check Discord Developer Portal:** 1. Go to: https://discord.com/developers/applications 2. Select your bot app → OAuth2 3. Under Redirects, verify you have: `https://discord-bot.firefrostgaming.com/auth/discord/callback` 4. Save changes if missing **Check .env file:** ```bash cat /opt/firefrost-discord-bot/.env | grep CALLBACK_URL ``` Should match Discord Developer Portal exactly. **Restart bot:** ```bash sudo systemctl restart firefrost-discord-bot ``` --- ### Problem: Bot Assigns Wrong Role After Config Update **Symptoms:** Config saved successfully, but webhook assigns incorrect role. **Causes:** 1. Product ID → Role ID mapping is wrong 2. In-memory config didn't update **Solutions:** **Verify mapping in admin panel:** Product 3 should map to Fire Elemental role ID, not Frost Elemental. **Check config.json:** ```bash cat /opt/firefrost-discord-bot/config.json ``` Should show correct mappings. **Restart bot (force reload):** ```bash sudo systemctl restart firefrost-discord-bot ``` **Test webhook again.** --- ## 🔄 MAINTENANCE ### Regular Tasks **Weekly:** - Check bot logs for errors: `sudo journalctl -u firefrost-discord-bot -n 100` - Verify SSL certificate is valid (auto-renewed by certbot) **Monthly:** - Review audit logs in Discord `#bot-audit-logs` - Verify backup config exists: `ls -la /opt/firefrost-discord-bot/config.json.backup` **As Needed:** - Update role mappings when creating new Discord roles - Add/remove admin users from `ALLOWED_ADMINS` in `.env` ### Backup Strategy **Config is backed up automatically:** - Every save creates `config.json.backup` - Contains last-known-good configuration **To restore from backup:** ```bash # SSH to Command Center cd /opt/firefrost-discord-bot # Copy backup to active config cp config.json.backup config.json # Restart bot sudo systemctl restart firefrost-discord-bot ``` ### Updating Backend Code If Gemini provides code updates: ```bash # SSH to Command Center cd /opt/firefrost-discord-bot # Backup current code cp bot.js bot.js.backup # Edit bot.js with new code sudo nano bot.js # Test syntax (optional) node --check bot.js # Restart bot sudo systemctl restart firefrost-discord-bot # Check logs sudo journalctl -u firefrost-discord-bot -n 50 ``` --- ## 📚 RELATED DOCUMENTATION **See also:** - `docs/guides/subscription-automation-guide.md` - Full subscription workflow - `docs/guides/server-side-mod-deployment-guide.md` - LuckPerms configuration --- ## 🙏 CREDITS **Architecture Design:** Gemini (Google AI) **Implementation:** Chronicler #40 (Claude) + Michael **Testing:** Holly + Michael **Consultation Date:** March 23, 2026 **Key Architectural Decisions by Gemini:** - Run as dedicated `firefrost-bot` user (NOT root) - critical security fix - In-memory config updates (no restart needed) - zero downtime - Discord OAuth2 (no password management) - better security - Atomic file writes with backup (prevents corruption) - reliability - Discord API validation (verify roles exist) - prevents errors **Thank you, Gemini, for the excellent architectural guidance.** 🙏 --- **Fire + Frost + Foundation = Where Love Builds Legacy** 🔥❄️ **Status:** Backend and Frontend code pending from Gemini **Last Updated:** March 23, 2026 **Next Update:** When Gemini provides implementation code --- **END OF GUIDE**