diff --git a/src/web/routes/dashboard.ts b/src/web/routes/dashboard.ts index fd9769a..7fbfc1b 100644 --- a/src/web/routes/dashboard.ts +++ b/src/web/routes/dashboard.ts @@ -43,20 +43,21 @@ router.get('/', (req, res) => { `; @@ -330,10 +331,10 @@ router.get('/', (req, res) => {

Tickets

-

Übersicht, Pipeline, SLA, Automationen, Knowledge-Base.

+

bersicht, Pipeline, SLA, Automationen, Knowledge-Base.

- + @@ -346,7 +347,7 @@ router.get('/', (req, res) => {

Ticketliste

-

Links auswählen, Details im Modal. Plus öffnet Panel-Erstellung.

+

Links auswhlen, Details im Modal. Plus ffnet Panel-Erstellung.

@@ -381,7 +382,7 @@ router.get('/', (req, res) => {

Status-Pipeline

-

Tickets nach Phase. Status per Dropdown ändern.

+

Tickets nach Phase. Status per Dropdown ndern.

@@ -430,7 +431,7 @@ router.get('/', (req, res) => {

Automationen

-

Regeln für Ticket-Aktionen.

+

Regeln fr Ticket-Aktionen.

@@ -467,7 +468,7 @@ router.get('/', (req, res) => {

Knowledge-Base

-

Artikel für Self-Service.

+

Artikel fr Self-Service.

@@ -782,6 +783,33 @@ router.get('/', (req, res) => {
+
+
+
+
+

Server Stats

+

Kategorie und Counter verwalten.

+
+
+ + + + +
+
+
+
+
+
+
+

Statistiken

+

Counter und Format anpassen.

+
+ +
+
+
+
@@ -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); });