Add management API endpoints for server configuration and whitelisting

Adds GET and POST endpoints for managing server configurations, a GET endpoint for whitelisted servers, and associated CSS styling for dashboard elements in `bot.js` and `dashboard.html`.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: d92bdd14-c3cd-4d3a-92c1-cb648a408c03
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/ocC7ZpF
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-08 04:50:38 +00:00
parent cada20b646
commit 43394c8fe1
3 changed files with 959 additions and 11 deletions

View file

@ -22,10 +22,6 @@ externalPort = 80
localPort = 8080
externalPort = 8080
[[ports]]
localPort = 35789
externalPort = 3000
[workflows]
runButton = "Project"

View file

@ -1317,6 +1317,254 @@ http
}
}
// =============================================================================
// MANAGEMENT API ENDPOINTS (Server Config, Whitelist, Roles, Announcements)
// =============================================================================
// GET /server-config/:guildId - Get server configuration
if (req.url.startsWith("/server-config/") && req.method === "GET") {
const guildId = req.url.split("/server-config/")[1].split("?")[0];
if (!guildId) {
res.writeHead(400);
res.end(JSON.stringify({ error: "Guild ID required" }));
return;
}
const config = serverConfigs.get(guildId) || {
guild_id: guildId,
welcome_channel: null,
goodbye_channel: null,
modlog_channel: null,
level_up_channel: null,
auto_role: null,
};
const guild = client.guilds.cache.get(guildId);
const channels = guild ? guild.channels.cache
.filter(c => c.type === 0) // Text channels
.map(c => ({ id: c.id, name: c.name })) : [];
const roles = guild ? guild.roles.cache
.filter(r => r.name !== '@everyone')
.map(r => ({ id: r.id, name: r.name, color: r.hexColor })) : [];
res.writeHead(200);
res.end(JSON.stringify({
config,
channels,
roles,
guildName: guild?.name || 'Unknown',
}));
return;
}
// POST /server-config - Save server configuration
if (req.url === "/server-config" && req.method === "POST") {
if (!checkAdminAuth(req)) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized" }));
return;
}
let body = "";
req.on("data", chunk => body += chunk);
req.on("end", async () => {
try {
const data = JSON.parse(body);
const guildId = data.guild_id;
if (!guildId) {
res.writeHead(400);
res.end(JSON.stringify({ error: "Guild ID required" }));
return;
}
const configData = {
guild_id: guildId,
welcome_channel: data.welcome_channel || null,
goodbye_channel: data.goodbye_channel || null,
modlog_channel: data.modlog_channel || null,
level_up_channel: data.level_up_channel || null,
auto_role: data.auto_role || null,
updated_at: new Date().toISOString(),
};
serverConfigs.set(guildId, configData);
if (supabase) {
const { error: dbError } = await supabase.from('server_config').upsert(configData);
if (dbError) {
console.error('[Config] Failed to save to database:', dbError.message);
res.writeHead(500);
res.end(JSON.stringify({ error: 'Failed to persist config to database', details: dbError.message }));
return;
}
}
res.writeHead(200);
res.end(JSON.stringify({ success: true, config: configData, persisted: !!supabase }));
} catch (error) {
res.writeHead(500);
res.end(JSON.stringify({ error: error.message }));
}
});
return;
}
// GET /whitelist - Get whitelisted servers and users
if (req.url === "/whitelist" && req.method === "GET") {
const whitelistedServers = WHITELISTED_GUILDS.map(guildId => {
const guild = client.guilds.cache.get(guildId);
return {
id: guildId,
name: guild?.name || 'Not Connected',
memberCount: guild?.memberCount || 0,
connected: !!guild,
};
});
res.writeHead(200);
res.end(JSON.stringify({
servers: whitelistedServers,
users: whitelistedUsers.map(id => ({ id, note: 'Whitelisted User' })),
timestamp: new Date().toISOString(),
}));
return;
}
// GET /roles - Get level roles and federation roles
if (req.url === "/roles" && req.method === "GET") {
(async () => {
const levelRoles = [];
const fedRoles = Array.from(federationMappings.entries()).map(([roleId, data]) => ({
roleId,
name: data.name,
guildId: data.guildId,
guildName: data.guildName,
linkedAt: new Date(data.linkedAt).toISOString(),
}));
// Try to get level roles from Supabase
if (supabase) {
try {
const { data } = await supabase.from('level_roles').select('*');
if (data) {
for (const lr of data) {
const guild = client.guilds.cache.get(lr.guild_id);
const role = guild?.roles.cache.get(lr.role_id);
levelRoles.push({
guildId: lr.guild_id,
guildName: guild?.name || 'Unknown',
roleId: lr.role_id,
roleName: role?.name || 'Unknown',
levelRequired: lr.level_required,
});
}
}
} catch (e) {
console.warn('Could not fetch level roles:', e.message);
}
}
res.writeHead(200);
res.end(JSON.stringify({
levelRoles,
federationRoles: fedRoles,
timestamp: new Date().toISOString(),
}));
})();
return;
}
// POST /announce - Send announcement to servers
if (req.url === "/announce" && req.method === "POST") {
if (!checkAdminAuth(req)) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized" }));
return;
}
let body = "";
req.on("data", chunk => body += chunk);
req.on("end", async () => {
try {
const { title, message, targets, color } = JSON.parse(body);
if (!title || !message) {
res.writeHead(400);
res.end(JSON.stringify({ error: "Title and message required" }));
return;
}
const embed = new EmbedBuilder()
.setTitle(title)
.setDescription(message)
.setColor(color ? parseInt(color.replace('#', ''), 16) : 0x5865F2)
.setTimestamp()
.setFooter({ text: 'AeThex Network Announcement' });
const results = [];
const targetGuilds = targets && targets.length > 0
? targets
: Array.from(client.guilds.cache.keys());
for (const guildId of targetGuilds) {
const guild = client.guilds.cache.get(guildId);
if (!guild) {
results.push({ guildId, success: false, error: 'Guild not found' });
continue;
}
try {
const config = serverConfigs.get(guildId);
let channel = null;
if (config?.welcome_channel) {
channel = guild.channels.cache.get(config.welcome_channel);
}
if (!channel) {
channel = guild.systemChannel ||
guild.channels.cache.find(c => c.type === 0 && c.permissionsFor(guild.members.me).has('SendMessages'));
}
if (channel) {
await channel.send({ embeds: [embed] });
results.push({ guildId, guildName: guild.name, success: true });
} else {
results.push({ guildId, guildName: guild.name, success: false, error: 'No suitable channel' });
}
} catch (error) {
results.push({ guildId, guildName: guild?.name, success: false, error: error.message });
}
}
addActivity('announcement', { title, targetCount: targetGuilds.length, successCount: results.filter(r => r.success).length });
res.writeHead(200);
res.end(JSON.stringify({ success: true, results }));
} catch (error) {
res.writeHead(500);
res.end(JSON.stringify({ error: error.message }));
}
});
return;
}
// GET /servers - List all connected servers (for dropdowns)
if (req.url === "/servers" && req.method === "GET") {
const servers = client.guilds.cache.map(g => ({
id: g.id,
name: g.name,
icon: g.iconURL({ size: 64 }),
memberCount: g.memberCount,
}));
res.writeHead(200);
res.end(JSON.stringify({ servers }));
return;
}
res.writeHead(404);
res.end(JSON.stringify({ error: "Not found" }));
})

View file

@ -911,6 +911,196 @@
border-color: var(--accent);
}
/* Config tabs */
.config-tab {
display: none;
}
.config-tab.active {
display: block;
}
/* Form elements */
.form-group {
margin-bottom: 1.25rem;
}
.form-label {
display: block;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.form-hint {
display: block;
font-size: 0.75rem;
color: var(--text-muted);
margin-top: 0.25rem;
}
.form-input, .form-select, .form-textarea {
width: 100%;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.625rem 0.875rem;
color: var(--text-primary);
font-size: 0.875rem;
transition: border-color 0.2s;
}
.form-input:focus, .form-select:focus, .form-textarea:focus {
outline: none;
border-color: var(--accent);
}
.form-input::placeholder, .form-textarea::placeholder {
color: var(--text-muted);
}
.form-textarea {
resize: vertical;
min-height: 100px;
}
.form-select {
cursor: pointer;
}
/* Checkbox group */
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: var(--text-secondary);
cursor: pointer;
}
.checkbox-item input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: var(--accent);
}
/* Role item */
.role-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem;
background: var(--bg-card);
border-radius: 8px;
margin-bottom: 0.5rem;
}
.role-item:last-child {
margin-bottom: 0;
}
.role-info {
display: flex;
align-items: center;
gap: 0.75rem;
}
.role-color {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--accent);
}
.role-name {
font-weight: 500;
}
.role-meta {
font-size: 0.8rem;
color: var(--text-muted);
}
/* Whitelist item */
.whitelist-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.625rem 0;
border-bottom: 1px solid var(--border);
}
.whitelist-item:last-child {
border-bottom: none;
}
.whitelist-info {
display: flex;
align-items: center;
gap: 0.75rem;
}
.whitelist-icon {
width: 32px;
height: 32px;
border-radius: 8px;
background: var(--bg-tertiary);
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 0.875rem;
}
.whitelist-name {
font-weight: 500;
}
.whitelist-id {
font-size: 0.75rem;
color: var(--text-muted);
font-family: monospace;
}
/* Announcement item */
.announce-item {
padding: 1rem;
background: var(--bg-card);
border-radius: 8px;
margin-bottom: 0.75rem;
}
.announce-item:last-child {
margin-bottom: 0;
}
.announce-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.announce-title {
font-weight: 600;
}
.announce-time {
font-size: 0.75rem;
color: var(--text-muted);
}
.announce-message {
font-size: 0.875rem;
color: var(--text-secondary);
}
/* Toast notification */
.toast {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
background: var(--bg-secondary);
border: 1px solid var(--border);
padding: 1rem 1.5rem;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
display: flex;
align-items: center;
gap: 0.75rem;
z-index: 1000;
animation: slideIn 0.3s ease;
}
.toast.success { border-left: 4px solid var(--success); }
.toast.error { border-left: 4px solid var(--danger); }
.toast.warning { border-left: 4px solid var(--warning); }
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
/* Scrollbar */
::-webkit-scrollbar {
width: 6px;
@ -1445,14 +1635,200 @@
<!-- Configuration Section -->
<section class="section" id="section-config">
<div class="card">
<div class="card-header">
<h3 class="card-title">Server Configuration</h3>
<div class="tab-buttons" style="margin-bottom: 1.5rem;">
<button class="tab-btn active" onclick="switchConfigTab('settings')">Server Settings</button>
<button class="tab-btn" onclick="switchConfigTab('roles')">Role Management</button>
<button class="tab-btn" onclick="switchConfigTab('whitelist')">Whitelist</button>
<button class="tab-btn" onclick="switchConfigTab('announcements')">Announcements</button>
</div>
<!-- Server Settings Tab -->
<div class="config-tab active" id="config-settings">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><circle cx="6" cy="6" r="1"/><circle cx="6" cy="18" r="1"/></svg>
Select Server
</h3>
</div>
<div class="card-body">
<select class="form-select" id="configServerSelect" onchange="loadServerConfig()">
<option value="">Select a server...</option>
</select>
</div>
</div>
<div class="card-body">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
<p>Configuration options coming soon</p>
<div id="serverConfigPanel" style="display: none;">
<div class="grid-2">
<div class="card">
<div class="card-header">
<h3 class="card-title">Channel Settings</h3>
</div>
<div class="card-body">
<div class="form-group">
<label class="form-label">Welcome Channel</label>
<input type="text" class="form-input" id="configWelcomeChannel" placeholder="Channel ID">
<span class="form-hint">Messages sent when members join</span>
</div>
<div class="form-group">
<label class="form-label">Goodbye Channel</label>
<input type="text" class="form-input" id="configGoodbyeChannel" placeholder="Channel ID">
<span class="form-hint">Messages sent when members leave</span>
</div>
<div class="form-group">
<label class="form-label">Mod Log Channel</label>
<input type="text" class="form-input" id="configModlogChannel" placeholder="Channel ID">
<span class="form-hint">Moderation action logs</span>
</div>
<div class="form-group">
<label class="form-label">Level-Up Channel</label>
<input type="text" class="form-input" id="configLevelupChannel" placeholder="Channel ID">
<span class="form-hint">Level-up announcements</span>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Role Settings</h3>
</div>
<div class="card-body">
<div class="form-group">
<label class="form-label">Auto-Role</label>
<input type="text" class="form-input" id="configAutoRole" placeholder="Role ID">
<span class="form-hint">Role given to new members</span>
</div>
<div class="form-group">
<label class="form-label">Verified Role</label>
<input type="text" class="form-input" id="configVerifiedRole" placeholder="Role ID">
<span class="form-hint">Role given after verification</span>
</div>
</div>
</div>
</div>
<div class="card" style="margin-top: 1rem;">
<div class="card-body" style="display: flex; gap: 1rem; justify-content: flex-end;">
<button class="btn btn-secondary" onclick="loadServerConfig()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
Reset
</button>
<button class="btn btn-primary" onclick="saveServerConfig()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
Save Configuration
</button>
</div>
</div>
</div>
</div>
<!-- Role Management Tab -->
<div class="config-tab" id="config-roles">
<div class="grid-2">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>
Level Roles
</h3>
<button class="btn btn-primary btn-sm" onclick="showAddLevelRoleModal()">Add Role</button>
</div>
<div class="card-body" id="levelRolesList">
<div class="empty-state">
<p>No level roles configured</p>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
Federation Roles
</h3>
</div>
<div class="card-body" id="federationRolesList">
<div class="empty-state">
<p>No federation roles linked</p>
</div>
</div>
</div>
</div>
</div>
<!-- Whitelist Tab -->
<div class="config-tab" id="config-whitelist">
<div class="grid-2">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><circle cx="6" cy="6" r="1"/><circle cx="6" cy="18" r="1"/></svg>
Whitelisted Servers
</h3>
</div>
<div class="card-body" id="whitelistServersList">
<div class="loading"><div class="spinner"></div>Loading...</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
Whitelisted Users (Heat Skip)
</h3>
</div>
<div class="card-body" id="whitelistUsersList">
<div class="loading"><div class="spinner"></div>Loading...</div>
</div>
</div>
</div>
</div>
<!-- Announcements Tab -->
<div class="config-tab" id="config-announcements">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m3 11 18-5v12L3 13v-2z"/><path d="M11.6 16.8a3 3 0 1 1-5.8-1.6"/></svg>
Send Announcement
</h3>
</div>
<div class="card-body">
<div class="form-group">
<label class="form-label">Title</label>
<input type="text" class="form-input" id="announceTitle" placeholder="Announcement title...">
</div>
<div class="form-group">
<label class="form-label">Message</label>
<textarea class="form-textarea" id="announceMessage" rows="4" placeholder="Announcement content..."></textarea>
</div>
<div class="form-group">
<label class="form-label">Target Servers</label>
<div id="announceTargets" class="checkbox-group">
<label class="checkbox-item">
<input type="checkbox" value="all" checked> All Servers
</label>
</div>
</div>
<div style="display: flex; gap: 1rem; justify-content: flex-end; margin-top: 1rem;">
<button class="btn btn-secondary" onclick="clearAnnouncement()">Clear</button>
<button class="btn btn-primary" onclick="sendAnnouncement()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
Send Announcement
</button>
</div>
</div>
</div>
<div class="card" style="margin-top: 1.5rem;">
<div class="card-header">
<h3 class="card-title">Recent Announcements</h3>
</div>
<div class="card-body" id="recentAnnouncements">
<div class="empty-state">
<p>No recent announcements</p>
</div>
</div>
</div>
</div>
@ -2086,6 +2462,334 @@
`;
}
// ============================================
// MANAGEMENT PANEL FUNCTIONS
// ============================================
let cachedServers = [];
let currentServerConfig = null;
function switchConfigTab(tabName) {
document.querySelectorAll('.config-tab').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('#section-config .tab-btn').forEach(btn => btn.classList.remove('active'));
document.getElementById(`config-${tabName}`).classList.add('active');
event.target.classList.add('active');
if (tabName === 'settings') {
populateServerSelect();
} else if (tabName === 'whitelist') {
fetchWhitelist();
} else if (tabName === 'roles') {
fetchRoles();
} else if (tabName === 'announcements') {
populateAnnounceTargets();
}
}
async function populateServerSelect() {
try {
const response = await fetch('/stats');
const data = await response.json();
cachedServers = data.guilds || [];
const select = document.getElementById('configServerSelect');
select.innerHTML = '<option value="">Select a server...</option>' +
cachedServers.map(g => `<option value="${g.id}">${g.name}</option>`).join('');
} catch (e) {
console.error('Failed to fetch servers:', e);
}
}
async function loadServerConfig() {
const guildId = document.getElementById('configServerSelect').value;
if (!guildId) {
document.getElementById('serverConfigPanel').style.display = 'none';
return;
}
try {
const response = await fetch(`/server-config/${guildId}`);
const data = await response.json();
currentServerConfig = data.config || {};
document.getElementById('configWelcomeChannel').value = currentServerConfig.welcome_channel || '';
document.getElementById('configGoodbyeChannel').value = currentServerConfig.goodbye_channel || '';
document.getElementById('configModlogChannel').value = currentServerConfig.modlog_channel || '';
document.getElementById('configLevelupChannel').value = currentServerConfig.level_up_channel || '';
document.getElementById('configAutoRole').value = currentServerConfig.auto_role || '';
document.getElementById('configVerifiedRole').value = currentServerConfig.verified_role || '';
document.getElementById('serverConfigPanel').style.display = 'block';
} catch (e) {
console.error('Failed to load server config:', e);
showToast('Failed to load configuration', 'error');
}
}
async function saveServerConfig() {
const guildId = document.getElementById('configServerSelect').value;
if (!guildId) return;
const config = {
guild_id: guildId,
welcome_channel: document.getElementById('configWelcomeChannel').value || null,
goodbye_channel: document.getElementById('configGoodbyeChannel').value || null,
modlog_channel: document.getElementById('configModlogChannel').value || null,
level_up_channel: document.getElementById('configLevelupChannel').value || null,
auto_role: document.getElementById('configAutoRole').value || null,
verified_role: document.getElementById('configVerifiedRole').value || null
};
try {
const token = localStorage.getItem('adminToken') || '';
const response = await fetch('/server-config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(config)
});
if (response.ok) {
showToast('Configuration saved successfully!', 'success');
} else {
throw new Error('Failed to save');
}
} catch (e) {
console.error('Failed to save config:', e);
showToast('Failed to save configuration', 'error');
}
}
async function fetchWhitelist() {
try {
const response = await fetch('/whitelist');
const data = await response.json();
const serversList = document.getElementById('whitelistServersList');
if (data.servers && data.servers.length > 0) {
serversList.innerHTML = data.servers.map(s => `
<div class="whitelist-item">
<div class="whitelist-info">
<div class="whitelist-icon">${(s.name || 'S').charAt(0).toUpperCase()}</div>
<div>
<div class="whitelist-name">${s.name || 'Unknown Server'}</div>
<div class="whitelist-id">${s.id}</div>
</div>
</div>
<span class="badge badge-success">Active</span>
</div>
`).join('');
} else {
serversList.innerHTML = '<div class="empty-state"><p>No whitelisted servers</p></div>';
}
const usersList = document.getElementById('whitelistUsersList');
if (data.users && data.users.length > 0) {
usersList.innerHTML = data.users.map(u => `
<div class="whitelist-item">
<div class="whitelist-info">
<div class="whitelist-icon">${(u.username || 'U').charAt(0).toUpperCase()}</div>
<div>
<div class="whitelist-name">${u.username || 'Unknown User'}</div>
<div class="whitelist-id">${u.id}</div>
</div>
</div>
<span class="badge badge-info">Heat Skip</span>
</div>
`).join('');
} else {
usersList.innerHTML = '<div class="empty-state"><p>No whitelisted users</p></div>';
}
} catch (e) {
console.error('Failed to fetch whitelist:', e);
document.getElementById('whitelistServersList').innerHTML = '<div class="empty-state"><p>Failed to load</p></div>';
document.getElementById('whitelistUsersList').innerHTML = '<div class="empty-state"><p>Failed to load</p></div>';
}
}
async function fetchRoles() {
try {
const response = await fetch('/roles');
const data = await response.json();
const levelRolesList = document.getElementById('levelRolesList');
if (data.levelRoles && data.levelRoles.length > 0) {
levelRolesList.innerHTML = data.levelRoles.map(r => `
<div class="role-item">
<div class="role-info">
<div class="role-color" style="background: ${r.color || 'var(--accent)'}"></div>
<div>
<div class="role-name">${r.name || 'Unknown Role'}</div>
<div class="role-meta">Level ${r.level} required</div>
</div>
</div>
<button class="btn btn-sm btn-secondary" onclick="removeLevelRole('${r.id}')">Remove</button>
</div>
`).join('');
} else {
levelRolesList.innerHTML = '<div class="empty-state"><p>No level roles configured</p></div>';
}
const federationRolesList = document.getElementById('federationRolesList');
if (data.federationRoles && data.federationRoles.length > 0) {
federationRolesList.innerHTML = data.federationRoles.map(r => `
<div class="role-item">
<div class="role-info">
<div class="role-color" style="background: ${r.color || 'var(--info)'}"></div>
<div>
<div class="role-name">${r.name || 'Unknown Role'}</div>
<div class="role-meta">${r.guildName || 'Unknown Server'}</div>
</div>
</div>
<span class="badge badge-info">Synced</span>
</div>
`).join('');
} else {
federationRolesList.innerHTML = '<div class="empty-state"><p>No federation roles linked</p></div>';
}
} catch (e) {
console.error('Failed to fetch roles:', e);
}
}
function showAddLevelRoleModal() {
showToast('Use /config levelrole command in Discord to add level roles', 'warning');
}
function removeLevelRole(roleId) {
showToast('Use /config remove-levelrole command in Discord to remove roles', 'warning');
}
async function populateAnnounceTargets() {
try {
const response = await fetch('/stats');
const data = await response.json();
const servers = data.guilds || [];
const targetsDiv = document.getElementById('announceTargets');
targetsDiv.innerHTML = `
<label class="checkbox-item">
<input type="checkbox" value="all" checked onchange="toggleAllServers(this)"> All Servers
</label>
${servers.map(s => `
<label class="checkbox-item">
<input type="checkbox" value="${s.id}" class="server-checkbox"> ${s.name}
</label>
`).join('')}
`;
} catch (e) {
console.error('Failed to populate announce targets:', e);
}
}
function toggleAllServers(checkbox) {
const serverCheckboxes = document.querySelectorAll('.server-checkbox');
serverCheckboxes.forEach(cb => {
cb.checked = checkbox.checked;
cb.disabled = checkbox.checked;
});
}
function clearAnnouncement() {
document.getElementById('announceTitle').value = '';
document.getElementById('announceMessage').value = '';
}
async function sendAnnouncement() {
const title = document.getElementById('announceTitle').value.trim();
const message = document.getElementById('announceMessage').value.trim();
if (!title || !message) {
showToast('Please fill in both title and message', 'warning');
return;
}
const allChecked = document.querySelector('input[value="all"]').checked;
let targets = [];
if (!allChecked) {
document.querySelectorAll('.server-checkbox:checked').forEach(cb => {
targets.push(cb.value);
});
if (targets.length === 0) {
showToast('Please select at least one server', 'warning');
return;
}
}
try {
const token = localStorage.getItem('adminToken') || '';
const response = await fetch('/announce', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
title,
message,
targets: allChecked ? 'all' : targets
})
});
if (response.ok) {
const result = await response.json();
showToast(`Announcement sent to ${result.sentCount || 0} servers!`, 'success');
clearAnnouncement();
fetchRecentAnnouncements();
} else {
throw new Error('Failed to send');
}
} catch (e) {
console.error('Failed to send announcement:', e);
showToast('Failed to send announcement', 'error');
}
}
async function fetchRecentAnnouncements() {
try {
const response = await fetch('/announcements');
const data = await response.json();
const container = document.getElementById('recentAnnouncements');
if (data.announcements && data.announcements.length > 0) {
container.innerHTML = data.announcements.slice(0, 5).map(a => `
<div class="announce-item">
<div class="announce-header">
<span class="announce-title">${a.title}</span>
<span class="announce-time">${formatTimeAgo(a.timestamp)}</span>
</div>
<div class="announce-message">${a.message}</div>
</div>
`).join('');
} else {
container.innerHTML = '<div class="empty-state"><p>No recent announcements</p></div>';
}
} catch (e) {
console.error('Failed to fetch announcements:', e);
}
}
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
${type === 'success' ? '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>' :
type === 'error' ? '<circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>' :
'<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>'}
</svg>
<span>${message}</span>
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideIn 0.3s ease reverse';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
// Load theme