modified: aethex-bot/server/webServer.js

This commit is contained in:
MrPiglr 2026-03-05 15:27:55 -07:00
parent 3a76186fc6
commit 1d69c3b9dc
4 changed files with 292 additions and 46 deletions

View file

@ -442,11 +442,19 @@ CREATE TABLE IF NOT EXISTS command_logs (
CREATE TABLE IF NOT EXISTS server_backups (
id SERIAL PRIMARY KEY,
guild_id VARCHAR(32) NOT NULL,
name VARCHAR(100),
description TEXT,
backup_type VARCHAR(20) DEFAULT 'manual', -- manual, auto
backup_data JSONB NOT NULL,
roles_count INTEGER DEFAULT 0,
channels_count INTEGER DEFAULT 0,
size_bytes BIGINT DEFAULT 0,
created_by VARCHAR(32),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_server_backups_guild ON server_backups(guild_id);
-- Backup Settings - Auto-backup config
CREATE TABLE IF NOT EXISTS backup_settings (
id SERIAL PRIMARY KEY,

View file

@ -0,0 +1,11 @@
-- Fix server_backups table - add missing columns
-- Run this if you already created the table with the old schema
ALTER TABLE server_backups ADD COLUMN IF NOT EXISTS name VARCHAR(100);
ALTER TABLE server_backups ADD COLUMN IF NOT EXISTS description TEXT;
ALTER TABLE server_backups ADD COLUMN IF NOT EXISTS backup_type VARCHAR(20) DEFAULT 'manual';
ALTER TABLE server_backups ADD COLUMN IF NOT EXISTS roles_count INTEGER DEFAULT 0;
ALTER TABLE server_backups ADD COLUMN IF NOT EXISTS channels_count INTEGER DEFAULT 0;
ALTER TABLE server_backups ADD COLUMN IF NOT EXISTS size_bytes BIGINT DEFAULT 0;
CREATE INDEX IF NOT EXISTS idx_server_backups_guild ON server_backups(guild_id);

View file

@ -8,6 +8,9 @@
<meta name="robots" content="noindex, nofollow">
<meta name="theme-color" content="#6366f1">
<link rel="icon" href="/logo.png" type="image/png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--background: #030712;
@ -32,7 +35,7 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Courier New', Courier, monospace;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: var(--background);
color: var(--foreground);
min-height: 100vh;
@ -183,15 +186,37 @@
border: 1px solid var(--card-border);
border-top: none;
border-radius: 0 0 12px 12px;
max-height: 280px;
overflow-y: auto;
backdrop-filter: blur(10px);
padding: 0.5rem;
}
.guild-dropdown.open {
display: block;
}
.guild-list {
max-height: 280px;
overflow-y: auto;
border-radius: 8px;
}
.guild-list::-webkit-scrollbar {
width: 6px;
}
.guild-list::-webkit-scrollbar-track {
background: transparent;
}
.guild-list::-webkit-scrollbar-thumb {
background: rgba(99, 102, 241, 0.3);
border-radius: 3px;
}
.guild-list::-webkit-scrollbar-thumb:hover {
background: rgba(99, 102, 241, 0.5);
}
.guild-card {
display: flex;
align-items: center;
@ -288,6 +313,91 @@
opacity: 1;
}
/* Server Filter Tabs */
.guild-filter-tabs {
display: flex;
gap: 0.25rem;
padding: 0.5rem;
background: rgba(15, 23, 42, 0.5);
border-radius: 8px;
margin-bottom: 0.75rem;
}
.guild-filter-tab {
flex: 1;
padding: 0.5rem 0.75rem;
border: none;
background: transparent;
color: var(--muted);
font-size: 0.75rem;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
}
.guild-filter-tab:hover {
color: var(--foreground);
background: rgba(99, 102, 241, 0.1);
}
.guild-filter-tab.active {
background: var(--primary);
color: white;
}
.guild-filter-tab .count {
font-size: 0.65rem;
padding: 0.125rem 0.375rem;
background: rgba(0,0,0,0.2);
border-radius: 4px;
}
/* Bot Status Badge */
.guild-bot-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.5rem;
border-radius: 6px;
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.guild-bot-badge.has-bot {
background: rgba(16, 185, 129, 0.15);
border: 1px solid rgba(16, 185, 129, 0.3);
color: #10b981;
}
.guild-bot-badge.no-bot {
background: rgba(100, 116, 139, 0.15);
border: 1px solid rgba(100, 116, 139, 0.3);
color: var(--muted);
}
.guild-invite-btn {
padding: 0.25rem 0.5rem;
background: var(--primary);
color: white;
border: none;
border-radius: 4px;
font-size: 0.65rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.guild-invite-btn:hover {
background: var(--primary-light);
}
.nav-section {
margin-bottom: 1.5rem;
}
@ -328,7 +438,22 @@
color: var(--foreground);
}
.nav-icon { width: 20px; text-align: center; font-size: 1rem; }
.nav-icon {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.nav-icon svg {
width: 18px;
height: 18px;
stroke: currentColor;
stroke-width: 2;
fill: none;
}
.user-card {
margin-top: auto;
@ -804,6 +929,16 @@
font-size: 3.5rem;
margin-bottom: 1rem;
opacity: 0.5;
display: flex;
justify-content: center;
}
.empty-state-icon svg {
width: 64px;
height: 64px;
stroke: var(--muted);
stroke-width: 1.5;
fill: none;
}
.login-prompt {
@ -1202,76 +1337,83 @@
<path d="M6 9l6 6 6-6"/>
</svg>
</div>
<div class="guild-dropdown" id="guildDropdown"></div>
<div class="guild-dropdown" id="guildDropdown">
<div class="guild-filter-tabs" id="guildFilterTabs">
<button class="guild-filter-tab active" data-filter="all" onclick="filterGuilds('all')">All <span class="count" id="countAll">0</span></button>
<button class="guild-filter-tab" data-filter="has-bot" onclick="filterGuilds('has-bot')">Active <span class="count" id="countActive">0</span></button>
<button class="guild-filter-tab" data-filter="no-bot" onclick="filterGuilds('no-bot')">Invite <span class="count" id="countInvite">0</span></button>
</div>
<div class="guild-list" id="guildList"></div>
</div>
</div>
<nav>
<div class="nav-section">
<div class="nav-section-title">Overview</div>
<div class="nav-item active" data-page="profile">
<span class="nav-icon">👤</span> Profile
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></span> Profile
</div>
<div class="nav-item" data-page="leaderboard">
<span class="nav-icon">🏆</span> Leaderboard
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M8 21v-8M12 21V9M16 21v-5"/><path d="M3 21h18"/></svg></span> Leaderboard
</div>
</div>
<div class="nav-section">
<div class="nav-section-title">Rewards</div>
<div class="nav-item" data-page="achievements">
<span class="nav-icon">🎖️</span> Achievements
<span class="nav-icon"><svg viewBox="0 0 24 24"><circle cx="12" cy="8" r="6"/><path d="M15.477 12.89 17 22l-5-3-5 3 1.523-9.11"/></svg></span> Achievements
</div>
<div class="nav-item" data-page="quests">
<span class="nav-icon">🎯</span> Quests
<span class="nav-icon"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg></span> Quests
</div>
<div class="nav-item" data-page="shop">
<span class="nav-icon">🛒</span> Shop
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg></span> Shop
</div>
<div class="nav-item" data-page="inventory">
<span class="nav-icon">🎒</span> Inventory
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg></span> Inventory
</div>
<div class="nav-item" data-page="titles">
<span class="nav-icon">🏷️</span> Titles
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M12 2 2 7l10 5 10-5-10-5Z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg></span> Titles
</div>
<div class="nav-item" data-page="coins">
<span class="nav-icon">🪙</span> Coins
<span class="nav-icon"><svg viewBox="0 0 24 24"><circle cx="8" cy="8" r="6"/><path d="M18.09 10.37A6 6 0 1 1 10.34 18"/><path d="M7 6h1v4"/><path d="m16.71 13.88.7.71-2.82 2.82"/></svg></span> Coins
</div>
</div>
<div class="nav-section" id="adminSection" style="display:none">
<div class="nav-section-title">Admin</div>
<div class="nav-item" data-page="admin-xp">
<span class="nav-icon">⚙️</span> XP Settings
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg></span> XP Settings
</div>
<div class="nav-item" data-page="admin-quests">
<span class="nav-icon">📝</span> Manage Quests
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/></svg></span> Manage Quests
</div>
<div class="nav-item" data-page="admin-achievements">
<span class="nav-icon">🏅</span> Manage Achievements
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg></span> Manage Achievements
</div>
<div class="nav-item" data-page="admin-shop">
<span class="nav-icon">🏪</span> Manage Shop
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="m2 7 4.41-4.41A2 2 0 0 1 7.83 2h8.34a2 2 0 0 1 1.42.59L22 7"/><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><path d="M15 22v-4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4"/><path d="M2 7h20"/><path d="M22 7v3a2 2 0 0 1-2 2v0a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 16 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 12 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 8 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 4 12v0a2 2 0 0 1-2-2V7"/></svg></span> Manage Shop
</div>
<div class="nav-item" data-page="moderation">
<span class="nav-icon">&#128737;</span> Moderation
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg></span> Moderation
</div>
<div class="nav-item" data-page="analytics">
<span class="nav-icon">📊</span> Analytics
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/></svg></span> Analytics
</div>
<div class="nav-item" data-page="activity-roles">
<span class="nav-icon">🎯</span> Activity Roles
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg></span> Activity Roles
</div>
<div class="nav-item" data-page="cooldowns">
<span class="nav-icon">⏱️</span> Cooldowns
<span class="nav-icon"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></span> Cooldowns
</div>
<div class="nav-item" data-page="federation">
<span class="nav-icon">🌐</span> Federation
<span class="nav-icon"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></span> Federation
</div>
<div class="nav-item" data-page="backups">
<span class="nav-icon">💾</span> Backups
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg></span> Backups
</div>
<div class="nav-item" data-page="branding">
<span class="nav-icon">🎨</span> Branding
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="m9.06 11.9 8.07-8.06a2.85 2.85 0 1 1 4.03 4.03l-8.06 8.08"/><path d="M7.07 14.94c-1.66 0-3 1.35-3 3.02 0 1.33-2.5 1.52-2 2.02 1.08 1.1 2.49 2.02 4 2.02 2.2 0 4-1.8 4-4.04a3.01 3.01 0 0 0-3-3.02z"/></svg></span> Branding
</div>
</div>
</nav>
@ -2488,7 +2630,7 @@
</div>
</div>
<div style="margin-top:1rem;display:flex;gap:1rem;flex-wrap:wrap">
<a href="/pricing.html#branding" target="_blank" class="btn btn-primary">View Plans</a>
<a href="/pricing#branding" target="_blank" class="btn btn-primary">View Plans</a>
<button class="btn btn-secondary" onclick="refreshBranding()">Refresh Status</button>
</div>
</div>
@ -2720,6 +2862,8 @@
let currentUser = null;
let currentGuild = null;
let currentPage = 'profile';
let allGuilds = [];
let currentFilter = 'all';
async function init() {
try {
@ -2736,10 +2880,19 @@
document.getElementById('userAvatar').src = currentUser.avatarUrl;
document.getElementById('userName').textContent = currentUser.globalName || currentUser.username;
populateGuildCards(currentUser.guilds);
allGuilds = currentUser.guilds;
updateFilterCounts();
populateGuildCards(allGuilds);
if (currentUser.guilds.length > 0) {
selectGuild(currentUser.guilds[0]);
// Auto-select first server with bot, or first admin server
const botServer = allGuilds.find(g => g.hasBot && g.isAdmin);
const firstAdmin = allGuilds.find(g => g.isAdmin);
if (botServer) {
selectGuild(botServer);
} else if (firstAdmin) {
selectGuild(firstAdmin);
} else if (allGuilds.length > 0) {
selectGuild(allGuilds[0]);
}
document.addEventListener('click', (e) => {
@ -2787,11 +2940,11 @@
}
function populateGuildCards(guilds) {
const dropdown = document.getElementById('guildDropdown');
dropdown.innerHTML = '';
const listEl = document.getElementById('guildList');
listEl.innerHTML = '';
if (guilds.length === 0) {
dropdown.innerHTML = '<div style="padding: 1rem; text-align: center; color: var(--muted);">No servers found</div>';
listEl.innerHTML = '<div style="padding: 1.5rem; text-align: center; color: var(--muted);">No servers match this filter</div>';
return;
}
@ -2800,35 +2953,84 @@
card.className = 'guild-card';
card.dataset.guildId = g.id;
card.dataset.isAdmin = g.isAdmin;
card.onclick = () => selectGuild(g);
card.dataset.hasBot = g.hasBot;
const iconContent = g.icon
? `<img src="${g.icon}" alt="${g.name}">`
: g.name.charAt(0).toUpperCase();
let statusHtml = '';
if (g.hasBot) {
statusHtml = '<span class="guild-bot-badge has-bot">Active</span>';
} else {
statusHtml = '<span class="guild-bot-badge no-bot">Not Active</span>';
}
if (g.isAdmin) {
statusHtml = '<span class="guild-admin-badge">Admin</span>';
statusHtml += '<span class="guild-admin-badge">Admin</span>';
}
if (g.memberCount) {
statusHtml += `<span class="guild-member-count">${g.memberCount.toLocaleString()} members</span>`;
statusHtml += `<span class="guild-member-count">${g.memberCount.toLocaleString()}</span>`;
}
// If bot not in server, show invite button
const actionHtml = g.hasBot
? `<svg class="guild-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M20 6L9 17l-5-5"/></svg>`
: `<button class="guild-invite-btn" onclick="event.stopPropagation(); inviteBot('${g.id}')">+ Add Bot</button>`;
card.innerHTML = `
<div class="guild-icon">${iconContent}</div>
<div class="guild-info">
<div class="guild-name">${escapeHtml(g.name)}</div>
<div class="guild-status">${statusHtml}</div>
</div>
<svg class="guild-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
<path d="M20 6L9 17l-5-5"/>
</svg>
${actionHtml}
`;
dropdown.appendChild(card);
if (g.hasBot) {
card.onclick = () => selectGuild(g);
} else {
card.style.opacity = '0.7';
card.style.cursor = 'default';
}
listEl.appendChild(card);
});
}
function updateFilterCounts() {
const withBot = allGuilds.filter(g => g.hasBot).length;
const withoutBot = allGuilds.filter(g => !g.hasBot).length;
document.getElementById('countAll').textContent = allGuilds.length;
document.getElementById('countActive').textContent = withBot;
document.getElementById('countInvite').textContent = withoutBot;
}
function filterGuilds(filter) {
currentFilter = filter;
// Update tab states
document.querySelectorAll('.guild-filter-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.filter === filter);
});
// Filter and render
let filtered = allGuilds;
if (filter === 'has-bot') {
filtered = allGuilds.filter(g => g.hasBot);
} else if (filter === 'no-bot') {
filtered = allGuilds.filter(g => !g.hasBot);
}
populateGuildCards(filtered);
}
function inviteBot(guildId) {
const clientId = '578971258073522187'; // Your bot's client ID
const permissions = '8'; // Admin permissions
const url = `https://discord.com/api/oauth2/authorize?client_id=${clientId}&permissions=${permissions}&scope=bot%20applications.commands&guild_id=${guildId}&disable_guild_select=true`;
window.open(url, '_blank', 'width=500,height=800');
}
function selectGuild(guild) {
currentGuild = guild.id;
@ -5013,7 +5215,8 @@
try {
const res = await fetch('/api/guild/' + currentGuild + '/branding');
if (res.ok) {
currentBranding = await res.json();
const response = await res.json();
currentBranding = response.branding || {};
populateBrandingForm(currentBranding);
} else {
currentBranding = null;

View file

@ -297,10 +297,15 @@ function createWebServer(discordClient, supabase, options = {}) {
avatarUrl: user.avatar
? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`
: `https://cdn.discordapp.com/embed/avatars/${parseInt(user.discriminator || '0') % 5}.png`,
guilds: user.guilds.map(g => ({
guilds: user.guilds.map(g => {
const botGuild = discordClient?.guilds?.cache?.get(g.id);
return {
...g,
icon: g.icon ? `https://cdn.discordapp.com/icons/${g.id}/${g.icon}.png` : null
}))
icon: g.icon ? `https://cdn.discordapp.com/icons/${g.id}/${g.icon}.png` : null,
hasBot: !!botGuild,
memberCount: botGuild?.memberCount || null
};
})
});
});
@ -3191,7 +3196,7 @@ function createWebServer(discordClient, supabase, options = {}) {
});
// Update branding config
app.post('/api/guild/:guildId/branding', async (req, res) => {
app.patch('/api/guild/:guildId/branding', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
@ -3208,9 +3213,28 @@ function createWebServer(discordClient, supabase, options = {}) {
}
try {
const result = await brandingManager.updateBranding(supabase, guildId, req.body, userId);
res.json(result);
const { custom_handle, ...otherUpdates } = req.body;
// If trying to set a custom handle, use claimHandle for validation
if (custom_handle !== undefined) {
const branding = await brandingManager.getBranding(supabase, guildId);
const handleResult = await brandingManager.claimHandle(supabase, guildId, custom_handle, branding.tier);
if (!handleResult.success) {
return res.status(400).json({ error: handleResult.error });
}
}
// Update other fields if any
if (Object.keys(otherUpdates).length > 0) {
const result = await brandingManager.updateBranding(supabase, guildId, otherUpdates, userId);
if (!result.success) {
return res.status(400).json({ error: result.error });
}
}
res.json({ success: true });
} catch (error) {
console.error('[Branding] Update error:', error);
res.status(500).json({ error: 'Failed to update branding' });
}
});