Add analytics tracking and display for bot activity
Introduces new analytics tracking functions in `bot.js` for command usage, XP distribution, new members, and mod actions. Also adds an `/analytics` API endpoint to serve this data and corresponding CSS styling for the dashboard's analytics section in `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: 324e6a0e-a696-412d-aaf0-df4936017eb3 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
0572e7ad61
commit
cada20b646
3 changed files with 595 additions and 5 deletions
2
.replit
2
.replit
|
|
@ -23,7 +23,7 @@ localPort = 8080
|
|||
externalPort = 8080
|
||||
|
||||
[[ports]]
|
||||
localPort = 44107
|
||||
localPort = 35789
|
||||
externalPort = 3000
|
||||
|
||||
[workflows]
|
||||
|
|
|
|||
|
|
@ -290,6 +290,59 @@ const MAX_ACTIVITY_EVENTS = 100;
|
|||
const threatAlerts = [];
|
||||
const MAX_THREAT_ALERTS = 50;
|
||||
|
||||
// Analytics tracking
|
||||
const analyticsData = {
|
||||
commandUsage: {},
|
||||
xpDistributed: 0,
|
||||
newMembers: 0,
|
||||
modActions: { warnings: 0, kicks: 0, bans: 0, timeouts: 0 },
|
||||
hourlyActivity: Array(24).fill(0),
|
||||
dailyActivity: Array(7).fill(0),
|
||||
lastReset: Date.now(),
|
||||
};
|
||||
|
||||
function trackCommand(commandName) {
|
||||
analyticsData.commandUsage[commandName] = (analyticsData.commandUsage[commandName] || 0) + 1;
|
||||
const hour = new Date().getHours();
|
||||
const day = new Date().getDay();
|
||||
analyticsData.hourlyActivity[hour]++;
|
||||
analyticsData.dailyActivity[day]++;
|
||||
}
|
||||
|
||||
function trackXP(amount) {
|
||||
analyticsData.xpDistributed += amount;
|
||||
}
|
||||
|
||||
function trackNewMember() {
|
||||
analyticsData.newMembers++;
|
||||
}
|
||||
|
||||
function trackModAction(type) {
|
||||
if (analyticsData.modActions[type] !== undefined) {
|
||||
analyticsData.modActions[type]++;
|
||||
}
|
||||
}
|
||||
|
||||
function resetDailyAnalytics() {
|
||||
const now = Date.now();
|
||||
const lastReset = analyticsData.lastReset;
|
||||
const dayMs = 24 * 60 * 60 * 1000;
|
||||
|
||||
if (now - lastReset > dayMs) {
|
||||
analyticsData.commandUsage = {};
|
||||
analyticsData.xpDistributed = 0;
|
||||
analyticsData.newMembers = 0;
|
||||
analyticsData.modActions = { warnings: 0, kicks: 0, bans: 0, timeouts: 0 };
|
||||
analyticsData.lastReset = now;
|
||||
}
|
||||
}
|
||||
|
||||
client.trackCommand = trackCommand;
|
||||
client.trackXP = trackXP;
|
||||
client.trackNewMember = trackNewMember;
|
||||
client.trackModAction = trackModAction;
|
||||
client.analyticsData = analyticsData;
|
||||
|
||||
function addActivity(type, data) {
|
||||
const event = {
|
||||
id: Date.now() + Math.random().toString(36).substr(2, 9),
|
||||
|
|
@ -456,6 +509,9 @@ client.on("interactionCreate", async (interaction) => {
|
|||
await command.execute(interaction, supabase, client);
|
||||
console.log(`[Command] Completed: ${interaction.commandName}`);
|
||||
|
||||
trackCommand(interaction.commandName);
|
||||
resetDailyAnalytics();
|
||||
|
||||
addActivity('command', {
|
||||
command: interaction.commandName,
|
||||
user: interaction.user.tag,
|
||||
|
|
@ -767,6 +823,89 @@ http
|
|||
return;
|
||||
}
|
||||
|
||||
if (req.url === "/analytics") {
|
||||
resetDailyAnalytics();
|
||||
|
||||
const commandsArray = Object.entries(analyticsData.commandUsage)
|
||||
.map(([name, count]) => ({ name, count }))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
const totalCommands = commandsArray.reduce((sum, c) => sum + c.count, 0);
|
||||
const totalModActions = Object.values(analyticsData.modActions).reduce((sum, c) => sum + c, 0);
|
||||
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify({
|
||||
commandsToday: totalCommands,
|
||||
xpDistributed: analyticsData.xpDistributed,
|
||||
newMembers: analyticsData.newMembers,
|
||||
modActionsTotal: totalModActions,
|
||||
modActions: analyticsData.modActions,
|
||||
commandUsage: commandsArray.slice(0, 15),
|
||||
hourlyActivity: analyticsData.hourlyActivity,
|
||||
dailyActivity: analyticsData.dailyActivity,
|
||||
lastReset: new Date(analyticsData.lastReset).toISOString(),
|
||||
timestamp: new Date().toISOString(),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url === "/leaderboard") {
|
||||
if (!supabase) {
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
message: "Supabase not configured",
|
||||
xpLeaders: [],
|
||||
topChatters: [],
|
||||
topMods: [],
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const { data: xpLeaders } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('id, username, avatar_url, xp')
|
||||
.order('xp', { ascending: false })
|
||||
.limit(10);
|
||||
|
||||
const { data: modActors } = await supabase
|
||||
.from('mod_actions')
|
||||
.select('moderator_id, moderator_tag')
|
||||
.limit(100);
|
||||
|
||||
const modCounts = {};
|
||||
(modActors || []).forEach(m => {
|
||||
modCounts[m.moderator_id] = modCounts[m.moderator_id] || { count: 0, tag: m.moderator_tag };
|
||||
modCounts[m.moderator_id].count++;
|
||||
});
|
||||
|
||||
const topMods = Object.entries(modCounts)
|
||||
.map(([id, data]) => ({ id, tag: data.tag, count: data.count }))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 10);
|
||||
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
xpLeaders: (xpLeaders || []).map((u, i) => ({
|
||||
rank: i + 1,
|
||||
username: u.username || 'Unknown',
|
||||
avatarUrl: u.avatar_url,
|
||||
xp: u.xp || 0,
|
||||
level: Math.floor(Math.sqrt((u.xp || 0) / 100)),
|
||||
})),
|
||||
topMods,
|
||||
}));
|
||||
} catch (error) {
|
||||
res.writeHead(500);
|
||||
res.end(JSON.stringify({ success: false, error: error.message }));
|
||||
}
|
||||
})();
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url === "/bot-status") {
|
||||
if (!checkAdminAuth(req)) {
|
||||
res.writeHead(401);
|
||||
|
|
|
|||
|
|
@ -608,6 +608,238 @@
|
|||
.badge-warning { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
|
||||
.badge-danger { background: rgba(239, 68, 68, 0.2); color: var(--danger); }
|
||||
.badge-info { background: rgba(59, 130, 246, 0.2); color: var(--info); }
|
||||
|
||||
/* Analytics Charts */
|
||||
.chart-bars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.chart-bar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.chart-bar-label {
|
||||
width: 80px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chart-bar-track {
|
||||
flex: 1;
|
||||
height: 24px;
|
||||
background: var(--bg-card);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chart-bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--accent), #a855f7);
|
||||
border-radius: 4px;
|
||||
transition: width 0.5s ease;
|
||||
min-width: 2px;
|
||||
}
|
||||
|
||||
.chart-bar-value {
|
||||
width: 40px;
|
||||
text-align: right;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Mod Breakdown */
|
||||
.mod-stat-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.mod-label {
|
||||
width: 80px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.mod-bar-container {
|
||||
flex: 1;
|
||||
height: 12px;
|
||||
background: var(--bg-card);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mod-bar {
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.mod-bar.warning { background: var(--warning); }
|
||||
.mod-bar.danger { background: var(--danger); }
|
||||
.mod-bar.purple { background: var(--accent); }
|
||||
.mod-bar.info { background: var(--info); }
|
||||
|
||||
.mod-count {
|
||||
width: 40px;
|
||||
text-align: right;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Heatmap */
|
||||
.heatmap-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.heatmap-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.heatmap-labels {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-muted);
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
.heatmap-grid {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.heatmap-cell {
|
||||
flex: 1;
|
||||
aspect-ratio: 1;
|
||||
min-width: 20px;
|
||||
max-width: 40px;
|
||||
background: var(--accent);
|
||||
border-radius: 3px;
|
||||
opacity: 0.1;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.heatmap-legend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.legend-boxes {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.legend-box {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: var(--accent);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Weekly Chart */
|
||||
.weekly-chart {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: flex-end;
|
||||
height: 150px;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.week-bar-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.week-bar {
|
||||
width: 40px;
|
||||
max-width: 60px;
|
||||
background: linear-gradient(180deg, var(--accent), #a855f7);
|
||||
border-radius: 4px 4px 0 0;
|
||||
transition: height 0.5s ease;
|
||||
min-height: 4px;
|
||||
}
|
||||
|
||||
.week-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Leaderboard */
|
||||
.leaderboard-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.leaderboard-item:last-child { border-bottom: none; }
|
||||
|
||||
.leaderboard-rank {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
font-weight: 700;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.leaderboard-rank.gold { background: rgba(255, 193, 7, 0.2); color: #ffc107; }
|
||||
.leaderboard-rank.silver { background: rgba(192, 192, 192, 0.2); color: #c0c0c0; }
|
||||
.leaderboard-rank.bronze { background: rgba(205, 127, 50, 0.2); color: #cd7f32; }
|
||||
.leaderboard-rank.default { background: var(--bg-card); color: var(--text-muted); }
|
||||
|
||||
.leaderboard-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.leaderboard-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||||
|
||||
.leaderboard-info { flex: 1; }
|
||||
|
||||
.leaderboard-name { font-weight: 500; }
|
||||
|
||||
.leaderboard-level {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.leaderboard-xp {
|
||||
text-align: right;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Tooltip */
|
||||
[data-tooltip] {
|
||||
|
|
@ -950,13 +1182,129 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<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"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
|
||||
Command Usage
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="commandUsageChart" style="min-height: 200px;">
|
||||
<div class="chart-bars" id="commandBars"></div>
|
||||
</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>
|
||||
Moderation Breakdown
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="modBreakdown">
|
||||
<div class="mod-stat-row">
|
||||
<span class="mod-label">Warnings</span>
|
||||
<div class="mod-bar-container">
|
||||
<div class="mod-bar warning" id="warnBar" style="width: 0%"></div>
|
||||
</div>
|
||||
<span class="mod-count" id="warnCount">0</span>
|
||||
</div>
|
||||
<div class="mod-stat-row">
|
||||
<span class="mod-label">Kicks</span>
|
||||
<div class="mod-bar-container">
|
||||
<div class="mod-bar purple" id="kickBar" style="width: 0%"></div>
|
||||
</div>
|
||||
<span class="mod-count" id="kickCountMod">0</span>
|
||||
</div>
|
||||
<div class="mod-stat-row">
|
||||
<span class="mod-label">Bans</span>
|
||||
<div class="mod-bar-container">
|
||||
<div class="mod-bar danger" id="banBar" style="width: 0%"></div>
|
||||
</div>
|
||||
<span class="mod-count" id="banCountMod">0</span>
|
||||
</div>
|
||||
<div class="mod-stat-row">
|
||||
<span class="mod-label">Timeouts</span>
|
||||
<div class="mod-bar-container">
|
||||
<div class="mod-bar info" id="timeoutBar" style="width: 0%"></div>
|
||||
</div>
|
||||
<span class="mod-count" id="timeoutCountMod">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Command Usage</h3>
|
||||
<h3 class="card-title">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
|
||||
Activity Heatmap
|
||||
</h3>
|
||||
<span class="badge badge-info">Last 24 hours</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="empty-state">
|
||||
<p>Analytics data coming soon</p>
|
||||
<div class="heatmap-container">
|
||||
<div class="heatmap-row">
|
||||
<div class="heatmap-labels" id="heatmapLabels"></div>
|
||||
<div class="heatmap-grid" id="heatmapGrid"></div>
|
||||
</div>
|
||||
<div class="heatmap-legend">
|
||||
<span>Less</span>
|
||||
<div class="legend-boxes">
|
||||
<div class="legend-box" style="opacity: 0.2"></div>
|
||||
<div class="legend-box" style="opacity: 0.4"></div>
|
||||
<div class="legend-box" style="opacity: 0.6"></div>
|
||||
<div class="legend-box" style="opacity: 0.8"></div>
|
||||
<div class="legend-box" style="opacity: 1"></div>
|
||||
</div>
|
||||
<span>More</span>
|
||||
</div>
|
||||
</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"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
|
||||
Weekly Activity
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="weekly-chart" id="weeklyChart">
|
||||
<div class="week-bar-wrapper">
|
||||
<div class="week-bar" id="bar-sun" style="height: 10%"></div>
|
||||
<span class="week-label">Sun</span>
|
||||
</div>
|
||||
<div class="week-bar-wrapper">
|
||||
<div class="week-bar" id="bar-mon" style="height: 10%"></div>
|
||||
<span class="week-label">Mon</span>
|
||||
</div>
|
||||
<div class="week-bar-wrapper">
|
||||
<div class="week-bar" id="bar-tue" style="height: 10%"></div>
|
||||
<span class="week-label">Tue</span>
|
||||
</div>
|
||||
<div class="week-bar-wrapper">
|
||||
<div class="week-bar" id="bar-wed" style="height: 10%"></div>
|
||||
<span class="week-label">Wed</span>
|
||||
</div>
|
||||
<div class="week-bar-wrapper">
|
||||
<div class="week-bar" id="bar-thu" style="height: 10%"></div>
|
||||
<span class="week-label">Thu</span>
|
||||
</div>
|
||||
<div class="week-bar-wrapper">
|
||||
<div class="week-bar" id="bar-fri" style="height: 10%"></div>
|
||||
<span class="week-label">Fri</span>
|
||||
</div>
|
||||
<div class="week-bar-wrapper">
|
||||
<div class="week-bar" id="bar-sat" style="height: 10%"></div>
|
||||
<span class="week-label">Sat</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1512,6 +1860,107 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Fetch analytics
|
||||
async function fetchAnalytics() {
|
||||
try {
|
||||
const response = await fetch('/analytics');
|
||||
const data = await response.json();
|
||||
|
||||
document.getElementById('commandsToday').textContent = data.commandsToday || 0;
|
||||
document.getElementById('xpToday').textContent = formatNumber(data.xpDistributed || 0);
|
||||
document.getElementById('newMembers').textContent = data.newMembers || 0;
|
||||
document.getElementById('modActions').textContent = data.modActionsTotal || 0;
|
||||
|
||||
const mods = data.modActions || {};
|
||||
const maxMod = Math.max(mods.warnings || 0, mods.kicks || 0, mods.bans || 0, mods.timeouts || 0, 1);
|
||||
|
||||
document.getElementById('warnCount').textContent = mods.warnings || 0;
|
||||
document.getElementById('kickCountMod').textContent = mods.kicks || 0;
|
||||
document.getElementById('banCountMod').textContent = mods.bans || 0;
|
||||
document.getElementById('timeoutCountMod').textContent = mods.timeouts || 0;
|
||||
|
||||
document.getElementById('warnBar').style.width = `${((mods.warnings || 0) / maxMod) * 100}%`;
|
||||
document.getElementById('kickBar').style.width = `${((mods.kicks || 0) / maxMod) * 100}%`;
|
||||
document.getElementById('banBar').style.width = `${((mods.bans || 0) / maxMod) * 100}%`;
|
||||
document.getElementById('timeoutBar').style.width = `${((mods.timeouts || 0) / maxMod) * 100}%`;
|
||||
|
||||
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 => `
|
||||
<div class="chart-bar-row">
|
||||
<span class="chart-bar-label">/${cmd.name}</span>
|
||||
<div class="chart-bar-track">
|
||||
<div class="chart-bar-fill" style="width: ${(cmd.count / maxCmd) * 100}%"></div>
|
||||
</div>
|
||||
<span class="chart-bar-value">${cmd.count}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
commandBars.innerHTML = '<div class="empty-state" style="padding: 1rem;"><p>No command usage yet</p></div>';
|
||||
}
|
||||
|
||||
const heatmapGrid = document.getElementById('heatmapGrid');
|
||||
if (data.hourlyActivity && data.hourlyActivity.length === 24) {
|
||||
const maxHour = Math.max(...data.hourlyActivity, 1);
|
||||
heatmapGrid.innerHTML = data.hourlyActivity.map((count, hour) => {
|
||||
const opacity = 0.1 + (count / maxHour) * 0.9;
|
||||
return `<div class="heatmap-cell" style="opacity: ${opacity}" title="${hour}:00 - ${count} events"></div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
if (data.dailyActivity && data.dailyActivity.length === 7) {
|
||||
const maxDay = Math.max(...data.dailyActivity, 1);
|
||||
const days = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
||||
days.forEach((day, i) => {
|
||||
const height = 10 + (data.dailyActivity[i] / maxDay) * 90;
|
||||
const bar = document.getElementById(`bar-${day}`);
|
||||
if (bar) bar.style.height = `${height}%`;
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch analytics:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch leaderboard
|
||||
async function fetchLeaderboard() {
|
||||
try {
|
||||
const response = await fetch('/leaderboard');
|
||||
const data = await response.json();
|
||||
|
||||
const leaderboardContent = document.getElementById('leaderboardContent');
|
||||
|
||||
if (data.success && data.xpLeaders && data.xpLeaders.length > 0) {
|
||||
leaderboardContent.innerHTML = data.xpLeaders.map(user => {
|
||||
const rankClass = user.rank === 1 ? 'gold' : user.rank === 2 ? 'silver' : user.rank === 3 ? 'bronze' : 'default';
|
||||
const initial = (user.username || 'U').charAt(0).toUpperCase();
|
||||
return `
|
||||
<div class="leaderboard-item">
|
||||
<div class="leaderboard-rank ${rankClass}">${user.rank}</div>
|
||||
<div class="leaderboard-avatar">${user.avatarUrl ? `<img src="${user.avatarUrl}" alt="">` : initial}</div>
|
||||
<div class="leaderboard-info">
|
||||
<div class="leaderboard-name">${user.username || 'Unknown'}</div>
|
||||
<div class="leaderboard-level">Level ${user.level}</div>
|
||||
</div>
|
||||
<div class="leaderboard-xp">${formatNumber(user.xp)} XP</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
} else {
|
||||
leaderboardContent.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<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>
|
||||
<p>No leaderboard data available</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch leaderboard:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch and update data
|
||||
async function refreshData() {
|
||||
try {
|
||||
|
|
@ -1594,7 +2043,9 @@
|
|||
fetchActivityFeed(),
|
||||
fetchTickets(),
|
||||
fetchThreats(),
|
||||
fetchSystemInfo()
|
||||
fetchSystemInfo(),
|
||||
fetchAnalytics(),
|
||||
fetchLeaderboard()
|
||||
]);
|
||||
|
||||
} catch (error) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue