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:
parent
cada20b646
commit
43394c8fe1
3 changed files with 959 additions and 11 deletions
4
.replit
4
.replit
|
|
@ -22,10 +22,6 @@ externalPort = 80
|
|||
localPort = 8080
|
||||
externalPort = 8080
|
||||
|
||||
[[ports]]
|
||||
localPort = 35789
|
||||
externalPort = 3000
|
||||
|
||||
[workflows]
|
||||
runButton = "Project"
|
||||
|
||||
|
|
|
|||
|
|
@ -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" }));
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue