[deploy] Add server stats module with dashboard controls
All checks were successful
Deploy Discord Bot / deploy (push) Successful in 37s
All checks were successful
Deploy Discord Bot / deploy (push) Successful in 37s
This commit is contained in:
@@ -9,26 +9,31 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model GuildSettings {
|
model GuildSettings {
|
||||||
guildId String @id
|
guildId String @id
|
||||||
welcomeChannelId String?
|
welcomeChannelId String?
|
||||||
logChannelId String?
|
logChannelId String?
|
||||||
automodEnabled Boolean?
|
automodEnabled Boolean?
|
||||||
automodConfig Json?
|
automodConfig Json?
|
||||||
levelingEnabled Boolean?
|
levelingEnabled Boolean?
|
||||||
ticketsEnabled Boolean?
|
ticketsEnabled Boolean?
|
||||||
musicEnabled Boolean?
|
musicEnabled Boolean?
|
||||||
statuspageEnabled Boolean?
|
statuspageEnabled Boolean?
|
||||||
statuspageConfig Json?
|
statuspageConfig Json?
|
||||||
dynamicVoiceEnabled Boolean?
|
dynamicVoiceEnabled Boolean?
|
||||||
dynamicVoiceConfig Json?
|
dynamicVoiceConfig Json?
|
||||||
supportLoginConfig Json?
|
supportLoginConfig Json?
|
||||||
birthdayEnabled Boolean?
|
birthdayEnabled Boolean?
|
||||||
birthdayConfig Json?
|
birthdayConfig Json?
|
||||||
reactionRolesEnabled Boolean?
|
reactionRolesEnabled Boolean?
|
||||||
reactionRolesConfig Json?
|
reactionRolesConfig Json?
|
||||||
eventsEnabled Boolean?\n registerEnabled Boolean?\n registerConfig Json?\n supportRoleId String?
|
eventsEnabled Boolean?
|
||||||
updatedAt DateTime @updatedAt
|
registerEnabled Boolean?
|
||||||
createdAt DateTime @default(now())
|
registerConfig Json?
|
||||||
|
serverStatsEnabled Boolean?
|
||||||
|
serverStatsConfig Json?
|
||||||
|
supportRoleId String?
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
createdAt DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
model Ticket {
|
model Ticket {
|
||||||
@@ -182,7 +187,7 @@ model RegisterFormField {
|
|||||||
label String
|
label String
|
||||||
type String
|
type String
|
||||||
required Boolean @default(false)
|
required Boolean @default(false)
|
||||||
"order" Int @default(0)
|
order Int @default(0)
|
||||||
|
|
||||||
form RegisterForm @relation(fields: [formId], references: [id], onDelete: Cascade)
|
form RegisterForm @relation(fields: [formId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { EventService } from '../services/eventService';
|
|||||||
import { TicketAutomationService } from '../services/ticketAutomationService';
|
import { TicketAutomationService } from '../services/ticketAutomationService';
|
||||||
import { KnowledgeBaseService } from '../services/knowledgeBaseService';
|
import { KnowledgeBaseService } from '../services/knowledgeBaseService';
|
||||||
import { RegisterService } from '../services/registerService';
|
import { RegisterService } from '../services/registerService';
|
||||||
|
import { StatsService } from '../services/statsService';
|
||||||
|
|
||||||
export const context = {
|
export const context = {
|
||||||
client: null as Client | null,
|
client: null as Client | null,
|
||||||
@@ -33,7 +34,8 @@ export const context = {
|
|||||||
events: new EventService(),
|
events: new EventService(),
|
||||||
ticketAutomation: new TicketAutomationService(),
|
ticketAutomation: new TicketAutomationService(),
|
||||||
knowledgeBase: new KnowledgeBaseService(),
|
knowledgeBase: new KnowledgeBaseService(),
|
||||||
register: new RegisterService()
|
register: new RegisterService(),
|
||||||
|
stats: new StatsService()
|
||||||
};
|
};
|
||||||
|
|
||||||
context.modules.setHooks({
|
context.modules.setHooks({
|
||||||
@@ -63,6 +65,10 @@ context.modules.setHooks({
|
|||||||
},
|
},
|
||||||
eventsEnabled: {
|
eventsEnabled: {
|
||||||
onEnable: async (guildId: string) => context.events.tick().catch(() => undefined)
|
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)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,15 @@ export interface GuildSettings {
|
|||||||
reviewChannelId?: string;
|
reviewChannelId?: string;
|
||||||
notifyRoleIds?: string[];
|
notifyRoleIds?: string[];
|
||||||
};
|
};
|
||||||
|
serverStatsEnabled?: boolean;
|
||||||
|
serverStatsConfig?: {
|
||||||
|
enabled?: boolean;
|
||||||
|
categoryId?: string;
|
||||||
|
categoryName?: string;
|
||||||
|
refreshMinutes?: number;
|
||||||
|
cleanupOrphans?: boolean;
|
||||||
|
items?: any[];
|
||||||
|
};
|
||||||
supportRoleId?: string;
|
supportRoleId?: string;
|
||||||
welcomeEnabled?: boolean;
|
welcomeEnabled?: boolean;
|
||||||
}
|
}
|
||||||
@@ -74,23 +83,23 @@ class SettingsStore {
|
|||||||
|
|
||||||
private applyModuleDefaults(cfg: GuildSettings): GuildSettings {
|
private applyModuleDefaults(cfg: GuildSettings): GuildSettings {
|
||||||
const normalized: GuildSettings = { ...cfg };
|
const normalized: GuildSettings = { ...cfg };
|
||||||
(
|
const defaultOn = [
|
||||||
[
|
'ticketsEnabled',
|
||||||
'ticketsEnabled',
|
'automodEnabled',
|
||||||
'automodEnabled',
|
'welcomeEnabled',
|
||||||
'welcomeEnabled',
|
'levelingEnabled',
|
||||||
'levelingEnabled',
|
'musicEnabled',
|
||||||
'musicEnabled',
|
'dynamicVoiceEnabled',
|
||||||
'dynamicVoiceEnabled',
|
'statuspageEnabled',
|
||||||
'statuspageEnabled',
|
'birthdayEnabled',
|
||||||
'birthdayEnabled',
|
'reactionRolesEnabled',
|
||||||
'reactionRolesEnabled',
|
'eventsEnabled',
|
||||||
'eventsEnabled',
|
'registerEnabled'
|
||||||
'registerEnabled'
|
] as const;
|
||||||
] as const
|
defaultOn.forEach((key) => {
|
||||||
).forEach((key) => {
|
|
||||||
if (normalized[key] === undefined) normalized[key] = true;
|
if (normalized[key] === undefined) normalized[key] = true;
|
||||||
});
|
});
|
||||||
|
if (normalized.serverStatsEnabled === undefined) normalized.serverStatsEnabled = false;
|
||||||
// keep welcomeConfig flag in sync when present
|
// keep welcomeConfig flag in sync when present
|
||||||
if (normalized.welcomeConfig && normalized.welcomeConfig.enabled === undefined && normalized.welcomeEnabled !== undefined) {
|
if (normalized.welcomeConfig && normalized.welcomeConfig.enabled === undefined && normalized.welcomeEnabled !== undefined) {
|
||||||
normalized.welcomeConfig = { ...normalized.welcomeConfig, enabled: normalized.welcomeEnabled };
|
normalized.welcomeConfig = { ...normalized.welcomeConfig, enabled: normalized.welcomeEnabled };
|
||||||
@@ -124,6 +133,8 @@ class SettingsStore {
|
|||||||
reactionRolesConfig: (row as any).reactionRolesConfig ?? undefined,
|
reactionRolesConfig: (row as any).reactionRolesConfig ?? undefined,
|
||||||
registerEnabled: (row as any).registerEnabled ?? undefined,
|
registerEnabled: (row as any).registerEnabled ?? undefined,
|
||||||
registerConfig: (row as any).registerConfig ?? undefined,
|
registerConfig: (row as any).registerConfig ?? undefined,
|
||||||
|
serverStatsEnabled: (row as any).serverStatsEnabled ?? undefined,
|
||||||
|
serverStatsConfig: (row as any).serverStatsConfig ?? undefined,
|
||||||
supportRoleId: row.supportRoleId ?? undefined
|
supportRoleId: row.supportRoleId ?? undefined
|
||||||
} satisfies GuildSettings;
|
} satisfies GuildSettings;
|
||||||
this.cache.set(row.guildId, this.applyModuleDefaults(cfg));
|
this.cache.set(row.guildId, this.applyModuleDefaults(cfg));
|
||||||
@@ -206,6 +217,8 @@ class SettingsStore {
|
|||||||
reactionRolesConfig: merged.reactionRolesConfig ?? null,
|
reactionRolesConfig: merged.reactionRolesConfig ?? null,
|
||||||
registerEnabled: merged.registerEnabled ?? null,
|
registerEnabled: merged.registerEnabled ?? null,
|
||||||
registerConfig: merged.registerConfig ?? null,
|
registerConfig: merged.registerConfig ?? null,
|
||||||
|
serverStatsEnabled: (merged as any).serverStatsEnabled ?? null,
|
||||||
|
serverStatsConfig: (merged as any).serverStatsConfig ?? null,
|
||||||
supportRoleId: merged.supportRoleId ?? null
|
supportRoleId: merged.supportRoleId ?? null
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
@@ -228,6 +241,8 @@ class SettingsStore {
|
|||||||
reactionRolesConfig: merged.reactionRolesConfig ?? null,
|
reactionRolesConfig: merged.reactionRolesConfig ?? null,
|
||||||
registerEnabled: merged.registerEnabled ?? null,
|
registerEnabled: merged.registerEnabled ?? null,
|
||||||
registerConfig: merged.registerConfig ?? null,
|
registerConfig: merged.registerConfig ?? null,
|
||||||
|
serverStatsEnabled: (merged as any).serverStatsEnabled ?? null,
|
||||||
|
serverStatsConfig: (merged as any).serverStatsConfig ?? null,
|
||||||
supportRoleId: merged.supportRoleId ?? null
|
supportRoleId: merged.supportRoleId ?? null
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Add server stats module configuration
|
||||||
|
ALTER TABLE "GuildSettings"
|
||||||
|
ADD COLUMN "serverStatsEnabled" BOOLEAN,
|
||||||
|
ADD COLUMN "serverStatsConfig" JSONB;
|
||||||
@@ -28,6 +28,8 @@ model GuildSettings {
|
|||||||
eventsEnabled Boolean?
|
eventsEnabled Boolean?
|
||||||
registerEnabled Boolean?
|
registerEnabled Boolean?
|
||||||
registerConfig Json?
|
registerConfig Json?
|
||||||
|
serverStatsEnabled Boolean?
|
||||||
|
serverStatsConfig Json?
|
||||||
supportRoleId String?
|
supportRoleId String?
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const event: EventHandler = {
|
|||||||
execute(channel: GuildChannel) {
|
execute(channel: GuildChannel) {
|
||||||
if (!channel.guild) return;
|
if (!channel.guild) return;
|
||||||
context.logging.logSystem(channel.guild, `Channel erstellt: ${channel.name} (${channel.id})`);
|
context.logging.logSystem(channel.guild, `Channel erstellt: ${channel.name} (${channel.id})`);
|
||||||
|
context.stats.refreshGuild(channel.guild.id).catch(() => undefined);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const event: EventHandler = {
|
|||||||
execute(channel: GuildChannel) {
|
execute(channel: GuildChannel) {
|
||||||
if (!channel.guild) return;
|
if (!channel.guild) return;
|
||||||
context.logging.logSystem(channel.guild, `Channel gelöscht: ${channel.name} (${channel.id})`);
|
context.logging.logSystem(channel.guild, `Channel gelöscht: ${channel.name} (${channel.id})`);
|
||||||
|
context.stats.refreshGuild(channel.guild.id).catch(() => undefined);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const event: EventHandler = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
context.logging.logMemberJoin(member);
|
context.logging.logMemberJoin(member);
|
||||||
|
context.stats.refreshGuild(member.guild.id).catch(() => undefined);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const event: EventHandler = {
|
|||||||
name: 'guildMemberRemove',
|
name: 'guildMemberRemove',
|
||||||
execute(member: GuildMember) {
|
execute(member: GuildMember) {
|
||||||
context.logging.logMemberLeave(member);
|
context.logging.logMemberLeave(member);
|
||||||
|
context.stats.refreshGuild(member.guild.id).catch(() => undefined);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ const event: EventHandler = {
|
|||||||
for (const gid of settingsStore.all().keys()) {
|
for (const gid of settingsStore.all().keys()) {
|
||||||
context.reactionRoles.resyncGuild(gid).catch((err) => logger.warn(`reaction roles sync failed for ${gid}: ${err}`));
|
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) {
|
} catch (err) {
|
||||||
logger.warn(`Ready handler failed: ${err}`);
|
logger.warn(`Ready handler failed: ${err}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ async function bootstrap() {
|
|||||||
context.events.setClient(client);
|
context.events.setClient(client);
|
||||||
context.events.startScheduler();
|
context.events.startScheduler();
|
||||||
context.register.setClient(client);
|
context.register.setClient(client);
|
||||||
|
context.stats.setClient(client);
|
||||||
await context.reactionRoles.loadCache();
|
await context.reactionRoles.loadCache();
|
||||||
logger.setSink((entry) => context.admin.pushLog(entry));
|
logger.setSink((entry) => context.admin.pushLog(entry));
|
||||||
for (const gid of settingsStore.all().keys()) {
|
for (const gid of settingsStore.all().keys()) {
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ export type ModuleKey =
|
|||||||
| 'birthdayEnabled'
|
| 'birthdayEnabled'
|
||||||
| 'reactionRolesEnabled'
|
| 'reactionRolesEnabled'
|
||||||
| 'eventsEnabled'
|
| 'eventsEnabled'
|
||||||
| 'registerEnabled';
|
| 'registerEnabled'
|
||||||
|
| 'serverStatsEnabled';
|
||||||
|
|
||||||
export interface GuildModuleState {
|
export interface GuildModuleState {
|
||||||
key: ModuleKey;
|
key: ModuleKey;
|
||||||
@@ -31,7 +32,8 @@ const MODULES: Record<ModuleKey, { name: string; description: string }> = {
|
|||||||
birthdayEnabled: { name: 'Birthday', description: 'Geburtstage speichern und Glueckwuensche senden.' },
|
birthdayEnabled: { name: 'Birthday', description: 'Geburtstage speichern und Glueckwuensche senden.' },
|
||||||
reactionRolesEnabled: { name: 'Reaction Roles', description: 'Reaktionen vergeben und entfernen Rollen.' },
|
reactionRolesEnabled: { name: 'Reaction Roles', description: 'Reaktionen vergeben und entfernen Rollen.' },
|
||||||
eventsEnabled: { name: 'Termine', description: 'Events planen, erinnern und Anmeldungen sammeln.' },
|
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 {
|
export class BotModuleService {
|
||||||
@@ -53,6 +55,7 @@ export class BotModuleService {
|
|||||||
if (key === 'birthdayEnabled') enabled = cfg.birthdayEnabled === true || cfg.birthdayConfig?.enabled === true;
|
if (key === 'birthdayEnabled') enabled = cfg.birthdayEnabled === true || cfg.birthdayConfig?.enabled === true;
|
||||||
if (key === 'reactionRolesEnabled') enabled = cfg.reactionRolesEnabled === true || cfg.reactionRolesConfig?.enabled === true;
|
if (key === 'reactionRolesEnabled') enabled = cfg.reactionRolesEnabled === true || cfg.reactionRolesConfig?.enabled === true;
|
||||||
if (key === 'eventsEnabled') enabled = (cfg as any).eventsEnabled !== false;
|
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 {
|
return {
|
||||||
key: key as ModuleKey,
|
key: key as ModuleKey,
|
||||||
name: meta.name,
|
name: meta.name,
|
||||||
|
|||||||
251
src/services/statsService.ts
Normal file
251
src/services/statsService.ts
Normal file
@@ -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<string, number>();
|
||||||
|
|
||||||
|
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<ServerStatsConfig> {
|
||||||
|
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<ServerStatsConfig>) {
|
||||||
|
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>): 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<ServerStatsConfig> {
|
||||||
|
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<string>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,7 +85,8 @@ router.get('/guild/info', requireAuth, async (req, res) => {
|
|||||||
dynamicVoiceEnabled: modules.dynamicVoiceEnabled !== false,
|
dynamicVoiceEnabled: modules.dynamicVoiceEnabled !== false,
|
||||||
statuspageEnabled: (modules as any).statuspageEnabled !== false,
|
statuspageEnabled: (modules as any).statuspageEnabled !== false,
|
||||||
birthdayEnabled: (modules as any).birthdayEnabled !== 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 });
|
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) => {
|
router.post('/settings', requireAuth, async (req, res) => {
|
||||||
const current = req.body.guildId ? settingsStore.get(req.body.guildId) ?? {} : {};
|
const current = req.body.guildId ? settingsStore.get(req.body.guildId) ?? {} : {};
|
||||||
const {
|
const {
|
||||||
@@ -779,7 +801,9 @@ router.post('/settings', requireAuth, async (req, res) => {
|
|||||||
reactionRolesEnabled,
|
reactionRolesEnabled,
|
||||||
reactionRolesConfig,
|
reactionRolesConfig,
|
||||||
registerEnabled,
|
registerEnabled,
|
||||||
registerConfig
|
registerConfig,
|
||||||
|
serverStatsEnabled,
|
||||||
|
serverStatsConfig
|
||||||
} = req.body;
|
} = req.body;
|
||||||
if (!guildId) return res.status(400).json({ error: 'guildId required' });
|
if (!guildId) return res.status(400).json({ error: 'guildId required' });
|
||||||
const normalizeArray = (val: any) =>
|
const normalizeArray = (val: any) =>
|
||||||
@@ -913,7 +937,9 @@ router.post('/settings', requireAuth, async (req, res) => {
|
|||||||
reactionRolesEnabled: parsedReactionRoles.enabled,
|
reactionRolesEnabled: parsedReactionRoles.enabled,
|
||||||
reactionRolesConfig: parsedReactionRoles,
|
reactionRolesConfig: parsedReactionRoles,
|
||||||
registerEnabled: parsedRegister.enabled,
|
registerEnabled: parsedRegister.enabled,
|
||||||
registerConfig: parsedRegister
|
registerConfig: parsedRegister,
|
||||||
|
serverStatsEnabled: typeof serverStatsEnabled === 'string' ? serverStatsEnabled === 'true' : serverStatsEnabled,
|
||||||
|
serverStatsConfig: serverStatsConfig
|
||||||
});
|
});
|
||||||
// Live update logging target
|
// Live update logging target
|
||||||
context.logging = new LoggingService(updated.logChannelId);
|
context.logging = new LoggingService(updated.logChannelId);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -43,18 +43,19 @@ router.get('/', (req, res) => {
|
|||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="brand">Papo Control</div>
|
<div class="brand">Papo Control</div>
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
<a class="active" href="#overview" data-target="overview"><span class="icon">🏠</span> Uebersicht</a>
|
<a class="active" href="#overview" data-target="overview"><span class="icon">ðŸ </span> Uebersicht</a>
|
||||||
<a href="#tickets" data-target="tickets"><span class="icon">🎫</span> Ticketsystem</a>
|
<a href="#tickets" data-target="tickets"><span class="icon">🎫</span> Ticketsystem</a>
|
||||||
<a href="#automod" data-target="automod" class="automod-link"><span class="icon">🛡️</span> Automod</a>
|
<a href="#automod" data-target="automod" class="automod-link"><span class="icon">🛡ï¸</span> Automod</a>
|
||||||
<a href="#welcome" data-target="welcome" class="welcome-link"><span class="icon">✨</span> Willkommen</a>
|
<a href="#welcome" data-target="welcome" class="welcome-link"><span class="icon">✨</span> Willkommen</a>
|
||||||
<a href="#dynamicvoice" data-target="dynamicvoice" class="dynamicvoice-link"><span class="icon">🎧</span> Dynamic Voice</a>
|
<a href="#dynamicvoice" data-target="dynamicvoice" class="dynamicvoice-link"><span class="icon">🎧</span> Dynamic Voice</a>
|
||||||
<a href="#birthday" data-target="birthday" class="birthday-link"><span class="icon">🎂</span> Birthday</a>
|
<a href="#birthday" data-target="birthday" class="birthday-link"><span class="icon">🎂</span> Birthday</a>
|
||||||
<a href="#reactionroles" data-target="reactionroles" class="reactionroles-link"><span class="icon">😎</span> Reaction Roles</a>
|
<a href="#reactionroles" data-target="reactionroles" class="reactionroles-link"><span class="icon">😎</span> Reaction Roles</a>
|
||||||
<a href="#statuspage" data-target="statuspage" class="statuspage-link"><span class="icon">🖥️</span> Statuspage</a>
|
<a href="#statuspage" data-target="statuspage" class="statuspage-link"><span class="icon">🖥ï¸</span> Statuspage</a>
|
||||||
<a href="#settings" data-target="settings"><span class="icon">⚙️</span> Einstellungen</a>
|
<a href="#serverstats" data-target="serverstats" class="serverstats-link"><span class="icon">??</span> Server Stats</a>
|
||||||
<a href="#modules" data-target="modules"><span class="icon">🧩</span> Module</a>
|
<a href="#settings" data-target="settings"><span class="icon">âš™ï¸</span> Einstellungen</a>
|
||||||
<a href="#events" data-target="events" class="events-link"><span class="icon">📅</span> Events</a>
|
<a href="#modules" data-target="modules"><span class="icon">🧩</span> Module</a>
|
||||||
<a href="#admin" data-target="admin" class="admin-link hidden"><span class="icon">🛡</span> Admin</a>
|
<a href="#events" data-target="events" class="events-link"><span class="icon">📅</span> Events</a>
|
||||||
|
<a href="#admin" data-target="admin" class="admin-link hidden"><span class="icon">🛡</span> Admin</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="muted">Angemeldet als <span id="userInfo"></span></div>
|
<div class="muted">Angemeldet als <span id="userInfo"></span></div>
|
||||||
<button id="logoutBtn" class="logout">Logout</button>
|
<button id="logoutBtn" class="logout">Logout</button>
|
||||||
@@ -330,10 +331,10 @@ router.get('/', (req, res) => {
|
|||||||
<div class="card" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center; justify-content:space-between;">
|
<div class="card" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center; justify-content:space-between;">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-title">Tickets</p>
|
<p class="section-title">Tickets</p>
|
||||||
<p class="section-sub"><EFBFBD>bersicht, Pipeline, SLA, Automationen, Knowledge-Base.</p>
|
<p class="section-sub">Übersicht, Pipeline, SLA, Automationen, Knowledge-Base.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" style="gap:8px; flex-wrap:wrap;">
|
<div class="row" style="gap:8px; flex-wrap:wrap;">
|
||||||
<button class="secondary-btn ticket-tab-btn active" data-tab="overview"><EFBFBD>bersicht</button>
|
<button class="secondary-btn ticket-tab-btn active" data-tab="overview">Übersicht</button>
|
||||||
<button class="secondary-btn ticket-tab-btn" data-tab="pipeline">Pipeline</button>
|
<button class="secondary-btn ticket-tab-btn" data-tab="pipeline">Pipeline</button>
|
||||||
<button class="secondary-btn ticket-tab-btn" data-tab="sla">SLA</button>
|
<button class="secondary-btn ticket-tab-btn" data-tab="sla">SLA</button>
|
||||||
<button class="secondary-btn ticket-tab-btn" data-tab="automations">Automationen</button>
|
<button class="secondary-btn ticket-tab-btn" data-tab="automations">Automationen</button>
|
||||||
@@ -346,7 +347,7 @@ router.get('/', (req, res) => {
|
|||||||
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
|
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-title">Ticketliste</p>
|
<p class="section-title">Ticketliste</p>
|
||||||
<p class="section-sub">Links ausw<EFBFBD>hlen, Details im Modal. Plus <EFBFBD>ffnet Panel-Erstellung.</p>
|
<p class="section-sub">Links auswählen, Details im Modal. Plus öffnet Panel-Erstellung.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" style="gap:10px;">
|
<div class="row" style="gap:10px;">
|
||||||
<button class="secondary-btn" id="openSupportLogin" type="button">Support-Login Einstellungen</button>
|
<button class="secondary-btn" id="openSupportLogin" type="button">Support-Login Einstellungen</button>
|
||||||
@@ -381,7 +382,7 @@ router.get('/', (req, res) => {
|
|||||||
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px;">
|
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px;">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-title">Status-Pipeline</p>
|
<p class="section-title">Status-Pipeline</p>
|
||||||
<p class="section-sub">Tickets nach Phase. Status per Dropdown <EFBFBD>ndern.</p>
|
<p class="section-sub">Tickets nach Phase. Status per Dropdown ändern.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(240px,1fr)); gap:12px;" id="pipelineGrid">
|
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(240px,1fr)); gap:12px;" id="pipelineGrid">
|
||||||
@@ -430,7 +431,7 @@ router.get('/', (req, res) => {
|
|||||||
<div style="display:flex; justify-content:space-between; gap:12px; flex-wrap:wrap;">
|
<div style="display:flex; justify-content:space-between; gap:12px; flex-wrap:wrap;">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-title">Automationen</p>
|
<p class="section-title">Automationen</p>
|
||||||
<p class="section-sub">Regeln f<EFBFBD>r Ticket-Aktionen.</p>
|
<p class="section-sub">Regeln für Ticket-Aktionen.</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="secondary-btn" id="addAutomation">Neue Regel</button>
|
<button class="secondary-btn" id="addAutomation">Neue Regel</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -467,7 +468,7 @@ router.get('/', (req, res) => {
|
|||||||
<div style="display:flex; justify-content:space-between; gap:12px; flex-wrap:wrap;">
|
<div style="display:flex; justify-content:space-between; gap:12px; flex-wrap:wrap;">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-title">Knowledge-Base</p>
|
<p class="section-title">Knowledge-Base</p>
|
||||||
<p class="section-sub">Artikel f<EFBFBD>r Self-Service.</p>
|
<p class="section-sub">Artikel für Self-Service.</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="secondary-btn" id="addKb">Neuer Artikel</button>
|
<button class="secondary-btn" id="addKb">Neuer Artikel</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -586,7 +587,7 @@ router.get('/', (req, res) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="form-label">Embed Footer</label>
|
<label class="form-label">Embed Footer</label>
|
||||||
<input id="welcomeFooter" placeholder="Schön, dass du da bist!" />
|
<input id="welcomeFooter" placeholder="Schön, dass du da bist!" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
@@ -609,7 +610,7 @@ router.get('/', (req, res) => {
|
|||||||
<div class="embed-color" id="welcomePreviewColor"></div>
|
<div class="embed-color" id="welcomePreviewColor"></div>
|
||||||
<div class="embed-body">
|
<div class="embed-body">
|
||||||
<div class="embed-title" id="welcomePreviewTitle">Willkommen!</div>
|
<div class="embed-title" id="welcomePreviewTitle">Willkommen!</div>
|
||||||
<div class="embed-desc" id="welcomePreviewDesc">Schön, dass du da bist.</div>
|
<div class="embed-desc" id="welcomePreviewDesc">Schön, dass du da bist.</div>
|
||||||
<div class="embed-footer" id="welcomePreviewFooter">Footer</div>
|
<div class="embed-footer" id="welcomePreviewFooter">Footer</div>
|
||||||
<img id="welcomePreviewImage" class="embed-image" style="display:none;" />
|
<img id="welcomePreviewImage" class="embed-image" style="display:none;" />
|
||||||
</div>
|
</div>
|
||||||
@@ -736,7 +737,7 @@ router.get('/', (req, res) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="form-label">Eintraege (Emoji | Role ID | Label | Beschreibung)</label>
|
<label class="form-label">Eintraege (Emoji | Role ID | Label | Beschreibung)</label>
|
||||||
<textarea id="reactionRoleEntries" rows="4" placeholder="😀 | 123456789 | Freunde | Erhaelt Freunde Rolle"></textarea>
|
<textarea id="reactionRoleEntries" rows="4" placeholder="😀 | 123456789 | Freunde | Erhaelt Freunde Rolle"></textarea>
|
||||||
<p class="muted">Eine Zeile pro Zuordnung. Label/Beschreibung optional.</p>
|
<p class="muted">Eine Zeile pro Zuordnung. Label/Beschreibung optional.</p>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; justify-content:flex-end; gap:10px;">
|
<div style="display:flex; justify-content:flex-end; gap:10px;">
|
||||||
@@ -782,6 +783,33 @@ router.get('/', (req, res) => {
|
|||||||
<div id="statuspageServices" class="module-list"></div>
|
<div id="statuspageServices" class="module-list"></div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="section" data-section="serverstats">
|
||||||
|
<section class="card">
|
||||||
|
<div class="row" style="justify-content:space-between; align-items:center;">
|
||||||
|
<div>
|
||||||
|
<p class="section-title">Server Stats</p>
|
||||||
|
<p class="section-sub">Erstellt eine Kategorie mit Voice-Statistiken.</p>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label class="form-label">Kategorie-Name</label>
|
||||||
|
<input id="statsCategoryName" placeholder="?? Server Stats" />
|
||||||
|
<label class="form-label">Refresh (Min)</label>
|
||||||
|
<input id="statsRefresh" type="number" min="1" step="1" placeholder="10" />
|
||||||
|
<div id="statsToggle" class="switch"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="card">
|
||||||
|
<div class="row" style="justify-content:space-between; align-items:center;">
|
||||||
|
<div>
|
||||||
|
<p class="section-title">Statistiken</p>
|
||||||
|
<p class="section-sub">Waehle Counter und Format.</p>
|
||||||
|
</div>
|
||||||
|
<button class="icon-button" id="statsAddItem">+</button>
|
||||||
|
</div>
|
||||||
|
<div id="statsItems" class="module-list"></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
<div class="section" data-section="events">
|
<div class="section" data-section="events">
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="row" style="justify-content:space-between; align-items:center;">
|
<div class="row" style="justify-content:space-between; align-items:center;">
|
||||||
@@ -810,7 +838,7 @@ router.get('/', (req, res) => {
|
|||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="row" style="justify-content:space-between;">
|
<div class="row" style="justify-content:space-between;">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-title">Aktivität (letzte 24h)</p>
|
<p class="section-title">Aktivität (letzte 24h)</p>
|
||||||
<p class="section-sub">Events/Commands pro Stunde</p>
|
<p class="section-sub">Events/Commands pro Stunde</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -820,7 +848,7 @@ router.get('/', (req, res) => {
|
|||||||
<div class="row" style="justify-content:space-between;">
|
<div class="row" style="justify-content:space-between;">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-title">Logs</p>
|
<p class="section-title">Logs</p>
|
||||||
<p class="section-sub">Neueste Einträge</p>
|
<p class="section-sub">Neueste Einträge</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="log-list" id="adminLogs"></ul>
|
<ul class="log-list" id="adminLogs"></ul>
|
||||||
@@ -847,13 +875,13 @@ router.get('/', (req, res) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="option-grid">
|
<div class="option-grid">
|
||||||
<div class="option-card"><span>👋 User Join / Leave</span><div id="logJoinLeave" class="switch on"></div></div>
|
<div class="option-card"><span>👋 User Join / Leave</span><div id="logJoinLeave" class="switch on"></div></div>
|
||||||
<div class="option-card"><span>✏️ Message Edit</span><div id="logMsgEdit" class="switch on"></div></div>
|
<div class="option-card"><span>âœï¸ Message Edit</span><div id="logMsgEdit" class="switch on"></div></div>
|
||||||
<div class="option-card"><span>🗑️ Message Delete</span><div id="logMsgDelete" class="switch on"></div></div>
|
<div class="option-card"><span>ðŸ—‘ï¸ Message Delete</span><div id="logMsgDelete" class="switch on"></div></div>
|
||||||
<div class="option-card"><span>🛡️ Automod Actions</span><div id="logAutomod" class="switch on"></div></div>
|
<div class="option-card"><span>ðŸ›¡ï¸ Automod Actions</span><div id="logAutomod" class="switch on"></div></div>
|
||||||
<div class="option-card"><span>🎫 Ticket Actions</span><div id="logTickets" class="switch on"></div></div>
|
<div class="option-card"><span>🎫 Ticket Actions</span><div id="logTickets" class="switch on"></div></div>
|
||||||
<div class="option-card"><span>🎵 Musik-Events</span><div id="logMusic" class="switch on"></div></div>
|
<div class="option-card"><span>🎵 Musik-Events</span><div id="logMusic" class="switch on"></div></div>
|
||||||
<div class="option-card"><span>⚙️ System / Channels</span><div id="logSystem" class="switch on"></div></div>
|
<div class="option-card"><span>âš™ï¸ System / Channels</span><div id="logSystem" class="switch on"></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; justify-content:flex-end; gap:10px; margin-top:12px;">
|
<div style="display:flex; justify-content:flex-end; gap:10px; margin-top:12px;">
|
||||||
<button id="loggingSave" type="button">Logging speichern</button>
|
<button id="loggingSave" type="button">Logging speichern</button>
|
||||||
@@ -1166,6 +1194,11 @@ router.get('/', (req, res) => {
|
|||||||
const statuspageChannel = document.getElementById('statuspageChannel');
|
const statuspageChannel = document.getElementById('statuspageChannel');
|
||||||
const statuspageServices = document.getElementById('statuspageServices');
|
const statuspageServices = document.getElementById('statuspageServices');
|
||||||
const statuspageAddService = document.getElementById('statuspageAddService');
|
const statuspageAddService = document.getElementById('statuspageAddService');
|
||||||
|
const statsToggle = document.getElementById('statsToggle');
|
||||||
|
const statsCategoryName = document.getElementById('statsCategoryName');
|
||||||
|
const statsRefresh = document.getElementById('statsRefresh');
|
||||||
|
const statsItems = document.getElementById('statsItems');
|
||||||
|
const statsAddItem = document.getElementById('statsAddItem');
|
||||||
const adminGuilds = document.getElementById('adminGuilds');
|
const adminGuilds = document.getElementById('adminGuilds');
|
||||||
const adminActiveGuilds = document.getElementById('adminActiveGuilds');
|
const adminActiveGuilds = document.getElementById('adminActiveGuilds');
|
||||||
const adminUptime = document.getElementById('adminUptime');
|
const adminUptime = document.getElementById('adminUptime');
|
||||||
@@ -1206,6 +1239,7 @@ router.get('/', (req, res) => {
|
|||||||
let dynamicVoiceCache = {};
|
let dynamicVoiceCache = {};
|
||||||
let isAdmin = false;
|
let isAdmin = false;
|
||||||
let statuspageCache = { services: [] };
|
let statuspageCache = { services: [] };
|
||||||
|
let serverStatsCache = { items: [] };
|
||||||
let birthdayCache = { config: {}, birthdays: [] };
|
let birthdayCache = { config: {}, birthdays: [] };
|
||||||
let reactionRoleCache = [];
|
let reactionRoleCache = [];
|
||||||
let editingReactionRole = null;
|
let editingReactionRole = null;
|
||||||
@@ -1240,6 +1274,7 @@ router.get('/', (req, res) => {
|
|||||||
const welcomeEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'welcomeEnabled') ? modulesCache['welcomeEnabled'] : true;
|
const welcomeEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'welcomeEnabled') ? modulesCache['welcomeEnabled'] : true;
|
||||||
const dynamicVoiceEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'dynamicVoiceEnabled') ? modulesCache['dynamicVoiceEnabled'] : true;
|
const dynamicVoiceEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'dynamicVoiceEnabled') ? modulesCache['dynamicVoiceEnabled'] : true;
|
||||||
const statuspageEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'statuspageEnabled') ? modulesCache['statuspageEnabled'] : true;
|
const statuspageEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'statuspageEnabled') ? modulesCache['statuspageEnabled'] : true;
|
||||||
|
const serverStatsEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'serverStatsEnabled') ? modulesCache['serverStatsEnabled'] : false;
|
||||||
const birthdayEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'birthdayEnabled') ? modulesCache['birthdayEnabled'] : true;
|
const birthdayEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'birthdayEnabled') ? modulesCache['birthdayEnabled'] : true;
|
||||||
const reactionRolesEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'reactionRolesEnabled') ? modulesCache['reactionRolesEnabled'] : true;
|
const reactionRolesEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'reactionRolesEnabled') ? modulesCache['reactionRolesEnabled'] : true;
|
||||||
const eventsEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'eventsEnabled') ? modulesCache['eventsEnabled'] : true;
|
const eventsEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'eventsEnabled') ? modulesCache['eventsEnabled'] : true;
|
||||||
@@ -1248,6 +1283,8 @@ router.get('/', (req, res) => {
|
|||||||
if (dynamicVoiceNav) dynamicVoiceNav.classList.toggle('hidden', !dynamicVoiceEnabled);
|
if (dynamicVoiceNav) dynamicVoiceNav.classList.toggle('hidden', !dynamicVoiceEnabled);
|
||||||
const statuspageNav = document.querySelector('.nav .statuspage-link');
|
const statuspageNav = document.querySelector('.nav .statuspage-link');
|
||||||
if (statuspageNav) statuspageNav.classList.toggle('hidden', !statuspageEnabled);
|
if (statuspageNav) statuspageNav.classList.toggle('hidden', !statuspageEnabled);
|
||||||
|
const serverstatsNav = document.querySelector('.nav .serverstats-link');
|
||||||
|
if (serverstatsNav) serverstatsNav.classList.toggle('hidden', !serverStatsEnabled);
|
||||||
const birthdayNav = document.querySelector('.nav .birthday-link');
|
const birthdayNav = document.querySelector('.nav .birthday-link');
|
||||||
if (birthdayNav) birthdayNav.classList.toggle('hidden', !birthdayEnabled);
|
if (birthdayNav) birthdayNav.classList.toggle('hidden', !birthdayEnabled);
|
||||||
const reactionRolesNav = document.querySelector('.nav .reactionroles-link');
|
const reactionRolesNav = document.querySelector('.nav .reactionroles-link');
|
||||||
@@ -1262,6 +1299,7 @@ router.get('/', (req, res) => {
|
|||||||
(current === 'welcome' && !welcomeEnabled) ||
|
(current === 'welcome' && !welcomeEnabled) ||
|
||||||
(current === 'dynamicvoice' && !dynamicVoiceEnabled) ||
|
(current === 'dynamicvoice' && !dynamicVoiceEnabled) ||
|
||||||
(current === 'statuspage' && !statuspageEnabled) ||
|
(current === 'statuspage' && !statuspageEnabled) ||
|
||||||
|
(current === 'serverstats' && !serverStatsEnabled) ||
|
||||||
(current === 'birthday' && !birthdayEnabled) ||
|
(current === 'birthday' && !birthdayEnabled) ||
|
||||||
(current === 'reactionroles' && !reactionRolesEnabled) ||
|
(current === 'reactionroles' && !reactionRolesEnabled) ||
|
||||||
(current === 'events' && !eventsEnabled) ||
|
(current === 'events' && !eventsEnabled) ||
|
||||||
@@ -1384,6 +1422,115 @@ router.get('/', (req, res) => {
|
|||||||
renderStatuspageServices();
|
renderStatuspageServices();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const STAT_LABELS = {
|
||||||
|
members_total: 'Mitglieder (gesamt)',
|
||||||
|
members_humans: 'Mitglieder (ohne Bots)',
|
||||||
|
members_bots: 'Bots',
|
||||||
|
boosts: 'Server Boosts',
|
||||||
|
text_channels: 'Text Channels',
|
||||||
|
voice_channels: 'Voice Channels',
|
||||||
|
roles: 'Rollen'
|
||||||
|
};
|
||||||
|
|
||||||
|
async function loadServerStats() {
|
||||||
|
if (!currentGuild) return;
|
||||||
|
const res = await fetch('/api/server-stats?guildId=' + encodeURIComponent(currentGuild));
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = await res.json();
|
||||||
|
serverStatsCache = data.config || { items: [] };
|
||||||
|
setSwitch(statsToggle, serverStatsCache.enabled !== false);
|
||||||
|
if (statsCategoryName) statsCategoryName.value = serverStatsCache.categoryName || '📊 Server Stats';
|
||||||
|
if (statsRefresh) statsRefresh.value = serverStatsCache.refreshMinutes ?? 10;
|
||||||
|
renderServerStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderServerStats() {
|
||||||
|
if (!statsItems) return;
|
||||||
|
statsItems.innerHTML = '';
|
||||||
|
(serverStatsCache.items || []).forEach((item) => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'module-item';
|
||||||
|
const meta = document.createElement('div');
|
||||||
|
meta.className = 'module-meta';
|
||||||
|
const label = STAT_LABELS[item.type] || item.type;
|
||||||
|
meta.innerHTML =
|
||||||
|
'<div class=\"module-title\">' +
|
||||||
|
label +
|
||||||
|
'</div><div class=\"module-desc\">' +
|
||||||
|
(item.label || label) +
|
||||||
|
' • ' +
|
||||||
|
(item.format || '{label}: {value}') +
|
||||||
|
'</div>';
|
||||||
|
const buttons = document.createElement('div');
|
||||||
|
buttons.className = 'row';
|
||||||
|
const edit = document.createElement('button');
|
||||||
|
edit.className = 'secondary-btn';
|
||||||
|
edit.textContent = 'Bearbeiten';
|
||||||
|
edit.addEventListener('click', () => editServerStat(item));
|
||||||
|
const del = document.createElement('button');
|
||||||
|
del.className = 'danger-btn';
|
||||||
|
del.textContent = 'Entfernen';
|
||||||
|
del.addEventListener('click', () => {
|
||||||
|
serverStatsCache.items = (serverStatsCache.items || []).filter((x) => x !== item);
|
||||||
|
renderServerStats();
|
||||||
|
saveServerStats();
|
||||||
|
});
|
||||||
|
buttons.appendChild(edit);
|
||||||
|
buttons.appendChild(del);
|
||||||
|
row.appendChild(meta);
|
||||||
|
row.appendChild(buttons);
|
||||||
|
statsItems.appendChild(row);
|
||||||
|
});
|
||||||
|
if (!(serverStatsCache.items || []).length) statsItems.innerHTML = '<div class=\"muted\">Keine Statistiken</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function editServerStat(item) {
|
||||||
|
const typeKeys = Object.keys(STAT_LABELS);
|
||||||
|
const nextType = prompt('Typ (' + typeKeys.join(', ') + ')', item?.type || 'members_total');
|
||||||
|
if (!nextType || !STAT_LABELS[nextType]) return;
|
||||||
|
const nextLabel = prompt('Label', item?.label || STAT_LABELS[nextType]) || STAT_LABELS[nextType];
|
||||||
|
const nextFormat = prompt('Format ({label} / {value})', item?.format || '{label}: {value}') || '{label}: {value}';
|
||||||
|
if (item) {
|
||||||
|
item.type = nextType;
|
||||||
|
item.label = nextLabel;
|
||||||
|
item.format = nextFormat;
|
||||||
|
} else {
|
||||||
|
(serverStatsCache.items = serverStatsCache.items || []).push({
|
||||||
|
id: (crypto.randomUUID && crypto.randomUUID()) || String(Date.now()),
|
||||||
|
type: nextType,
|
||||||
|
label: nextLabel,
|
||||||
|
format: nextFormat
|
||||||
|
});
|
||||||
|
}
|
||||||
|
renderServerStats();
|
||||||
|
saveServerStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveServerStats() {
|
||||||
|
if (!currentGuild) return;
|
||||||
|
const payload = {
|
||||||
|
guildId: currentGuild,
|
||||||
|
config: {
|
||||||
|
enabled: getSwitch(statsToggle),
|
||||||
|
categoryName: statsCategoryName?.value || undefined,
|
||||||
|
refreshMinutes: statsRefresh?.value ? Number(statsRefresh.value) : undefined,
|
||||||
|
items: serverStatsCache.items || []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const res = await fetch('/api/server-stats', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
showToast('Server Stats speichern fehlgeschlagen', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
serverStatsCache = data.config || serverStatsCache;
|
||||||
|
renderServerStats();
|
||||||
|
}
|
||||||
|
|
||||||
function renderStatuspageServices() {
|
function renderStatuspageServices() {
|
||||||
if (!statuspageServices) return;
|
if (!statuspageServices) return;
|
||||||
statuspageServices.innerHTML = '';
|
statuspageServices.innerHTML = '';
|
||||||
@@ -1419,7 +1566,7 @@ router.get('/', (req, res) => {
|
|||||||
actions.className = 'row';
|
actions.className = 'row';
|
||||||
const del = document.createElement('button');
|
const del = document.createElement('button');
|
||||||
del.className = 'danger-btn';
|
del.className = 'danger-btn';
|
||||||
del.textContent = 'Löschen';
|
del.textContent = 'Löschen';
|
||||||
del.addEventListener('click', async () => {
|
del.addEventListener('click', async () => {
|
||||||
await deleteService(svc.id);
|
await deleteService(svc.id);
|
||||||
});
|
});
|
||||||
@@ -1477,7 +1624,7 @@ router.get('/', (req, res) => {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
await loadStatuspage();
|
await loadStatuspage();
|
||||||
} else {
|
} else {
|
||||||
showToast('Service löschen fehlgeschlagen', true);
|
showToast('Service löschen fehlgeschlagen', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1755,7 +1902,7 @@ router.get('/', (req, res) => {
|
|||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class=\"ticket-meta\">User: ' +
|
'<div class=\"ticket-meta\">User: ' +
|
||||||
(t.userId || '-') +
|
(t.userId || '-') +
|
||||||
(t.claimedBy ? ' <EFBFBD> Supporter: ' + t.claimedBy : '') +
|
(t.claimedBy ? ' · Supporter: ' + t.claimedBy : '') +
|
||||||
'</div>';
|
'</div>';
|
||||||
const select = document.createElement('select');
|
const select = document.createElement('select');
|
||||||
select.innerHTML =
|
select.innerHTML =
|
||||||
@@ -1869,14 +2016,14 @@ router.get('/', (req, res) => {
|
|||||||
edit.addEventListener('click', () => fillAutomationForm(r));
|
edit.addEventListener('click', () => fillAutomationForm(r));
|
||||||
const del = document.createElement('button');
|
const del = document.createElement('button');
|
||||||
del.className = 'danger-btn';
|
del.className = 'danger-btn';
|
||||||
del.textContent = 'L<EFBFBD>schen';
|
del.textContent = 'Löschen';
|
||||||
del.addEventListener('click', async () => {
|
del.addEventListener('click', async () => {
|
||||||
const res = await fetch('/api/automations/' + r.id, {
|
const res = await fetch('/api/automations/' + r.id, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ guildId: currentGuild })
|
body: JSON.stringify({ guildId: currentGuild })
|
||||||
});
|
});
|
||||||
showToast(res.ok ? 'Regel gel<EFBFBD>scht' : 'L<EFBFBD>schen fehlgeschlagen', !res.ok);
|
showToast(res.ok ? 'Regel gelöscht' : 'Löschen fehlgeschlagen', !res.ok);
|
||||||
if (res.ok) loadAutomations();
|
if (res.ok) loadAutomations();
|
||||||
});
|
});
|
||||||
actions.appendChild(edit);
|
actions.appendChild(edit);
|
||||||
@@ -1936,14 +2083,14 @@ router.get('/', (req, res) => {
|
|||||||
edit.addEventListener('click', () => fillKbForm(a));
|
edit.addEventListener('click', () => fillKbForm(a));
|
||||||
const del = document.createElement('button');
|
const del = document.createElement('button');
|
||||||
del.className = 'danger-btn';
|
del.className = 'danger-btn';
|
||||||
del.textContent = 'L<EFBFBD>schen';
|
del.textContent = 'Löschen';
|
||||||
del.addEventListener('click', async () => {
|
del.addEventListener('click', async () => {
|
||||||
const res = await fetch('/api/kb/' + a.id, {
|
const res = await fetch('/api/kb/' + a.id, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ guildId: currentGuild })
|
body: JSON.stringify({ guildId: currentGuild })
|
||||||
});
|
});
|
||||||
showToast(res.ok ? 'Artikel gel<EFBFBD>scht' : 'L<EFBFBD>schen fehlgeschlagen', !res.ok);
|
showToast(res.ok ? 'Artikel gelöscht' : 'Löschen fehlgeschlagen', !res.ok);
|
||||||
if (res.ok) loadKb();
|
if (res.ok) loadKb();
|
||||||
});
|
});
|
||||||
actions.appendChild(edit);
|
actions.appendChild(edit);
|
||||||
@@ -2116,7 +2263,7 @@ router.get('/', (req, res) => {
|
|||||||
(s.userId || '-') +
|
(s.userId || '-') +
|
||||||
'</strong></div><div class="muted">Ende: ' +
|
'</strong></div><div class="muted">Ende: ' +
|
||||||
formatDate(s.endedAt || Date.now()) +
|
formatDate(s.endedAt || Date.now()) +
|
||||||
' · Dauer: ' +
|
' · Dauer: ' +
|
||||||
dur +
|
dur +
|
||||||
'</div>';
|
'</div>';
|
||||||
supportRecentList.appendChild(div);
|
supportRecentList.appendChild(div);
|
||||||
@@ -2174,9 +2321,9 @@ router.get('/', (req, res) => {
|
|||||||
(ev.repeatType || 'none') +
|
(ev.repeatType || 'none') +
|
||||||
'</span></div><div class="ticket-meta">Start: ' +
|
'</span></div><div class="ticket-meta">Start: ' +
|
||||||
formatDate(ev.startTime) +
|
formatDate(ev.startTime) +
|
||||||
' · Channel: ' +
|
' · Channel: ' +
|
||||||
(ev.channelId || '-') +
|
(ev.channelId || '-') +
|
||||||
' · Anmeldungen: ' +
|
' · Anmeldungen: ' +
|
||||||
(ev._count?.signups ?? 0) +
|
(ev._count?.signups ?? 0) +
|
||||||
'</div>';
|
'</div>';
|
||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
@@ -2389,7 +2536,7 @@ router.get('/', (req, res) => {
|
|||||||
meta.className = 'module-meta';
|
meta.className = 'module-meta';
|
||||||
const descParts = ['Channel: ' + (set.channelId || '-')];
|
const descParts = ['Channel: ' + (set.channelId || '-')];
|
||||||
if (set.messageId) descParts.push('Message: ' + set.messageId);
|
if (set.messageId) descParts.push('Message: ' + set.messageId);
|
||||||
meta.innerHTML = '<div class="module-title">' + (set.title || 'Reaction Role') + '</div><div class="module-desc">' + descParts.join(' • ') + '</div>';
|
meta.innerHTML = '<div class="module-title">' + (set.title || 'Reaction Role') + '</div><div class="module-desc">' + descParts.join(' • ') + '</div>';
|
||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
actions.className = 'row';
|
actions.className = 'row';
|
||||||
const editBtn = document.createElement('button');
|
const editBtn = document.createElement('button');
|
||||||
@@ -2457,6 +2604,7 @@ router.get('/', (req, res) => {
|
|||||||
list.innerHTML = '';
|
list.innerHTML = '';
|
||||||
let ticketsActive = false;
|
let ticketsActive = false;
|
||||||
let statuspageActive = false;
|
let statuspageActive = false;
|
||||||
|
let serverStatsActive = false;
|
||||||
let birthdayActive = false;
|
let birthdayActive = false;
|
||||||
let reactionRolesActive = false;
|
let reactionRolesActive = false;
|
||||||
let eventsActive = false;
|
let eventsActive = false;
|
||||||
@@ -2481,10 +2629,12 @@ router.get('/', (req, res) => {
|
|||||||
if (m.key === 'automodEnabled') modulesCache['automodEnabled'] = willEnable;
|
if (m.key === 'automodEnabled') modulesCache['automodEnabled'] = willEnable;
|
||||||
if (m.key === 'welcomeEnabled') modulesCache['welcomeEnabled'] = willEnable;
|
if (m.key === 'welcomeEnabled') modulesCache['welcomeEnabled'] = willEnable;
|
||||||
if (m.key === 'statuspageEnabled') modulesCache['statuspageEnabled'] = willEnable;
|
if (m.key === 'statuspageEnabled') modulesCache['statuspageEnabled'] = willEnable;
|
||||||
|
if (m.key === 'serverStatsEnabled') modulesCache['serverStatsEnabled'] = willEnable;
|
||||||
if (m.key === 'birthdayEnabled') modulesCache['birthdayEnabled'] = willEnable;
|
if (m.key === 'birthdayEnabled') modulesCache['birthdayEnabled'] = willEnable;
|
||||||
if (m.key === 'reactionRolesEnabled') modulesCache['reactionRolesEnabled'] = willEnable;
|
if (m.key === 'reactionRolesEnabled') modulesCache['reactionRolesEnabled'] = willEnable;
|
||||||
if (m.key === 'eventsEnabled') modulesCache['eventsEnabled'] = willEnable;
|
if (m.key === 'eventsEnabled') modulesCache['eventsEnabled'] = willEnable;
|
||||||
applyNavVisibility();
|
applyNavVisibility();
|
||||||
|
if (m.key === 'serverStatsEnabled' && willEnable) loadServerStats();
|
||||||
} else {
|
} else {
|
||||||
showToast('Speichern fehlgeschlagen', true);
|
showToast('Speichern fehlgeschlagen', true);
|
||||||
}
|
}
|
||||||
@@ -2497,6 +2647,7 @@ router.get('/', (req, res) => {
|
|||||||
if (m.key === 'welcomeEnabled') modulesCache['welcomeEnabled'] = !!m.enabled;
|
if (m.key === 'welcomeEnabled') modulesCache['welcomeEnabled'] = !!m.enabled;
|
||||||
if (m.key === 'dynamicVoiceEnabled') modulesCache['dynamicVoiceEnabled'] = !!m.enabled;
|
if (m.key === 'dynamicVoiceEnabled') modulesCache['dynamicVoiceEnabled'] = !!m.enabled;
|
||||||
if (m.key === 'statuspageEnabled') statuspageActive = !!m.enabled;
|
if (m.key === 'statuspageEnabled') statuspageActive = !!m.enabled;
|
||||||
|
if (m.key === 'serverStatsEnabled') serverStatsActive = !!m.enabled;
|
||||||
if (m.key === 'birthdayEnabled') birthdayActive = !!m.enabled;
|
if (m.key === 'birthdayEnabled') birthdayActive = !!m.enabled;
|
||||||
if (m.key === 'reactionRolesEnabled') reactionRolesActive = !!m.enabled;
|
if (m.key === 'reactionRolesEnabled') reactionRolesActive = !!m.enabled;
|
||||||
if (m.key === 'eventsEnabled') eventsActive = !!m.enabled;
|
if (m.key === 'eventsEnabled') eventsActive = !!m.enabled;
|
||||||
@@ -2504,6 +2655,7 @@ router.get('/', (req, res) => {
|
|||||||
applyNavVisibility();
|
applyNavVisibility();
|
||||||
applyTicketsVisibility(ticketsActive);
|
applyTicketsVisibility(ticketsActive);
|
||||||
if (statuspageActive) loadStatuspage();
|
if (statuspageActive) loadStatuspage();
|
||||||
|
if (serverStatsActive) loadServerStats();
|
||||||
if (birthdayActive) loadBirthday();
|
if (birthdayActive) loadBirthday();
|
||||||
if (reactionRolesActive) loadReactionRoles();
|
if (reactionRolesActive) loadReactionRoles();
|
||||||
if (eventsActive) loadEvents();
|
if (eventsActive) loadEvents();
|
||||||
@@ -2512,7 +2664,7 @@ router.get('/', (req, res) => {
|
|||||||
async function saveModuleToggle(key, enabled) {
|
async function saveModuleToggle(key, enabled) {
|
||||||
if (!currentGuild) return false;
|
if (!currentGuild) return false;
|
||||||
const payload = { guildId: currentGuild };
|
const payload = { guildId: currentGuild };
|
||||||
['ticketsEnabled', 'automodEnabled', 'welcomeEnabled', 'musicEnabled', 'levelingEnabled', 'statuspageEnabled', 'birthdayEnabled', 'reactionRolesEnabled', 'eventsEnabled'].forEach((k) => {
|
['ticketsEnabled', 'automodEnabled', 'welcomeEnabled', 'musicEnabled', 'levelingEnabled', 'statuspageEnabled', 'serverStatsEnabled', 'birthdayEnabled', 'reactionRolesEnabled', 'eventsEnabled'].forEach((k) => {
|
||||||
if (modulesCache[k] !== undefined) payload[k] = modulesCache[k];
|
if (modulesCache[k] !== undefined) payload[k] = modulesCache[k];
|
||||||
});
|
});
|
||||||
payload['dynamicVoiceEnabled'] = modulesCache['dynamicVoiceEnabled'];
|
payload['dynamicVoiceEnabled'] = modulesCache['dynamicVoiceEnabled'];
|
||||||
@@ -2716,6 +2868,10 @@ router.get('/', (req, res) => {
|
|||||||
if (statuspageInterval) statuspageInterval.addEventListener('change', saveStatuspageConfig);
|
if (statuspageInterval) statuspageInterval.addEventListener('change', saveStatuspageConfig);
|
||||||
if (statuspageChannel) statuspageChannel.addEventListener('change', saveStatuspageConfig);
|
if (statuspageChannel) statuspageChannel.addEventListener('change', saveStatuspageConfig);
|
||||||
if (statuspageAddService) statuspageAddService.addEventListener('click', addServicePrompt);
|
if (statuspageAddService) statuspageAddService.addEventListener('click', addServicePrompt);
|
||||||
|
if (statsToggle) statsToggle.addEventListener('click', async () => { statsToggle.classList.toggle('on'); await saveServerStats(); });
|
||||||
|
if (statsCategoryName) statsCategoryName.addEventListener('change', saveServerStats);
|
||||||
|
if (statsRefresh) statsRefresh.addEventListener('change', saveServerStats);
|
||||||
|
if (statsAddItem) statsAddItem.addEventListener('click', () => editServerStat(null));
|
||||||
[welcomeTitle, welcomeDescription, welcomeFooter, welcomeColor].forEach((el) => {
|
[welcomeTitle, welcomeDescription, welcomeFooter, welcomeColor].forEach((el) => {
|
||||||
if (el) el.addEventListener('input', updateWelcomePreview);
|
if (el) el.addEventListener('input', updateWelcomePreview);
|
||||||
});
|
});
|
||||||
@@ -2883,3 +3039,4 @@ router.get('/settings', (_req, res) => {
|
|||||||
export default router;
|
export default router;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user