From 5aef575f4126dc559794b4d9eb5bb2fa834f97aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Prie=C3=9Fnitz?= Date: Thu, 4 Dec 2025 11:37:49 +0100 Subject: [PATCH] [deploy] Add server stats module with dashboard controls --- prisma/schema.prisma | 39 +-- src/config/context.ts | 8 +- src/config/state.ts | 45 ++-- .../migration.sql | 4 + src/database/schema.prisma | 2 + src/events/channelCreate.ts | 1 + src/events/channelDelete.ts | 1 + src/events/guildMemberAdd.ts | 1 + src/events/guildMemberRemove.ts | 1 + src/events/ready.ts | 4 + src/index.ts | 1 + src/services/moduleService.ts | 7 +- src/services/statsService.ts | 251 ++++++++++++++++++ src/web/routes/api.ts | 32 ++- src/web/routes/dashboard.ts | 243 ++++++++++++++--- 15 files changed, 559 insertions(+), 81 deletions(-) create mode 100644 src/database/migrations/20251204000000_add_server_stats/migration.sql create mode 100644 src/services/statsService.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index aaa1dff..70c790e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -9,26 +9,31 @@ datasource db { } model GuildSettings { - guildId String @id - welcomeChannelId String? - logChannelId String? - automodEnabled Boolean? - automodConfig Json? - levelingEnabled Boolean? - ticketsEnabled Boolean? - musicEnabled Boolean? - statuspageEnabled Boolean? - statuspageConfig Json? + guildId String @id + welcomeChannelId String? + logChannelId String? + automodEnabled Boolean? + automodConfig Json? + levelingEnabled Boolean? + ticketsEnabled Boolean? + musicEnabled Boolean? + statuspageEnabled Boolean? + statuspageConfig Json? dynamicVoiceEnabled Boolean? dynamicVoiceConfig Json? - supportLoginConfig Json? - birthdayEnabled Boolean? - birthdayConfig Json? + supportLoginConfig Json? + birthdayEnabled Boolean? + birthdayConfig Json? reactionRolesEnabled Boolean? reactionRolesConfig Json? - eventsEnabled Boolean?\n registerEnabled Boolean?\n registerConfig Json?\n supportRoleId String? - updatedAt DateTime @updatedAt - createdAt DateTime @default(now()) + eventsEnabled Boolean? + registerEnabled Boolean? + registerConfig Json? + serverStatsEnabled Boolean? + serverStatsConfig Json? + supportRoleId String? + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) } model Ticket { @@ -182,7 +187,7 @@ model RegisterFormField { label String type String required Boolean @default(false) - "order" Int @default(0) + order Int @default(0) form RegisterForm @relation(fields: [formId], references: [id], onDelete: Cascade) } diff --git a/src/config/context.ts b/src/config/context.ts index 7c4ebcb..301d006 100644 --- a/src/config/context.ts +++ b/src/config/context.ts @@ -15,6 +15,7 @@ import { EventService } from '../services/eventService'; import { TicketAutomationService } from '../services/ticketAutomationService'; import { KnowledgeBaseService } from '../services/knowledgeBaseService'; import { RegisterService } from '../services/registerService'; +import { StatsService } from '../services/statsService'; export const context = { client: null as Client | null, @@ -33,7 +34,8 @@ export const context = { events: new EventService(), ticketAutomation: new TicketAutomationService(), knowledgeBase: new KnowledgeBaseService(), - register: new RegisterService() + register: new RegisterService(), + stats: new StatsService() }; context.modules.setHooks({ @@ -63,6 +65,10 @@ context.modules.setHooks({ }, eventsEnabled: { onEnable: async (guildId: string) => context.events.tick().catch(() => undefined) + }, + serverStatsEnabled: { + onEnable: async (guildId: string) => context.stats.refreshGuild(guildId).catch(() => undefined), + onDisable: async (guildId: string) => context.stats.disableGuild(guildId).catch(() => undefined) } }); diff --git a/src/config/state.ts b/src/config/state.ts index 7270d20..f2ce02f 100644 --- a/src/config/state.ts +++ b/src/config/state.ts @@ -65,6 +65,15 @@ export interface GuildSettings { reviewChannelId?: string; notifyRoleIds?: string[]; }; + serverStatsEnabled?: boolean; + serverStatsConfig?: { + enabled?: boolean; + categoryId?: string; + categoryName?: string; + refreshMinutes?: number; + cleanupOrphans?: boolean; + items?: any[]; + }; supportRoleId?: string; welcomeEnabled?: boolean; } @@ -74,23 +83,23 @@ class SettingsStore { private applyModuleDefaults(cfg: GuildSettings): GuildSettings { const normalized: GuildSettings = { ...cfg }; - ( - [ - 'ticketsEnabled', - 'automodEnabled', - 'welcomeEnabled', - 'levelingEnabled', - 'musicEnabled', - 'dynamicVoiceEnabled', - 'statuspageEnabled', - 'birthdayEnabled', - 'reactionRolesEnabled', - 'eventsEnabled', - 'registerEnabled' - ] as const - ).forEach((key) => { + const defaultOn = [ + 'ticketsEnabled', + 'automodEnabled', + 'welcomeEnabled', + 'levelingEnabled', + 'musicEnabled', + 'dynamicVoiceEnabled', + 'statuspageEnabled', + 'birthdayEnabled', + 'reactionRolesEnabled', + 'eventsEnabled', + 'registerEnabled' + ] as const; + defaultOn.forEach((key) => { if (normalized[key] === undefined) normalized[key] = true; }); + if (normalized.serverStatsEnabled === undefined) normalized.serverStatsEnabled = false; // keep welcomeConfig flag in sync when present if (normalized.welcomeConfig && normalized.welcomeConfig.enabled === undefined && normalized.welcomeEnabled !== undefined) { normalized.welcomeConfig = { ...normalized.welcomeConfig, enabled: normalized.welcomeEnabled }; @@ -124,6 +133,8 @@ class SettingsStore { reactionRolesConfig: (row as any).reactionRolesConfig ?? undefined, registerEnabled: (row as any).registerEnabled ?? undefined, registerConfig: (row as any).registerConfig ?? undefined, + serverStatsEnabled: (row as any).serverStatsEnabled ?? undefined, + serverStatsConfig: (row as any).serverStatsConfig ?? undefined, supportRoleId: row.supportRoleId ?? undefined } satisfies GuildSettings; this.cache.set(row.guildId, this.applyModuleDefaults(cfg)); @@ -206,6 +217,8 @@ class SettingsStore { reactionRolesConfig: merged.reactionRolesConfig ?? null, registerEnabled: merged.registerEnabled ?? null, registerConfig: merged.registerConfig ?? null, + serverStatsEnabled: (merged as any).serverStatsEnabled ?? null, + serverStatsConfig: (merged as any).serverStatsConfig ?? null, supportRoleId: merged.supportRoleId ?? null }, create: { @@ -228,6 +241,8 @@ class SettingsStore { reactionRolesConfig: merged.reactionRolesConfig ?? null, registerEnabled: merged.registerEnabled ?? null, registerConfig: merged.registerConfig ?? null, + serverStatsEnabled: (merged as any).serverStatsEnabled ?? null, + serverStatsConfig: (merged as any).serverStatsConfig ?? null, supportRoleId: merged.supportRoleId ?? null } }); diff --git a/src/database/migrations/20251204000000_add_server_stats/migration.sql b/src/database/migrations/20251204000000_add_server_stats/migration.sql new file mode 100644 index 0000000..8fa5c38 --- /dev/null +++ b/src/database/migrations/20251204000000_add_server_stats/migration.sql @@ -0,0 +1,4 @@ +-- Add server stats module configuration +ALTER TABLE "GuildSettings" + ADD COLUMN "serverStatsEnabled" BOOLEAN, + ADD COLUMN "serverStatsConfig" JSONB; diff --git a/src/database/schema.prisma b/src/database/schema.prisma index 3e94926..a1c13f4 100644 --- a/src/database/schema.prisma +++ b/src/database/schema.prisma @@ -28,6 +28,8 @@ model GuildSettings { eventsEnabled Boolean? registerEnabled Boolean? registerConfig Json? + serverStatsEnabled Boolean? + serverStatsConfig Json? supportRoleId String? updatedAt DateTime @updatedAt createdAt DateTime @default(now()) diff --git a/src/events/channelCreate.ts b/src/events/channelCreate.ts index 840ca9f..685ffda 100644 --- a/src/events/channelCreate.ts +++ b/src/events/channelCreate.ts @@ -7,6 +7,7 @@ const event: EventHandler = { execute(channel: GuildChannel) { if (!channel.guild) return; context.logging.logSystem(channel.guild, `Channel erstellt: ${channel.name} (${channel.id})`); + context.stats.refreshGuild(channel.guild.id).catch(() => undefined); } }; diff --git a/src/events/channelDelete.ts b/src/events/channelDelete.ts index a8591eb..51a5252 100644 --- a/src/events/channelDelete.ts +++ b/src/events/channelDelete.ts @@ -7,6 +7,7 @@ const event: EventHandler = { execute(channel: GuildChannel) { if (!channel.guild) return; context.logging.logSystem(channel.guild, `Channel gelöscht: ${channel.name} (${channel.id})`); + context.stats.refreshGuild(channel.guild.id).catch(() => undefined); } }; diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts index 4b9b479..faa1575 100644 --- a/src/events/guildMemberAdd.ts +++ b/src/events/guildMemberAdd.ts @@ -47,6 +47,7 @@ const event: EventHandler = { } } context.logging.logMemberJoin(member); + context.stats.refreshGuild(member.guild.id).catch(() => undefined); } }; diff --git a/src/events/guildMemberRemove.ts b/src/events/guildMemberRemove.ts index 573f97c..c39a78d 100644 --- a/src/events/guildMemberRemove.ts +++ b/src/events/guildMemberRemove.ts @@ -6,6 +6,7 @@ const event: EventHandler = { name: 'guildMemberRemove', execute(member: GuildMember) { context.logging.logMemberLeave(member); + context.stats.refreshGuild(member.guild.id).catch(() => undefined); } }; diff --git a/src/events/ready.ts b/src/events/ready.ts index 182aa6a..a0efc83 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -38,6 +38,10 @@ const event: EventHandler = { for (const gid of settingsStore.all().keys()) { context.reactionRoles.resyncGuild(gid).catch((err) => logger.warn(`reaction roles sync failed for ${gid}: ${err}`)); } + context.stats.startScheduler(); + for (const [gid] of client.guilds.cache) { + context.stats.refreshGuild(gid).catch((err) => logger.warn(`stats refresh failed for ${gid}: ${err}`)); + } } catch (err) { logger.warn(`Ready handler failed: ${err}`); } diff --git a/src/index.ts b/src/index.ts index f96dbe6..423e7f0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,6 +35,7 @@ async function bootstrap() { context.events.setClient(client); context.events.startScheduler(); context.register.setClient(client); + context.stats.setClient(client); await context.reactionRoles.loadCache(); logger.setSink((entry) => context.admin.pushLog(entry)); for (const gid of settingsStore.all().keys()) { diff --git a/src/services/moduleService.ts b/src/services/moduleService.ts index 0ee722d..01104d4 100644 --- a/src/services/moduleService.ts +++ b/src/services/moduleService.ts @@ -11,7 +11,8 @@ export type ModuleKey = | 'birthdayEnabled' | 'reactionRolesEnabled' | 'eventsEnabled' - | 'registerEnabled'; + | 'registerEnabled' + | 'serverStatsEnabled'; export interface GuildModuleState { key: ModuleKey; @@ -31,7 +32,8 @@ const MODULES: Record = { birthdayEnabled: { name: 'Birthday', description: 'Geburtstage speichern und Glueckwuensche senden.' }, reactionRolesEnabled: { name: 'Reaction Roles', description: 'Reaktionen vergeben und entfernen Rollen.' }, eventsEnabled: { name: 'Termine', description: 'Events planen, erinnern und Anmeldungen sammeln.' }, - registerEnabled: { name: 'Register', description: 'Registrierungsformulare und Bewerbungen.' } + registerEnabled: { name: 'Register', description: 'Registrierungsformulare und Bewerbungen.' }, + serverStatsEnabled: { name: 'Server Stats', description: 'Zeigt Member-/Channel-Zahlen als Voice-Statistiken an.' } }; export class BotModuleService { @@ -53,6 +55,7 @@ export class BotModuleService { if (key === 'birthdayEnabled') enabled = cfg.birthdayEnabled === true || cfg.birthdayConfig?.enabled === true; if (key === 'reactionRolesEnabled') enabled = cfg.reactionRolesEnabled === true || cfg.reactionRolesConfig?.enabled === true; if (key === 'eventsEnabled') enabled = (cfg as any).eventsEnabled !== false; + if (key === 'serverStatsEnabled') enabled = (cfg as any).serverStatsEnabled === true || (cfg as any).serverStatsConfig?.enabled === true; return { key: key as ModuleKey, name: meta.name, diff --git a/src/services/statsService.ts b/src/services/statsService.ts new file mode 100644 index 0000000..8a57375 --- /dev/null +++ b/src/services/statsService.ts @@ -0,0 +1,251 @@ +import { randomUUID } from 'crypto'; +import { CategoryChannel, ChannelType, Client, Guild, PermissionFlagsBits } from 'discord.js'; +import { settingsStore } from '../config/state'; +import { logger } from '../utils/logger'; + +export type StatCounterType = + | 'members_total' + | 'members_humans' + | 'members_bots' + | 'boosts' + | 'text_channels' + | 'voice_channels' + | 'roles'; + +export interface StatCounter { + id: string; + type: StatCounterType; + label?: string; + format?: string; + channelId?: string; +} + +export interface ServerStatsConfig { + enabled: boolean; + categoryId?: string; + categoryName?: string; + refreshMinutes: number; + cleanupOrphans?: boolean; + items: StatCounter[]; +} + +const STAT_META: Record< + StatCounterType, + { + label: string; + defaultFormat: string; + } +> = { + members_total: { label: 'Mitglieder', defaultFormat: '{label}: {value}' }, + members_humans: { label: 'Menschen', defaultFormat: '{label}: {value}' }, + members_bots: { label: 'Bots', defaultFormat: '{label}: {value}' }, + boosts: { label: 'Boosts', defaultFormat: '{label}: {value}' }, + text_channels: { label: 'Text Channels', defaultFormat: '{label}: {value}' }, + voice_channels: { label: 'Voice Channels', defaultFormat: '{label}: {value}' }, + roles: { label: 'Rollen', defaultFormat: '{label}: {value}' } +}; + +export class StatsService { + private client: Client | null = null; + private interval?: NodeJS.Timeout; + private lastRun = new Map(); + + public setClient(client: Client) { + this.client = client; + } + + public stop() { + if (this.interval) clearInterval(this.interval); + this.interval = undefined; + } + + public startScheduler() { + this.stop(); + this.interval = setInterval(() => this.tick(), 60 * 1000); + } + + public async getConfig(guildId: string): Promise { + const cfg = settingsStore.get(guildId); + const statsCfg = (cfg as any)?.serverStatsConfig || {}; + const enabled = (cfg as any)?.serverStatsEnabled ?? statsCfg.enabled ?? false; + return this.normalizeConfig({ ...statsCfg, enabled }); + } + + public async saveConfig(guildId: string, config: Partial) { + const previous = await this.getConfig(guildId); + const normalized = this.normalizeConfig({ ...previous, ...config }); + const synced = await this.syncGuild(guildId, normalized, previous); + await settingsStore.set(guildId, { serverStatsEnabled: synced.enabled, serverStatsConfig: synced } as any); + this.lastRun.set(guildId, Date.now()); + return synced; + } + + public async refreshGuild(guildId: string) { + const cfg = await this.getConfig(guildId); + if (!cfg.enabled) return cfg; + const synced = await this.syncGuild(guildId, cfg, cfg); + await settingsStore.set(guildId, { serverStatsEnabled: synced.enabled, serverStatsConfig: synced } as any); + this.lastRun.set(guildId, Date.now()); + return synced; + } + + public async disableGuild(guildId: string) { + await settingsStore.set(guildId, { serverStatsEnabled: false } as any); + } + + private normalizeConfig(config: Partial): ServerStatsConfig { + const fallbackItems = Array.isArray(config.items) ? config.items : []; + const items = fallbackItems + .filter((i) => i && (i as any).type && STAT_META[(i as any).type as StatCounterType]) + .slice(0, 8) + .map((i) => { + const type = (i as any).type as StatCounterType; + const meta = STAT_META[type]; + return { + id: i.id || randomUUID(), + type, + label: i.label || meta.label, + format: i.format || meta.defaultFormat, + channelId: i.channelId + } as StatCounter; + }); + const refreshMinutes = Number.isFinite(config.refreshMinutes) ? Number(config.refreshMinutes) : 10; + return { + enabled: !!config.enabled, + categoryId: config.categoryId || undefined, + categoryName: config.categoryName || '📊 Server Stats', + refreshMinutes: Math.max(1, Math.min(180, refreshMinutes)), + cleanupOrphans: config.cleanupOrphans ?? false, + items: items.length ? items : [this.defaultItem()] + }; + } + + private defaultItem(): StatCounter { + return { + id: randomUUID(), + type: 'members_total', + label: STAT_META['members_total'].label, + format: STAT_META['members_total'].defaultFormat + }; + } + + private formatName(item: StatCounter, value: number) { + const meta = STAT_META[item.type]; + const label = item.label || meta.label; + const base = (item.format || meta.defaultFormat || '{label}: {value}') + .replace('{label}', label) + .replace('{value}', value.toLocaleString('de-DE')); + return base.slice(0, 96); + } + + private async syncGuild(guildId: string, cfg: ServerStatsConfig, previous?: ServerStatsConfig): Promise { + if (!this.client) return cfg; + const guild = await this.client.guilds.fetch(guildId).catch(() => null); + if (!guild) return cfg; + const category = await this.ensureCategory(guild, cfg); + const managedIds = new Set(); + + for (const item of cfg.items) { + const value = this.computeValue(guild, item.type); + const desiredName = this.formatName(item, value); + let channel = + (item.channelId && (await guild.channels.fetch(item.channelId).catch(() => null))) || + null; + if (!channel) { + channel = await guild.channels + .create({ + name: desiredName, + type: ChannelType.GuildVoice, + parent: category?.id, + permissionOverwrites: [{ id: guild.roles.everyone.id, deny: [PermissionFlagsBits.Connect] }], + userLimit: 0, + bitrate: 8000 + }) + .catch((err) => { + logger.warn(`Failed to create stats channel in ${guild.id}: ${err?.message || err}`); + return null; + }); + if (channel) item.channelId = channel.id; + } else if (channel.type === ChannelType.GuildVoice || channel.type === ChannelType.GuildStageVoice) { + const needsParent = category && channel.parentId !== category.id; + const overwritesMissing = !channel.permissionOverwrites.cache.some( + (ow) => ow.id === guild.roles.everyone.id && ow.deny.has(PermissionFlagsBits.Connect) + ); + const editData: any = { name: desiredName }; + if (needsParent) editData.parent = category.id; + if (overwritesMissing) editData.permissionOverwrites = [{ id: guild.roles.everyone.id, deny: [PermissionFlagsBits.Connect] }]; + if ((channel as any).userLimit !== 0) editData.userLimit = 0; + await channel.edit(editData).catch(() => undefined); + } + if (channel?.id) managedIds.add(channel.id); + } + + if (cfg.cleanupOrphans && previous?.items) { + for (const old of previous.items) { + if (old.channelId && !managedIds.has(old.channelId)) { + const ch = await guild.channels.fetch(old.channelId).catch(() => null); + if (ch && ch.parentId === category?.id) { + await ch.delete('Papo Server Stats entfernt').catch(() => undefined); + } + } + } + } + + return { ...cfg, categoryId: category?.id }; + } + + private async ensureCategory(guild: Guild, cfg: ServerStatsConfig) { + if (cfg.categoryId) { + const existing = await guild.channels.fetch(cfg.categoryId).catch(() => null); + if (existing && existing.type === ChannelType.GuildCategory) return existing as CategoryChannel; + } + const name = cfg.categoryName || '📊 Server Stats'; + const found = guild.channels.cache.find( + (c) => c.type === ChannelType.GuildCategory && c.name.toLowerCase() === name.toLowerCase() + ) as CategoryChannel | undefined; + if (found) return found; + const created = await guild.channels + .create({ + name, + type: ChannelType.GuildCategory, + permissionOverwrites: [{ id: guild.roles.everyone.id, deny: [PermissionFlagsBits.Connect] }] + }) + .catch(() => null); + return created as CategoryChannel | null; + } + + private computeValue(guild: Guild, type: StatCounterType): number { + switch (type) { + case 'members_total': + return guild.memberCount ?? 0; + case 'members_humans': { + const humans = guild.members.cache.filter((m) => !m.user.bot).size; + return humans || Math.max(0, (guild.memberCount ?? 0) - guild.members.cache.filter((m) => m.user.bot).size); + } + case 'members_bots': + return guild.members.cache.filter((m) => m.user.bot).size; + case 'boosts': + return guild.premiumSubscriptionCount ?? 0; + case 'text_channels': + return guild.channels.cache.filter((c) => c.isTextBased() && c.type !== ChannelType.GuildVoice && c.type !== ChannelType.GuildStageVoice).size; + case 'voice_channels': + return guild.channels.cache.filter((c) => c.isVoiceBased()).size; + case 'roles': + return guild.roles.cache.size; + default: + return guild.memberCount ?? 0; + } + } + + private async tick() { + const now = Date.now(); + for (const guildId of settingsStore.all().keys()) { + const cfg = await this.getConfig(guildId); + if (!cfg.enabled) continue; + const last = this.lastRun.get(guildId) || 0; + const intervalMs = Math.max(1, cfg.refreshMinutes) * 60 * 1000; + if (now - last < intervalMs) continue; + await this.refreshGuild(guildId).catch(() => undefined); + } + } +} diff --git a/src/web/routes/api.ts b/src/web/routes/api.ts index 2ac3a5f..a0d9624 100644 --- a/src/web/routes/api.ts +++ b/src/web/routes/api.ts @@ -85,7 +85,8 @@ router.get('/guild/info', requireAuth, async (req, res) => { dynamicVoiceEnabled: modules.dynamicVoiceEnabled !== false, statuspageEnabled: (modules as any).statuspageEnabled !== false, birthdayEnabled: (modules as any).birthdayEnabled !== false, - reactionRolesEnabled: (modules as any).reactionRolesEnabled !== false + reactionRolesEnabled: (modules as any).reactionRolesEnabled !== false, + serverStatsEnabled: (modules as any).serverStatsEnabled === true || (modules as any).serverStatsConfig?.enabled === true } } }); @@ -754,6 +755,27 @@ router.delete('/statuspage/service/:id', requireAuth, async (req, res) => { res.json({ ok: true }); }); +router.get('/server-stats', requireAuth, async (req, res) => { + const guildId = typeof req.query.guildId === 'string' ? req.query.guildId : undefined; + if (!guildId) return res.status(400).json({ error: 'guildId required' }); + const cfg = await context.stats.getConfig(guildId); + res.json({ config: cfg }); +}); + +router.post('/server-stats', requireAuth, async (req, res) => { + const guildId = typeof req.body.guildId === 'string' ? req.body.guildId : undefined; + if (!guildId) return res.status(400).json({ error: 'guildId required' }); + const cfg = await context.stats.saveConfig(guildId, req.body.config || {}); + res.json({ config: cfg }); +}); + +router.post('/server-stats/refresh', requireAuth, async (req, res) => { + const guildId = typeof req.body.guildId === 'string' ? req.body.guildId : undefined; + if (!guildId) return res.status(400).json({ error: 'guildId required' }); + await context.stats.refreshGuild(guildId); + res.json({ ok: true }); +}); + router.post('/settings', requireAuth, async (req, res) => { const current = req.body.guildId ? settingsStore.get(req.body.guildId) ?? {} : {}; const { @@ -779,7 +801,9 @@ router.post('/settings', requireAuth, async (req, res) => { reactionRolesEnabled, reactionRolesConfig, registerEnabled, - registerConfig + registerConfig, + serverStatsEnabled, + serverStatsConfig } = req.body; if (!guildId) return res.status(400).json({ error: 'guildId required' }); const normalizeArray = (val: any) => @@ -913,7 +937,9 @@ router.post('/settings', requireAuth, async (req, res) => { reactionRolesEnabled: parsedReactionRoles.enabled, reactionRolesConfig: parsedReactionRoles, registerEnabled: parsedRegister.enabled, - registerConfig: parsedRegister + registerConfig: parsedRegister, + serverStatsEnabled: typeof serverStatsEnabled === 'string' ? serverStatsEnabled === 'true' : serverStatsEnabled, + serverStatsConfig: serverStatsConfig }); // Live update logging target context.logging = new LoggingService(updated.logChannelId); diff --git a/src/web/routes/dashboard.ts b/src/web/routes/dashboard.ts index fd9769a..b7b0f04 100644 --- a/src/web/routes/dashboard.ts +++ b/src/web/routes/dashboard.ts @@ -1,4 +1,4 @@ -import { Router } from 'express'; +import { Router } from 'express'; const router = Router(); @@ -43,18 +43,19 @@ router.get('/', (req, res) => {