Files
firefrost-operations-manual/docs/guides/discord-bot-admin-panel.md
Claude 8ea691ec05 feat: Add .env.template to Discord Bot Admin Panel guide - DEPLOYMENT PACKAGE COMPLETE
ADDED: Complete .env.template file to Part 4 Step 2

.env.template Features:
- Clear section headers (Server Config, Discord Bot, OAuth2, Security)
- Detailed comments explaining each variable
- Example values showing format
- Instructions on where to find each value
- Security reminder: DO NOT commit .env to version control

Two-Option Approach:
- Option A (Recommended): Create .env.template first, then copy to .env
- Option B (Alternative): Create .env directly

Variables Included (11 total):
1. NODE_ENV (production/development)
2. PORT (internal port for Node app)
3. SESSION_SECRET (random string for cookie encryption)
4. DISCORD_TOKEN (bot token from Developer Portal)
5. GUILD_ID (Discord server ID)
6. DISCORD_CLIENT_ID (OAuth2 client ID)
7. DISCORD_CLIENT_SECRET (OAuth2 client secret)
8. CALLBACK_URL (OAuth2 redirect URL)
9. ALLOWED_ADMINS (Holly + Michael Discord user IDs)
10. AUDIT_CHANNEL_ID (#bot-audit-logs channel ID)

Security Notes:
- Template shows format without exposing secrets
- Actual .env must be chmod 600
- Actual .env owned by firefrost-bot user
- DO NOT commit .env to git

Template provided by: Gemini (Google AI) - March 23, 2026

STATUS: DEPLOYMENT PACKAGE 100% COMPLETE

All code files ready:
 bot.js (350+ lines)
 index.html (Fire/Frost branded)
 style.css (mobile-responsive)
 app.js (frontend logic)
 .env.template (complete)

All configuration ready:
 Systemd service file
 Nginx reverse proxy config
 Let's Encrypt SSL commands
 Environment variable template

All documentation ready:
 Complete deployment guide (2400+ lines)
 Step-by-step walkthrough
 Troubleshooting guide
 Holly's usage guide

READY FOR PRODUCTION DEPLOYMENT (March 24, 2026)

Chronicler #40
2026-03-22 13:48:02 +00:00

58 KiB

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
  2. Architecture
  3. Why We Built This
  4. Part 1: Prerequisites
  5. Part 2: Create Dedicated Bot User
  6. Part 3: Set Up Discord OAuth2 Application
  7. Part 4: Deploy Backend Code
  8. Part 5: Deploy Frontend Code
  9. Part 6: Configure Nginx & SSL
  10. Part 7: Holly's Usage Guide
  11. Testing & Verification
  12. Troubleshooting
  13. 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:

ssh root@63.143.34.217

Create dedicated user:

# 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:

# 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:

sudo nano /etc/systemd/system/firefrost-discord-bot.service

Replace contents with this complete configuration:

[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:

# 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:

ssh root@63.143.34.217
cd /opt/firefrost-discord-bot

Install required npm packages:

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:

nano /opt/firefrost-discord-bot/.env.template

Paste this complete template:

# ==========================================
# 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:

# 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:

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:

# 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):

# 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:

nano /opt/firefrost-discord-bot/bot.js

Paste the complete production-ready code below.

⚠️ COPY THE ENTIRE FILE - ALL 8 SECTIONS:

// ============================================================================
// 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:

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:

# 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:

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:

nano /opt/firefrost-discord-bot/bot.js

Add this line after app.use(express.json());:

app.use(express.static('public'));

Full context in bot.js:

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:

nano /opt/firefrost-discord-bot/public/index.html

Paste this complete HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Firefrost Gaming - Command Center</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="login-view" class="view hidden">
        <div class="card login-card">
            <h1>🔥 Firefrost Command ❄️</h1>
            <p>Authenticate via Discord to manage role mappings.</p>
            <a href="/auth/discord" class="btn login-btn">Login with Discord</a>
        </div>
    </div>

    <div id="dashboard-view" class="view hidden">
        <nav class="navbar">
            <div class="brand">🔥 Firefrost Command ❄️</div>
            <div class="nav-actions">
                <span id="bot-status" class="status-badge checking">Checking Status...</span>
                <a href="/logout" class="btn logout-btn">Logout</a>
            </div>
        </nav>

        <main class="container">
            <section class="mapping-section">
                <h2>Discord Role Mappings</h2>
                <p class="subtitle">Update the Discord Role ID assigned to each Paymenter product.</p>
                <div id="roles-container" class="roles-grid">
                </div>
            </section>

            <section class="logs-section">
                <div class="logs-header">
                    <h2>Recent Webhook Logs</h2>
                    <button id="refresh-logs" class="btn secondary-btn">Refresh Logs</button>
                </div>
                <div class="table-container">
                    <table id="logs-table">
                        <thead>
                            <tr>
                                <th>Time</th>
                                <th>Product ID</th>
                                <th>Status</th>
                            </tr>
                        </thead>
                        <tbody id="logs-body">
                        </tbody>
                    </table>
                </div>
            </section>
        </main>
    </div>

    <script src="app.js"></script>
</body>
</html>

Save and exit: Ctrl+X, Y, Enter


Step 4: Create style.css

Create the CSS stylesheet with Fire/Frost branding:

nano /opt/firefrost-discord-bot/public/style.css

Paste this complete 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:

nano /opt/firefrost-discord-bot/public/app.js

Paste this complete 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 = `
            <div class="role-info">
                <strong>Product ${product.id}: ${product.name}</strong>
                <span>${product.type}</span>
            </div>
            <input type="text" id="input-${product.id}" class="role-input" value="${currentRoleId}" placeholder="18-digit Role ID">
            <button class="btn save-btn" onclick="saveRole('${product.id}')" id="btn-${product.id}">Save</button>
            <div id="error-${product.id}" class="error-text hidden"></div>
        `;
        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 = '<tr><td colspan="3" style="text-align: center; color: var(--text-muted);">No recent events.</td></tr>';
        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 = `
            <td>${new Date(log.timestamp).toLocaleTimeString()}</td>
            <td>Product ${log.productId}</td>
            <td style="color: ${statusColor}">${statusText}</td>
        `;
        tbody.appendChild(tr);
    });
}

Save and exit: Ctrl+X, Y, Enter


Step 6: Set File Permissions

Ensure firefrost-bot user owns all frontend files:

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:

nano /opt/firefrost-discord-bot/bot.js

Add this endpoint after the /api/config routes:

// 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:

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:

# 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:

sudo nano /etc/nginx/sites-available/discord-bot.firefrostgaming.com

Paste this complete configuration:

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:

sudo ln -s /etc/nginx/sites-available/discord-bot.firefrostgaming.com /etc/nginx/sites-enabled/

Test Nginx configuration for syntax errors:

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:

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:

certbot --version

If not installed:

# 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:

sudo certbot --nginx -d discord-bot.firefrostgaming.com

Certbot will ask:

  1. Email address: (for renewal notices)

  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:

sudo systemctl status certbot.timer

Should show: Active: active (waiting)

Test renewal (dry run, doesn't actually renew):

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:

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:

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;
}

Add security headers to HTTPS server block:

sudo nano /etc/nginx/sites-available/discord-bot.firefrostgaming.com

Add these lines inside the server { listen 443 ssl; ... } block:

    # 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:

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

# 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

# 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

# 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:

# 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:

# 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:

# On Command Center
cat /opt/firefrost-discord-bot/config.json

Should show your latest role IDs.

If config.json is outdated:

# 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:

cat /opt/firefrost-discord-bot/.env | grep CALLBACK_URL

Should match Discord Developer Portal exactly.

Restart bot:

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:

cat /opt/firefrost-discord-bot/config.json

Should show correct mappings.

Restart bot (force reload):

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:

# 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:

# 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

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