Add moderation and analytics pages to the dashboard interface

Implement new API endpoints for moderation statistics, warnings, bans, and activity feed data in webServer.js, and add corresponding sections and navigation to 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: 72415ade-753e-448a-a127-8d4fee249942
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/bakeZwZ
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-12 23:26:55 +00:00
parent 8e47011cbc
commit 84d7720819
2 changed files with 823 additions and 1 deletions

View file

@ -1171,8 +1171,14 @@
<div class="nav-item" data-page="admin-shop">
<span class="nav-icon">🏪</span> Manage Shop
</div>
<div class="nav-item" data-page="moderation">
<span class="nav-icon">&#128737;</span> Moderation
</div>
<div class="nav-item" data-page="analytics">
<span class="nav-icon">📊</span> Analytics
</div>
<div class="nav-item" data-page="federation">
<span class="nav-icon">&#128737;</span> Federation
<span class="nav-icon">🌐</span> Federation
</div>
</div>
</nav>
@ -1712,6 +1718,171 @@
</div>
</div>
<div id="page-moderation" class="page hidden">
<div class="page-header">
<h1 class="page-title"><span class="text-gradient">Moderation</span> Dashboard</h1>
<p class="page-subtitle">View and manage warnings, bans, and moderation actions</p>
</div>
<div class="card" style="margin-bottom:1.5rem">
<div class="card-header">
<h3 class="card-title">User Search</h3>
</div>
<div class="card-body">
<div style="display:flex;gap:1rem;flex-wrap:wrap">
<input type="text" id="modSearchInput" class="form-input" placeholder="Search by username or user ID..." style="flex:1;min-width:200px">
<button class="btn btn-primary" onclick="searchModUser()">Search</button>
</div>
<div id="modSearchResults" style="margin-top:1rem"></div>
</div>
</div>
<div class="stats-grid" style="margin-bottom:1.5rem">
<div class="stat-card">
<div class="stat-label">Total Warnings</div>
<div class="stat-value" id="modTotalWarnings">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Active Bans</div>
<div class="stat-value" id="modActiveBans">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Recent Actions</div>
<div class="stat-value" id="modRecentActions">-</div>
</div>
</div>
<div class="tabs" style="margin-bottom:1.5rem">
<div class="tab active" data-mod-tab="mod-warnings">Warnings</div>
<div class="tab" data-mod-tab="mod-bans">Bans</div>
<div class="tab" data-mod-tab="mod-activity">Activity Feed</div>
</div>
<div id="mod-warnings" class="mod-section">
<div class="card">
<div class="card-header">
<h3 class="card-title">Server Warnings</h3>
</div>
<div class="table-container">
<table class="table">
<thead>
<tr>
<th>User</th>
<th>Reason</th>
<th>Moderator</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="modWarningsList">
<tr><td colspan="5" class="empty-state">Loading warnings...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div id="mod-bans" class="mod-section hidden">
<div class="card">
<div class="card-header">
<h3 class="card-title">Server Bans</h3>
</div>
<div class="table-container">
<table class="table">
<thead>
<tr>
<th>User</th>
<th>Reason</th>
<th>Moderator</th>
<th>Date</th>
</tr>
</thead>
<tbody id="modBansList">
<tr><td colspan="4" class="empty-state">Loading bans...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div id="mod-activity" class="mod-section hidden">
<div class="card">
<div class="card-header">
<h3 class="card-title">Recent Activity Feed</h3>
</div>
<div class="card-body" id="modActivityFeed">
<div class="empty-state">Loading activity...</div>
</div>
</div>
</div>
</div>
<div id="page-analytics" class="page hidden">
<div class="page-header">
<h1 class="page-title"><span class="text-gradient">Analytics</span></h1>
<p class="page-subtitle">Server activity insights and statistics</p>
</div>
<div class="stats-grid" style="margin-bottom:1.5rem">
<div class="stat-card">
<div class="stat-label">Messages Today</div>
<div class="stat-value" id="analyticsMessagesToday">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Active Users (7d)</div>
<div class="stat-value" id="analyticsActiveUsers">-</div>
</div>
<div class="stat-card">
<div class="stat-label">XP Earned Today</div>
<div class="stat-value" id="analyticsXpToday">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Commands Used</div>
<div class="stat-value" id="analyticsCommandsUsed">-</div>
</div>
</div>
<div class="card" style="margin-bottom:1.5rem">
<div class="card-header">
<h3 class="card-title">Activity Over Time (Last 7 Days)</h3>
</div>
<div class="card-body">
<div id="activityChart" style="height:200px;display:flex;align-items:flex-end;gap:0.5rem;padding:1rem 0">
<div class="empty-state" style="width:100%">Loading chart...</div>
</div>
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:1.5rem">
<div class="card">
<div class="card-header">
<h3 class="card-title">Top XP Earners (Today)</h3>
</div>
<div class="card-body" id="analyticsTopEarners">
<div class="empty-state">Loading...</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Most Active Channels</h3>
</div>
<div class="card-body" id="analyticsTopChannels">
<div class="empty-state">Loading...</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Command Usage</h3>
</div>
<div class="card-body" id="analyticsCommandStats">
<div class="empty-state">Loading...</div>
</div>
</div>
</div>
</div>
<div id="page-federation" class="page hidden">
<div class="page-header">
<h1 class="page-title"><span class="text-gradient">Federation</span> Management</h1>
@ -1997,6 +2168,8 @@
case 'admin-achievements': await loadAdminAchievements(); break;
case 'admin-shop': await loadAdminShop(); break;
case 'federation': loadFederationData(); break;
case 'moderation': loadModerationData(); break;
case 'analytics': loadAnalyticsData(); break;
}
}
@ -2877,6 +3050,302 @@
loadFederationLeaderboard();
}
// Moderation Tab Event Listeners
document.querySelectorAll('[data-mod-tab]').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('[data-mod-tab]').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.mod-section').forEach(s => s.classList.add('hidden'));
tab.classList.add('active');
document.getElementById(tab.dataset.modTab).classList.remove('hidden');
});
});
function loadModerationData() {
loadModerationStats();
loadWarnings();
loadBans();
loadActivityFeed();
}
async function loadModerationStats() {
if (!currentGuild) return;
try {
const res = await fetch('/api/guild/' + currentGuild + '/moderation/stats');
const data = await res.json();
document.getElementById('modTotalWarnings').textContent = data.totalWarnings || 0;
document.getElementById('modActiveBans').textContent = data.activeBans || 0;
document.getElementById('modRecentActions').textContent = data.recentActions || 0;
} catch (e) {
console.error('Failed to load moderation stats:', e);
}
}
async function loadWarnings() {
if (!currentGuild) return;
const tbody = document.getElementById('modWarningsList');
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">Loading warnings...</td></tr>';
try {
const res = await fetch('/api/guild/' + currentGuild + '/moderation/warnings');
const data = await res.json();
if (!data.warnings || data.warnings.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No warnings found</td></tr>';
return;
}
tbody.innerHTML = data.warnings.map(w => `
<tr>
<td>${escapeHtml(w.username || w.user_id)}</td>
<td>${escapeHtml((w.reason || 'No reason').substring(0, 50))}${(w.reason || '').length > 50 ? '...' : ''}</td>
<td>${escapeHtml(w.moderator_name || w.moderator_id || 'Unknown')}</td>
<td>${new Date(w.created_at).toLocaleDateString()}</td>
<td><button class="btn-icon danger" onclick="deleteWarning('${w.id}')" title="Delete Warning"></button></td>
</tr>
`).join('');
} catch (e) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">Failed to load warnings</td></tr>';
}
}
async function loadBans() {
if (!currentGuild) return;
const tbody = document.getElementById('modBansList');
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">Loading bans...</td></tr>';
try {
const res = await fetch('/api/guild/' + currentGuild + '/moderation/bans');
const data = await res.json();
if (!data.bans || data.bans.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">No bans found</td></tr>';
return;
}
tbody.innerHTML = data.bans.map(b => `
<tr>
<td>${escapeHtml(b.username || b.user_id)}</td>
<td>${escapeHtml((b.reason || 'No reason').substring(0, 50))}${(b.reason || '').length > 50 ? '...' : ''}</td>
<td>${escapeHtml(b.moderator_name || b.moderator_id || 'Unknown')}</td>
<td>${new Date(b.created_at).toLocaleDateString()}</td>
</tr>
`).join('');
} catch (e) {
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">Failed to load bans</td></tr>';
}
}
async function loadActivityFeed() {
if (!currentGuild) return;
const container = document.getElementById('modActivityFeed');
container.innerHTML = '<div class="empty-state">Loading activity...</div>';
try {
const res = await fetch('/api/guild/' + currentGuild + '/moderation/activity');
const data = await res.json();
if (!data.activity || data.activity.length === 0) {
container.innerHTML = '<div class="empty-state">No recent moderation activity</div>';
return;
}
const actionColors = { warn: 'var(--warning)', kick: '#f97316', ban: 'var(--danger)', timeout: 'var(--primary)', unban: 'var(--success)' };
container.innerHTML = data.activity.map(a => `
<div style="display:flex;align-items:center;gap:1rem;padding:0.75rem;border-bottom:1px solid var(--border)">
<div style="width:8px;height:8px;border-radius:50%;background:${actionColors[a.action_type] || 'var(--muted)'}"></div>
<div style="flex:1">
<div style="font-weight:500">${escapeHtml(a.target_name || a.target_id)} was ${a.action_type}${a.action_type.endsWith('n') ? 'ned' : 'ed'}</div>
<div style="font-size:0.8rem;color:var(--muted)">by ${escapeHtml(a.moderator_name || a.moderator_id)} - ${a.reason ? escapeHtml(a.reason.substring(0, 40)) : 'No reason'}</div>
</div>
<div style="font-size:0.75rem;color:var(--muted)">${new Date(a.created_at).toLocaleString()}</div>
</div>
`).join('');
} catch (e) {
container.innerHTML = '<div class="empty-state">Failed to load activity</div>';
}
}
async function searchModUser() {
if (!currentGuild) return;
const query = document.getElementById('modSearchInput').value.trim();
if (!query) return;
const container = document.getElementById('modSearchResults');
container.innerHTML = '<div style="display:flex;justify-content:center;padding:1rem"><div class="loading-spinner"></div></div>';
try {
const res = await fetch('/api/guild/' + currentGuild + '/moderation/search?q=' + encodeURIComponent(query));
const data = await res.json();
if (!data.results || data.results.length === 0) {
container.innerHTML = '<div style="padding:1rem;color:var(--muted)">No users found matching "' + escapeHtml(query) + '"</div>';
return;
}
container.innerHTML = data.results.map(u => `
<div style="display:flex;align-items:center;gap:1rem;padding:0.75rem;background:var(--card);border:1px solid var(--card-border);border-radius:8px;margin-bottom:0.5rem">
<div style="width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,var(--primary),#3b82f6);display:flex;align-items:center;justify-content:center;font-weight:600">${(u.username || 'U').charAt(0).toUpperCase()}</div>
<div style="flex:1">
<div style="font-weight:500">${escapeHtml(u.username || u.user_id)}</div>
<div style="font-size:0.8rem;color:var(--muted)">${u.warnings_count || 0} warnings</div>
</div>
<div style="font-size:0.8rem;color:var(--muted)">${u.is_banned ? '<span style="color:var(--danger)">BANNED</span>' : ''}</div>
</div>
`).join('');
} catch (e) {
container.innerHTML = '<div style="padding:1rem;color:var(--danger)">Search failed</div>';
}
}
async function deleteWarning(warningId) {
if (!confirm('Delete this warning?')) return;
if (!currentGuild) return;
try {
const res = await fetch('/api/guild/' + currentGuild + '/moderation/warnings/' + warningId, {
method: 'DELETE'
});
if (res.ok) {
loadWarnings();
loadModerationStats();
} else {
alert('Failed to delete warning');
}
} catch (e) {
alert('Failed to delete warning');
}
}
// Analytics Functions
function loadAnalyticsData() {
loadAnalyticsStats();
loadActivityChart();
loadTopEarners();
loadTopChannels();
loadCommandStats();
}
async function loadAnalyticsStats() {
if (!currentGuild) return;
try {
const res = await fetch('/api/guild/' + currentGuild + '/analytics/stats');
const data = await res.json();
document.getElementById('analyticsMessagesToday').textContent = (data.messagesToday || 0).toLocaleString();
document.getElementById('analyticsActiveUsers').textContent = (data.activeUsers || 0).toLocaleString();
document.getElementById('analyticsXpToday').textContent = (data.xpToday || 0).toLocaleString();
document.getElementById('analyticsCommandsUsed').textContent = (data.commandsUsed || 0).toLocaleString();
} catch (e) {
console.error('Failed to load analytics stats:', e);
}
}
async function loadActivityChart() {
if (!currentGuild) return;
const container = document.getElementById('activityChart');
try {
const res = await fetch('/api/guild/' + currentGuild + '/analytics/activity');
const data = await res.json();
if (!data.days || data.days.length === 0) {
container.innerHTML = '<div class="empty-state" style="width:100%">No activity data available</div>';
return;
}
const maxVal = Math.max(...data.days.map(d => d.messages || 0), 1);
container.innerHTML = data.days.map(d => {
const height = Math.max(10, ((d.messages || 0) / maxVal) * 150);
return `
<div style="flex:1;display:flex;flex-direction:column;align-items:center;gap:0.5rem">
<div style="width:100%;max-width:40px;height:${height}px;background:linear-gradient(180deg,var(--primary),#3b82f6);border-radius:4px 4px 0 0;transition:height 0.3s"></div>
<div style="font-size:0.7rem;color:var(--muted)">${d.label || ''}</div>
<div style="font-size:0.65rem;color:var(--muted)">${(d.messages || 0).toLocaleString()}</div>
</div>
`;
}).join('');
} catch (e) {
container.innerHTML = '<div class="empty-state" style="width:100%">Failed to load chart</div>';
}
}
async function loadTopEarners() {
if (!currentGuild) return;
const container = document.getElementById('analyticsTopEarners');
try {
const res = await fetch('/api/guild/' + currentGuild + '/analytics/top-earners');
const data = await res.json();
if (!data.earners || data.earners.length === 0) {
container.innerHTML = '<div class="empty-state">No XP earned today</div>';
return;
}
container.innerHTML = data.earners.map((e, i) => `
<div style="display:flex;align-items:center;gap:0.75rem;padding:0.5rem 0;${i < data.earners.length - 1 ? 'border-bottom:1px solid var(--border)' : ''}">
<div style="width:24px;font-weight:600;color:${i < 3 ? 'var(--primary)' : 'var(--muted)'}">#${i + 1}</div>
<div style="flex:1">${escapeHtml(e.username || e.user_id)}</div>
<div style="font-weight:600;color:var(--primary)">${(e.xp_earned || 0).toLocaleString()} XP</div>
</div>
`).join('');
} catch (e) {
container.innerHTML = '<div class="empty-state">Failed to load</div>';
}
}
async function loadTopChannels() {
if (!currentGuild) return;
const container = document.getElementById('analyticsTopChannels');
try {
const res = await fetch('/api/guild/' + currentGuild + '/analytics/top-channels');
const data = await res.json();
if (!data.channels || data.channels.length === 0) {
container.innerHTML = '<div class="empty-state">No channel data</div>';
return;
}
container.innerHTML = data.channels.map((c, i) => `
<div style="display:flex;align-items:center;gap:0.75rem;padding:0.5rem 0;${i < data.channels.length - 1 ? 'border-bottom:1px solid var(--border)' : ''}">
<div style="color:var(--muted)">#</div>
<div style="flex:1">${escapeHtml(c.channel_name || c.channel_id)}</div>
<div style="font-size:0.85rem;color:var(--muted)">${(c.message_count || 0).toLocaleString()} msgs</div>
</div>
`).join('');
} catch (e) {
container.innerHTML = '<div class="empty-state">Failed to load</div>';
}
}
async function loadCommandStats() {
if (!currentGuild) return;
const container = document.getElementById('analyticsCommandStats');
try {
const res = await fetch('/api/guild/' + currentGuild + '/analytics/commands');
const data = await res.json();
if (!data.commands || data.commands.length === 0) {
container.innerHTML = '<div class="empty-state">No command data</div>';
return;
}
container.innerHTML = data.commands.map((c, i) => `
<div style="display:flex;align-items:center;gap:0.75rem;padding:0.5rem 0;${i < data.commands.length - 1 ? 'border-bottom:1px solid var(--border)' : ''}">
<div style="color:var(--primary)">/${escapeHtml(c.command_name)}</div>
<div style="flex:1"></div>
<div style="font-size:0.85rem;color:var(--muted)">${(c.use_count || 0).toLocaleString()} uses</div>
</div>
`).join('');
} catch (e) {
container.innerHTML = '<div class="empty-state">Failed to load</div>';
}
}
init();
</script>
</body>

View file

@ -1426,6 +1426,359 @@ function createWebServer(discordClient, supabase, options = {}) {
}
});
// Moderation API Endpoints
app.get('/api/guild/:guildId/moderation/stats', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId } = req.params;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
try {
const [warningsRes, bansRes, actionsRes] = await Promise.all([
supabase.from('warnings').select('*', { count: 'exact', head: true }).eq('guild_id', guildId),
supabase.from('mod_actions').select('*', { count: 'exact', head: true }).eq('guild_id', guildId).eq('action_type', 'ban'),
supabase.from('mod_actions').select('*', { count: 'exact', head: true }).eq('guild_id', guildId).gte('created_at', new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString())
]);
res.json({
totalWarnings: warningsRes.count || 0,
activeBans: bansRes.count || 0,
recentActions: actionsRes.count || 0
});
} catch (error) {
res.status(500).json({ error: 'Failed to fetch stats' });
}
});
app.get('/api/guild/:guildId/moderation/warnings', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId } = req.params;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
try {
const { data: warnings } = await supabase
.from('warnings')
.select('*')
.eq('guild_id', guildId)
.order('created_at', { ascending: false })
.limit(100);
res.json({ warnings: warnings || [] });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch warnings' });
}
});
app.delete('/api/guild/:guildId/moderation/warnings/:warningId', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId, warningId } = req.params;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
try {
const { error } = await supabase
.from('warnings')
.delete()
.eq('id', warningId)
.eq('guild_id', guildId);
if (error) throw error;
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: 'Failed to delete warning' });
}
});
app.get('/api/guild/:guildId/moderation/bans', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId } = req.params;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
try {
const { data: bans } = await supabase
.from('mod_actions')
.select('*')
.eq('guild_id', guildId)
.eq('action_type', 'ban')
.order('created_at', { ascending: false })
.limit(100);
res.json({ bans: bans || [] });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch bans' });
}
});
app.get('/api/guild/:guildId/moderation/activity', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId } = req.params;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
try {
const { data: activity } = await supabase
.from('mod_actions')
.select('*')
.eq('guild_id', guildId)
.order('created_at', { ascending: false })
.limit(50);
res.json({ activity: activity || [] });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch activity' });
}
});
app.get('/api/guild/:guildId/moderation/search', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId } = req.params;
const { q } = req.query;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
if (!q) return res.json({ results: [] });
try {
const { data: warnings } = await supabase
.from('warnings')
.select('user_id, username')
.eq('guild_id', guildId)
.or(`user_id.eq.${q},username.ilike.%${q}%`)
.limit(20);
const userMap = new Map();
(warnings || []).forEach(w => {
if (!userMap.has(w.user_id)) {
userMap.set(w.user_id, { user_id: w.user_id, username: w.username, warnings_count: 0, is_banned: false });
}
userMap.get(w.user_id).warnings_count++;
});
const { data: bans } = await supabase
.from('mod_actions')
.select('target_id')
.eq('guild_id', guildId)
.eq('action_type', 'ban');
const bannedIds = new Set((bans || []).map(b => b.target_id));
userMap.forEach(u => { u.is_banned = bannedIds.has(u.user_id); });
res.json({ results: Array.from(userMap.values()) });
} catch (error) {
res.status(500).json({ error: 'Search failed' });
}
});
// Analytics API Endpoints
app.get('/api/guild/:guildId/analytics/stats', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId } = req.params;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
const today = new Date();
today.setHours(0, 0, 0, 0);
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
try {
const [messagesRes, activeRes, xpRes, commandsRes] = await Promise.all([
supabase.from('message_logs').select('*', { count: 'exact', head: true }).eq('guild_id', guildId).gte('created_at', today.toISOString()),
supabase.from('user_stats').select('*', { count: 'exact', head: true }).eq('guild_id', guildId).gte('last_active', weekAgo.toISOString()),
supabase.from('xp_logs').select('xp_amount').eq('guild_id', guildId).gte('created_at', today.toISOString()),
supabase.from('command_logs').select('*', { count: 'exact', head: true }).eq('guild_id', guildId).gte('created_at', today.toISOString())
]);
const xpToday = (xpRes.data || []).reduce((sum, r) => sum + (r.xp_amount || 0), 0);
res.json({
messagesToday: messagesRes.count || 0,
activeUsers: activeRes.count || 0,
xpToday,
commandsUsed: commandsRes.count || 0
});
} catch (error) {
res.json({ messagesToday: 0, activeUsers: 0, xpToday: 0, commandsUsed: 0 });
}
});
app.get('/api/guild/:guildId/analytics/activity', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId } = req.params;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
try {
const days = [];
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
for (let i = 6; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
date.setHours(0, 0, 0, 0);
const nextDate = new Date(date);
nextDate.setDate(nextDate.getDate() + 1);
const { count } = await supabase
.from('message_logs')
.select('*', { count: 'exact', head: true })
.eq('guild_id', guildId)
.gte('created_at', date.toISOString())
.lt('created_at', nextDate.toISOString());
days.push({
label: dayNames[date.getDay()],
messages: count || 0
});
}
res.json({ days });
} catch (error) {
res.json({ days: [] });
}
});
app.get('/api/guild/:guildId/analytics/top-earners', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId } = req.params;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
const today = new Date();
today.setHours(0, 0, 0, 0);
try {
const { data } = await supabase
.from('xp_logs')
.select('user_id, xp_amount')
.eq('guild_id', guildId)
.gte('created_at', today.toISOString());
const userXp = new Map();
(data || []).forEach(r => {
userXp.set(r.user_id, (userXp.get(r.user_id) || 0) + (r.xp_amount || 0));
});
const earners = Array.from(userXp.entries())
.map(([user_id, xp_earned]) => ({ user_id, xp_earned }))
.sort((a, b) => b.xp_earned - a.xp_earned)
.slice(0, 10);
res.json({ earners });
} catch (error) {
res.json({ earners: [] });
}
});
app.get('/api/guild/:guildId/analytics/top-channels', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId } = req.params;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
const today = new Date();
today.setHours(0, 0, 0, 0);
try {
const { data } = await supabase
.from('message_logs')
.select('channel_id, channel_name')
.eq('guild_id', guildId)
.gte('created_at', today.toISOString());
const channelCounts = new Map();
(data || []).forEach(r => {
const key = r.channel_id;
if (!channelCounts.has(key)) {
channelCounts.set(key, { channel_id: r.channel_id, channel_name: r.channel_name, message_count: 0 });
}
channelCounts.get(key).message_count++;
});
const channels = Array.from(channelCounts.values())
.sort((a, b) => b.message_count - a.message_count)
.slice(0, 10);
res.json({ channels });
} catch (error) {
res.json({ channels: [] });
}
});
app.get('/api/guild/:guildId/analytics/commands', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId } = req.params;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
try {
const { data } = await supabase
.from('command_logs')
.select('command_name')
.eq('guild_id', guildId);
const commandCounts = new Map();
(data || []).forEach(r => {
commandCounts.set(r.command_name, (commandCounts.get(r.command_name) || 0) + 1);
});
const commands = Array.from(commandCounts.entries())
.map(([command_name, use_count]) => ({ command_name, use_count }))
.sort((a, b) => b.use_count - a.use_count)
.slice(0, 10);
res.json({ commands });
} catch (error) {
res.json({ commands: [] });
}
});
app.get('/health', (req, res) => {
res.json({
status: 'ok',