@zumito-team/analytics-module 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +33 -4
- package/dist/routes/AdminAnalytics.d.ts +1 -3
- package/dist/routes/AdminAnalytics.js +2 -23
- package/dist/routes/AdminAnalyticsCommands.d.ts +7 -0
- package/dist/routes/AdminAnalyticsCommands.js +34 -0
- package/dist/routes/AdminAnalyticsGrowth.d.ts +7 -0
- package/dist/routes/AdminAnalyticsGrowth.js +34 -0
- package/dist/routes/AdminAnalyticsMessages.d.ts +7 -0
- package/dist/routes/AdminAnalyticsMessages.js +32 -0
- package/dist/routes/UserPanelAnalytics.js +1 -17
- package/dist/routes/UserPanelAnalyticsCommands.d.ts +8 -0
- package/dist/routes/UserPanelAnalyticsCommands.js +48 -0
- package/dist/routes/UserPanelAnalyticsMessages.d.ts +8 -0
- package/dist/routes/UserPanelAnalyticsMessages.js +51 -0
- package/dist/routes/UserPanelAnalyticsVoice.d.ts +8 -0
- package/dist/routes/UserPanelAnalyticsVoice.js +53 -0
- package/dist/services/AnalyticsCollector.js +1 -1
- package/dist/views/admin-analytics-commands.ejs +55 -0
- package/dist/views/admin-analytics-growth.ejs +46 -0
- package/dist/views/admin-analytics-messages.ejs +30 -0
- package/dist/views/admin-analytics.ejs +38 -132
- package/dist/views/user-analytics-commands.ejs +55 -0
- package/dist/views/user-analytics-messages.ejs +31 -0
- package/dist/views/user-analytics-voice.ejs +47 -0
- package/dist/views/user-analytics.ejs +25 -166
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -24,7 +24,10 @@ export class AnalyticsModule extends Module {
|
|
|
24
24
|
{
|
|
25
25
|
label: 'Analiticas',
|
|
26
26
|
items: [
|
|
27
|
-
{ label: '
|
|
27
|
+
{ label: 'Resumen', url: '/admin/analytics' },
|
|
28
|
+
{ label: 'Mensajes', url: '/admin/analytics/messages' },
|
|
29
|
+
{ label: 'Comandos', url: '/admin/analytics/commands' },
|
|
30
|
+
{ label: 'Crecimiento', url: '/admin/analytics/growth' },
|
|
28
31
|
],
|
|
29
32
|
},
|
|
30
33
|
],
|
|
@@ -37,9 +40,35 @@ export class AnalyticsModule extends Module {
|
|
|
37
40
|
try {
|
|
38
41
|
const { UserPanelNavigationService } = await import('@zumito-team/user-panel-module/services/UserPanelNavigationService');
|
|
39
42
|
const nav = ServiceContainer.getService(UserPanelNavigationService);
|
|
40
|
-
nav.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
nav.registerItem({
|
|
44
|
+
id: 'analytics',
|
|
45
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-discord-white/60 group-hover:text-white" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M7 16l4-8 4 5 4-3"/></svg>`,
|
|
46
|
+
label: 'analytics.sidebarTitle',
|
|
47
|
+
url: '/panel/:guildId/analytics',
|
|
48
|
+
order: 3,
|
|
49
|
+
category: 'general',
|
|
50
|
+
sidebar: {
|
|
51
|
+
showDropdown: false,
|
|
52
|
+
sections: [
|
|
53
|
+
{
|
|
54
|
+
id: 'analytics-overview',
|
|
55
|
+
label: 'analytics.overview',
|
|
56
|
+
items: [
|
|
57
|
+
{ label: 'analytics.overview', url: '/panel/:guildId/analytics' },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'analytics-details',
|
|
62
|
+
label: 'analytics.detailedStats',
|
|
63
|
+
items: [
|
|
64
|
+
{ label: 'analytics.messages', url: '/panel/:guildId/analytics/messages' },
|
|
65
|
+
{ label: 'analytics.voice', url: '/panel/:guildId/analytics/voice' },
|
|
66
|
+
{ label: 'analytics.commands', url: '/panel/:guildId/analytics/commands' },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
});
|
|
43
72
|
}
|
|
44
73
|
catch (e) {
|
|
45
74
|
console.warn('[AnalyticsModule] User panel not available, skipping user panel integration');
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { Route, RouteMethod } from 'zumito-framework';
|
|
2
|
-
import { Client } from 'zumito-framework/discord';
|
|
3
2
|
import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
|
|
4
3
|
export declare class AdminAnalytics extends Route {
|
|
5
4
|
private collector;
|
|
6
|
-
private client;
|
|
7
5
|
method: RouteMethod;
|
|
8
6
|
path: string;
|
|
9
|
-
constructor(collector?: AnalyticsCollector
|
|
7
|
+
constructor(collector?: AnalyticsCollector);
|
|
10
8
|
execute(req: any, res: any): Promise<void>;
|
|
11
9
|
}
|
|
@@ -2,14 +2,12 @@ import { Route, RouteMethod, ServiceContainer } from 'zumito-framework';
|
|
|
2
2
|
import path, { dirname } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import ejs from 'ejs';
|
|
5
|
-
import { Client } from 'zumito-framework/discord';
|
|
6
5
|
import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
|
|
7
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
7
|
export class AdminAnalytics extends Route {
|
|
9
|
-
constructor(collector = ServiceContainer.getService(AnalyticsCollector)
|
|
8
|
+
constructor(collector = ServiceContainer.getService(AnalyticsCollector)) {
|
|
10
9
|
super();
|
|
11
10
|
this.collector = collector;
|
|
12
|
-
this.client = client;
|
|
13
11
|
this.method = RouteMethod.get;
|
|
14
12
|
this.path = '/admin/analytics';
|
|
15
13
|
}
|
|
@@ -22,26 +20,7 @@ export class AdminAnalytics extends Route {
|
|
|
22
20
|
const summary = await this.collector.getGlobalStatsSummary(daysBack);
|
|
23
21
|
const guildGrowth = await this.collector.getGuildGrowth(daysBack);
|
|
24
22
|
const messagesPerDay = await this.collector.getMessagesPerDay(daysBack);
|
|
25
|
-
const
|
|
26
|
-
const topCommands = await this.collector.getTopCommands(null, daysBack);
|
|
27
|
-
const slowestCommands = await this.collector.getSlowestCommands(null, daysBack);
|
|
28
|
-
const chartData = {
|
|
29
|
-
guildGrowth,
|
|
30
|
-
messagesPerDay,
|
|
31
|
-
commandsPerDay,
|
|
32
|
-
topCommands,
|
|
33
|
-
slowestCommands,
|
|
34
|
-
};
|
|
35
|
-
const { TranslationManager } = await import('zumito-framework');
|
|
36
|
-
const tm = ServiceContainer.getService(TranslationManager);
|
|
37
|
-
const t = (key, params) => tm.get(key, 'en', params);
|
|
38
|
-
const content = await ejs.renderFile(path.resolve(__dirname, '../views/admin-analytics.ejs'), {
|
|
39
|
-
summary,
|
|
40
|
-
chartData,
|
|
41
|
-
slowestCommands: slowestCommands.length > 0 ? slowestCommands : undefined,
|
|
42
|
-
t,
|
|
43
|
-
daysBack,
|
|
44
|
-
});
|
|
23
|
+
const content = await ejs.renderFile(path.resolve(__dirname, '../views/admin-analytics.ejs'), { summary, guildGrowth, messagesPerDay });
|
|
45
24
|
const { AdminViewService } = await import('@zumito-team/admin-module/services/AdminViewService.js');
|
|
46
25
|
const view = ServiceContainer.getService(AdminViewService);
|
|
47
26
|
const html = await view.render({
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Route, RouteMethod, ServiceContainer } from 'zumito-framework';
|
|
2
|
+
import path, { dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import ejs from 'ejs';
|
|
5
|
+
import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
export class AdminAnalyticsCommands extends Route {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(...arguments);
|
|
10
|
+
this.method = RouteMethod.get;
|
|
11
|
+
this.path = '/admin/analytics/commands';
|
|
12
|
+
this.collector = ServiceContainer.getService(AnalyticsCollector);
|
|
13
|
+
}
|
|
14
|
+
async execute(req, res) {
|
|
15
|
+
const { AdminAuthService } = await import('@zumito-team/admin-module/services/AdminAuthService.js');
|
|
16
|
+
const auth = await ServiceContainer.getService(AdminAuthService).isLoginValid(req);
|
|
17
|
+
if (!auth?.isValid)
|
|
18
|
+
return res.redirect('/admin/login');
|
|
19
|
+
const daysBack = parseInt(req.query.days) || 7;
|
|
20
|
+
const commandsPerDay = await this.collector.getCommandsPerDay(null, daysBack);
|
|
21
|
+
const topCommands = await this.collector.getTopCommands(null, daysBack, 15);
|
|
22
|
+
const slowestCommands = await this.collector.getSlowestCommands(null, daysBack, 10);
|
|
23
|
+
const content = await ejs.renderFile(path.resolve(__dirname, '../views/admin-analytics-commands.ejs'), { commandsPerDay, topCommands, slowestCommands, daysBack });
|
|
24
|
+
const { AdminViewService } = await import('@zumito-team/admin-module/services/AdminViewService.js');
|
|
25
|
+
const view = ServiceContainer.getService(AdminViewService);
|
|
26
|
+
const html = await view.render({
|
|
27
|
+
title: 'Analiticas - Comandos',
|
|
28
|
+
content,
|
|
29
|
+
reqPath: this.path,
|
|
30
|
+
user: { name: 'Admin' },
|
|
31
|
+
});
|
|
32
|
+
res.send(html);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Route, RouteMethod, ServiceContainer } from 'zumito-framework';
|
|
2
|
+
import path, { dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import ejs from 'ejs';
|
|
5
|
+
import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
export class AdminAnalyticsGrowth extends Route {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(...arguments);
|
|
10
|
+
this.method = RouteMethod.get;
|
|
11
|
+
this.path = '/admin/analytics/growth';
|
|
12
|
+
this.collector = ServiceContainer.getService(AnalyticsCollector);
|
|
13
|
+
}
|
|
14
|
+
async execute(req, res) {
|
|
15
|
+
const { AdminAuthService } = await import('@zumito-team/admin-module/services/AdminAuthService.js');
|
|
16
|
+
const auth = await ServiceContainer.getService(AdminAuthService).isLoginValid(req);
|
|
17
|
+
if (!auth?.isValid)
|
|
18
|
+
return res.redirect('/admin/login');
|
|
19
|
+
const daysBack = parseInt(req.query.days) || 30;
|
|
20
|
+
const guildGrowth = await this.collector.getGuildGrowth(daysBack);
|
|
21
|
+
const summary = await this.collector.getGlobalStatsSummary(daysBack);
|
|
22
|
+
const totalGuilds = Math.max(summary.totalGuilds, guildGrowth.length > 0 ? guildGrowth[guildGrowth.length - 1].guildCount : 0);
|
|
23
|
+
const content = await ejs.renderFile(path.resolve(__dirname, '../views/admin-analytics-growth.ejs'), { guildGrowth, totalGuilds, summary, daysBack });
|
|
24
|
+
const { AdminViewService } = await import('@zumito-team/admin-module/services/AdminViewService.js');
|
|
25
|
+
const view = ServiceContainer.getService(AdminViewService);
|
|
26
|
+
const html = await view.render({
|
|
27
|
+
title: 'Analiticas - Crecimiento',
|
|
28
|
+
content,
|
|
29
|
+
reqPath: this.path,
|
|
30
|
+
user: { name: 'Admin' },
|
|
31
|
+
});
|
|
32
|
+
res.send(html);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Route, RouteMethod, ServiceContainer } from 'zumito-framework';
|
|
2
|
+
import path, { dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import ejs from 'ejs';
|
|
5
|
+
import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
export class AdminAnalyticsMessages extends Route {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(...arguments);
|
|
10
|
+
this.method = RouteMethod.get;
|
|
11
|
+
this.path = '/admin/analytics/messages';
|
|
12
|
+
this.collector = ServiceContainer.getService(AnalyticsCollector);
|
|
13
|
+
}
|
|
14
|
+
async execute(req, res) {
|
|
15
|
+
const { AdminAuthService } = await import('@zumito-team/admin-module/services/AdminAuthService.js');
|
|
16
|
+
const auth = await ServiceContainer.getService(AdminAuthService).isLoginValid(req);
|
|
17
|
+
if (!auth?.isValid)
|
|
18
|
+
return res.redirect('/admin/login');
|
|
19
|
+
const daysBack = parseInt(req.query.days) || 7;
|
|
20
|
+
const messagesPerDay = await this.collector.getMessagesPerDay(daysBack);
|
|
21
|
+
const content = await ejs.renderFile(path.resolve(__dirname, '../views/admin-analytics-messages.ejs'), { messagesPerDay, daysBack });
|
|
22
|
+
const { AdminViewService } = await import('@zumito-team/admin-module/services/AdminViewService.js');
|
|
23
|
+
const view = ServiceContainer.getService(AdminViewService);
|
|
24
|
+
const html = await view.render({
|
|
25
|
+
title: 'Analiticas - Mensajes',
|
|
26
|
+
content,
|
|
27
|
+
reqPath: this.path,
|
|
28
|
+
user: { name: 'Admin' },
|
|
29
|
+
});
|
|
30
|
+
res.send(html);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -57,23 +57,7 @@ export class UserPanelAnalytics extends Route {
|
|
|
57
57
|
leavesPerDay.push({ date: s.date, count: s.leave_count });
|
|
58
58
|
voicePerDay.push({ date: s.date, count: s.voice_minutes });
|
|
59
59
|
}
|
|
60
|
-
const
|
|
61
|
-
const topCommands = await this.collector.getTopCommands(guildId, daysBack);
|
|
62
|
-
const slowestCommands = await this.collector.getSlowestCommands(guildId, daysBack);
|
|
63
|
-
const channelVoice = await this.collector.getVoiceChannelStats(guildId, daysBack);
|
|
64
|
-
const chartData = {
|
|
65
|
-
messagesPerDay, joinsPerDay, leavesPerDay, voicePerDay,
|
|
66
|
-
commandsPerDay, topCommands, slowestCommands, channelVoice,
|
|
67
|
-
};
|
|
68
|
-
const config = await this.collector.getConfig(guildId);
|
|
69
|
-
const content = await ejs.renderFile(path.resolve(__dirname, '../views/user-analytics.ejs'), {
|
|
70
|
-
guildSummary,
|
|
71
|
-
chartData,
|
|
72
|
-
slowestCommands: (config.track_command_performance && slowestCommands.length > 0) ? slowestCommands : undefined,
|
|
73
|
-
channelVoice: (config.track_per_channel_voice && channelVoice.length > 0) ? channelVoice : undefined,
|
|
74
|
-
t,
|
|
75
|
-
daysBack,
|
|
76
|
-
});
|
|
60
|
+
const content = await ejs.renderFile(path.resolve(__dirname, '../views/user-analytics.ejs'), { guildSummary, messagesPerDay, joinsPerDay, leavesPerDay, t });
|
|
77
61
|
const view = ServiceContainer.getService(UserPanelViewService);
|
|
78
62
|
const html = await view.render({ content, reqPath: req.path, req, res });
|
|
79
63
|
res.send(html);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Route, RouteMethod, ServiceContainer } from 'zumito-framework';
|
|
2
|
+
import path, { dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import ejs from 'ejs';
|
|
5
|
+
import { Client, PermissionFlagsBits } from 'zumito-framework/discord';
|
|
6
|
+
import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
export class UserPanelAnalyticsCommands extends Route {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(...arguments);
|
|
11
|
+
this.method = RouteMethod.get;
|
|
12
|
+
this.path = '/panel/:guildId/analytics/commands';
|
|
13
|
+
this.collector = ServiceContainer.getService(AnalyticsCollector);
|
|
14
|
+
this.client = ServiceContainer.getService(Client);
|
|
15
|
+
}
|
|
16
|
+
async execute(req, res) {
|
|
17
|
+
const { UserPanelAuthService } = await import('@zumito-team/user-panel-module/services/UserPanelAuthService');
|
|
18
|
+
const { UserPanelViewService } = await import('@zumito-team/user-panel-module/services/UserPanelViewService');
|
|
19
|
+
const { UserPanelLanguageManager } = await import('@zumito-team/user-panel-module/services/UserPanelLanguageManager');
|
|
20
|
+
const authService = ServiceContainer.getService(UserPanelAuthService);
|
|
21
|
+
const authData = await authService.isLoginValid(req);
|
|
22
|
+
if (!authData.isValid)
|
|
23
|
+
return res.redirect('/panel/login');
|
|
24
|
+
const userId = authData.data.discordUserData.id;
|
|
25
|
+
const guildId = req.params.guildId;
|
|
26
|
+
const guild = this.client.guilds.cache.get(guildId);
|
|
27
|
+
if (!guild)
|
|
28
|
+
return res.status(404).send('Server not found');
|
|
29
|
+
let member = guild.members.cache.get(userId);
|
|
30
|
+
if (!member)
|
|
31
|
+
member = await guild.members.fetch(userId).catch(() => null);
|
|
32
|
+
if (!member || !(member.permissions.has(PermissionFlagsBits.Administrator) ||
|
|
33
|
+
member.permissions.has(PermissionFlagsBits.ManageGuild) || guild.ownerId === userId)) {
|
|
34
|
+
return res.status(403).send('No tienes permisos en este servidor');
|
|
35
|
+
}
|
|
36
|
+
const daysBack = parseInt(req.query.days) || 7;
|
|
37
|
+
const langMgr = ServiceContainer.getService(UserPanelLanguageManager);
|
|
38
|
+
const { t } = langMgr.getLanguageVariables(req, res);
|
|
39
|
+
const commandsPerDay = await this.collector.getCommandsPerDay(guildId, daysBack);
|
|
40
|
+
const topCommands = await this.collector.getTopCommands(guildId, daysBack, 15);
|
|
41
|
+
const slowestCommands = await this.collector.getSlowestCommands(guildId, daysBack, 10);
|
|
42
|
+
const config = await this.collector.getConfig(guildId);
|
|
43
|
+
const content = await ejs.renderFile(path.resolve(__dirname, '../views/user-analytics-commands.ejs'), { commandsPerDay, topCommands, slowestCommands: config.track_command_performance ? slowestCommands : null, t, daysBack });
|
|
44
|
+
const view = ServiceContainer.getService(UserPanelViewService);
|
|
45
|
+
const html = await view.render({ content, reqPath: req.path, req, res });
|
|
46
|
+
res.send(html);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Route, RouteMethod, ServiceContainer } from 'zumito-framework';
|
|
2
|
+
import path, { dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import ejs from 'ejs';
|
|
5
|
+
import { Client, PermissionFlagsBits } from 'zumito-framework/discord';
|
|
6
|
+
import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
export class UserPanelAnalyticsMessages extends Route {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(...arguments);
|
|
11
|
+
this.method = RouteMethod.get;
|
|
12
|
+
this.path = '/panel/:guildId/analytics/messages';
|
|
13
|
+
this.collector = ServiceContainer.getService(AnalyticsCollector);
|
|
14
|
+
this.client = ServiceContainer.getService(Client);
|
|
15
|
+
}
|
|
16
|
+
async execute(req, res) {
|
|
17
|
+
const { UserPanelAuthService } = await import('@zumito-team/user-panel-module/services/UserPanelAuthService');
|
|
18
|
+
const { UserPanelViewService } = await import('@zumito-team/user-panel-module/services/UserPanelViewService');
|
|
19
|
+
const { UserPanelLanguageManager } = await import('@zumito-team/user-panel-module/services/UserPanelLanguageManager');
|
|
20
|
+
const authService = ServiceContainer.getService(UserPanelAuthService);
|
|
21
|
+
const authData = await authService.isLoginValid(req);
|
|
22
|
+
if (!authData.isValid)
|
|
23
|
+
return res.redirect('/panel/login');
|
|
24
|
+
const userId = authData.data.discordUserData.id;
|
|
25
|
+
const guildId = req.params.guildId;
|
|
26
|
+
const guild = this.client.guilds.cache.get(guildId);
|
|
27
|
+
if (!guild)
|
|
28
|
+
return res.status(404).send('Server not found');
|
|
29
|
+
let member = guild.members.cache.get(userId);
|
|
30
|
+
if (!member)
|
|
31
|
+
member = await guild.members.fetch(userId).catch(() => null);
|
|
32
|
+
if (!member || !(member.permissions.has(PermissionFlagsBits.Administrator) ||
|
|
33
|
+
member.permissions.has(PermissionFlagsBits.ManageGuild) || guild.ownerId === userId)) {
|
|
34
|
+
return res.status(403).send('No tienes permisos en este servidor');
|
|
35
|
+
}
|
|
36
|
+
const daysBack = parseInt(req.query.days) || 7;
|
|
37
|
+
const langMgr = ServiceContainer.getService(UserPanelLanguageManager);
|
|
38
|
+
const { t } = langMgr.getLanguageVariables(req, res);
|
|
39
|
+
const stats = await this.collector.getGuildStats(guildId, daysBack);
|
|
40
|
+
let totalMessages = 0;
|
|
41
|
+
const messagesPerDay = [];
|
|
42
|
+
for (const s of stats) {
|
|
43
|
+
totalMessages += s.message_count;
|
|
44
|
+
messagesPerDay.push({ date: s.date, count: s.message_count });
|
|
45
|
+
}
|
|
46
|
+
const content = await ejs.renderFile(path.resolve(__dirname, '../views/user-analytics-messages.ejs'), { totalMessages, messagesPerDay, t, daysBack });
|
|
47
|
+
const view = ServiceContainer.getService(UserPanelViewService);
|
|
48
|
+
const html = await view.render({ content, reqPath: req.path, req, res });
|
|
49
|
+
res.send(html);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Route, RouteMethod, ServiceContainer } from 'zumito-framework';
|
|
2
|
+
import path, { dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import ejs from 'ejs';
|
|
5
|
+
import { Client, PermissionFlagsBits } from 'zumito-framework/discord';
|
|
6
|
+
import { AnalyticsCollector } from '../services/AnalyticsCollector.js';
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
export class UserPanelAnalyticsVoice extends Route {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(...arguments);
|
|
11
|
+
this.method = RouteMethod.get;
|
|
12
|
+
this.path = '/panel/:guildId/analytics/voice';
|
|
13
|
+
this.collector = ServiceContainer.getService(AnalyticsCollector);
|
|
14
|
+
this.client = ServiceContainer.getService(Client);
|
|
15
|
+
}
|
|
16
|
+
async execute(req, res) {
|
|
17
|
+
const { UserPanelAuthService } = await import('@zumito-team/user-panel-module/services/UserPanelAuthService');
|
|
18
|
+
const { UserPanelViewService } = await import('@zumito-team/user-panel-module/services/UserPanelViewService');
|
|
19
|
+
const { UserPanelLanguageManager } = await import('@zumito-team/user-panel-module/services/UserPanelLanguageManager');
|
|
20
|
+
const authService = ServiceContainer.getService(UserPanelAuthService);
|
|
21
|
+
const authData = await authService.isLoginValid(req);
|
|
22
|
+
if (!authData.isValid)
|
|
23
|
+
return res.redirect('/panel/login');
|
|
24
|
+
const userId = authData.data.discordUserData.id;
|
|
25
|
+
const guildId = req.params.guildId;
|
|
26
|
+
const guild = this.client.guilds.cache.get(guildId);
|
|
27
|
+
if (!guild)
|
|
28
|
+
return res.status(404).send('Server not found');
|
|
29
|
+
let member = guild.members.cache.get(userId);
|
|
30
|
+
if (!member)
|
|
31
|
+
member = await guild.members.fetch(userId).catch(() => null);
|
|
32
|
+
if (!member || !(member.permissions.has(PermissionFlagsBits.Administrator) ||
|
|
33
|
+
member.permissions.has(PermissionFlagsBits.ManageGuild) || guild.ownerId === userId)) {
|
|
34
|
+
return res.status(403).send('No tienes permisos en este servidor');
|
|
35
|
+
}
|
|
36
|
+
const daysBack = parseInt(req.query.days) || 7;
|
|
37
|
+
const langMgr = ServiceContainer.getService(UserPanelLanguageManager);
|
|
38
|
+
const { t } = langMgr.getLanguageVariables(req, res);
|
|
39
|
+
const stats = await this.collector.getGuildStats(guildId, daysBack);
|
|
40
|
+
const channelVoice = await this.collector.getVoiceChannelStats(guildId, daysBack);
|
|
41
|
+
let totalVoice = 0;
|
|
42
|
+
const voicePerDay = [];
|
|
43
|
+
for (const s of stats) {
|
|
44
|
+
totalVoice += s.voice_minutes;
|
|
45
|
+
voicePerDay.push({ date: s.date, count: s.voice_minutes });
|
|
46
|
+
}
|
|
47
|
+
const config = await this.collector.getConfig(guildId);
|
|
48
|
+
const content = await ejs.renderFile(path.resolve(__dirname, '../views/user-analytics-voice.ejs'), { totalVoice, voicePerDay, channelVoice: config.track_per_channel_voice ? channelVoice : null, t, daysBack });
|
|
49
|
+
const view = ServiceContainer.getService(UserPanelViewService);
|
|
50
|
+
const html = await view.render({ content, reqPath: req.path, req, res });
|
|
51
|
+
res.send(html);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<div class="p-4">
|
|
2
|
+
<div class="flex items-center justify-between mb-6">
|
|
3
|
+
<h1 class="text-2xl font-bold text-discord-foreground">Comandos</h1>
|
|
4
|
+
<div class="flex gap-2">
|
|
5
|
+
<a href="?days=7" class="text-sm px-3 py-1 rounded bg-discord-primary text-white">7d</a>
|
|
6
|
+
<a href="?days=30" class="text-sm px-3 py-1 rounded bg-discord-dark-100 text-discord-foreground/70 hover:text-discord-foreground">30d</a>
|
|
7
|
+
<a href="?days=90" class="text-sm px-3 py-1 rounded bg-discord-dark-100 text-discord-foreground/70 hover:text-discord-foreground">90d</a>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
11
|
+
<div class="card">
|
|
12
|
+
<h3 class="text-lg font-semibold mb-4 text-discord-foreground">Comandos por Dia</h3>
|
|
13
|
+
<canvas id="chartCommands" width="400" height="200"></canvas>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="card">
|
|
16
|
+
<h3 class="text-lg font-semibold mb-4 text-discord-foreground">Top Comandos</h3>
|
|
17
|
+
<canvas id="chartTop" width="400" height="200"></canvas>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<% if (typeof slowestCommands !== 'undefined' && slowestCommands.length > 0) { %>
|
|
21
|
+
<div class="card mt-6">
|
|
22
|
+
<h3 class="text-lg font-semibold mb-4 text-discord-foreground">Comandos Mas Lentos (ms)</h3>
|
|
23
|
+
<canvas id="chartSlow" width="700" height="250"></canvas>
|
|
24
|
+
</div>
|
|
25
|
+
<% } %>
|
|
26
|
+
</div>
|
|
27
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
28
|
+
<script>
|
|
29
|
+
new Chart(document.getElementById('chartCommands'), {
|
|
30
|
+
type: 'bar',
|
|
31
|
+
data: {
|
|
32
|
+
labels: <%- JSON.stringify(commandsPerDay.map(v => v.date)) %>,
|
|
33
|
+
datasets: [{ label: 'Comandos', data: <%- JSON.stringify(commandsPerDay.map(v => v.count)) %>, backgroundColor: '#5865f2', borderRadius: 4 }]
|
|
34
|
+
},
|
|
35
|
+
options: { responsive: true, plugins: { legend: { display: false } } }
|
|
36
|
+
});
|
|
37
|
+
new Chart(document.getElementById('chartTop'), {
|
|
38
|
+
type: 'bar',
|
|
39
|
+
data: {
|
|
40
|
+
labels: <%- JSON.stringify(topCommands.map(v => v.command_name)) %>,
|
|
41
|
+
datasets: [{ label: 'Usos', data: <%- JSON.stringify(topCommands.map(v => v.usage_count)) %>, backgroundColor: topCommands.map((_, i) => 'rgba(88,101,242,' + Math.max(0.3, 1 - i * 0.08) + ')'), borderRadius: 4 }]
|
|
42
|
+
},
|
|
43
|
+
options: { indexAxis: 'y', responsive: true, plugins: { legend: { display: false } } }
|
|
44
|
+
});
|
|
45
|
+
<% if (typeof slowestCommands !== 'undefined' && slowestCommands.length > 0) { %>
|
|
46
|
+
new Chart(document.getElementById('chartSlow'), {
|
|
47
|
+
type: 'bar',
|
|
48
|
+
data: {
|
|
49
|
+
labels: <%- JSON.stringify(slowestCommands.map(v => v.command_name)) %>,
|
|
50
|
+
datasets: [{ label: 'ms', data: <%- JSON.stringify(slowestCommands.map(v => v.avgExecutionTimeMs)) %>, backgroundColor: '#fee75c', borderRadius: 4 }]
|
|
51
|
+
},
|
|
52
|
+
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { title: { display: true, text: 'ms' } } } }
|
|
53
|
+
});
|
|
54
|
+
<% } %>
|
|
55
|
+
</script>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<div class="p-4">
|
|
2
|
+
<div class="flex items-center justify-between mb-6">
|
|
3
|
+
<h1 class="text-2xl font-bold text-discord-foreground">Crecimiento</h1>
|
|
4
|
+
<div class="flex gap-2">
|
|
5
|
+
<a href="?days=30" class="text-sm px-3 py-1 rounded bg-discord-primary text-white">30d</a>
|
|
6
|
+
<a href="?days=60" class="text-sm px-3 py-1 rounded bg-discord-dark-100 text-discord-foreground/70 hover:text-discord-foreground">60d</a>
|
|
7
|
+
<a href="?days=90" class="text-sm px-3 py-1 rounded bg-discord-dark-100 text-discord-foreground/70 hover:text-discord-foreground">90d</a>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="grid grid-cols-3 gap-4 mb-6">
|
|
11
|
+
<div class="card text-center">
|
|
12
|
+
<div class="text-3xl font-bold text-discord-foreground"><%= totalGuilds %></div>
|
|
13
|
+
<div class="text-xs text-discord-foreground/60 mt-1">Servidores Totales</div>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="card text-center">
|
|
16
|
+
<div class="text-3xl font-bold text-discord-foreground"><%= summary.totalMessages %></div>
|
|
17
|
+
<div class="text-xs text-discord-foreground/60 mt-1">Mensajes</div>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="card text-center">
|
|
20
|
+
<div class="text-3xl font-bold text-discord-foreground"><%= summary.totalCommands %></div>
|
|
21
|
+
<div class="text-xs text-discord-foreground/60 mt-1">Comandos</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="card">
|
|
25
|
+
<h3 class="text-lg font-semibold mb-4 text-discord-foreground">Servidores Activos por Dia</h3>
|
|
26
|
+
<canvas id="chartGrowth" width="700" height="300"></canvas>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
30
|
+
<script>
|
|
31
|
+
new Chart(document.getElementById('chartGrowth'), {
|
|
32
|
+
type: 'line',
|
|
33
|
+
data: {
|
|
34
|
+
labels: <%- JSON.stringify(guildGrowth.map(v => v.date)) %>,
|
|
35
|
+
datasets: [{
|
|
36
|
+
label: 'Servidores',
|
|
37
|
+
data: <%- JSON.stringify(guildGrowth.map(v => v.guildCount)) %>,
|
|
38
|
+
borderColor: '#57f287',
|
|
39
|
+
backgroundColor: 'rgba(87,242,135,0.1)',
|
|
40
|
+
fill: true,
|
|
41
|
+
tension: 0.3,
|
|
42
|
+
}]
|
|
43
|
+
},
|
|
44
|
+
options: { responsive: true, plugins: { legend: { display: false } } }
|
|
45
|
+
});
|
|
46
|
+
</script>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<div class="p-4">
|
|
2
|
+
<div class="flex items-center justify-between mb-6">
|
|
3
|
+
<h1 class="text-2xl font-bold text-discord-foreground">Mensajes</h1>
|
|
4
|
+
<div class="flex gap-2">
|
|
5
|
+
<a href="?days=7" class="text-sm px-3 py-1 rounded bg-discord-primary text-white">7d</a>
|
|
6
|
+
<a href="?days=30" class="text-sm px-3 py-1 rounded bg-discord-dark-100 text-discord-foreground/70 hover:text-discord-foreground">30d</a>
|
|
7
|
+
<a href="?days=90" class="text-sm px-3 py-1 rounded bg-discord-dark-100 text-discord-foreground/70 hover:text-discord-foreground">90d</a>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="card">
|
|
11
|
+
<h3 class="text-lg font-semibold mb-4 text-discord-foreground">Mensajes por Dia (<%= daysBack %> dias)</h3>
|
|
12
|
+
<canvas id="chartMessages" width="700" height="300"></canvas>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
16
|
+
<script>
|
|
17
|
+
new Chart(document.getElementById('chartMessages'), {
|
|
18
|
+
type: 'bar',
|
|
19
|
+
data: {
|
|
20
|
+
labels: <%- JSON.stringify(messagesPerDay.map(v => v.date)) %>,
|
|
21
|
+
datasets: [{
|
|
22
|
+
label: 'Mensajes',
|
|
23
|
+
data: <%- JSON.stringify(messagesPerDay.map(v => v.count)) %>,
|
|
24
|
+
backgroundColor: '#5865f2',
|
|
25
|
+
borderRadius: 4,
|
|
26
|
+
}]
|
|
27
|
+
},
|
|
28
|
+
options: { responsive: true, plugins: { legend: { display: false } } }
|
|
29
|
+
});
|
|
30
|
+
</script>
|
|
@@ -1,170 +1,76 @@
|
|
|
1
1
|
<div class="p-4">
|
|
2
2
|
<div class="flex items-center justify-between mb-6">
|
|
3
|
-
<h1 class="text-2xl font-bold text-discord-foreground"
|
|
4
|
-
<div class="flex gap-2">
|
|
5
|
-
<button data-days="7" class="date-btn btn-primary text-sm px-3 py-1 rounded"><%= t('analytics.last7days') %></button>
|
|
6
|
-
<button data-days="30" class="date-btn text-sm px-3 py-1 rounded bg-discord-dark-100 text-discord-foreground/70 hover:text-discord-foreground"><%= t('analytics.last30days') %></button>
|
|
7
|
-
<button data-days="90" class="date-btn text-sm px-3 py-1 rounded bg-discord-dark-100 text-discord-foreground/70 hover:text-discord-foreground"><%= t('analytics.last90days') %></button>
|
|
8
|
-
</div>
|
|
3
|
+
<h1 class="text-2xl font-bold text-discord-foreground">Analiticas</h1>
|
|
9
4
|
</div>
|
|
10
5
|
|
|
11
|
-
<div class="grid grid-cols-2 md:grid-cols-
|
|
12
|
-
<div class="card text-center">
|
|
13
|
-
<div class="text-2xl font-bold text-discord-foreground"><%= summary.totalGuilds %></div>
|
|
14
|
-
<div class="text-xs text-discord-foreground/60 mt-1"><%= t('analytics.totalGuilds') %></div>
|
|
15
|
-
</div>
|
|
6
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|
16
7
|
<div class="card text-center">
|
|
17
|
-
<div class="text-2xl font-bold text-discord-
|
|
18
|
-
<div class="text-xs text-discord-foreground/60 mt-1"
|
|
8
|
+
<div class="text-2xl font-bold text-discord-primary"><%= summary.totalGuilds %></div>
|
|
9
|
+
<div class="text-xs text-discord-foreground/60 mt-1">Servidores</div>
|
|
19
10
|
</div>
|
|
20
11
|
<div class="card text-center">
|
|
21
|
-
<div class="text-2xl font-bold text-discord-
|
|
22
|
-
<div class="text-xs text-discord-foreground/60 mt-1"
|
|
12
|
+
<div class="text-2xl font-bold text-discord-success"><%= summary.totalMessages %></div>
|
|
13
|
+
<div class="text-xs text-discord-foreground/60 mt-1">Mensajes</div>
|
|
23
14
|
</div>
|
|
24
15
|
<div class="card text-center">
|
|
25
|
-
<div class="text-2xl font-bold text-discord-
|
|
26
|
-
<div class="text-xs text-discord-foreground/60 mt-1"
|
|
16
|
+
<div class="text-2xl font-bold text-discord-warning"><%= summary.totalCommands %></div>
|
|
17
|
+
<div class="text-xs text-discord-foreground/60 mt-1">Comandos</div>
|
|
27
18
|
</div>
|
|
28
19
|
<div class="card text-center">
|
|
29
20
|
<div class="text-2xl font-bold text-discord-foreground"><%= Math.round(summary.totalVoiceMinutes) %></div>
|
|
30
|
-
<div class="text-xs text-discord-foreground/60 mt-1"
|
|
21
|
+
<div class="text-xs text-discord-foreground/60 mt-1">Min. Voz</div>
|
|
31
22
|
</div>
|
|
32
23
|
</div>
|
|
33
24
|
|
|
34
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-6
|
|
25
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
35
26
|
<div class="card">
|
|
36
|
-
<h3 class="text-lg font-semibold mb-4 text-discord-foreground"
|
|
37
|
-
<canvas id="
|
|
27
|
+
<h3 class="text-lg font-semibold mb-4 text-discord-foreground">Crecimiento de Servidores</h3>
|
|
28
|
+
<canvas id="chartGrowth" width="400" height="200"></canvas>
|
|
38
29
|
</div>
|
|
39
30
|
<div class="card">
|
|
40
|
-
<h3 class="text-lg font-semibold mb-4 text-discord-foreground"
|
|
31
|
+
<h3 class="text-lg font-semibold mb-4 text-discord-foreground">Mensajes por Dia</h3>
|
|
41
32
|
<canvas id="chartMessages" width="400" height="200"></canvas>
|
|
42
33
|
</div>
|
|
43
|
-
<div class="card">
|
|
44
|
-
<h3 class="text-lg font-semibold mb-4 text-discord-foreground"><%= t('analytics.commandsPerDay') %></h3>
|
|
45
|
-
<canvas id="chartCommands" width="400" height="200"></canvas>
|
|
46
|
-
</div>
|
|
47
|
-
<div class="card">
|
|
48
|
-
<h3 class="text-lg font-semibold mb-4 text-discord-foreground"><%= t('analytics.topCommands') %></h3>
|
|
49
|
-
<canvas id="chartTopCommands" width="400" height="200"></canvas>
|
|
50
|
-
</div>
|
|
51
34
|
</div>
|
|
52
|
-
|
|
53
|
-
<% if (typeof slowestCommands !== 'undefined' && slowestCommands.length > 0) { %>
|
|
54
|
-
<div class="card mb-6">
|
|
55
|
-
<h3 class="text-lg font-semibold mb-4 text-discord-foreground"><%= t('analytics.slowestCommands') %></h3>
|
|
56
|
-
<canvas id="chartSlowestCommands" width="600" height="200"></canvas>
|
|
57
|
-
</div>
|
|
58
|
-
<% } %>
|
|
59
35
|
</div>
|
|
60
36
|
|
|
61
37
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
62
38
|
<script>
|
|
63
39
|
(function() {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
success: '#57f287',
|
|
67
|
-
warning: '#fee75c',
|
|
68
|
-
danger: '#ed4245',
|
|
69
|
-
foreground: '#ffffff',
|
|
70
|
-
foreground60: 'rgba(255,255,255,0.6)',
|
|
71
|
-
dark100: '#36393f',
|
|
72
|
-
dark200: '#2f3136',
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
Chart.defaults.color = colors.foreground60;
|
|
76
|
-
Chart.defaults.borderColor = colors.dark100;
|
|
77
|
-
Chart.defaults.plugins.legend.labels.color = colors.foreground60;
|
|
40
|
+
Chart.defaults.color = 'rgba(255,255,255,0.6)';
|
|
41
|
+
Chart.defaults.borderColor = '#36393f';
|
|
78
42
|
|
|
79
|
-
const
|
|
43
|
+
const guildGrowth = <%- JSON.stringify(guildGrowth) %>;
|
|
44
|
+
const messages = <%- JSON.stringify(messagesPerDay) %>;
|
|
80
45
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function createBarChart(canvasId, label, values, keyX, keyY) {
|
|
100
|
-
new Chart(document.getElementById(canvasId), {
|
|
101
|
-
type: 'bar',
|
|
102
|
-
data: {
|
|
103
|
-
labels: values.map(v => v[keyX]),
|
|
104
|
-
datasets: [{
|
|
105
|
-
label: label,
|
|
106
|
-
data: values.map(v => v[keyY]),
|
|
107
|
-
backgroundColor: colors.primary,
|
|
108
|
-
borderRadius: 4,
|
|
109
|
-
}]
|
|
110
|
-
},
|
|
111
|
-
options: { responsive: true, plugins: { legend: { display: false } } }
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function createHorizontalBarChart(canvasId, label, values, keyX, keyY) {
|
|
116
|
-
new Chart(document.getElementById(canvasId), {
|
|
117
|
-
type: 'bar',
|
|
118
|
-
data: {
|
|
119
|
-
labels: values.map(v => v[keyX]),
|
|
120
|
-
datasets: [{
|
|
121
|
-
label: label,
|
|
122
|
-
data: values.map(v => v[keyY]),
|
|
123
|
-
backgroundColor: values.map((_, i) => {
|
|
124
|
-
const alpha = 1 - (i * 0.08);
|
|
125
|
-
return `rgba(88,101,242,${Math.max(0.3, alpha)})`;
|
|
126
|
-
}),
|
|
127
|
-
borderRadius: 4,
|
|
128
|
-
}]
|
|
129
|
-
},
|
|
130
|
-
options: {
|
|
131
|
-
indexAxis: 'y',
|
|
132
|
-
responsive: true,
|
|
133
|
-
plugins: { legend: { display: false } }
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
createLineChart('chartGuildGrowth', '<%= t("analytics.guildGrowth") %>', data.guildGrowth, 'date', 'guildCount');
|
|
139
|
-
createBarChart('chartMessages', '<%= t("analytics.messagesPerDay") %>', data.messagesPerDay, 'date', 'count');
|
|
140
|
-
createBarChart('chartCommands', '<%= t("analytics.commandsPerDay") %>', data.commandsPerDay, 'date', 'count');
|
|
141
|
-
createHorizontalBarChart('chartTopCommands', '<%= t("analytics.topCommands") %>', data.topCommands, 'command_name', 'usage_count');
|
|
46
|
+
new Chart(document.getElementById('chartGrowth'), {
|
|
47
|
+
type: 'line',
|
|
48
|
+
data: {
|
|
49
|
+
labels: guildGrowth.map(v => v.date),
|
|
50
|
+
datasets: [{
|
|
51
|
+
label: 'Servidores',
|
|
52
|
+
data: guildGrowth.map(v => v.guildCount),
|
|
53
|
+
borderColor: '#57f287',
|
|
54
|
+
backgroundColor: 'rgba(87,242,135,0.1)',
|
|
55
|
+
fill: true,
|
|
56
|
+
tension: 0.3,
|
|
57
|
+
}]
|
|
58
|
+
},
|
|
59
|
+
options: { responsive: true, plugins: { legend: { display: false } } }
|
|
60
|
+
});
|
|
142
61
|
|
|
143
|
-
|
|
144
|
-
new Chart(document.getElementById('chartSlowestCommands'), {
|
|
62
|
+
new Chart(document.getElementById('chartMessages'), {
|
|
145
63
|
type: 'bar',
|
|
146
64
|
data: {
|
|
147
|
-
labels:
|
|
65
|
+
labels: messages.map(v => v.date),
|
|
148
66
|
datasets: [{
|
|
149
|
-
label: '
|
|
150
|
-
data:
|
|
151
|
-
backgroundColor:
|
|
67
|
+
label: 'Mensajes',
|
|
68
|
+
data: messages.map(v => v.count),
|
|
69
|
+
backgroundColor: '#5865f2',
|
|
152
70
|
borderRadius: 4,
|
|
153
71
|
}]
|
|
154
72
|
},
|
|
155
|
-
options: {
|
|
156
|
-
responsive: true,
|
|
157
|
-
plugins: { legend: { display: false } },
|
|
158
|
-
scales: { y: { title: { display: true, text: 'ms' } } }
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
<% } %>
|
|
162
|
-
|
|
163
|
-
document.querySelectorAll('.date-btn').forEach(btn => {
|
|
164
|
-
btn.addEventListener('click', () => {
|
|
165
|
-
const days = btn.dataset.days;
|
|
166
|
-
window.location.href = '/admin/analytics?days=' + days;
|
|
167
|
-
});
|
|
73
|
+
options: { responsive: true, plugins: { legend: { display: false } } }
|
|
168
74
|
});
|
|
169
75
|
})();
|
|
170
76
|
</script>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<div class="p-4">
|
|
2
|
+
<div class="flex items-center justify-between mb-6">
|
|
3
|
+
<h1 class="text-2xl font-bold text-discord-foreground"><%= t('analytics.commands') %></h1>
|
|
4
|
+
<div class="flex gap-2">
|
|
5
|
+
<a href="?days=7" class="text-sm px-3 py-1 rounded bg-discord-primary text-white">7d</a>
|
|
6
|
+
<a href="?days=30" class="text-sm px-3 py-1 rounded bg-discord-dark-100 text-discord-foreground/70 hover:text-discord-foreground">30d</a>
|
|
7
|
+
<a href="?days=90" class="text-sm px-3 py-1 rounded bg-discord-dark-100 text-discord-foreground/70 hover:text-discord-foreground">90d</a>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
11
|
+
<div class="card">
|
|
12
|
+
<h3 class="text-lg font-semibold mb-4 text-discord-foreground"><%= t('analytics.commandsPerDay') %></h3>
|
|
13
|
+
<canvas id="chartCommands" width="400" height="200"></canvas>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="card">
|
|
16
|
+
<h3 class="text-lg font-semibold mb-4 text-discord-foreground"><%= t('analytics.topCommands') %></h3>
|
|
17
|
+
<canvas id="chartTop" width="400" height="200"></canvas>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<% if (slowestCommands && slowestCommands.length > 0) { %>
|
|
21
|
+
<div class="card mt-6">
|
|
22
|
+
<h3 class="text-lg font-semibold mb-4 text-discord-foreground"><%= t('analytics.slowestCommands') %></h3>
|
|
23
|
+
<canvas id="chartSlow" width="700" height="250"></canvas>
|
|
24
|
+
</div>
|
|
25
|
+
<% } %>
|
|
26
|
+
</div>
|
|
27
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
28
|
+
<script>
|
|
29
|
+
new Chart(document.getElementById('chartCommands'), {
|
|
30
|
+
type: 'bar',
|
|
31
|
+
data: {
|
|
32
|
+
labels: <%- JSON.stringify(commandsPerDay.map(v => v.date)) %>,
|
|
33
|
+
datasets: [{ label: '<%= t("analytics.commands") %>', data: <%- JSON.stringify(commandsPerDay.map(v => v.count)) %>, backgroundColor: '#5865f2', borderRadius: 4 }]
|
|
34
|
+
},
|
|
35
|
+
options: { responsive: true, plugins: { legend: { display: false } } }
|
|
36
|
+
});
|
|
37
|
+
new Chart(document.getElementById('chartTop'), {
|
|
38
|
+
type: 'bar',
|
|
39
|
+
data: {
|
|
40
|
+
labels: <%- JSON.stringify(topCommands.map(v => v.command_name)) %>,
|
|
41
|
+
datasets: [{ label: '<%= t("analytics.usageCount") %>', data: <%- JSON.stringify(topCommands.map(v => v.usage_count)) %>, backgroundColor: topCommands.map((_, i) => 'rgba(88,101,242,' + Math.max(0.3, 1 - i * 0.08) + ')'), borderRadius: 4 }]
|
|
42
|
+
},
|
|
43
|
+
options: { indexAxis: 'y', responsive: true, plugins: { legend: { display: false } } }
|
|
44
|
+
});
|
|
45
|
+
<% if (slowestCommands && slowestCommands.length > 0) { %>
|
|
46
|
+
new Chart(document.getElementById('chartSlow'), {
|
|
47
|
+
type: 'bar',
|
|
48
|
+
data: {
|
|
49
|
+
labels: <%- JSON.stringify(slowestCommands.map(v => v.command_name)) %>,
|
|
50
|
+
datasets: [{ label: 'ms', data: <%- JSON.stringify(slowestCommands.map(v => v.avgExecutionTimeMs)) %>, backgroundColor: '#fee75c', borderRadius: 4 }]
|
|
51
|
+
},
|
|
52
|
+
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { title: { display: true, text: 'ms' } } } }
|
|
53
|
+
});
|
|
54
|
+
<% } %>
|
|
55
|
+
</script>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<div class="p-4">
|
|
2
|
+
<div class="flex items-center justify-between mb-6">
|
|
3
|
+
<h1 class="text-2xl font-bold text-discord-foreground"><%= t('analytics.messages') %></h1>
|
|
4
|
+
<div class="flex gap-2">
|
|
5
|
+
<a href="?days=7" class="text-sm px-3 py-1 rounded bg-discord-primary text-white">7d</a>
|
|
6
|
+
<a href="?days=30" class="text-sm px-3 py-1 rounded bg-discord-dark-100 text-discord-foreground/70 hover:text-discord-foreground">30d</a>
|
|
7
|
+
<a href="?days=90" class="text-sm px-3 py-1 rounded bg-discord-dark-100 text-discord-foreground/70 hover:text-discord-foreground">90d</a>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="card mb-6">
|
|
11
|
+
<div class="text-center py-4">
|
|
12
|
+
<div class="text-4xl font-bold text-discord-primary"><%= totalMessages %></div>
|
|
13
|
+
<div class="text-sm text-discord-foreground/60"><%= t('analytics.messages') %> (<%= daysBack %> <%= t('analytics.minutes') %>)</div>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="card">
|
|
17
|
+
<h3 class="text-lg font-semibold mb-4 text-discord-foreground"><%= t('analytics.messagesPerDay') %></h3>
|
|
18
|
+
<canvas id="chartMessages" width="700" height="300"></canvas>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
22
|
+
<script>
|
|
23
|
+
new Chart(document.getElementById('chartMessages'), {
|
|
24
|
+
type: 'bar',
|
|
25
|
+
data: {
|
|
26
|
+
labels: <%- JSON.stringify(messagesPerDay.map(v => v.date)) %>,
|
|
27
|
+
datasets: [{ label: '<%= t("analytics.messages") %>', data: <%- JSON.stringify(messagesPerDay.map(v => v.count)) %>, backgroundColor: '#5865f2', borderRadius: 4 }]
|
|
28
|
+
},
|
|
29
|
+
options: { responsive: true, plugins: { legend: { display: false } } }
|
|
30
|
+
});
|
|
31
|
+
</script>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<div class="p-4">
|
|
2
|
+
<div class="flex items-center justify-between mb-6">
|
|
3
|
+
<h1 class="text-2xl font-bold text-discord-foreground"><%= t('analytics.voice') %></h1>
|
|
4
|
+
<div class="flex gap-2">
|
|
5
|
+
<a href="?days=7" class="text-sm px-3 py-1 rounded bg-discord-primary text-white">7d</a>
|
|
6
|
+
<a href="?days=30" class="text-sm px-3 py-1 rounded bg-discord-dark-100 text-discord-foreground/70 hover:text-discord-foreground">30d</a>
|
|
7
|
+
<a href="?days=90" class="text-sm px-3 py-1 rounded bg-discord-dark-100 text-discord-foreground/70 hover:text-discord-foreground">90d</a>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="card mb-6">
|
|
11
|
+
<div class="text-center py-4">
|
|
12
|
+
<div class="text-4xl font-bold text-discord-primary"><%= Math.round(totalVoice) %> min</div>
|
|
13
|
+
<div class="text-sm text-discord-foreground/60"><%= t('analytics.voiceActivity') %></div>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="card mb-6">
|
|
17
|
+
<h3 class="text-lg font-semibold mb-4 text-discord-foreground"><%= t('analytics.voiceActivity') %> (<%= daysBack %> dias)</h3>
|
|
18
|
+
<canvas id="chartVoice" width="700" height="300"></canvas>
|
|
19
|
+
</div>
|
|
20
|
+
<% if (channelVoice && channelVoice.length > 0) { %>
|
|
21
|
+
<div class="card">
|
|
22
|
+
<h3 class="text-lg font-semibold mb-4 text-discord-foreground"><%= t('analytics.perChannelVoice') %></h3>
|
|
23
|
+
<canvas id="chartChannels" width="700" height="250"></canvas>
|
|
24
|
+
</div>
|
|
25
|
+
<% } %>
|
|
26
|
+
</div>
|
|
27
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
28
|
+
<script>
|
|
29
|
+
new Chart(document.getElementById('chartVoice'), {
|
|
30
|
+
type: 'bar',
|
|
31
|
+
data: {
|
|
32
|
+
labels: <%- JSON.stringify(voicePerDay.map(v => v.date)) %>,
|
|
33
|
+
datasets: [{ label: 'min', data: <%- JSON.stringify(voicePerDay.map(v => v.count)) %>, backgroundColor: '#57f287', borderRadius: 4 }]
|
|
34
|
+
},
|
|
35
|
+
options: { responsive: true, plugins: { legend: { display: false } } }
|
|
36
|
+
});
|
|
37
|
+
<% if (channelVoice && channelVoice.length > 0) { %>
|
|
38
|
+
new Chart(document.getElementById('chartChannels'), {
|
|
39
|
+
type: 'bar',
|
|
40
|
+
data: {
|
|
41
|
+
labels: <%- JSON.stringify(channelVoice.map(v => v.channel_id)) %>,
|
|
42
|
+
datasets: [{ label: 'min', data: <%- JSON.stringify(channelVoice.map(v => v.total_minutes)) %>, backgroundColor: '#5865f2', borderRadius: 4 }]
|
|
43
|
+
},
|
|
44
|
+
options: { responsive: true, plugins: { legend: { display: false } } }
|
|
45
|
+
});
|
|
46
|
+
<% } %>
|
|
47
|
+
</script>
|
|
@@ -1,37 +1,28 @@
|
|
|
1
1
|
<div class="p-4">
|
|
2
|
-
<div class="
|
|
2
|
+
<div class="mb-6">
|
|
3
3
|
<h1 class="text-2xl font-bold text-discord-foreground"><%= t('analytics.serverStats') %></h1>
|
|
4
|
-
<div class="flex gap-2">
|
|
5
|
-
<button data-days="7" class="date-btn btn-primary text-sm px-3 py-1 rounded"><%= t('analytics.last7days') %></button>
|
|
6
|
-
<button data-days="30" class="date-btn text-sm px-3 py-1 rounded bg-discord-dark-100 text-discord-foreground/70 hover:text-discord-foreground"><%= t('analytics.last30days') %></button>
|
|
7
|
-
<button data-days="90" class="date-btn text-sm px-3 py-1 rounded bg-discord-dark-100 text-discord-foreground/70 hover:text-discord-foreground"><%= t('analytics.last90days') %></button>
|
|
8
|
-
</div>
|
|
9
4
|
</div>
|
|
10
5
|
|
|
11
|
-
<div class="grid grid-cols-2 md:grid-cols-
|
|
6
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|
12
7
|
<div class="card text-center">
|
|
13
|
-
<div class="text-2xl font-bold text-discord-
|
|
8
|
+
<div class="text-2xl font-bold text-discord-primary"><%= guildSummary.totalMessages %></div>
|
|
14
9
|
<div class="text-xs text-discord-foreground/60 mt-1"><%= t('analytics.messages') %></div>
|
|
15
10
|
</div>
|
|
16
11
|
<div class="card text-center">
|
|
17
|
-
<div class="text-2xl font-bold text-discord-
|
|
12
|
+
<div class="text-2xl font-bold text-discord-success"><%= guildSummary.totalCommands %></div>
|
|
18
13
|
<div class="text-xs text-discord-foreground/60 mt-1"><%= t('analytics.commands') %></div>
|
|
19
14
|
</div>
|
|
20
|
-
<div class="card text-center">
|
|
21
|
-
<div class="text-2xl font-bold text-discord-foreground"><%= guildSummary.totalJoins %></div>
|
|
22
|
-
<div class="text-xs text-discord-foreground/60 mt-1"><%= t('analytics.joins') %></div>
|
|
23
|
-
</div>
|
|
24
|
-
<div class="card text-center">
|
|
25
|
-
<div class="text-2xl font-bold text-discord-foreground"><%= guildSummary.totalLeaves %></div>
|
|
26
|
-
<div class="text-xs text-discord-foreground/60 mt-1"><%= t('analytics.leaves') %></div>
|
|
27
|
-
</div>
|
|
28
15
|
<div class="card text-center">
|
|
29
16
|
<div class="text-2xl font-bold text-discord-foreground"><%= Math.round(guildSummary.totalVoiceMinutes) %></div>
|
|
30
17
|
<div class="text-xs text-discord-foreground/60 mt-1"><%= t('analytics.voice') %> (min)</div>
|
|
31
18
|
</div>
|
|
19
|
+
<div class="card text-center">
|
|
20
|
+
<div class="text-2xl font-bold"><span class="text-discord-success"><%= guildSummary.totalJoins %></span> / <span class="text-discord-danger"><%= guildSummary.totalLeaves %></span></div>
|
|
21
|
+
<div class="text-xs text-discord-foreground/60 mt-1"><%= t('analytics.joins') %> / <%= t('analytics.leaves') %></div>
|
|
22
|
+
</div>
|
|
32
23
|
</div>
|
|
33
24
|
|
|
34
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-6
|
|
25
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
35
26
|
<div class="card">
|
|
36
27
|
<h3 class="text-lg font-semibold mb-4 text-discord-foreground"><%= t('analytics.messagesPerDay') %></h3>
|
|
37
28
|
<canvas id="chartMessages" width="400" height="200"></canvas>
|
|
@@ -40,170 +31,38 @@
|
|
|
40
31
|
<h3 class="text-lg font-semibold mb-4 text-discord-foreground"><%= t('analytics.joinsVsLeaves') %></h3>
|
|
41
32
|
<canvas id="chartMembers" width="400" height="200"></canvas>
|
|
42
33
|
</div>
|
|
43
|
-
<div class="card">
|
|
44
|
-
<h3 class="text-lg font-semibold mb-4 text-discord-foreground"><%= t('analytics.voiceActivity') %></h3>
|
|
45
|
-
<canvas id="chartVoice" width="400" height="200"></canvas>
|
|
46
|
-
</div>
|
|
47
|
-
<div class="card">
|
|
48
|
-
<h3 class="text-lg font-semibold mb-4 text-discord-foreground"><%= t('analytics.commandsPerDay') %></h3>
|
|
49
|
-
<canvas id="chartCommands" width="400" height="200"></canvas>
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
|
|
53
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
|
54
|
-
<div class="card">
|
|
55
|
-
<h3 class="text-lg font-semibold mb-4 text-discord-foreground"><%= t('analytics.topCommands') %></h3>
|
|
56
|
-
<canvas id="chartTopCommands" width="400" height="200"></canvas>
|
|
57
|
-
</div>
|
|
58
|
-
<% if (typeof slowestCommands !== 'undefined' && slowestCommands.length > 0) { %>
|
|
59
|
-
<div class="card">
|
|
60
|
-
<h3 class="text-lg font-semibold mb-4 text-discord-foreground"><%= t('analytics.slowestCommands') %></h3>
|
|
61
|
-
<canvas id="chartSlowestCommands" width="400" height="200"></canvas>
|
|
62
|
-
</div>
|
|
63
|
-
<% } %>
|
|
64
34
|
</div>
|
|
65
|
-
|
|
66
|
-
<% if (typeof channelVoice !== 'undefined' && channelVoice.length > 0) { %>
|
|
67
|
-
<div class="card mb-6">
|
|
68
|
-
<h3 class="text-lg font-semibold mb-4 text-discord-foreground"><%= t('analytics.perChannelVoice') %></h3>
|
|
69
|
-
<canvas id="chartChannelVoice" width="600" height="200"></canvas>
|
|
70
|
-
</div>
|
|
71
|
-
<% } %>
|
|
72
35
|
</div>
|
|
73
36
|
|
|
74
37
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
75
38
|
<script>
|
|
76
39
|
(function() {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
success: '#57f287',
|
|
80
|
-
warning: '#fee75c',
|
|
81
|
-
danger: '#ed4245',
|
|
82
|
-
foreground: '#ffffff',
|
|
83
|
-
foreground60: 'rgba(255,255,255,0.6)',
|
|
84
|
-
dark100: '#36393f',
|
|
85
|
-
dark200: '#2f3136',
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
Chart.defaults.color = colors.foreground60;
|
|
89
|
-
Chart.defaults.borderColor = colors.dark100;
|
|
90
|
-
Chart.defaults.plugins.legend.labels.color = colors.foreground60;
|
|
40
|
+
Chart.defaults.color = 'rgba(255,255,255,0.6)';
|
|
41
|
+
Chart.defaults.borderColor = '#36393f';
|
|
91
42
|
|
|
92
|
-
const
|
|
43
|
+
const messages = <%- JSON.stringify(messagesPerDay) %>;
|
|
44
|
+
const joins = <%- JSON.stringify(joinsPerDay) %>;
|
|
45
|
+
const leaves = <%- JSON.stringify(leavesPerDay) %>;
|
|
93
46
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
backgroundColor: colors.primary,
|
|
103
|
-
borderRadius: 4,
|
|
104
|
-
}]
|
|
105
|
-
},
|
|
106
|
-
options: { responsive: true, plugins: { legend: { display: false } } }
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function createHorizontalBarChart(canvasId, label, values, keyX, keyY) {
|
|
111
|
-
new Chart(document.getElementById(canvasId), {
|
|
112
|
-
type: 'bar',
|
|
113
|
-
data: {
|
|
114
|
-
labels: values.map(v => v[keyX]),
|
|
115
|
-
datasets: [{
|
|
116
|
-
label: label,
|
|
117
|
-
data: values.map(v => v[keyY]),
|
|
118
|
-
backgroundColor: values.map((_, i) => {
|
|
119
|
-
const alpha = 1 - (i * 0.08);
|
|
120
|
-
return `rgba(88,101,242,${Math.max(0.3, alpha)})`;
|
|
121
|
-
}),
|
|
122
|
-
borderRadius: 4,
|
|
123
|
-
}]
|
|
124
|
-
},
|
|
125
|
-
options: {
|
|
126
|
-
indexAxis: 'y',
|
|
127
|
-
responsive: true,
|
|
128
|
-
plugins: { legend: { display: false } }
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
createBarChart('chartMessages', '<%= t("analytics.messagesPerDay") %>', data.messagesPerDay, 'date', 'count');
|
|
134
|
-
createBarChart('chartVoice', '<%= t("analytics.voiceActivity") %>', data.voicePerDay, 'date', 'count');
|
|
135
|
-
createBarChart('chartCommands', '<%= t("analytics.commandsPerDay") %>', data.commandsPerDay, 'date', 'count');
|
|
136
|
-
createHorizontalBarChart('chartTopCommands', '<%= t("analytics.topCommands") %>', data.topCommands, 'command_name', 'usage_count');
|
|
47
|
+
new Chart(document.getElementById('chartMessages'), {
|
|
48
|
+
type: 'bar',
|
|
49
|
+
data: {
|
|
50
|
+
labels: messages.map(v => v.date),
|
|
51
|
+
datasets: [{ label: '<%= t("analytics.messages") %>', data: messages.map(v => v.count), backgroundColor: '#5865f2', borderRadius: 4 }]
|
|
52
|
+
},
|
|
53
|
+
options: { responsive: true, plugins: { legend: { display: false } } }
|
|
54
|
+
});
|
|
137
55
|
|
|
138
56
|
new Chart(document.getElementById('chartMembers'), {
|
|
139
57
|
type: 'line',
|
|
140
58
|
data: {
|
|
141
|
-
labels:
|
|
59
|
+
labels: joins.map(v => v.date),
|
|
142
60
|
datasets: [
|
|
143
|
-
{
|
|
144
|
-
|
|
145
|
-
data: data.joinsPerDay.map(v => v.count),
|
|
146
|
-
borderColor: colors.success,
|
|
147
|
-
backgroundColor: 'rgba(87,242,135,0.1)',
|
|
148
|
-
fill: true,
|
|
149
|
-
tension: 0.3,
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
label: '<%= t("analytics.leaves") %>',
|
|
153
|
-
data: data.leavesPerDay.map(v => v.count),
|
|
154
|
-
borderColor: colors.danger,
|
|
155
|
-
backgroundColor: 'rgba(237,66,69,0.1)',
|
|
156
|
-
fill: true,
|
|
157
|
-
tension: 0.3,
|
|
158
|
-
}
|
|
61
|
+
{ label: '<%= t("analytics.joins") %>', data: joins.map(v => v.count), borderColor: '#57f287', backgroundColor: 'rgba(87,242,135,0.1)', fill: true, tension: 0.3 },
|
|
62
|
+
{ label: '<%= t("analytics.leaves") %>', data: leaves.map(v => v.count), borderColor: '#ed4245', backgroundColor: 'rgba(237,66,69,0.1)', fill: true, tension: 0.3 },
|
|
159
63
|
]
|
|
160
64
|
},
|
|
161
65
|
options: { responsive: true }
|
|
162
66
|
});
|
|
163
|
-
|
|
164
|
-
<% if (typeof slowestCommands !== 'undefined' && slowestCommands.length > 0) { %>
|
|
165
|
-
new Chart(document.getElementById('chartSlowestCommands'), {
|
|
166
|
-
type: 'bar',
|
|
167
|
-
data: {
|
|
168
|
-
labels: data.slowestCommands.map(v => v.command_name),
|
|
169
|
-
datasets: [{
|
|
170
|
-
label: '<%= t("analytics.avgExecutionTime") %> (ms)',
|
|
171
|
-
data: data.slowestCommands.map(v => v.avgExecutionTimeMs),
|
|
172
|
-
backgroundColor: colors.warning,
|
|
173
|
-
borderRadius: 4,
|
|
174
|
-
}]
|
|
175
|
-
},
|
|
176
|
-
options: {
|
|
177
|
-
responsive: true,
|
|
178
|
-
plugins: { legend: { display: false } },
|
|
179
|
-
scales: { y: { title: { display: true, text: 'ms' } } }
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
<% } %>
|
|
183
|
-
|
|
184
|
-
<% if (typeof channelVoice !== 'undefined' && channelVoice.length > 0) { %>
|
|
185
|
-
new Chart(document.getElementById('chartChannelVoice'), {
|
|
186
|
-
type: 'bar',
|
|
187
|
-
data: {
|
|
188
|
-
labels: data.channelVoice.map(v => v.channel_id),
|
|
189
|
-
datasets: [{
|
|
190
|
-
label: '<%= t("analytics.totalMinutes") %>',
|
|
191
|
-
data: data.channelVoice.map(v => v.total_minutes),
|
|
192
|
-
backgroundColor: colors.primary,
|
|
193
|
-
borderRadius: 4,
|
|
194
|
-
}]
|
|
195
|
-
},
|
|
196
|
-
options: { responsive: true, plugins: { legend: { display: false } } }
|
|
197
|
-
});
|
|
198
|
-
<% } %>
|
|
199
|
-
|
|
200
|
-
document.querySelectorAll('.date-btn').forEach(btn => {
|
|
201
|
-
btn.addEventListener('click', () => {
|
|
202
|
-
const days = btn.dataset.days;
|
|
203
|
-
const currentUrl = new URL(window.location.href);
|
|
204
|
-
currentUrl.searchParams.set('days', days);
|
|
205
|
-
window.location.href = currentUrl.toString();
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
67
|
})();
|
|
209
68
|
</script>
|