@@ -1203,6 +1231,7 @@ router.get('/', (req, res) => {
let activeModal = null;
let automodConfigCache = {};
let modulesCache = {};
+ let serverStatsCache = { items: [] };
let dynamicVoiceCache = {};
let isAdmin = false;
let statuspageCache = { services: [] };
@@ -1240,6 +1269,7 @@ router.get('/', (req, res) => {
const welcomeEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'welcomeEnabled') ? modulesCache['welcomeEnabled'] : true;
const dynamicVoiceEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'dynamicVoiceEnabled') ? modulesCache['dynamicVoiceEnabled'] : 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 reactionRolesEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'reactionRolesEnabled') ? modulesCache['reactionRolesEnabled'] : true;
const eventsEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'eventsEnabled') ? modulesCache['eventsEnabled'] : true;
@@ -1248,6 +1278,8 @@ router.get('/', (req, res) => {
if (dynamicVoiceNav) dynamicVoiceNav.classList.toggle('hidden', !dynamicVoiceEnabled);
const statuspageNav = document.querySelector('.nav .statuspage-link');
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');
if (birthdayNav) birthdayNav.classList.toggle('hidden', !birthdayEnabled);
const reactionRolesNav = document.querySelector('.nav .reactionroles-link');
@@ -1262,6 +1294,7 @@ router.get('/', (req, res) => {
(current === 'welcome' && !welcomeEnabled) ||
(current === 'dynamicvoice' && !dynamicVoiceEnabled) ||
(current === 'statuspage' && !statuspageEnabled) ||
+ (current === 'serverstats' && !serverStatsEnabled) ||
(current === 'birthday' && !birthdayEnabled) ||
(current === 'reactionroles' && !reactionRolesEnabled) ||
(current === 'events' && !eventsEnabled) ||
@@ -1372,6 +1405,103 @@ router.get('/', (req, res) => {
if (!logs.length) guildLogs.innerHTML = '
Keine Logs';
}
+ 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 =
+ '
' + label + '
' + (item.label || label) + ' - ' + (item.format || '{label}: {value}') + '
';
+ 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 = 'Loeschen';
+ 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 = '
Keine Statistiken
';
+ }
+
+ 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);
+ }
+
async function loadStatuspage() {
if (!currentGuild) return;
const res = await fetch('/api/statuspage?guildId=' + encodeURIComponent(currentGuild));
@@ -1755,7 +1885,7 @@ router.get('/', (req, res) => {
'
' +
'User: ' +
(t.userId || '-') +
- (t.claimedBy ? ' · Supporter: ' + t.claimedBy : '') +
+ (t.claimedBy ? ' Supporter: ' + t.claimedBy : '') +
'
';
const select = document.createElement('select');
select.innerHTML =
@@ -1869,14 +1999,14 @@ router.get('/', (req, res) => {
edit.addEventListener('click', () => fillAutomationForm(r));
const del = document.createElement('button');
del.className = 'danger-btn';
- del.textContent = 'Löschen';
+ del.textContent = 'Lschen';
del.addEventListener('click', async () => {
const res = await fetch('/api/automations/' + r.id, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ guildId: currentGuild })
});
- showToast(res.ok ? 'Regel gelöscht' : 'Löschen fehlgeschlagen', !res.ok);
+ showToast(res.ok ? 'Regel gelscht' : 'Lschen fehlgeschlagen', !res.ok);
if (res.ok) loadAutomations();
});
actions.appendChild(edit);
@@ -1936,14 +2066,14 @@ router.get('/', (req, res) => {
edit.addEventListener('click', () => fillKbForm(a));
const del = document.createElement('button');
del.className = 'danger-btn';
- del.textContent = 'Löschen';
+ del.textContent = 'Lschen';
del.addEventListener('click', async () => {
const res = await fetch('/api/kb/' + a.id, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ guildId: currentGuild })
});
- showToast(res.ok ? 'Artikel gelöscht' : 'Löschen fehlgeschlagen', !res.ok);
+ showToast(res.ok ? 'Artikel gelscht' : 'Lschen fehlgeschlagen', !res.ok);
if (res.ok) loadKb();
});
actions.appendChild(edit);
@@ -1991,6 +2121,7 @@ router.get('/', (req, res) => {
modulesCache['welcomeEnabled'] = (cfg.welcomeConfig?.enabled ?? cfg.automodConfig?.welcomeConfig?.enabled ?? true) !== false;
modulesCache['dynamicVoiceEnabled'] = cfg.dynamicVoiceEnabled !== false;
modulesCache['statuspageEnabled'] = cfg.statuspageEnabled !== false && cfg.automodConfig?.statuspageEnabled !== false;
+ modulesCache['serverStatsEnabled'] = cfg.serverStatsEnabled === true || cfg.serverStatsConfig?.enabled === true;
modulesCache['birthdayEnabled'] = cfg.birthdayEnabled !== false && cfg.birthdayConfig?.enabled !== false;
modulesCache['reactionRolesEnabled'] = cfg.reactionRolesEnabled !== false && cfg.reactionRolesConfig?.enabled !== false;
modulesCache['eventsEnabled'] = cfg.eventsEnabled !== false;
@@ -2457,6 +2588,7 @@ router.get('/', (req, res) => {
list.innerHTML = '';
let ticketsActive = false;
let statuspageActive = false;
+ let serverStatsActive = false;
let birthdayActive = false;
let reactionRolesActive = false;
let eventsActive = false;
@@ -2478,17 +2610,18 @@ router.get('/', (req, res) => {
showToast(willEnable ? m.name + ' aktiviert' : m.name + ' deaktiviert');
modulesCache[m.key] = willEnable;
if (m.key === 'ticketsEnabled') applyTicketsVisibility(willEnable);
- if (m.key === 'automodEnabled') modulesCache['automodEnabled'] = willEnable;
- if (m.key === 'welcomeEnabled') modulesCache['welcomeEnabled'] = willEnable;
- if (m.key === 'statuspageEnabled') modulesCache['statuspageEnabled'] = willEnable;
- if (m.key === 'birthdayEnabled') modulesCache['birthdayEnabled'] = willEnable;
- if (m.key === 'reactionRolesEnabled') modulesCache['reactionRolesEnabled'] = willEnable;
- if (m.key === 'eventsEnabled') modulesCache['eventsEnabled'] = willEnable;
- applyNavVisibility();
- } else {
- showToast('Speichern fehlgeschlagen', true);
- }
- });
+ if (m.key === 'automodEnabled') modulesCache['automodEnabled'] = willEnable;
+ if (m.key === 'welcomeEnabled') modulesCache['welcomeEnabled'] = 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 === 'reactionRolesEnabled') modulesCache['reactionRolesEnabled'] = willEnable;
+ if (m.key === 'eventsEnabled') modulesCache['eventsEnabled'] = willEnable;
+ applyNavVisibility();
+ } else {
+ showToast('Speichern fehlgeschlagen', true);
+ }
+ });
row.appendChild(meta);
row.appendChild(toggle);
list.appendChild(row);
@@ -2497,6 +2630,7 @@ router.get('/', (req, res) => {
if (m.key === 'welcomeEnabled') modulesCache['welcomeEnabled'] = !!m.enabled;
if (m.key === 'dynamicVoiceEnabled') modulesCache['dynamicVoiceEnabled'] = !!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 === 'reactionRolesEnabled') reactionRolesActive = !!m.enabled;
if (m.key === 'eventsEnabled') eventsActive = !!m.enabled;
@@ -2504,6 +2638,7 @@ router.get('/', (req, res) => {
applyNavVisibility();
applyTicketsVisibility(ticketsActive);
if (statuspageActive) loadStatuspage();
+ if (serverStatsActive) loadServerStats();
if (birthdayActive) loadBirthday();
if (reactionRolesActive) loadReactionRoles();
if (eventsActive) loadEvents();
@@ -2512,7 +2647,7 @@ router.get('/', (req, res) => {
async function saveModuleToggle(key, enabled) {
if (!currentGuild) return false;
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];
});
payload['dynamicVoiceEnabled'] = modulesCache['dynamicVoiceEnabled'];
@@ -2716,6 +2851,10 @@ router.get('/', (req, res) => {
if (statuspageInterval) statuspageInterval.addEventListener('change', saveStatuspageConfig);
if (statuspageChannel) statuspageChannel.addEventListener('change', saveStatuspageConfig);
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) => {
if (el) el.addEventListener('input', updateWelcomePreview);
});