[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:
@@ -1,4 +1,4 @@
|
||||
import { Router } from 'express';
|
||||
import { Router } from 'express';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -43,18 +43,19 @@ router.get('/', (req, res) => {
|
||||
<aside class="sidebar">
|
||||
<div class="brand">Papo Control</div>
|
||||
<div class="nav">
|
||||
<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="#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="#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="#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="#settings" data-target="settings"><span class="icon">⚙️</span> Einstellungen</a>
|
||||
<a href="#modules" data-target="modules"><span class="icon">🧩</span> Module</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>
|
||||
<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="#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="#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="#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="#serverstats" data-target="serverstats" class="serverstats-link"><span class="icon">??</span> Server Stats</a>
|
||||
<a href="#settings" data-target="settings"><span class="icon">âš™ï¸</span> Einstellungen</a>
|
||||
<a href="#modules" data-target="modules"><span class="icon">🧩</span> Module</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 class="muted">Angemeldet als <span id="userInfo"></span></div>
|
||||
<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>
|
||||
<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 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="sla">SLA</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>
|
||||
<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 class="row" style="gap:10px;">
|
||||
<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>
|
||||
<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 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>
|
||||
<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>
|
||||
<button class="secondary-btn" id="addAutomation">Neue Regel</button>
|
||||
</div>
|
||||
@@ -467,7 +468,7 @@ router.get('/', (req, res) => {
|
||||
<div style="display:flex; justify-content:space-between; gap:12px; flex-wrap:wrap;">
|
||||
<div>
|
||||
<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>
|
||||
<button class="secondary-btn" id="addKb">Neuer Artikel</button>
|
||||
</div>
|
||||
@@ -586,7 +587,7 @@ router.get('/', (req, res) => {
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<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 class="form-field">
|
||||
@@ -609,7 +610,7 @@ router.get('/', (req, res) => {
|
||||
<div class="embed-color" id="welcomePreviewColor"></div>
|
||||
<div class="embed-body">
|
||||
<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>
|
||||
<img id="welcomePreviewImage" class="embed-image" style="display:none;" />
|
||||
</div>
|
||||
@@ -736,7 +737,7 @@ router.get('/', (req, res) => {
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</section>
|
||||
</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">
|
||||
<section class="card">
|
||||
<div class="row" style="justify-content:space-between; align-items:center;">
|
||||
@@ -810,7 +838,7 @@ router.get('/', (req, res) => {
|
||||
<section class="card">
|
||||
<div class="row" style="justify-content:space-between;">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -820,7 +848,7 @@ router.get('/', (req, res) => {
|
||||
<div class="row" style="justify-content:space-between;">
|
||||
<div>
|
||||
<p class="section-title">Logs</p>
|
||||
<p class="section-sub">Neueste Einträge</p>
|
||||
<p class="section-sub">Neueste Einträge</p>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="log-list" id="adminLogs"></ul>
|
||||
@@ -847,13 +875,13 @@ router.get('/', (req, res) => {
|
||||
</div>
|
||||
</div>
|
||||
<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>✏️ 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>🛡️ 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>🎵 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>👋 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 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>🎫 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>âš™ï¸ System / Channels</span><div id="logSystem" class="switch on"></div></div>
|
||||
</div>
|
||||
<div style="display:flex; justify-content:flex-end; gap:10px; margin-top:12px;">
|
||||
<button id="loggingSave" type="button">Logging speichern</button>
|
||||
@@ -1166,6 +1194,11 @@ router.get('/', (req, res) => {
|
||||
const statuspageChannel = document.getElementById('statuspageChannel');
|
||||
const statuspageServices = document.getElementById('statuspageServices');
|
||||
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 adminActiveGuilds = document.getElementById('adminActiveGuilds');
|
||||
const adminUptime = document.getElementById('adminUptime');
|
||||
@@ -1206,6 +1239,7 @@ router.get('/', (req, res) => {
|
||||
let dynamicVoiceCache = {};
|
||||
let isAdmin = false;
|
||||
let statuspageCache = { services: [] };
|
||||
let serverStatsCache = { items: [] };
|
||||
let birthdayCache = { config: {}, birthdays: [] };
|
||||
let reactionRoleCache = [];
|
||||
let editingReactionRole = null;
|
||||
@@ -1240,6 +1274,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 +1283,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 +1299,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) ||
|
||||
@@ -1384,6 +1422,115 @@ router.get('/', (req, res) => {
|
||||
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() {
|
||||
if (!statuspageServices) return;
|
||||
statuspageServices.innerHTML = '';
|
||||
@@ -1419,7 +1566,7 @@ router.get('/', (req, res) => {
|
||||
actions.className = 'row';
|
||||
const del = document.createElement('button');
|
||||
del.className = 'danger-btn';
|
||||
del.textContent = 'Löschen';
|
||||
del.textContent = 'Löschen';
|
||||
del.addEventListener('click', async () => {
|
||||
await deleteService(svc.id);
|
||||
});
|
||||
@@ -1477,7 +1624,7 @@ router.get('/', (req, res) => {
|
||||
if (res.ok) {
|
||||
await loadStatuspage();
|
||||
} else {
|
||||
showToast('Service löschen fehlgeschlagen', true);
|
||||
showToast('Service löschen fehlgeschlagen', true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1755,7 +1902,7 @@ router.get('/', (req, res) => {
|
||||
'</div>' +
|
||||
'<div class=\"ticket-meta\">User: ' +
|
||||
(t.userId || '-') +
|
||||
(t.claimedBy ? ' <EFBFBD> Supporter: ' + t.claimedBy : '') +
|
||||
(t.claimedBy ? ' · Supporter: ' + t.claimedBy : '') +
|
||||
'</div>';
|
||||
const select = document.createElement('select');
|
||||
select.innerHTML =
|
||||
@@ -1869,14 +2016,14 @@ router.get('/', (req, res) => {
|
||||
edit.addEventListener('click', () => fillAutomationForm(r));
|
||||
const del = document.createElement('button');
|
||||
del.className = 'danger-btn';
|
||||
del.textContent = 'L<EFBFBD>schen';
|
||||
del.textContent = 'Löschen';
|
||||
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<EFBFBD>scht' : 'L<EFBFBD>schen fehlgeschlagen', !res.ok);
|
||||
showToast(res.ok ? 'Regel gelöscht' : 'Löschen fehlgeschlagen', !res.ok);
|
||||
if (res.ok) loadAutomations();
|
||||
});
|
||||
actions.appendChild(edit);
|
||||
@@ -1936,14 +2083,14 @@ router.get('/', (req, res) => {
|
||||
edit.addEventListener('click', () => fillKbForm(a));
|
||||
const del = document.createElement('button');
|
||||
del.className = 'danger-btn';
|
||||
del.textContent = 'L<EFBFBD>schen';
|
||||
del.textContent = 'Löschen';
|
||||
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<EFBFBD>scht' : 'L<EFBFBD>schen fehlgeschlagen', !res.ok);
|
||||
showToast(res.ok ? 'Artikel gelöscht' : 'Löschen fehlgeschlagen', !res.ok);
|
||||
if (res.ok) loadKb();
|
||||
});
|
||||
actions.appendChild(edit);
|
||||
@@ -2116,7 +2263,7 @@ router.get('/', (req, res) => {
|
||||
(s.userId || '-') +
|
||||
'</strong></div><div class="muted">Ende: ' +
|
||||
formatDate(s.endedAt || Date.now()) +
|
||||
' · Dauer: ' +
|
||||
' · Dauer: ' +
|
||||
dur +
|
||||
'</div>';
|
||||
supportRecentList.appendChild(div);
|
||||
@@ -2174,9 +2321,9 @@ router.get('/', (req, res) => {
|
||||
(ev.repeatType || 'none') +
|
||||
'</span></div><div class="ticket-meta">Start: ' +
|
||||
formatDate(ev.startTime) +
|
||||
' · Channel: ' +
|
||||
' · Channel: ' +
|
||||
(ev.channelId || '-') +
|
||||
' · Anmeldungen: ' +
|
||||
' · Anmeldungen: ' +
|
||||
(ev._count?.signups ?? 0) +
|
||||
'</div>';
|
||||
const actions = document.createElement('div');
|
||||
@@ -2389,7 +2536,7 @@ router.get('/', (req, res) => {
|
||||
meta.className = 'module-meta';
|
||||
const descParts = ['Channel: ' + (set.channelId || '-')];
|
||||
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');
|
||||
actions.className = 'row';
|
||||
const editBtn = document.createElement('button');
|
||||
@@ -2457,6 +2604,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;
|
||||
@@ -2481,10 +2629,12 @@ router.get('/', (req, res) => {
|
||||
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();
|
||||
if (m.key === 'serverStatsEnabled' && willEnable) loadServerStats();
|
||||
} else {
|
||||
showToast('Speichern fehlgeschlagen', true);
|
||||
}
|
||||
@@ -2497,6 +2647,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 +2655,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 +2664,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 +2868,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);
|
||||
});
|
||||
@@ -2883,3 +3039,4 @@ router.get('/settings', (_req, res) => {
|
||||
export default router;
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user