[deploy] Restore server stats UI and sanitize dashboard encoding
All checks were successful
Deploy Discord Bot / deploy (push) Successful in 18s

This commit is contained in:
Pascal Prießnitz
2025-12-04 15:41:34 +01:00
parent 8127e81564
commit 9579dc7510

View File

@@ -43,20 +43,21 @@ 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>
</aside> </aside>
`; `;
@@ -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 auswhlen, 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 fr 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 fr 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>
@@ -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; gap:10px; flex-wrap:wrap;">
<div>
<p class="section-title">Server Stats</p>
<p class="section-sub">Kategorie und Counter verwalten.</p>
</div>
<div class="row" style="gap:8px; align-items:center;">
<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">Counter und Format anpassen.</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;">
@@ -1203,6 +1231,7 @@ router.get('/', (req, res) => {
let activeModal = null; let activeModal = null;
let automodConfigCache = {}; let automodConfigCache = {};
let modulesCache = {}; let modulesCache = {};
let serverStatsCache = { items: [] };
let dynamicVoiceCache = {}; let dynamicVoiceCache = {};
let isAdmin = false; let isAdmin = false;
let statuspageCache = { services: [] }; let statuspageCache = { services: [] };
@@ -1240,6 +1269,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 +1278,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 +1294,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) ||
@@ -1372,6 +1405,103 @@ router.get('/', (req, res) => {
if (!logs.length) guildLogs.innerHTML = '<li class="muted">Keine Logs</li>'; if (!logs.length) guildLogs.innerHTML = '<li class="muted">Keine Logs</li>';
} }
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 = '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 = '<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);
}
async function loadStatuspage() { async function loadStatuspage() {
if (!currentGuild) return; if (!currentGuild) return;
const res = await fetch('/api/statuspage?guildId=' + encodeURIComponent(currentGuild)); const res = await fetch('/api/statuspage?guildId=' + encodeURIComponent(currentGuild));
@@ -1755,7 +1885,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 +1999,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 = 'Lschen';
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 gelscht' : 'Lschen fehlgeschlagen', !res.ok);
if (res.ok) loadAutomations(); if (res.ok) loadAutomations();
}); });
actions.appendChild(edit); actions.appendChild(edit);
@@ -1936,14 +2066,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 = 'Lschen';
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 gelscht' : 'Lschen fehlgeschlagen', !res.ok);
if (res.ok) loadKb(); if (res.ok) loadKb();
}); });
actions.appendChild(edit); actions.appendChild(edit);
@@ -1991,6 +2121,7 @@ router.get('/', (req, res) => {
modulesCache['welcomeEnabled'] = (cfg.welcomeConfig?.enabled ?? cfg.automodConfig?.welcomeConfig?.enabled ?? true) !== false; modulesCache['welcomeEnabled'] = (cfg.welcomeConfig?.enabled ?? cfg.automodConfig?.welcomeConfig?.enabled ?? true) !== false;
modulesCache['dynamicVoiceEnabled'] = cfg.dynamicVoiceEnabled !== false; modulesCache['dynamicVoiceEnabled'] = cfg.dynamicVoiceEnabled !== false;
modulesCache['statuspageEnabled'] = cfg.statuspageEnabled !== false && cfg.automodConfig?.statuspageEnabled !== 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['birthdayEnabled'] = cfg.birthdayEnabled !== false && cfg.birthdayConfig?.enabled !== false;
modulesCache['reactionRolesEnabled'] = cfg.reactionRolesEnabled !== false && cfg.reactionRolesConfig?.enabled !== false; modulesCache['reactionRolesEnabled'] = cfg.reactionRolesEnabled !== false && cfg.reactionRolesConfig?.enabled !== false;
modulesCache['eventsEnabled'] = cfg.eventsEnabled !== false; modulesCache['eventsEnabled'] = cfg.eventsEnabled !== false;
@@ -2457,6 +2588,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;
@@ -2478,17 +2610,18 @@ router.get('/', (req, res) => {
showToast(willEnable ? m.name + ' aktiviert' : m.name + ' deaktiviert'); showToast(willEnable ? m.name + ' aktiviert' : m.name + ' deaktiviert');
modulesCache[m.key] = willEnable; modulesCache[m.key] = willEnable;
if (m.key === 'ticketsEnabled') applyTicketsVisibility(willEnable); if (m.key === 'ticketsEnabled') applyTicketsVisibility(willEnable);
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 === 'birthdayEnabled') modulesCache['birthdayEnabled'] = willEnable; if (m.key === 'serverStatsEnabled') modulesCache['serverStatsEnabled'] = willEnable;
if (m.key === 'reactionRolesEnabled') modulesCache['reactionRolesEnabled'] = willEnable; if (m.key === 'birthdayEnabled') modulesCache['birthdayEnabled'] = willEnable;
if (m.key === 'eventsEnabled') modulesCache['eventsEnabled'] = willEnable; if (m.key === 'reactionRolesEnabled') modulesCache['reactionRolesEnabled'] = willEnable;
applyNavVisibility(); if (m.key === 'eventsEnabled') modulesCache['eventsEnabled'] = willEnable;
} else { applyNavVisibility();
showToast('Speichern fehlgeschlagen', true); } else {
} showToast('Speichern fehlgeschlagen', true);
}); }
});
row.appendChild(meta); row.appendChild(meta);
row.appendChild(toggle); row.appendChild(toggle);
list.appendChild(row); list.appendChild(row);
@@ -2497,6 +2630,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 +2638,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 +2647,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 +2851,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);
}); });