From 0c0d19e7f1ec6ee6606ecbcf83fe2c9447c3a7bd Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Mar 2026 13:33:20 +0000 Subject: [PATCH] feat: Add complete frontend code to Discord Bot Admin Panel guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADDED: Part 5 complete implementation (9 steps, production-ready) Frontend Files (by Gemini/Google AI): - index.html (login + dashboard views, Fire/Frost branding) - style.css (mobile-responsive, CSS variables for theming) - app.js (vanilla JavaScript, fetch API, per-row save logic) Key Features Implemented: - Discord OAuth login flow - 10 product → role ID input fields (Awakened through Sovereign) - Per-row save buttons with validation feedback - Inline error messages (shows under specific failed field) - Bot status indicator (Online/Offline) - Recent webhook logs table (manual refresh, last 50 events) - Mobile-responsive design (flexbox, touch-friendly) UI/UX Decisions (Gemini's recommendations): - Save per row (not Save All) - prevents one error blocking all saves - Validate on save (not on blur) - prevents API spam - Inline errors - Holly knows exactly what to fix - Manual log refresh - prevents layout shifting, lower memory Technical Details: - No frameworks (vanilla JS, fast loading) - CSS variables for Fire (#FF6B35) / Frost (#4ECDC4) theming - Monospace font for role ID inputs (easier to verify 18-digit IDs) - Button state changes: Save → Saving... → Saved! → Save - Color-coded status: green = success, red = error Added Backend Requirements: - app.use(express.static('public')); - serve static files - GET /api/logs endpoint - return webhookLogs array - Webhook logging in POST /webhook/paymenter - populate logs array - Circular buffer (max 50 logs, shift oldest when full) File Permissions: - chown firefrost-bot:firefrost-bot public/ - chmod 644 public/* - read-only for security Status: Frontend code COMPLETE and ready to deploy Next: Nginx + SSL configuration (Part 6) Code credit: Gemini (Google AI) - March 23, 2026 Chronicler #40 --- docs/guides/discord-bot-admin-panel.md | 679 ++++++++++++++++++++++++- 1 file changed, 669 insertions(+), 10 deletions(-) diff --git a/docs/guides/discord-bot-admin-panel.md b/docs/guides/discord-bot-admin-panel.md index 4bf4ccb..66fd78c 100644 --- a/docs/guides/discord-bot-admin-panel.md +++ b/docs/guides/discord-bot-admin-panel.md @@ -386,20 +386,679 @@ chown firefrost-bot:firefrost-bot /opt/firefrost-discord-bot/.env ## 🎨 PART 5: DEPLOY FRONTEND CODE -**⚠️ WAITING ON GEMINI:** The frontend HTML/CSS/JS is being written by Gemini (Google AI). +### Overview -**Once received, the frontend will include:** +The frontend provides Holly with a clean, mobile-friendly interface to manage Discord role mappings. It features: - Discord OAuth login flow -- Role mapping management form (10 product → role ID pairs) -- Save functionality with validation feedback -- Bot status display -- Recent webhook logs table -- Logout button -- Fire/Frost branding +- 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) -**Files will be created in:** `/opt/firefrost-discord-bot/public/` +**Tech:** Vanilla HTML/CSS/JavaScript (no frameworks) -**Status:** Awaiting Gemini's response with complete frontend implementation. +**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) ---