diff --git a/aethex-bot/public/dashboard.html b/aethex-bot/public/dashboard.html index eb4c1af..e14273a 100644 --- a/aethex-bot/public/dashboard.html +++ b/aethex-bot/public/dashboard.html @@ -1502,6 +1502,59 @@ + + +
+
+
+

+ + Daily Command Trends +

+ Last 7 days +
+
+
+
+

Loading chart data...

+
+
+
+
+ +
+
+

+ + Hourly Activity (24h) +

+
+
+
+
+

Loading chart data...

+
+
+
+
+
+ +
+
+

+ + Top Command Users +

+ Last 7 days +
+
+
+
+

No user data available yet

+
+
+
+
@@ -2541,8 +2594,12 @@ // Fetch analytics async function fetchAnalytics() { try { - const response = await fetch('/analytics'); - const data = await response.json(); + const [analyticsRes, cmdAnalyticsRes] = await Promise.all([ + fetch('/analytics'), + fetch('/command-analytics?days=7') + ]); + const data = await analyticsRes.json(); + const cmdAnalytics = await cmdAnalyticsRes.json(); document.getElementById('commandsToday').textContent = data.commandsToday || 0; document.getElementById('xpToday').textContent = formatNumber(data.xpDistributed || 0); @@ -2562,22 +2619,52 @@ document.getElementById('banBar').style.width = `${((mods.bans || 0) / maxMod) * 100}%`; document.getElementById('timeoutBar').style.width = `${((mods.timeouts || 0) / maxMod) * 100}%`; + // Use database command analytics if available const commandBars = document.getElementById('commandBars'); - if (data.commandUsage && data.commandUsage.length > 0) { - const maxCmd = data.commandUsage[0].count || 1; - commandBars.innerHTML = data.commandUsage.slice(0, 8).map(cmd => ` -
- /${cmd.name} -
-
+ const commands = cmdAnalytics.commands && cmdAnalytics.commands.length > 0 + ? cmdAnalytics.commands + : (data.commandUsage || []); + + if (commands.length > 0) { + const maxCmd = Math.max(...commands.map(c => parseInt(c.count) || 0), 1); + commandBars.innerHTML = commands.slice(0, 8).map(cmd => { + const name = cmd.command_name || cmd.name || 'unknown'; + const count = parseInt(cmd.count) || 0; + const successRate = cmd.success_count ? Math.round((parseInt(cmd.success_count) / count) * 100) : 100; + const avgTime = cmd.avg_time ? Math.round(parseFloat(cmd.avg_time)) : 0; + return ` +
+ /${name} +
+
+
+ ${count}
- ${cmd.count} -
- `).join(''); + `; + }).join(''); } else { commandBars.innerHTML = '

No command usage yet

'; } + // Render daily line chart from database + const dailyChartEl = document.getElementById('dailyLineChart'); + if (dailyChartEl && cmdAnalytics.daily && cmdAnalytics.daily.length > 0) { + renderDailyLineChart(dailyChartEl, cmdAnalytics.daily); + } + + // Render hourly chart from database + const hourlyChartEl = document.getElementById('hourlyBarChart'); + if (hourlyChartEl && cmdAnalytics.hourly && cmdAnalytics.hourly.length > 0) { + renderHourlyChart(hourlyChartEl, cmdAnalytics.hourly); + } + + // Render top users + const topUsersEl = document.getElementById('topUsersChart'); + if (topUsersEl && cmdAnalytics.topUsers && cmdAnalytics.topUsers.length > 0) { + renderTopUsersChart(topUsersEl, cmdAnalytics.topUsers); + } + + // Fallback heatmap from in-memory data const heatmapGrid = document.getElementById('heatmapGrid'); if (data.hourlyActivity && data.hourlyActivity.length === 24) { const maxHour = Math.max(...data.hourlyActivity, 1); @@ -2601,6 +2688,112 @@ console.error('Failed to fetch analytics:', error); } } + + // Render daily line chart (SVG-based) + function renderDailyLineChart(container, data) { + const width = container.clientWidth || 400; + const height = 120; + const padding = 30; + + if (data.length === 0) { + container.innerHTML = '

No data available

'; + return; + } + + const counts = data.map(d => parseInt(d.count) || 0); + const maxCount = Math.max(...counts, 1); + const minCount = 0; + + const xStep = (width - padding * 2) / Math.max(data.length - 1, 1); + const yScale = (height - padding * 2) / (maxCount - minCount || 1); + + const points = data.map((d, i) => ({ + x: padding + i * xStep, + y: height - padding - (parseInt(d.count) || 0) * yScale, + date: d.date, + count: parseInt(d.count) || 0 + })); + + const linePath = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' '); + const areaPath = `${linePath} L ${points[points.length - 1].x} ${height - padding} L ${points[0].x} ${height - padding} Z`; + + container.innerHTML = ` + + + + + + + + + + ${points.map(p => ` + + ${p.date}: ${p.count} commands + + `).join('')} + ${data[0]?.date?.split('-').slice(1).join('/') || ''} + ${data[data.length - 1]?.date?.split('-').slice(1).join('/') || ''} + + `; + } + + // Render hourly bar chart + function renderHourlyChart(container, data) { + const hours = Array(24).fill(0); + data.forEach(d => { + const h = parseInt(d.hour); + if (h >= 0 && h < 24) hours[h] = parseInt(d.count) || 0; + }); + + const maxCount = Math.max(...hours, 1); + + container.innerHTML = ` +
+ ${hours.map((count, h) => { + const heightPct = 10 + (count / maxCount) * 90; + return ` +
+ `; + }).join('')} +
+
+ 0:00 + 6:00 + 12:00 + 18:00 + 23:00 +
+ `; + } + + // Render top users chart + function renderTopUsersChart(container, data) { + if (data.length === 0) { + container.innerHTML = '

No user data available

'; + return; + } + + const maxCount = Math.max(...data.map(d => parseInt(d.command_count) || 0), 1); + + container.innerHTML = data.slice(0, 5).map((user, i) => { + const count = parseInt(user.command_count) || 0; + const pct = (count / maxCount) * 100; + const rankClass = i === 0 ? 'gold' : i === 1 ? 'silver' : i === 2 ? 'bronze' : 'default'; + const tag = user.user_tag || `User ${user.user_id?.slice(-4) || '????'}`; + + return ` +
+ ${i + 1} + ${tag} +
+
+
+ ${count} +
+ `; + }).join(''); + } // Fetch leaderboard async function fetchLeaderboard() { @@ -3504,6 +3697,251 @@ }, 3000); } + // ============================================================================= + // WEBSOCKET CONNECTION FOR REAL-TIME UPDATES + // ============================================================================= + + let ws = null; + let wsReconnectAttempts = 0; + const MAX_RECONNECT_ATTEMPTS = 5; + const WS_RECONNECT_DELAY = 3000; + let wsConnected = false; + + function initWebSocket() { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}`; + + try { + ws = new WebSocket(wsUrl); + + ws.onopen = () => { + console.log('[WebSocket] Connected'); + wsConnected = true; + wsReconnectAttempts = 0; + updateConnectionStatus(true); + + // Reduce polling interval since we have real-time updates + if (refreshInterval) { + clearInterval(refreshInterval); + refreshInterval = setInterval(refreshData, 60000); // Slower polling as backup + } + }; + + ws.onmessage = (event) => { + try { + const message = JSON.parse(event.data); + handleWebSocketMessage(message); + } catch (e) { + console.error('[WebSocket] Parse error:', e); + } + }; + + ws.onclose = () => { + console.log('[WebSocket] Disconnected'); + wsConnected = false; + updateConnectionStatus(false); + + // Immediately restore polling while disconnected + if (!refreshInterval) { + console.log('[WebSocket] Restoring polling while disconnected'); + refreshInterval = setInterval(refreshData, 30000); + } + + // Attempt reconnection with exponential backoff + if (wsReconnectAttempts < MAX_RECONNECT_ATTEMPTS) { + wsReconnectAttempts++; + const backoffDelay = WS_RECONNECT_DELAY * Math.pow(1.5, wsReconnectAttempts - 1); + console.log(`[WebSocket] Reconnecting in ${Math.round(backoffDelay/1000)}s (attempt ${wsReconnectAttempts})...`); + setTimeout(initWebSocket, backoffDelay); + } else { + console.log('[WebSocket] Max reconnect attempts reached, continuing with polling'); + } + }; + + ws.onerror = (error) => { + console.error('[WebSocket] Error:', error); + }; + } catch (e) { + console.error('[WebSocket] Failed to connect:', e); + } + } + + function handleWebSocketMessage(message) { + switch (message.type) { + case 'stats': + updateStatsFromWebSocket(message.data); + break; + case 'activity': + addActivityFromWebSocket(message.data); + break; + case 'command': + updateCommandFromWebSocket(message.data); + break; + case 'threat': + addThreatFromWebSocket(message.data); + break; + case 'ticket': + updateTicketFromWebSocket(message.data); + break; + default: + console.log('[WebSocket] Unknown message type:', message.type); + } + } + + function updateStatsFromWebSocket(data) { + if (data.guilds !== undefined) { + document.getElementById('serverCount').textContent = data.guilds; + } + if (data.totalMembers !== undefined) { + document.getElementById('memberCount').textContent = formatNumber(data.totalMembers); + } + if (data.uptime !== undefined) { + document.getElementById('uptime').textContent = formatUptime(data.uptime); + document.getElementById('systemUptime').textContent = formatUptime(data.uptime); + } + if (data.heatMapSize !== undefined) { + document.getElementById('heatMapSize').textContent = data.heatMapSize; + document.getElementById('sentinelHeatMap').textContent = data.heatMapSize; + } + if (data.activeTickets !== undefined) { + document.getElementById('activeTickets').textContent = data.activeTickets; + document.getElementById('ticketBadge').textContent = data.activeTickets; + } + if (data.federationLinks !== undefined) { + document.getElementById('federationCount').textContent = data.federationLinks; + document.getElementById('sentinelFederation').textContent = data.federationLinks; + } + if (data.commandsExecuted !== undefined) { + document.getElementById('commandCount').textContent = data.commandsExecuted; + } + + // Update last update time + document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString(); + } + + function addActivityFromWebSocket(data) { + const activityFeed = document.getElementById('activityFeed'); + if (!activityFeed) return; + + const iconClass = data.type === 'command' ? 'purple' : + data.type === 'moderation' ? 'red' : + data.type === 'member' ? 'green' : 'blue'; + const iconSvg = data.type === 'command' ? '' : + data.type === 'moderation' ? '' : + data.type === 'member' ? '' : + ''; + + const newActivity = document.createElement('div'); + newActivity.className = 'activity-item'; + newActivity.innerHTML = ` +
+ ${iconSvg} +
+
+
${data.message || 'Activity'}
+
Just now
+
+ `; + + // Add animation + newActivity.style.animation = 'slideIn 0.3s ease'; + + // Insert at top + if (activityFeed.firstChild) { + activityFeed.insertBefore(newActivity, activityFeed.firstChild); + } else { + activityFeed.appendChild(newActivity); + } + + // Limit to 20 items + while (activityFeed.children.length > 20) { + activityFeed.removeChild(activityFeed.lastChild); + } + } + + function updateCommandFromWebSocket(data) { + // Update command analytics if on that section + if (data.commandName && data.success !== undefined) { + // Could update command usage chart here + console.log(`[WebSocket] Command executed: ${data.commandName}`); + } + } + + function addThreatFromWebSocket(data) { + const threatFeed = document.getElementById('threatsFeed'); + if (!threatFeed) return; + + const levelClass = data.level === 'critical' ? 'danger' : + data.level === 'high' ? 'warning' : 'info'; + + const newThreat = document.createElement('div'); + newThreat.className = 'activity-item'; + newThreat.innerHTML = ` +
+ + + + +
+
+
${data.message || 'Security alert'}
+
Just now - ${data.level || 'unknown'}
+
+ `; + + newThreat.style.animation = 'slideIn 0.3s ease'; + + if (threatFeed.firstChild) { + threatFeed.insertBefore(newThreat, threatFeed.firstChild); + } else { + threatFeed.appendChild(newThreat); + } + + // Update threat badge + const badge = document.getElementById('threatBadge'); + if (badge) { + badge.textContent = parseInt(badge.textContent || 0) + 1; + } + } + + function updateTicketFromWebSocket(data) { + if (data.action === 'create') { + const badge = document.getElementById('ticketBadge'); + if (badge) { + badge.textContent = parseInt(badge.textContent || 0) + 1; + } + document.getElementById('activeTickets').textContent = + parseInt(document.getElementById('activeTickets').textContent || 0) + 1; + } else if (data.action === 'close') { + const badge = document.getElementById('ticketBadge'); + if (badge) { + const current = parseInt(badge.textContent || 0); + badge.textContent = Math.max(0, current - 1); + } + const activeEl = document.getElementById('activeTickets'); + activeEl.textContent = Math.max(0, parseInt(activeEl.textContent || 0) - 1); + } + } + + function updateConnectionStatus(connected) { + const statusDot = document.getElementById('statusDot'); + const statusText = document.getElementById('statusText'); + + if (connected) { + // Add WebSocket indicator + if (!document.getElementById('wsIndicator')) { + const indicator = document.createElement('span'); + indicator.id = 'wsIndicator'; + indicator.style.cssText = 'margin-left: 8px; font-size: 0.7rem; color: var(--success);'; + indicator.textContent = '(Live)'; + statusText.parentNode.appendChild(indicator); + } + } else { + const indicator = document.getElementById('wsIndicator'); + if (indicator) indicator.remove(); + } + } + // Initialize document.addEventListener('DOMContentLoaded', () => { // Load theme @@ -3511,6 +3949,9 @@ document.documentElement.setAttribute('data-theme', savedTheme); updateThemeIcon(savedTheme); + // Initialize WebSocket connection + initWebSocket(); + // Initial fetch refreshData(); fetchExtendedStatus(); @@ -3518,7 +3959,7 @@ fetchFoundationStats(); fetchIntegrationStatus(); - // Auto-refresh every 30s + // Auto-refresh every 30s (will be reduced when WebSocket connects) refreshInterval = setInterval(refreshData, 30000); });