Files
Papo/src/web/routes/dashboard.ts
Pascal Prießnitz 313b2c0613
All checks were successful
Deploy Discord Bot / deploy (push) Successful in 37s
[deploy] Guard settings inputs in dashboard loadSettings
2025-12-04 12:53:38 +01:00

2686 lines
143 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Router } from 'express';
const router = Router();
router.get('/', (req, res) => {
if (!req.session?.user) {
const baseRoot = process.env.WEB_BASE_PATH || '/ucp';
return res.redirect(`${baseRoot}/auth/discord`);
}
const guildId = typeof req.query.guildId === 'string' ? req.query.guildId : '';
const baseRoot = process.env.WEB_BASE_PATH || '/ucp';
const baseDashboard = `${baseRoot}/dashboard`;
const baseAuth = `${baseRoot}/auth`;
const baseApi = `${baseRoot}/api`;
const baseScript = `
const BASE_ROOT = '${baseRoot}';
const BASE_DASH = '${baseDashboard}';
const BASE_API = '${baseApi}';
const BASE_AUTH = '${baseAuth}';
const prependBase = (url) => {
if (typeof url !== 'string') return url;
if (BASE_ROOT && url.startsWith(BASE_ROOT + '/')) return url;
if (url.startsWith('/')) return (BASE_ROOT || '') + url;
return url;
};
const _fetch = window.fetch.bind(window);
window.fetch = (u, o) => _fetch(prependBase(u), o);
const gotoDashboard = (guildId) => {
const qs = guildId ? ('?guildId=' + encodeURIComponent(guildId)) : '';
window.location.href = (BASE_DASH || '/dashboard') + qs;
};
`;
// TODO: TICKETS: Dashboard-Layout neu aufsetzen.
// - Filter/Status/Live-Ansicht statt statischem Inline-HTML.
// - Ticketdaten per API/WebSocket laden und live aktualisieren.
// TODO: MODULE: Musik-Modul als toggelbares Modul mit Status- und Queue-Ansicht einbinden.
// - Play/Pause/Skip/Loop als UI-Controls anbieten und Bot-Status spiegeln.
// TODO: AUTOMOD: Konfiguration (Schwellenwerte, Filter, Logging) im Dashboard editierbar machen.
// - Grenzwerte, Whitelist/Badwords und Log-Ziel in ein Formular ueberfuehren.
const sidebar = `
<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="#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>
</aside>
`;
if (!guildId) {
res.send(`
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Papo Dashboard - Auswahl</title>
<style>
:root { --bg:#080c15; --card:rgba(18,20,28,0.65); --text:#f8fafc; --accent:#f97316; --accent-strong:#ff9b3d; --muted:#aab4c5; --border:rgba(255,255,255,0.08); }
body { margin:0; font-family: 'Inter', system-ui, -apple-system, sans-serif; background:radial-gradient(circle at 14% 18%, rgba(249,115,22,0.12), transparent 32%), radial-gradient(circle at 80% -6%, rgba(255,153,73,0.12), transparent 30%), linear-gradient(135deg, #070a11 0%, #0b0f18 50%, #080c15 100%); color:var(--text); min-height:100vh; }
.layout { display:flex; width:100%; min-height:100vh; background:radial-gradient(circle at 50% 24%, rgba(255,153,73,0.06), transparent 30%); }
.sidebar { width:240px; background:linear-gradient(180deg, rgba(12,14,22,0.85), rgba(10,12,18,0.78)); border-right:1px solid var(--border); padding:24px 20px; box-sizing:border-box; display:flex; flex-direction:column; gap:18px; backdrop-filter:blur(14px); box-shadow:8px 0 32px rgba(0,0,0,0.45); }
.brand { font-size:20px; font-weight:800; letter-spacing:0.6px; }
.nav { display:flex; flex-direction:column; gap:8px; }
.nav a { display:flex; align-items:center; gap:10px; padding:11px 13px; border-radius:14px; color:var(--muted); text-decoration:none; font-weight:700; transition:background 140ms ease, color 140ms ease, box-shadow 140ms ease, border 140ms ease; border:1px solid transparent; background:rgba(255,255,255,0.02); }
.nav a .icon { opacity:0.85; width:18px; display:inline-flex; justify-content:center; }
.nav a.active { background:rgba(249,115,22,0.18); color:var(--accent-strong); border-color:rgba(249,115,22,0.45); box-shadow:0 10px 30px rgba(249,115,22,0.28); }
.nav a:hover { background:rgba(255,255,255,0.06); color:var(--text); border-color:rgba(255,255,255,0.08); box-shadow:0 8px 22px rgba(0,0,0,0.28); }
.content { flex:1; padding:24px 32px 48px; box-sizing:border-box; }
h1 { margin:0; font-size:24px; letter-spacing:0.5px; font-weight:800; }
.muted { color:var(--muted); font-size:13px; }
main { display:grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap:16px; margin-top:18px; }
.card { background:var(--card); border:1px solid var(--border); border-radius:18px; padding:16px 18px; box-shadow:0 18px 36px rgba(0,0,0,0.35); backdrop-filter:blur(12px); cursor:pointer; transition:transform 150ms ease, border-color 150ms ease, box-shadow 150ms ease, background 150ms ease; }
.card:hover { transform:translateY(-2px); border-color: rgba(249,115,22,0.35); box-shadow:0 22px 40px rgba(0,0,0,0.4); background:rgba(255,255,255,0.05); }
.row { display:flex; gap:12px; align-items:center; }
.pill { display:inline-block; padding:6px 10px; border-radius:999px; font-size:12px; background:rgba(249,115,22,0.16); color:var(--text); border:1px solid rgba(249,115,22,0.3); box-shadow:0 8px 18px rgba(249,115,22,0.18); }
img { width:42px; height:42px; border-radius:12px; object-fit:cover; }
.logout { margin-top:auto; border:none; color:white; padding:10px 12px; border-radius:12px; cursor:pointer; background:linear-gradient(135deg, #ef4444, #b91c1c); box-shadow:0 12px 28px rgba(239,68,68,0.32); width:100%; transition:transform 120ms ease, box-shadow 120ms ease; }
.logout:hover { transform:translateY(-1px); box-shadow:0 14px 32px rgba(239,68,68,0.36); }
</style>
</head>
<body>
<div class="layout">
${sidebar}
<div class="content">
<h1>Waehle eine Guild aus</h1>
<div class="muted">Nur Guilds, auf denen der Bot ist.</div>
<main id="guildGrid"></main>
</div>
</div>
<script>
${baseScript}
async function ensureAuth() {
const res = await fetch('/api/me');
if (res.status === 401) { window.location.href = BASE_AUTH + '/discord'; return null; }
const data = await res.json();
const userInfo = document.getElementById('userInfo');
if (userInfo) userInfo.textContent = data.user ? data.user.username + '#' + data.user.discriminator : '';
if (data.user?.isAdmin) document.querySelector('.nav .admin-link')?.classList.remove('hidden');
return data;
}
async function loadGuilds() {
const res = await fetch('/api/guilds');
if (res.status === 401) { window.location.href = BASE_AUTH + '/discord'; return; }
const data = await res.json();
const grid = document.getElementById('guildGrid');
grid.innerHTML = '';
(data.guilds || []).forEach(g => {
const card = document.createElement('div'); card.className = 'card';
card.innerHTML =
'<div class="row">' +
'<img src="' + (g.icon ? 'https://cdn.discordapp.com/icons/' + g.id + '/' + g.icon + '.png' : 'https://cdn.discordapp.com/embed/avatars/0.png') + '" alt="icon"/>' +
'<div><div style="font-weight:700;">' + g.name + '</div><div class="muted">ID: ' + g.id + '</div></div>' +
'</div>' +
'<div style="margin-top:10px;" class="pill">Zum Dashboard</div>';
card.addEventListener('click', () => gotoDashboard(g.id));
grid.appendChild(card);
});
if (!data.guilds?.length) {
grid.innerHTML = '<div class="muted">Bot ist in keiner Guild. Bitte Bot zu einer Guild einladen.</div>';
}
}
document.getElementById('logoutBtn').addEventListener('click', () => window.location.href = BASE_AUTH + '/logout');
ensureAuth().then(loadGuilds);
</script>
</body>
</html>
`);
return;
}
res.send(`
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Papo Dashboard</title>
<style>
:root { --bg:#080c15; --card:rgba(16,19,28,0.65); --text:#f7fafc; --accent:#f97316; --accent-strong:#ff9b3d; --muted:#a8b2c5; --border:rgba(255,255,255,0.08); --surface:rgba(12,15,22,0.75); }
body { margin:0; font-family: 'Inter', system-ui, -apple-system, sans-serif; background:radial-gradient(circle at 12% 18%, rgba(249,115,22,0.12), transparent 32%), radial-gradient(circle at 78% -6%, rgba(255,153,73,0.12), transparent 32%), linear-gradient(135deg, #070a11 0%, #0b0f18 50%, #080c15 100%); color:var(--text); min-height:100vh; }
.layout { display:flex; width:100%; min-height:100vh; background:radial-gradient(circle at 50% 20%, rgba(255,153,73,0.08), transparent 30%); }
.sidebar { width:240px; background:linear-gradient(180deg, rgba(12,14,22,0.85), rgba(10,12,18,0.78)); border-right:1px solid rgba(255,255,255,0.06); padding:24px 20px; box-sizing:border-box; display:flex; flex-direction:column; gap:18px; backdrop-filter:blur(14px); box-shadow:8px 0 32px rgba(0,0,0,0.45); }
.brand { font-size:20px; font-weight:800; letter-spacing:0.6px; color:var(--text); }
.nav { display:flex; flex-direction:column; gap:8px; }
.nav a { display:flex; align-items:center; gap:10px; padding:11px 13px; border-radius:14px; color:var(--muted); text-decoration:none; font-weight:700; transition:background 140ms ease, color 140ms ease, box-shadow 140ms ease, border 140ms ease; border:1px solid transparent; background:rgba(255,255,255,0.02); }
.nav a .icon { opacity:0.85; width:18px; display:inline-flex; justify-content:center; }
.nav a.active { background:rgba(249,115,22,0.18); color:var(--accent-strong); border-color:rgba(249,115,22,0.45); box-shadow:0 10px 30px rgba(249,115,22,0.28); }
.nav a:hover { background:rgba(255,255,255,0.06); color:var(--text); border-color:rgba(255,255,255,0.08); box-shadow:0 8px 22px rgba(0,0,0,0.28); }
.content { flex:1; padding:28px 34px 56px; box-sizing:border-box; background:linear-gradient(145deg, rgba(255,153,73,0.05) 0%, rgba(255,255,255,0) 32%); }
h1 { margin:0; font-size:26px; letter-spacing:0.6px; font-weight:800; }
.muted { color:var(--muted); font-size:13px; }
main { padding-top:18px; display:flex; flex-direction:column; gap:18px; }
.section { display:none; }
.section.active { display:block; }
.section.hidden { display:none !important; }
.grid { display:grid; gap:14px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.card { background:var(--card); border:1px solid var(--border); border-radius:18px; padding:18px 20px; box-shadow:0 18px 45px rgba(0,0,0,0.35); backdrop-filter:blur(14px); }
.stat { font-size:34px; font-weight:800; margin:0; color:var(--text); }
.label { color:var(--muted); margin:4px 0 0 0; letter-spacing:0.2px; }
.tickets-grid { display:grid; grid-template-columns: repeat(3, minmax(260px, 1fr)); gap:18px; align-items:start; }
.ticket-list-pane { display:flex; flex-direction:column; gap:10px; max-height:520px; overflow-y:auto; padding:6px; margin-top:12px; }
.ticket-list-pane::-webkit-scrollbar { width:6px; }
.ticket-list-pane::-webkit-scrollbar-thumb { background:rgba(255,255,255,0.18); border-radius:10px; }
.ticket-list-item { border:1px solid var(--border); background:rgba(255,255,255,0.04); border-radius:14px; padding:13px 15px; display:flex; flex-direction:column; gap:6px; cursor:pointer; transition:transform 140ms ease, border-color 140ms ease, box-shadow 140ms ease, background 140ms ease; box-shadow:0 14px 32px rgba(0,0,0,0.3); }
.ticket-list-item:hover { transform:translateY(-2px); border-color:rgba(249,115,22,0.35); background:rgba(255,255,255,0.06); box-shadow:0 16px 38px rgba(0,0,0,0.36); }
.ticket-item-top { display:flex; justify-content:space-between; gap:10px; align-items:center; }
.ticket-title { font-weight:750; font-size:15px; }
.ticket-meta { display:flex; flex-wrap:wrap; gap:8px; color:var(--muted); font-size:12px; }
.ticket-status-badge { padding:5px 11px; border-radius:999px; font-weight:700; font-size:12px; text-transform:capitalize; border:1px solid rgba(255,255,255,0.1); background:rgba(255,255,255,0.05); color:var(--text); box-shadow:0 10px 22px rgba(0,0,0,0.24); }
.status-open { background:rgba(249,115,22,0.18); color:var(--accent-strong); border-color:rgba(249,115,22,0.45); }
.status-in-progress { background:rgba(255,184,70,0.16); color:#fbbf24; border-color:rgba(255,184,70,0.42); }
.status-closed { background:rgba(239,68,68,0.16); color:#f87171; border-color:rgba(239,68,68,0.42); }
.ticket-empty { padding:20px; text-align:center; color:var(--muted); border:1px dashed rgba(255,255,255,0.16); border-radius:14px; background:rgba(255,255,255,0.03); }
.ticket-tab { display:none; }
.ticket-tab.active { display:block; }
.ticket-tab-btn.active { background:rgba(249,115,22,0.18); border-color:rgba(249,115,22,0.45); color:var(--accent-strong); }
form { display:flex; flex-direction:column; gap:10px; margin-top:4px; }
.form-field { display:flex; flex-direction:column; gap:6px; }
.form-label { font-size:13px; color:var(--muted); letter-spacing:0.2px; font-weight:600; }
input, select, textarea { padding:12px 12px; border-radius:14px; border:1px solid rgba(255,255,255,0.12); background:rgba(255,255,255,0.06); color:var(--text); backdrop-filter:blur(8px); }
input::placeholder, textarea::placeholder { color:rgba(229,231,235,0.65); }
button { padding:12px 14px; border-radius:14px; border:1px solid rgba(249,115,22,0.4); background:linear-gradient(130deg, #ff9b3d, #f97316); color:white; font-weight:800; cursor:pointer; box-shadow:0 14px 30px rgba(249,115,22,0.35); transition:transform 140ms ease, box-shadow 140ms ease, filter 140ms ease; }
button:hover { transform:translateY(-1px); filter:brightness(1.05); box-shadow:0 18px 40px rgba(249,115,22,0.4); }
.secondary-btn { background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.14); box-shadow:0 10px 22px rgba(0,0,0,0.24); color:var(--text); }
.danger-btn { background:linear-gradient(135deg, #ef4444, #dc2626); box-shadow:0 12px 30px rgba(239,68,68,0.35); border-color:rgba(239,68,68,0.5); }
.row { display:flex; gap:12px; align-items:center; }
select { min-height:44px; }
.icon-button { width:40px; height:40px; border-radius:12px; border:1px solid rgba(249,115,22,0.3); background:rgba(249,115,22,0.16); color:var(--text); font-size:18px; display:inline-flex; align-items:center; justify-content:center; cursor:pointer; transition:transform 140ms ease, border-color 140ms ease, background 140ms ease, box-shadow 140ms ease; box-shadow:0 12px 28px rgba(0,0,0,0.28); }
.icon-button:hover { transform:translateY(-1px); border-color:rgba(249,115,22,0.6); background:rgba(249,115,22,0.22); box-shadow:0 14px 34px rgba(0,0,0,0.32); }
#status { margin-top:6px; color:#fbbf77; }
.logout { margin-top:auto; border:none; color:white; padding:10px 12px; border-radius:12px; cursor:pointer; background:linear-gradient(135deg, #ef4444, #b91c1c); width:100%; box-shadow:0 12px 28px rgba(239,68,68,0.32); }
.nav a.hidden { display:none; }
.module-list { display:flex; flex-direction:column; gap:10px; }
.module-item { display:flex; align-items:center; justify-content:space-between; padding:12px 14px; border-radius:14px; border:1px solid rgba(255,255,255,0.07); background:rgba(255,255,255,0.03); box-shadow:0 12px 26px rgba(0,0,0,0.25); }
.toggle { position:relative; width:48px; height:26px; background:rgba(255,255,255,0.14); border-radius:999px; cursor:pointer; transition:background 140ms ease, border 140ms ease, box-shadow 140ms ease; border:1px solid rgba(255,255,255,0.1); box-shadow:inset 0 2px 8px rgba(0,0,0,0.2); }
.toggle::after { content:''; position:absolute; top:3px; left:3px; width:20px; height:20px; border-radius:50%; background:#9ca3af; transition:transform 160ms ease, background 160ms ease, box-shadow 160ms ease; box-shadow:0 4px 10px rgba(0,0,0,0.3); }
.toggle.active { background:rgba(249,115,22,0.3); border-color:rgba(249,115,22,0.6); box-shadow:0 8px 20px rgba(249,115,22,0.24); }
.toggle.active::after { transform:translateX(22px); background:#ffd9b3; box-shadow:0 6px 14px rgba(249,115,22,0.35); }
.module-meta { display:flex; flex-direction:column; gap:4px; }
.module-title { font-weight:700; color:var(--text); }
.module-desc { color:var(--muted); font-size:13px; }
.toast { position:fixed; top:20px; right:24px; padding:12px 14px; border-radius:12px; background:rgba(249,115,22,0.18); border:1px solid rgba(249,115,22,0.45); color:#ffe6d0; font-weight:700; box-shadow:0 12px 32px rgba(0,0,0,0.34); opacity:0; transform:translateY(-10px); transition:opacity 150ms ease, transform 150ms ease; z-index:1100; backdrop-filter:blur(10px); }
.toast.error { background:rgba(239,68,68,0.18); border-color:rgba(239,68,68,0.4); color:#ffe4e6; }
.toast.show { opacity:1; transform:translateY(0); }
.option-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:12px; margin-top:10px; }
.option-card { border:1px solid rgba(255,255,255,0.08); border-radius:14px; padding:13px; background:rgba(255,255,255,0.04); display:flex; align-items:center; justify-content:space-between; gap:10px; box-shadow:0 12px 26px rgba(0,0,0,0.25); }
.option-card label { font-weight:700; color:var(--text); }
.switch { position:relative; width:52px; height:28px; border-radius:28px; background:rgba(255,255,255,0.14); border:1px solid rgba(255,255,255,0.1); cursor:pointer; transition:background 140ms ease, border 140ms ease, box-shadow 140ms ease; box-shadow:inset 0 2px 8px rgba(0,0,0,0.2); }
.switch::after { content:''; position:absolute; top:3px; left:3px; width:22px; height:22px; border-radius:50%; background:#9ca3af; transition:transform 160ms ease, background 160ms ease, box-shadow 160ms ease; box-shadow:0 4px 10px rgba(0,0,0,0.3); }
.switch.on { background:rgba(249,115,22,0.32); border-color:rgba(249,115,22,0.6); box-shadow:0 8px 20px rgba(249,115,22,0.22); }
.switch.on::after { transform:translateX(22px); background:#ffd9b3; box-shadow:0 6px 14px rgba(249,115,22,0.35); }
.admin-grid { display:grid; gap:14px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.activity-bars { display:flex; gap:8px; align-items:flex-end; min-height:140px; padding:10px; border-radius:14px; background:rgba(255,255,255,0.02); border:1px solid rgba(255,255,255,0.06); overflow:hidden; }
.activity-bar { flex:1; background:linear-gradient(130deg, rgba(249,115,22,0.4), rgba(249,115,22,0.24)); border-radius:10px 10px 4px 4px; position:relative; min-height:6px; box-shadow:0 10px 22px rgba(0,0,0,0.26); }
.activity-bar span { position:absolute; bottom:100%; left:50%; transform:translate(-50%, -4px); font-size:11px; color:var(--muted); white-space:nowrap; }
.log-list { margin:0; padding:0; list-style:none; display:flex; flex-direction:column; gap:8px; max-height:280px; overflow:auto; }
.log-item { padding:10px 12px; border-radius:12px; border:1px solid rgba(255,255,255,0.08); background:rgba(255,255,255,0.03); display:flex; gap:10px; align-items:flex-start; }
.log-level { padding:4px 8px; border-radius:999px; font-weight:800; font-size:12px; }
.log-level.info { background:rgba(59,130,246,0.16); border:1px solid rgba(59,130,246,0.35); color:#bfdbfe; }
.log-level.warn { background:rgba(251,191,36,0.18); border:1px solid rgba(251,191,36,0.35); color:#fef08a; }
.log-level.error { background:rgba(239,68,68,0.18); border:1px solid rgba(239,68,68,0.4); color:#fecaca; }
.status-card { display:flex; flex-direction:column; gap:6px; border:1px solid rgba(255,255,255,0.08); border-radius:14px; padding:12px; background:rgba(255,255,255,0.03); box-shadow:0 12px 28px rgba(0,0,0,0.24); }
.status-top { display:flex; justify-content:space-between; gap:8px; align-items:center; }
.status-meta { display:flex; gap:8px; flex-wrap:wrap; color:var(--muted); font-size:12px; }
.status-badge { padding:4px 10px; border-radius:999px; font-weight:800; font-size:12px; }
.status-up { background:rgba(34,197,94,0.18); border:1px solid rgba(34,197,94,0.4); color:#bbf7d0; }
.status-down { background:rgba(239,68,68,0.18); border:1px solid rgba(239,68,68,0.4); color:#fecaca; }
.status-unknown { background:rgba(148,163,184,0.18); border:1px solid rgba(148,163,184,0.4); color:#e2e8f0; }
.module-badges { display:flex; gap:8px; flex-wrap:wrap; }
.badge { padding:6px 10px; border-radius:12px; font-weight:700; font-size:12px; border:1px solid rgba(255,255,255,0.12); background:rgba(255,255,255,0.06); }
.badge.active { border-color:rgba(249,115,22,0.4); color:#f97316; box-shadow:0 6px 18px rgba(249,115,22,0.25); }
.inline { display:flex; align-items:center; gap:8px; }
.form-row { display:flex; gap:12px; flex-wrap:wrap; }
.form-row .form-field { flex:1; min-width:220px; }
.embed-preview { margin-top:10px; border:1px solid rgba(255,255,255,0.1); background:rgba(255,255,255,0.04); border-radius:14px; padding:14px; display:flex; gap:12px; box-shadow:0 12px 28px rgba(0,0,0,0.26); }
.embed-color { width:6px; border-radius:6px; background:var(--accent); }
.embed-body { flex:1; display:flex; flex-direction:column; gap:6px; }
.embed-title { font-weight:750; }
.embed-desc { white-space:pre-wrap; color:#d7dce6; }
.embed-footer { color:#9ca3af; font-size:12px; }
.embed-image { margin-top:8px; max-width:240px; border-radius:10px; display:block; }
.modal-backdrop { position:fixed; inset:0; background:rgba(4,5,8,0.7); opacity:0; pointer-events:none; transition:opacity 160ms ease; z-index:900; backdrop-filter:blur(4px); }
.modal-backdrop.show { opacity:1; pointer-events:auto; }
.modal { position:fixed; top:50%; left:50%; transform:translate(-50%, -50%) scale(0.98); width:min(540px, calc(100% - 32px)); background:var(--card); border:1px solid var(--border); border-radius:18px; box-shadow:0 25px 70px rgba(0,0,0,0.5); padding:18px 18px 16px; opacity:0; pointer-events:none; transition:opacity 160ms ease, transform 160ms ease; z-index:1000; backdrop-filter:blur(14px); }
.modal.show { opacity:1; pointer-events:auto; transform:translate(-50%, -50%) scale(1); }
.modal-header { display:flex; justify-content:space-between; align-items:center; gap:12px; }
.modal-body { margin-top:12px; display:flex; flex-direction:column; gap:10px; }
.modal-actions { margin-top:14px; display:flex; justify-content:flex-end; gap:10px; }
.detail-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap:10px; }
.detail-tile { background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.1); border-radius:14px; padding:10px 12px; box-shadow:0 12px 26px rgba(0,0,0,0.24); }
.detail-label { color:var(--muted); font-size:12px; margin:0 0 4px; }
.detail-value { font-weight:700; font-size:14px; }
.linkish { color:var(--accent-strong); text-decoration:none; font-weight:700; }
.linkish:hover { text-decoration:underline; }
.section-title { font-size:17px; font-weight:800; margin:0; }
.section-sub { color:var(--muted); margin:0; font-size:13px; }
@media (max-width: 1100px) {
.tickets-grid { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
}
</style>
</head>
<body>
<div class="layout">
${sidebar}
<div class="content">
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px;">
<div>
<h1>Guild Dashboard</h1>
<div class="muted">Dashboard & Guild-Config</div>
</div>
<div>
<p class="label" style="margin:0;">Guild</p>
<select id="guildSelect"></select>
<div id="status" class="muted"></div>
</div>
</div>
<main>
<div class="section" data-section="overview">
<section class="card" style="display:flex; gap:12px; align-items:center;">
<img id="guildIcon" src="https://cdn.discordapp.com/embed/avatars/0.png" alt="icon" style="width:54px;height:54px;border-radius:14px;object-fit:cover;border:1px solid rgba(255,255,255,0.1);" />
<div style="flex:1;">
<p class="section-title" id="guildName">Guild</p>
<p class="section-sub">ID: <span id="guildIdLabel">-</span></p>
<div class="module-badges" id="guildModules"></div>
</div>
<div class="pill">Bot aktiv</div>
</section>
<div class="grid">
<section class="card">
<p class="section-title">Guild Infos</p>
<div class="detail-grid" style="margin-top:8px;">
<div class="detail-tile"><div class="detail-label">Owner</div><div class="detail-value" id="guildOwner">-</div></div>
<div class="detail-tile"><div class="detail-label">Erstellt</div><div class="detail-value" id="guildCreated">-</div></div>
<div class="detail-tile"><div class="detail-label">Member</div><div class="detail-value" id="guildMembers">-</div></div>
<div class="detail-tile"><div class="detail-label">Channels</div><div class="detail-value" id="guildChannels">-</div></div>
<div class="detail-tile"><div class="detail-label">Tickets offen</div><div class="detail-value" id="openCount">-</div></div>
<div class="detail-tile"><div class="detail-label">Tickets IP / Closed</div><div class="detail-value"><span id="ipCount">-</span> / <span id="closedCount">-</span></div></div>
</div>
</section>
<section class="card">
<p class="section-title">Activity</p>
<div class="detail-grid" style="margin-top:8px;">
<div class="detail-tile"><div class="detail-label">Messages (24h)</div><div class="detail-value" id="actMessages">-</div></div>
<div class="detail-tile"><div class="detail-label">Commands (24h)</div><div class="detail-value" id="actCommands">-</div></div>
<div class="detail-tile"><div class="detail-label">Automod (24h)</div><div class="detail-value" id="actAutomod">-</div></div>
</div>
</section>
<section class="card">
<p class="section-title">Guild Logs</p>
<ul class="log-list" id="guildLogs"></ul>
</section>
</div>
</div>
<div class="section" data-section="tickets">
<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">Ü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">Ü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>
<button class="secondary-btn ticket-tab-btn" data-tab="kb">Knowledge-Base</button>
</div>
</div>
<div id="ticketTabOverview" class="ticket-tab active">
<section class="card">
<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ä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>
<button class="icon-button" id="openPanelModal" title="Ticket-Panel erstellen">+</button>
</div>
</div>
<div class="ticket-list-pane" id="ticketListPane"></div>
</section>
<section class="card">
<div class="row" style="justify-content:space-between; align-items:center;">
<div>
<p class="section-title">Support-Login Status</p>
<p class="section-sub">Aktive Supporter und letzte Sessions.</p>
</div>
<button class="icon-button" id="refreshSupportStatus">?</button>
</div>
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(240px,1fr)); gap:12px;">
<div>
<p class="label">Aktiv</p>
<div id="supportActiveList" class="ticket-list-pane" style="min-height:80px;"></div>
</div>
<div>
<p class="label">Letzte Sessions</p>
<div id="supportRecentList" class="ticket-list-pane" style="min-height:80px;"></div>
</div>
</div>
</section>
</div>
<div id="ticketTabPipeline" class="ticket-tab">
<section class="card">
<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 ändern.</p>
</div>
</div>
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(240px,1fr)); gap:12px;" id="pipelineGrid">
<div><p class="label">Neu</p><div class="ticket-list-pane" id="pipelineNeu"></div></div>
<div><p class="label">In Bearbeitung</p><div class="ticket-list-pane" id="pipelineIn"></div></div>
<div><p class="label">Warten auf User</p><div class="ticket-list-pane" id="pipelineWait"></div></div>
<div><p class="label">Erledigt</p><div class="ticket-list-pane" id="pipelineDone"></div></div>
</div>
</section>
</div>
<div id="ticketTabSla" class="ticket-tab">
<section class="card">
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px;">
<div>
<p class="section-title">SLA / Response-Zeiten</p>
<p class="section-sub">Average Time to Claim / First Response.</p>
</div>
<select id="slaRange">
<option value="7">Letzte 7 Tage</option>
<option value="30" selected>Letzte 30 Tage</option>
<option value="90">Letzte 90 Tage</option>
</select>
</div>
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(300px,1fr)); gap:12px;">
<div>
<p class="label">SLA pro Supporter</p>
<table style="width:100%; border-collapse:collapse;" id="slaSupporterTable">
<thead><tr><th style="text-align:left;">Supporter</th><th>Tickets</th><th>TTC</th><th>TTFR</th></tr></thead>
<tbody id="slaSupporterBody"></tbody>
</table>
</div>
<div>
<p class="label">SLA pro Tag</p>
<table style="width:100%; border-collapse:collapse;" id="slaDaysTable">
<thead><tr><th style="text-align:left;">Datum</th><th>Tickets</th><th>TTC</th><th>TTFR</th></tr></thead>
<tbody id="slaDaysBody"></tbody>
</table>
</div>
</div>
</section>
</div>
<div id="ticketTabAutomations" class="ticket-tab">
<section class="card">
<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ür Ticket-Aktionen.</p>
</div>
<button class="secondary-btn" id="addAutomation">Neue Regel</button>
</div>
<div id="automationList" class="ticket-list-pane" style="margin-top:10px;"></div>
<div id="automationFormWrap" style="margin-top:12px;">
<p class="label">Regel bearbeiten</p>
<form id="automationForm" style="display:grid; grid-template-columns: repeat(auto-fit, minmax(180px,1fr)); gap:10px;">
<input type="hidden" id="automationId" />
<div class="form-field"><label class="form-label">Name</label><input id="automationName" placeholder="Regelname" /></div>
<div class="form-field"><label class="form-label">Bedingung</label><select id="automationConditionType">
<option value="category">Kategorie</option>
<option value="status">Status</option>
<option value="age">Ticketalter (Stunden)</option>
</select></div>
<div class="form-field"><label class="form-label">Wert</label><input id="automationConditionValue" placeholder="z.B. Bug oder 24" /></div>
<div class="form-field"><label class="form-label">Aktion</label><select id="automationActionType">
<option value="pingRole">Rolle pingen</option>
<option value="reminder">Reminder posten</option>
<option value="flag">Status setzen</option>
</select></div>
<div class="form-field"><label class="form-label">Rolle/Status/Nachricht</label><input id="automationActionValue" placeholder="Rollen-ID, Status oder Nachricht" /></div>
<div class="form-field"><label class="form-label">Aktiv</label><div id="automationActive" class="switch on"></div></div>
<div class="form-field" style="grid-column:1/-1; display:flex; gap:8px;">
<button type="submit">Speichern</button>
<button type="button" class="secondary-btn" id="automationReset">Reset</button>
</div>
</form>
</div>
</section>
</div>
<div id="ticketTabKb" class="ticket-tab">
<section class="card">
<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ür Self-Service.</p>
</div>
<button class="secondary-btn" id="addKb">Neuer Artikel</button>
</div>
<div id="kbList" class="ticket-list-pane" style="margin-top:10px;"></div>
<div id="kbFormWrap" style="margin-top:12px;">
<form id="kbForm" style="display:grid; grid-template-columns: repeat(auto-fit, minmax(200px,1fr)); gap:10px;">
<input type="hidden" id="kbId" />
<div class="form-field"><label class="form-label">Titel</label><input id="kbTitle" placeholder="Titel" /></div>
<div class="form-field"><label class="form-label">Keywords (kommagetrennt)</label><input id="kbKeywords" placeholder="bug, musik, ticket" /></div>
<div class="form-field" style="grid-column:1/-1;"><label class="form-label">Inhalt/Link</label><textarea id="kbContent" rows="3" placeholder="Kurze Anleitung oder Link"></textarea></div>
<div class="form-field" style="grid-column:1/-1; display:flex; gap:8px;">
<button type="submit">Speichern</button>
<button type="button" class="secondary-btn" id="kbReset">Reset</button>
</div>
</form>
</div>
</section>
</div>
</div><div class="section" data-section="automod">
<section class="card">
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap;">
<div>
<p class="section-title">Automod Einstellungen</p>
<p class="section-sub">Automod pro Guild aktivieren und Regeln konfigurieren.</p>
</div>
<div class="inline">
<span class="form-label">Automod aktivieren</span>
<div id="automodToggle" class="switch"></div>
</div>
</div>
<form id="automodForm" style="margin-top:14px; display:flex; flex-direction:column; gap:12px;">
<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>
<div style="display:flex; justify-content:flex-end; gap:10px; margin-top:12px;">
<button id="loggingSave" type="button">Logging speichern</button>
</div>
<p class="muted" id="loggingStatus"></p>
</section>
</div>
</main>
</div>
</div>
<div id="modalBackdrop" class="modal-backdrop"></div>
<div class="modal" id="ticketModal">
<div class="modal-header">
<div>
<p class="section-title" id="detailTitle">Ticket</p>
<p class="section-sub" id="detailSubtitle">Details</p>
</div>
<button class="icon-button" data-close-modal>&times;</button>
</div>
<div class="modal-body">
<div class="detail-grid">
<div class="detail-tile">
<p class="detail-label">Ticket-ID</p>
<p class="detail-value" id="detailTicketId">-</p>
</div>
<div class="detail-tile">
<p class="detail-label">Status</p>
<p class="detail-value" id="detailStatus">-</p>
</div>
<div class="detail-tile">
<p class="detail-label">Prioritaet</p>
<p class="detail-value" id="detailPriority">-</p>
</div>
<div class="detail-tile">
<p class="detail-label">Erstellt am</p>
<p class="detail-value" id="detailCreated">-</p>
</div>
</div>
<div class="detail-tile">
<p class="detail-label">Ersteller</p>
<p class="detail-value" id="detailCreator">-</p>
</div>
<div class="detail-tile" id="detailTranscriptRow" style="display:none;">
<p class="detail-label">Transcript</p>
<a id="detailTranscript" class="linkish" target="_blank" rel="noopener">Transcript anzeigen</a>
</div>
</div>
<div class="modal-actions">
<button class="secondary-btn" type="button" data-close-modal>Schliessen</button>
<button class="danger-btn" type="button" id="closeTicketAction">Ticket schliessen</button>
</div>
</div>
<div class="modal" id="panelModal">
<div class="modal-header">
<div>
<p class="section-title">Ticket-Panel erstellen</p>
<p class="section-sub">Channel und Kategorien definieren, dann Panel senden.</p>
</div>
<button class="icon-button" data-close-modal>&times;</button>
</div>
<div class="modal-body">
<form id="panelForm">
<div class="form-field">
<label class="form-label">Channel ID (Textkanal)</label>
<input name="channelId" placeholder="123456789012345678" required />
</div>
<div class="form-field">
<label class="form-label">Panel Titel (optional)</label>
<input name="panelTitle" placeholder="Support Panel" />
</div>
<div class="form-field">
<label class="form-label">Beschreibung (optional)</label>
<textarea name="panelDescription" placeholder="Kurze Beschreibung" rows="3"></textarea>
</div>
<div class="form-field">
<label class="form-label">Kategorien (pro Zeile: Label|CustomId|Emoji optional)</label>
<textarea name="panelCategories" placeholder="Technische Hilfe|support|:bulb:" rows="4"></textarea>
<p class="muted">Beispiel: Billing|billing|:credit_card:</p>
</div>
<p class="muted" id="panelStatus"></p>
</form>
</div>
<div class="modal-actions">
<button class="secondary-btn" type="button" data-close-modal>Abbrechen</button>
<button type="button" id="panelSubmit">Panel senden</button>
</div>
</div>
<div class="modal" id="supportLoginModal">
<div class="modal-header">
<div>
<p class="section-title">Support-Login Einstellungen</p>
<p class="section-sub">Channel, Texte und Buttons fuer das Support-Panel.</p>
</div>
<button class="icon-button" data-close-modal>&times;</button>
</div>
<div class="modal-body">
<form id="supportLoginForm">
<div class="form-field">
<label class="form-label">Panel Channel ID</label>
<input id="supportLoginChannel" placeholder="123456789012345678" required />
</div>
<div class="form-field">
<label class="form-label">Titel</label>
<input id="supportLoginTitle" placeholder="Support Login" />
</div>
<div class="form-field">
<label class="form-label">Beschreibung</label>
<textarea id="supportLoginDescription" rows="3" placeholder="Melde dich als Support an/ab."></textarea>
</div>
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(200px,1fr)); gap:12px;">
<div class="form-field">
<label class="form-label">Button Label Login</label>
<input id="supportLoginLabel" placeholder="Ich bin jetzt im Support" />
</div>
<div class="form-field">
<label class="form-label">Button Label Logout</label>
<input id="supportLogoutLabel" placeholder="Ich bin nicht mehr im Support" />
</div>
</div>
<div class="row" style="justify-content:flex-start; align-items:center; gap:10px;">
<span class="form-label">Panel automatisch aktualisieren</span>
<div id="supportLoginAuto" class="switch"></div>
</div>
<p class="muted" id="supportLoginStatus"></p>
</form>
</div>
<div class="modal-actions">
<button class="secondary-btn" type="button" data-close-modal>Abbrechen</button>
<button type="button" id="supportLoginSave">Speichern &amp; Panel aktualisieren</button>
</div>
</div>
<div class="modal" id="eventModal">
<div class="modal-header">
<div>
<p class="section-title">Event erstellen/bearbeiten</p>
<p class="section-sub">Titel, Zeit und Reminder festlegen.</p>
</div>
<button class="icon-button" data-close-modal>&times;</button>
</div>
<div class="modal-body">
<form id="eventForm">
<input type="hidden" id="eventId" />
<div class="form-field">
<label class="form-label">Titel</label>
<input id="eventTitle" required />
</div>
<div class="form-field">
<label class="form-label">Beschreibung</label>
<textarea id="eventDescription" rows="3"></textarea>
</div>
<div class="form-field">
<label class="form-label">Channel ID</label>
<input id="eventChannel" placeholder="123456789012345678" required />
</div>
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(200px,1fr)); gap:12px;">
<div class="form-field">
<label class="form-label">Start Datum</label>
<input id="eventDate" type="date" />
</div>
<div class="form-field">
<label class="form-label">Start Zeit</label>
<input id="eventTime" type="time" />
</div>
<div class="form-field">
<label class="form-label">Wiederholung</label>
<select id="eventRepeat">
<option value="none">einmalig</option>
<option value="daily">taeglich</option>
<option value="weekly">woechentlich</option>
<option value="monthly">monatlich</option>
</select>
</div>
</div>
<div class="form-field" id="eventWeeklyRow" style="display:none;">
<label class="form-label">Wochentage (0=So)</label>
<input id="eventWeeklyDays" placeholder="1,3,5" />
</div>
<div class="form-field" id="eventMonthlyRow" style="display:none;">
<label class="form-label">Tag im Monat (1-31)</label>
<input id="eventMonthlyDay" type="number" min="1" max="31" />
</div>
<div class="form-field">
<label class="form-label">Reminder (Minuten vorher)</label>
<input id="eventReminder" type="number" min="0" placeholder="120" />
</div>
<div class="form-field">
<label class="form-label">Ping-Rolle (optional)</label>
<input id="eventRole" placeholder="Role ID" />
</div>
<p class="muted" id="eventStatus"></p>
</form>
</div>
<div class="modal-actions">
<button class="secondary-btn" type="button" data-close-modal>Abbrechen</button>
<button type="button" id="eventSave">Speichern</button>
</div>
</div>
<div id="toast" class="toast"></div>
<script>
${baseScript}
const guildSelect = document.getElementById('guildSelect');
const statusEl = document.getElementById('status');
const userInfo = document.getElementById('userInfo');
const sections = Array.from(document.querySelectorAll('.section'));
const navLinks = Array.from(document.querySelectorAll('.nav a'));
const automodNav = document.querySelector('.nav .automod-link');
const welcomeNav = document.querySelector('.nav .welcome-link');
const dynamicVoiceNav = document.querySelector('.nav .dynamicvoice-link');
const toastEl = document.getElementById('toast');
const ticketListPane = document.getElementById('ticketListPane');
const modalBackdrop = document.getElementById('modalBackdrop');
const ticketModal = document.getElementById('ticketModal');
const panelModal = document.getElementById('panelModal');
const supportLoginModal = document.getElementById('supportLoginModal');
const automodForm = document.getElementById('automodForm');
const automodToggle = document.getElementById('automodToggle');
const badWordToggle = document.getElementById('badWordFilter');
const linkFilterToggle = document.getElementById('linkFilter');
const spamFilterToggle = document.getElementById('spamFilter');
const capsFilterToggle = document.getElementById('capsFilter');
const automodLogChannel = document.getElementById('automodLogChannel');
const automodSensitivity = document.getElementById('automodSensitivity');
const automodWhitelist = document.getElementById('automodWhitelist');
const automodStatus = document.getElementById('automodStatus');
const automodBadwords = document.getElementById('automodBadwords');
const automodWhitelistRoles = document.getElementById('automodWhitelistRoles');
const loggingChannel = document.getElementById('loggingChannel');
const loggingStatus = document.getElementById('loggingStatus');
const logJoinLeave = document.getElementById('logJoinLeave');
const logMsgEdit = document.getElementById('logMsgEdit');
const logMsgDelete = document.getElementById('logMsgDelete');
const logAutomod = document.getElementById('logAutomod');
const logTickets = document.getElementById('logTickets');
const logMusic = document.getElementById('logMusic');
const logSystem = document.getElementById('logSystem');
const supportLoginForm = document.getElementById('supportLoginForm');
const supportLoginChannel = document.getElementById('supportLoginChannel');
const supportLoginTitle = document.getElementById('supportLoginTitle');
const supportLoginDescription = document.getElementById('supportLoginDescription');
const supportLoginLabel = document.getElementById('supportLoginLabel');
const supportLogoutLabel = document.getElementById('supportLogoutLabel');
const supportLoginAuto = document.getElementById('supportLoginAuto');
const supportLoginStatus = document.getElementById('supportLoginStatus');
const supportActiveList = document.getElementById('supportActiveList');
const supportRecentList = document.getElementById('supportRecentList');
const welcomeToggle = document.getElementById('welcomeToggle');
const welcomeForm = document.getElementById('welcomeForm');
const welcomeChannel = document.getElementById('welcomeChannel');
const welcomeColor = document.getElementById('welcomeColor');
const welcomeTitle = document.getElementById('welcomeTitle');
const welcomeFooter = document.getElementById('welcomeFooter');
const welcomeDescription = document.getElementById('welcomeDescription');
const welcomeThumbnail = document.getElementById('welcomeThumbnail');
const welcomeThumbnailFile = document.getElementById('welcomeThumbnailFile');
const welcomeImage = document.getElementById('welcomeImage');
const welcomeImageFile = document.getElementById('welcomeImageFile');
const welcomeStatus = document.getElementById('welcomeStatus');
const dynamicVoiceToggle = document.getElementById('dynamicVoiceToggle');
const dynamicVoiceLobby = document.getElementById('dynamicVoiceLobby');
const dynamicVoiceCategory = document.getElementById('dynamicVoiceCategory');
const dynamicVoiceTemplate = document.getElementById('dynamicVoiceTemplate');
const dynamicVoiceUserLimit = document.getElementById('dynamicVoiceUserLimit');
const dynamicVoiceBitrate = document.getElementById('dynamicVoiceBitrate');
const dynamicVoiceStatus = document.getElementById('dynamicVoiceStatus');
const birthdayToggle = document.getElementById('birthdayToggle');
const birthdayChannel = document.getElementById('birthdayChannel');
const birthdayHour = document.getElementById('birthdayHour');
const birthdayTemplate = document.getElementById('birthdayTemplate');
const birthdayList = document.getElementById('birthdayList');
const birthdayStatus = document.getElementById('birthdayStatus');
const birthdaySave = document.getElementById('birthdaySave');
const reactionRoleForm = document.getElementById('reactionRoleForm');
const reactionRoleId = document.getElementById('reactionRoleId');
const reactionRoleChannel = document.getElementById('reactionRoleChannel');
const reactionRoleMessageId = document.getElementById('reactionRoleMessageId');
const reactionRoleTitle = document.getElementById('reactionRoleTitle');
const reactionRoleDescription = document.getElementById('reactionRoleDescription');
const reactionRoleEntries = document.getElementById('reactionRoleEntries');
const reactionRoleStatus = document.getElementById('reactionRoleStatus');
const reactionRoleList = document.getElementById('reactionRoleList');
const reactionRoleReset = document.getElementById('reactionRoleReset');
const reactionRoleSubmit = document.getElementById('reactionRoleSubmit');
const openSupportLogin = document.getElementById('openSupportLogin');
const supportLoginSave = document.getElementById('supportLoginSave');
const refreshSupportStatus = document.getElementById('refreshSupportStatus');
const eventList = document.getElementById('eventList');
const openEventModal = document.getElementById('openEventModal');
const eventModal = document.getElementById('eventModal');
const eventId = document.getElementById('eventId');
const eventTitle = document.getElementById('eventTitle');
const eventDescription = document.getElementById('eventDescription');
const eventChannel = document.getElementById('eventChannel');
const eventDate = document.getElementById('eventDate');
const eventTime = document.getElementById('eventTime');
const eventRepeat = document.getElementById('eventRepeat');
const eventWeeklyDays = document.getElementById('eventWeeklyDays');
const eventMonthlyDay = document.getElementById('eventMonthlyDay');
const eventReminder = document.getElementById('eventReminder');
const eventRole = document.getElementById('eventRole');
const eventStatus = document.getElementById('eventStatus');
const eventWeeklyRow = document.getElementById('eventWeeklyRow');
const eventMonthlyRow = document.getElementById('eventMonthlyRow');
const eventSave = document.getElementById('eventSave');
const statuspageToggle = document.getElementById('statuspageToggle');
const statuspageInterval = document.getElementById('statuspageInterval');
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');
const adminStart = document.getElementById('adminStart');
const adminActivity = document.getElementById('adminActivity');
const adminLogs = document.getElementById('adminLogs');
const guildIcon = document.getElementById('guildIcon');
const guildNameEl = document.getElementById('guildName');
const guildIdLabel = document.getElementById('guildIdLabel');
const guildModules = document.getElementById('guildModules');
const guildOwner = document.getElementById('guildOwner');
const guildCreated = document.getElementById('guildCreated');
const guildMembers = document.getElementById('guildMembers');
const guildChannels = document.getElementById('guildChannels');
const actMessages = document.getElementById('actMessages');
const actCommands = document.getElementById('actCommands');
const actAutomod = document.getElementById('actAutomod');
const guildLogs = document.getElementById('guildLogs');
const musicActiveEl = document.getElementById('musicActive');
const welcomePreview = {
color: document.getElementById('welcomePreviewColor'),
title: document.getElementById('welcomePreviewTitle'),
desc: document.getElementById('welcomePreviewDesc'),
footer: document.getElementById('welcomePreviewFooter'),
image: document.getElementById('welcomePreviewImage')
};
let currentGuild = '${guildId}';
let ticketCache = [];
let pipelineCache = { neu: [], in_bearbeitung: [], warten_auf_user: [], erledigt: [] };
let slaSupporters = [];
let slaDays = [];
let automationCache = [];
let kbCache = [];
let selectedTicket = null;
let activeModal = null;
let automodConfigCache = {};
let modulesCache = {};
let dynamicVoiceCache = {};
let isAdmin = false;
let statuspageCache = { services: [] };
let serverStatsCache = { items: [] };
let birthdayCache = { config: {}, birthdays: [] };
let reactionRoleCache = [];
let editingReactionRole = null;
let supportLoginCache = {};
let eventsCache = [];
function activateSection(key) {
sections.forEach((s) => s.classList.toggle('active', s.dataset.section === key));
navLinks.forEach((l) => l.classList.toggle('active', l.dataset.target === key));
if (key === 'admin' && isAdmin) loadAdminAll();
}
function showToast(message, isError=false) {
if (!toastEl) return;
toastEl.textContent = message;
toastEl.classList.toggle('error', isError);
toastEl.classList.add('show');
setTimeout(() => toastEl.classList.remove('show'), 2200);
}
function formatDuration(ms) {
if (ms === null || ms === undefined) return '-';
const minutes = Math.max(0, Math.round(ms / 60000));
if (minutes < 60) return minutes + 'm';
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return hours + 'h ' + (mins ? mins + 'm' : '');
}
function applyNavVisibility() {
const autoEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'automodEnabled') ? modulesCache['automodEnabled'] : true;
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;
if (automodNav) automodNav.classList.toggle('hidden', !autoEnabled);
if (welcomeNav) welcomeNav.classList.toggle('hidden', !welcomeEnabled);
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');
if (reactionRolesNav) reactionRolesNav.classList.toggle('hidden', !reactionRolesEnabled);
const eventsNav = document.querySelector('.nav .events-link');
if (eventsNav) eventsNav.classList.toggle('hidden', !eventsEnabled);
const adminNav = document.querySelector('.nav .admin-link');
if (adminNav) adminNav.classList.toggle('hidden', !isAdmin);
const current = location.hash.replace('#','') || 'overview';
if (
(current === 'automod' && !autoEnabled) ||
(current === 'welcome' && !welcomeEnabled) ||
(current === 'dynamicvoice' && !dynamicVoiceEnabled) ||
(current === 'statuspage' && !statuspageEnabled) ||
(current === 'serverstats' && !serverStatsEnabled) ||
(current === 'birthday' && !birthdayEnabled) ||
(current === 'reactionroles' && !reactionRolesEnabled) ||
(current === 'events' && !eventsEnabled) ||
(current === 'admin' && !isAdmin)
) {
activateSection('overview');
history.replaceState(null, '', '#overview');
}
}
function setSwitch(el, on) {
if (!el) return;
el.classList.toggle('on', !!on);
}
function getSwitch(el) {
return !!el?.classList.contains('on');
}
function parseList(val) {
return (val || '')
.split(/[,\\n]/)
.map((s) => s.trim())
.filter(Boolean);
}
function formatDuration(ms) {
const totalMinutes = Math.floor(ms / 60000);
const days = Math.floor(totalMinutes / 1440);
const hours = Math.floor((totalMinutes % 1440) / 60);
const minutes = totalMinutes % 60;
return days + 'd ' + hours + 'h ' + minutes + 'm';
}
async function loadGuildInfo() {
if (!currentGuild) return;
const res = await fetch('/api/guild/info?guildId=' + encodeURIComponent(currentGuild));
if (!res.ok) {
if (guildNameEl) guildNameEl.textContent = 'Guild';
return;
}
const data = await res.json();
const g = data.guild || {};
if (guildNameEl) guildNameEl.textContent = g.name || 'Guild';
if (guildIdLabel) guildIdLabel.textContent = g.id || '-';
if (guildOwner) guildOwner.textContent = g.owner?.tag || '-';
if (guildCreated) guildCreated.textContent = g.createdAt ? formatDate(g.createdAt) : '-';
if (guildMembers) guildMembers.textContent = g.memberCount ?? 0;
if (guildChannels) guildChannels.textContent = (g.textCount || 0) + ' Text / ' + (g.voiceCount || 0) + ' Voice';
if (guildIcon && g.icon) guildIcon.src = 'https://cdn.discordapp.com/icons/' + g.id + '/' + g.icon + '.png';
if (guildIcon && !g.icon) guildIcon.src = 'https://cdn.discordapp.com/embed/avatars/0.png';
if (guildModules) {
guildModules.innerHTML = '';
const mods = g.modules || {};
Object.entries(mods).forEach(([key, on]) => {
const badge = document.createElement('span');
badge.className = 'badge' + (on ? ' active' : '');
badge.textContent = key.replace('Enabled', '');
guildModules.appendChild(badge);
});
}
}
async function loadGuildActivity() {
if (!currentGuild) return;
const res = await fetch('/api/guild/activity?guildId=' + encodeURIComponent(currentGuild));
if (!res.ok) {
if (actMessages) actMessages.textContent = '0';
if (actCommands) actCommands.textContent = '0';
if (actAutomod) actAutomod.textContent = '0';
return;
}
const data = await res.json();
const a = data.activity || {};
if (actMessages) actMessages.textContent = a.messages24h ?? 0;
if (actCommands) actCommands.textContent = a.commands24h ?? 0;
if (actAutomod) actAutomod.textContent = a.automod24h ?? 0;
}
async function loadGuildLogs() {
if (!currentGuild) return;
const res = await fetch('/api/guild/logs?guildId=' + encodeURIComponent(currentGuild));
if (!res.ok) return;
const data = await res.json();
const logs = data.logs || [];
if (!guildLogs) return;
guildLogs.innerHTML = '';
logs.forEach((l) => {
const li = document.createElement('li');
li.className = 'log-item';
const level = document.createElement('span');
const lvl = (l.level || 'info').toString().toLowerCase();
level.className = 'log-level ' + lvl;
level.textContent = (l.level || '').toUpperCase();
const body = document.createElement('div');
body.style.flex = '1';
const time = document.createElement('div');
time.className = 'muted';
time.textContent = formatDate(l.timestamp || Date.now());
const msg = document.createElement('div');
msg.textContent = (l.category ? '[' + l.category + '] ' : '') + (l.message || '');
body.appendChild(time);
body.appendChild(msg);
li.appendChild(level);
li.appendChild(body);
guildLogs.appendChild(li);
});
if (!logs.length) guildLogs.innerHTML = '<li class="muted">Keine Logs</li>';
}
async function loadStatuspage() {
if (!currentGuild) return;
const res = await fetch('/api/statuspage?guildId=' + encodeURIComponent(currentGuild));
if (!res.ok) return;
const data = await res.json();
statuspageCache = data.config || { services: [] };
setSwitch(statuspageToggle, statuspageCache.enabled !== false);
if (statuspageInterval) statuspageInterval.value = statuspageCache.intervalMs ?? 60000;
if (statuspageChannel) statuspageChannel.value = statuspageCache.statusChannelId ?? '';
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 = '';
(statuspageCache.services || []).forEach((svc) => {
const card = document.createElement('div');
card.className = 'status-card';
const top = document.createElement('div');
top.className = 'status-top';
const name = document.createElement('div');
name.innerHTML = '<strong>' + (svc.name || 'Service') + '</strong><div class="muted">' + (svc.description || '') + '</div>';
const badge = document.createElement('span');
badge.className = 'status-badge ' + (svc.status === 'down' ? 'status-down' : svc.status === 'up' ? 'status-up' : 'status-unknown');
badge.textContent = (svc.status || 'unknown').toUpperCase();
top.appendChild(name);
top.appendChild(badge);
const meta = document.createElement('div');
meta.className = 'status-meta';
const uptime =
svc.upChecks && svc.totalChecks
? Math.round(((svc.upChecks ?? 0) / Math.max(1, svc.totalChecks ?? 1)) * 100)
: 0;
meta.innerHTML =
'<span>Type: ' +
(svc.type || 'unknown') +
'</span><span>Target: ' +
(svc.target || '') +
'</span><span>Uptime: ' +
uptime +
'%</span><span>Last: ' +
(svc.lastChecked ? formatDate(svc.lastChecked) : 'n/a') +
'</span>';
const actions = document.createElement('div');
actions.className = 'row';
const del = document.createElement('button');
del.className = 'danger-btn';
del.textContent = 'Löschen';
del.addEventListener('click', async () => {
await deleteService(svc.id);
});
actions.appendChild(del);
card.appendChild(top);
card.appendChild(meta);
card.appendChild(actions);
statuspageServices.appendChild(card);
});
if (!(statuspageCache.services || []).length) statuspageServices.innerHTML = '<div class="muted">Keine Services</div>';
}
async function saveStatuspageConfig() {
if (!currentGuild) return;
const cfg = {
enabled: getSwitch(statuspageToggle),
intervalMs: statuspageInterval?.value ? Number(statuspageInterval.value) : undefined,
statusChannelId: statuspageChannel?.value || undefined,
services: statuspageCache.services || []
};
const res = await fetch('/api/statuspage', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ guildId: currentGuild, config: cfg })
});
if (!res.ok) showToast('Statuspage speichern fehlgeschlagen', true);
}
async function addServicePrompt() {
if (!currentGuild) return;
const name = prompt('Name?');
if (!name) return;
const target = prompt('Target (URL/Host)?') || '';
const type = prompt('Typ (http/ping/tcp/custom)?') || 'http';
const description = prompt('Beschreibung?') || '';
const res = await fetch('/api/statuspage/service', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ guildId: currentGuild, service: { name, target, type, description } })
});
if (res.ok) {
await loadStatuspage();
} else {
showToast('Service konnte nicht angelegt werden', true);
}
}
async function deleteService(id) {
if (!currentGuild || !id) return;
const res = await fetch('/api/statuspage/service/' + encodeURIComponent(id), {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ guildId: currentGuild })
});
if (res.ok) {
await loadStatuspage();
} else {
showToast('Service löschen fehlgeschlagen', true);
}
}
async function loadAdminOverview() {
if (!isAdmin) return;
const res = await fetch('/api/admin/overview');
if (!res.ok) return;
const data = await res.json();
const ov = data.overview || {};
if (adminGuilds) adminGuilds.textContent = ov.guildCount ?? '-';
if (adminActiveGuilds) adminActiveGuilds.textContent = ov.activeGuilds24 ?? '-';
if (adminUptime) adminUptime.textContent = typeof ov.uptimeMs === 'number' ? formatDuration(ov.uptimeMs) : '-';
if (adminStart) adminStart.textContent = ov.startTime ? formatDate(ov.startTime) : '-';
}
async function loadAdminActivity() {
if (!isAdmin || !adminActivity) return;
const res = await fetch('/api/admin/activity');
if (!res.ok) return;
const data = await res.json();
const points = (data.points || []).slice(-24);
adminActivity.innerHTML = '';
const max = Math.max(...points.map((p) => p.count || 0), 1);
points.forEach((p) => {
const bar = document.createElement('div');
bar.className = 'activity-bar';
const height = Math.max(6, Math.round((p.count / max) * 120));
bar.style.height = height + 'px';
const label = document.createElement('span');
const d = new Date(p.hour);
label.textContent = d.getHours().toString().padStart(2, '0') + ':00';
bar.appendChild(label);
adminActivity.appendChild(bar);
});
if (!points.length) adminActivity.innerHTML = '<div class="muted">Keine Daten</div>';
}
async function loadAdminLogs() {
if (!isAdmin || !adminLogs) return;
const res = await fetch('/api/admin/logs');
if (!res.ok) return;
const data = await res.json();
const logs = data.logs || [];
adminLogs.innerHTML = '';
logs.forEach((l) => {
const li = document.createElement('li');
li.className = 'log-item';
const level = document.createElement('span');
const lvl = (l.level || 'info').toString().toLowerCase();
level.className = 'log-level ' + lvl;
level.textContent = (l.level || '').toUpperCase();
const body = document.createElement('div');
body.style.flex = '1';
const time = document.createElement('div');
time.className = 'muted';
time.textContent = formatDate(l.timestamp || Date.now());
const msg = document.createElement('div');
msg.textContent = l.message || '';
body.appendChild(time);
body.appendChild(msg);
li.appendChild(level);
li.appendChild(body);
adminLogs.appendChild(li);
});
if (!logs.length) adminLogs.innerHTML = '<li class="muted">Keine Logs</li>';
}
async function loadAdminAll() {
await Promise.all([loadAdminOverview(), loadAdminActivity(), loadAdminLogs()]);
}
function updateWelcomePreview() {
if (welcomePreview.color) welcomePreview.color.style.background = '#' + (welcomeColor?.value || 'f97316').replace('#','');
if (welcomePreview.title) welcomePreview.title.textContent = welcomeTitle?.value || 'Willkommen!';
if (welcomePreview.desc) welcomePreview.desc.textContent = welcomeDescription?.value || 'Schoen, dass du da bist.';
if (welcomePreview.footer) welcomePreview.footer.textContent = welcomeFooter?.value || '';
const imgSrc = welcomeImageData || welcomeImage?.value || '';
if (welcomePreview.image) {
if (imgSrc) {
welcomePreview.image.src = imgSrc;
welcomePreview.image.style.display = 'block';
} else {
welcomePreview.image.style.display = 'none';
}
}
}
function readFileToDataUrl(input, cb) {
const file = input?.files?.[0];
if (!file) return cb('');
const reader = new FileReader();
reader.onload = () => cb(typeof reader.result === 'string' ? reader.result : '');
reader.readAsDataURL(file);
}
function formatDate(value) {
if (!value) return '-';
const dt = new Date(value);
return isNaN(dt.getTime()) ? '-' : dt.toLocaleString('de-DE');
}
function showModal(el) {
if (!el) return;
activeModal = el;
modalBackdrop.classList.add('show');
el.classList.add('show');
}
function hideModal() {
if (activeModal) activeModal.classList.remove('show');
modalBackdrop.classList.remove('show');
activeModal = null;
}
function statusClass(status) {
const key = String(status || 'open').toLowerCase().replace(/\\s+/g, '-');
if (key === 'in-progress') return 'status-in-progress';
if (key === 'closed') return 'status-closed';
return 'status-open';
}
function renderTickets() {
if (!ticketListPane) return;
ticketListPane.innerHTML = '';
if (!ticketCache.length) {
ticketListPane.innerHTML = '<div class="ticket-empty">Keine Tickets vorhanden.</div>';
return;
}
ticketCache.forEach((t) => {
const item = document.createElement('div');
item.className = 'ticket-list-item';
const stat = document.createElement('span');
stat.className = 'ticket-status-badge ' + statusClass(t.status);
stat.textContent = t.status || 'open';
const top = document.createElement('div');
top.className = 'ticket-item-top';
const title = document.createElement('div');
title.className = 'ticket-title';
title.textContent = t.topic || 'Ticket';
top.appendChild(title);
top.appendChild(stat);
const meta = document.createElement('div');
meta.className = 'ticket-meta';
meta.innerHTML =
'<span>#' + (t.id || t.channelId || '-') + '</span>' +
'<span>Prio: ' + (t.priority || 'normal') + '</span>' +
'<span>' + formatDate(t.createdAt) + '</span>';
item.appendChild(top);
item.appendChild(meta);
item.addEventListener('click', () => openTicketDetail(t));
ticketListPane.appendChild(item);
});
}
function openTicketDetail(ticket) {
selectedTicket = ticket;
const creator = ticket.userTag || ticket.userName || ticket.userId || '-';
document.getElementById('detailTitle').textContent = ticket.topic || 'Ticket';
document.getElementById('detailSubtitle').textContent = 'Ticket Details';
document.getElementById('detailTicketId').textContent = ticket.id || ticket.channelId || '-';
document.getElementById('detailStatus').textContent = ticket.status || 'open';
document.getElementById('detailPriority').textContent = ticket.priority || 'normal';
document.getElementById('detailCreated').textContent = formatDate(ticket.createdAt);
document.getElementById('detailCreator').textContent = creator;
const transcriptRow = document.getElementById('detailTranscriptRow');
const transcriptLink = document.getElementById('detailTranscript');
if (ticket.transcript) {
transcriptRow.style.display = 'block';
transcriptLink.href = prependBase('/api/tickets/' + ticket.id + '/transcript');
} else {
transcriptRow.style.display = 'none';
}
showModal(ticketModal);
}
async function ensureAuth() {
const res = await fetch('/api/me');
if (res.status === 401) { window.location.href = BASE_AUTH + '/discord'; return null; }
const data = await res.json();
if (userInfo) userInfo.textContent = data.user ? data.user.username + '#' + data.user.discriminator : '';
isAdmin = !!data.user?.isAdmin;
const adminNav = document.querySelector('.nav .admin-link');
if (adminNav) adminNav.classList.toggle('hidden', !isAdmin);
applyNavVisibility();
return data;
}
async function loadGuilds() {
const res = await fetch('/api/guilds');
if (res.status === 401) { window.location.href = BASE_AUTH + '/discord'; return; }
const data = await res.json();
guildSelect.innerHTML = '';
(data.guilds || []).forEach(g => {
const opt = document.createElement('option');
opt.value = g.id;
opt.textContent = g.name;
guildSelect.appendChild(opt);
});
if (data.guilds?.length) {
currentGuild = currentGuild || guildSelect.value || data.guilds[0].id;
guildSelect.value = currentGuild;
await loadSettings(currentGuild);
await loadModules();
await loadOverview();
await loadTickets();
await loadPipeline();
await loadSla();
await loadAutomations();
await loadKb();
await loadAutomodSettings(currentGuild);
await loadLoggingSettings(currentGuild);
await loadWelcomeSettings(currentGuild);
} else {
statusEl.textContent = 'Keine Guilds verfuegbar. Fuege den Bot zu einer Guild hinzu.';
}
}
async function loadOverview() {
if (!currentGuild) return;
const res = await fetch('/api/overview?guildId=' + encodeURIComponent(currentGuild));
if (!res.ok) return;
const data = await res.json();
document.getElementById('openCount').textContent = data.tickets?.open ?? '-';
document.getElementById('ipCount').textContent = data.tickets?.inProgress ?? '-';
document.getElementById('closedCount').textContent = data.tickets?.closed ?? '-';
if (musicActiveEl) musicActiveEl.textContent = data.music?.activeGuilds ?? 0;
await loadGuildInfo();
await loadGuildActivity();
await loadGuildLogs();
}
async function loadTickets() {
if (!currentGuild) return;
const res = await fetch('/api/tickets?guildId=' + encodeURIComponent(currentGuild));
const data = await res.json();
ticketCache = data.tickets || [];
renderTickets();
}
async function loadPipeline() {
if (!currentGuild) return;
const res = await fetch('/api/tickets/pipeline?guildId=' + encodeURIComponent(currentGuild));
if (!res.ok) return;
const data = await res.json();
pipelineCache = data.pipeline || pipelineCache;
renderPipeline();
}
function renderPipeline() {
const states = [
{ key: 'neu', el: document.getElementById('pipelineNeu') },
{ key: 'in_bearbeitung', el: document.getElementById('pipelineIn') },
{ key: 'warten_auf_user', el: document.getElementById('pipelineWait') },
{ key: 'erledigt', el: document.getElementById('pipelineDone') }
];
states.forEach((s) => {
if (!s.el) return;
const list = pipelineCache[s.key] || [];
if (!list.length) {
s.el.innerHTML = '<div class=\"ticket-empty\">Keine Tickets.</div>';
return;
}
s.el.innerHTML = '';
list.forEach((t) => {
const card = document.createElement('div');
card.className = 'ticket-list-item';
card.innerHTML =
'<div class=\"ticket-item-top\"><div class=\"ticket-title\">' +
(t.topic || 'Ticket') +
'</div><div class=\"ticket-status-badge\">' +
(t.status || '') +
'</div></div>' +
'<div class=\"ticket-meta\">Erstellt: ' +
(t.createdAt ? new Date(t.createdAt).toLocaleString() : '-') +
'</div>' +
'<div class=\"ticket-meta\">User: ' +
(t.userId || '-') +
(t.claimedBy ? ' · Supporter: ' + t.claimedBy : '') +
'</div>';
const select = document.createElement('select');
select.innerHTML =
'<option value=\"neu\">Neu</option><option value=\"in_bearbeitung\">In Bearbeitung</option><option value=\"warten_auf_user\">Warten auf User</option><option value=\"erledigt\">Erledigt</option>';
select.value = ['neu', 'in_bearbeitung', 'warten_auf_user', 'erledigt'].includes(t.status) ? t.status : 'neu';
select.addEventListener('change', async (e) => {
await fetch('/api/tickets/' + t.id + '/status', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: e.target.value })
});
await loadPipeline();
await loadTickets();
});
select.style.marginTop = '6px';
card.appendChild(select);
s.el.appendChild(card);
});
});
}
async function loadSla() {
if (!currentGuild) return;
const days = document.getElementById('slaRange')?.value || '30';
const res = await fetch('/api/tickets/sla?guildId=' + encodeURIComponent(currentGuild) + '&days=' + days);
if (!res.ok) return;
const data = await res.json();
slaSupporters = data.supporters || [];
slaDays = data.days || [];
renderSla();
}
function renderSla() {
const supBody = document.getElementById('slaSupporterBody');
const dayBody = document.getElementById('slaDaysBody');
if (supBody) {
supBody.innerHTML = '';
if (!slaSupporters.length) supBody.innerHTML = '<tr><td colspan=\"4\" class=\"muted\">Keine Daten</td></tr>';
slaSupporters.forEach((s) => {
const tr = document.createElement('tr');
tr.innerHTML =
'<td>' +
(s.supporterId || '-') +
'</td><td style=\"text-align:center;\">' +
(s.tickets ?? '-') +
'</td><td style=\"text-align:center;\">' +
formatDuration(s.avgTTC) +
'</td><td style=\"text-align:center;\">' +
formatDuration(s.avgTTFR) +
'</td>';
supBody.appendChild(tr);
});
}
if (dayBody) {
dayBody.innerHTML = '';
if (!slaDays.length) dayBody.innerHTML = '<tr><td colspan=\"4\" class=\"muted\">Keine Daten</td></tr>';
slaDays.forEach((d) => {
const tr = document.createElement('tr');
tr.innerHTML =
'<td>' +
(d.date || '-') +
'</td><td style=\"text-align:center;\">' +
(d.tickets ?? '-') +
'</td><td style=\"text-align:center;\">' +
formatDuration(d.avgTTC) +
'</td><td style=\"text-align:center;\">' +
formatDuration(d.avgTTFR) +
'</td>';
dayBody.appendChild(tr);
});
}
}
async function loadAutomations() {
if (!currentGuild) return;
const res = await fetch('/api/automations?guildId=' + encodeURIComponent(currentGuild));
if (!res.ok) return;
const data = await res.json();
automationCache = data.rules || [];
renderAutomations();
}
function renderAutomations() {
const list = document.getElementById('automationList');
if (!list) return;
list.innerHTML = '';
if (!automationCache.length) {
list.innerHTML = '<div class=\"ticket-empty\">Keine Regeln.</div>';
return;
}
automationCache.forEach((r) => {
const row = document.createElement('div');
row.className = 'ticket-list-item';
row.innerHTML =
'<div class=\"ticket-item-top\"><div class=\"ticket-title\">' +
(r.name || 'Regel') +
'</div><div class=\"ticket-status-badge\">' +
(r.active ? 'aktiv' : 'inaktiv') +
'</div></div>' +
'<div class=\"ticket-meta\">Condition: ' +
JSON.stringify(r.condition || {}) +
'</div>' +
'<div class=\"ticket-meta\">Action: ' +
JSON.stringify(r.action || {}) +
'</div>';
const actions = document.createElement('div');
actions.className = 'row';
const edit = document.createElement('button');
edit.className = 'secondary-btn';
edit.textContent = 'Bearbeiten';
edit.addEventListener('click', () => fillAutomationForm(r));
const del = document.createElement('button');
del.className = 'danger-btn';
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öscht' : 'Löschen fehlgeschlagen', !res.ok);
if (res.ok) loadAutomations();
});
actions.appendChild(edit);
actions.appendChild(del);
row.appendChild(actions);
list.appendChild(row);
});
}
function fillAutomationForm(rule) {
document.getElementById('automationId').value = rule.id || '';
document.getElementById('automationName').value = rule.name || '';
document.getElementById('automationConditionType').value =
rule.condition?.category ? 'category' : rule.condition?.status ? 'status' : rule.condition?.minHours ? 'age' : 'category';
document.getElementById('automationConditionValue').value =
rule.condition?.category || rule.condition?.status || rule.condition?.minHours || '';
document.getElementById('automationActionType').value =
rule.action?.type === 'reminder' ? 'reminder' : rule.action?.type === 'flag' ? 'flag' : 'pingRole';
document.getElementById('automationActionValue').value =
rule.action?.roleId || rule.action?.message || rule.action?.status || '';
setSwitch(document.getElementById('automationActive'), rule.active !== false);
}
async function loadKb() {
if (!currentGuild) return;
const res = await fetch('/api/kb?guildId=' + encodeURIComponent(currentGuild));
if (!res.ok) return;
const data = await res.json();
kbCache = data.articles || [];
renderKb();
}
function renderKb() {
const list = document.getElementById('kbList');
if (!list) return;
list.innerHTML = '';
if (!kbCache.length) {
list.innerHTML = '<div class=\"ticket-empty\">Keine Artikel.</div>';
return;
}
kbCache.forEach((a) => {
const row = document.createElement('div');
row.className = 'ticket-list-item';
row.innerHTML =
'<div class=\"ticket-item-top\"><div class=\"ticket-title\">' +
(a.title || 'Artikel') +
'</div></div><div class=\"ticket-meta\">Keywords: ' +
(Array.isArray(a.keywords) ? a.keywords.join(', ') : '') +
'</div><div class=\"ticket-meta\">' +
(a.content || '') +
'</div>';
const actions = document.createElement('div');
actions.className = 'row';
const edit = document.createElement('button');
edit.className = 'secondary-btn';
edit.textContent = 'Bearbeiten';
edit.addEventListener('click', () => fillKbForm(a));
const del = document.createElement('button');
del.className = 'danger-btn';
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öscht' : 'Löschen fehlgeschlagen', !res.ok);
if (res.ok) loadKb();
});
actions.appendChild(edit);
actions.appendChild(del);
row.appendChild(actions);
list.appendChild(row);
});
}
function fillKbForm(a) {
document.getElementById('kbId').value = a.id || '';
document.getElementById('kbTitle').value = a.title || '';
document.getElementById('kbKeywords').value = Array.isArray(a.keywords) ? a.keywords.join(', ') : '';
document.getElementById('kbContent').value = a.content || '';
}
function parseCategories(raw) {
const lines = (raw || '').split('\\n').map((l) => l.trim()).filter(Boolean);
return lines.slice(0, 5).map((line) => {
const parts = line.split('|').map((s) => (s ? s.trim() : ''));
const label = parts[0];
const customId = parts[1];
const emoji = parts[2] || undefined;
return { label, customId, emoji };
}).filter((c) => c.label && c.customId);
}
async function createPanel(channelId, title, description, categoriesRaw) {
const categories = parseCategories(categoriesRaw);
const res = await fetch('/api/tickets/panel', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ guildId: currentGuild, channelId, title, description, categories }) });
return res.ok;
}
async function loadSettings(guildId) {
const res = await fetch('/api/settings?guildId=' + encodeURIComponent(guildId));
if (!res.ok) return;
const data = await res.json();
const cfg = data.settings || {};
const welcomeChannelInput = document.querySelector('input[name="welcomeChannelId"]');
const logChannelInput = document.querySelector('input[name="logChannelId"]');
const supportRoleInput = document.querySelector('input[name="supportRoleId"]');
if (welcomeChannelInput) (welcomeChannelInput as HTMLInputElement).value = cfg.welcomeChannelId || '';
if (logChannelInput) (logChannelInput as HTMLInputElement).value = cfg.logChannelId || '';
if (supportRoleInput) (supportRoleInput as HTMLInputElement).value = cfg.supportRoleId || '';
automodConfigCache = cfg;
modulesCache['ticketsEnabled'] = cfg.ticketsEnabled !== false;
modulesCache['automodEnabled'] = cfg.automodEnabled !== false;
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['birthdayEnabled'] = cfg.birthdayEnabled !== false && cfg.birthdayConfig?.enabled !== false;
modulesCache['reactionRolesEnabled'] = cfg.reactionRolesEnabled !== false && cfg.reactionRolesConfig?.enabled !== false;
modulesCache['eventsEnabled'] = cfg.eventsEnabled !== false;
dynamicVoiceCache = cfg.dynamicVoiceConfig || {};
applyNavVisibility();
await loadAutomodSettings(guildId);
await loadLoggingSettings(guildId);
await loadWelcomeSettings(guildId);
await loadDynamicVoiceSettings(guildId);
await loadStatuspage();
await loadBirthday();
await loadReactionRoles();
await loadSupportLogin();
await loadEvents();
}
async function loadAutomodSettings(guildId) {
const res = await fetch('/api/settings?guildId=' + encodeURIComponent(guildId));
if (!res.ok) return;
const data = await res.json();
const cfg = data.settings || {};
const auto = cfg.automodConfig || {};
setSwitch(automodToggle, cfg.automodEnabled !== false);
setSwitch(badWordToggle, auto.badWordFilter ?? false);
setSwitch(linkFilterToggle, auto.linkFilter ?? auto.deleteLinks ?? false);
setSwitch(spamFilterToggle, auto.spamFilter ?? auto.spamDetection ?? false);
setSwitch(capsFilterToggle, auto.capsFilter ?? false);
if (automodLogChannel) automodLogChannel.value = auto.logChannelId || cfg.logChannelId || '';
if (automodSensitivity) automodSensitivity.value = auto.sensitivity || 'medium';
if (automodWhitelist) automodWhitelist.value = (auto.linkWhitelist || []).join(', ');
if (automodBadwords) automodBadwords.value = (auto.customBadwords || []).join(', ');
if (automodWhitelistRoles) automodWhitelistRoles.value = (auto.whitelistRoles || []).join(', ');
}
async function loadLoggingSettings(guildId) {
const res = await fetch('/api/settings?guildId=' + encodeURIComponent(guildId));
if (!res.ok) return;
const data = await res.json();
const cfg = data.settings || {};
const logging = cfg.loggingConfig || cfg.automodConfig?.loggingConfig || {};
const cats = logging.categories || {};
setSwitch(logJoinLeave, cats.joinLeave !== false);
setSwitch(logMsgEdit, cats.messageEdit !== false);
setSwitch(logMsgDelete, cats.messageDelete !== false);
setSwitch(logAutomod, cats.automodActions !== false);
setSwitch(logTickets, cats.ticketActions !== false);
setSwitch(logMusic, cats.musicEvents !== false);
setSwitch(logSystem, cats.system !== false);
if (loggingChannel) loggingChannel.value = logging.logChannelId || cfg.logChannelId || '';
}
async function loadWelcomeSettings(guildId) {
const res = await fetch('/api/settings?guildId=' + encodeURIComponent(guildId));
if (!res.ok) return;
const data = await res.json();
const cfg = data.settings || {};
const welcome = cfg.welcomeConfig || cfg.automodConfig?.welcomeConfig || {};
setSwitch(welcomeToggle, welcome.enabled !== false);
if (welcomeChannel) welcomeChannel.value = welcome.channelId || cfg.welcomeChannelId || '';
const colorValue = '#' + (welcome.embedColor || 'f97316').replace('#', '');
if (welcomeColor) welcomeColor.value = colorValue;
if (welcomeTitle) welcomeTitle.value = welcome.embedTitle || 'Willkommen!';
if (welcomeFooter) welcomeFooter.value = welcome.embedFooter || '';
if (welcomeDescription) welcomeDescription.value = welcome.embedDescription || '';
if (welcomeThumbnail) welcomeThumbnail.value = welcome.embedThumbnail || '';
if (welcomeImage) welcomeImage.value = welcome.embedImage || '';
welcomeThumbData = welcome.embedThumbnailData || '';
welcomeImageData = welcome.embedImageData || '';
updateWelcomePreview();
}
async function loadDynamicVoiceSettings(guildId) {
const res = await fetch('/api/settings?guildId=' + encodeURIComponent(guildId));
if (!res.ok) return;
const data = await res.json();
const cfg = data.settings || {};
const dv = cfg.dynamicVoiceConfig || {};
setSwitch(dynamicVoiceToggle, cfg.dynamicVoiceEnabled !== false);
if (dynamicVoiceLobby) dynamicVoiceLobby.value = dv.lobbyChannelId || '';
if (dynamicVoiceCategory) dynamicVoiceCategory.value = dv.categoryId || '';
if (dynamicVoiceTemplate) dynamicVoiceTemplate.value = dv.template || '{user}s Channel';
if (dynamicVoiceUserLimit) dynamicVoiceUserLimit.value = dv.userLimit ?? '';
if (dynamicVoiceBitrate) dynamicVoiceBitrate.value = dv.bitrate ?? '';
}
async function loadSupportLogin() {
if (!currentGuild) return;
const res = await fetch('/api/tickets/support-login?guildId=' + encodeURIComponent(currentGuild));
if (!res.ok) return;
const data = await res.json();
supportLoginCache = data.config || {};
if (supportLoginChannel) supportLoginChannel.value = supportLoginCache.panelChannelId || '';
if (supportLoginTitle) supportLoginTitle.value = supportLoginCache.title || '';
if (supportLoginDescription) supportLoginDescription.value = supportLoginCache.description || '';
if (supportLoginLabel) supportLoginLabel.value = supportLoginCache.loginLabel || '';
if (supportLogoutLabel) supportLogoutLabel.value = supportLoginCache.logoutLabel || '';
setSwitch(supportLoginAuto, supportLoginCache.autoRefresh !== false);
renderSupportStatus(data.status || {});
}
function renderSupportStatus(status) {
if (supportActiveList) {
supportActiveList.innerHTML = '';
const active = status.active || [];
if (!active.length) supportActiveList.innerHTML = '<div class="muted">Niemand aktiv.</div>';
active.forEach((s) => {
const div = document.createElement('div');
div.className = 'ticket-list-item';
div.innerHTML = '<div><strong>' + (s.userId || '-') + '</strong></div><div class="muted">Seit: ' + formatDate(s.startedAt || Date.now()) + '</div>';
supportActiveList.appendChild(div);
});
}
if (supportRecentList) {
supportRecentList.innerHTML = '';
const recent = status.recent || [];
if (!recent.length) supportRecentList.innerHTML = '<div class="muted">Keine Sessions</div>';
recent.forEach((s) => {
const div = document.createElement('div');
div.className = 'ticket-list-item';
const dur = s.durationSeconds ? Math.round(s.durationSeconds / 60) + 'm' : '-';
div.innerHTML =
'<div><strong>' +
(s.userId || '-') +
'</strong></div><div class="muted">Ende: ' +
formatDate(s.endedAt || Date.now()) +
' · Dauer: ' +
dur +
'</div>';
supportRecentList.appendChild(div);
});
}
}
async function saveSupportLogin() {
if (!currentGuild) return;
const payload = {
guildId: currentGuild,
panelChannelId: supportLoginChannel?.value || '',
title: supportLoginTitle?.value || '',
description: supportLoginDescription?.value || '',
loginLabel: supportLoginLabel?.value || '',
logoutLabel: supportLogoutLabel?.value || '',
autoRefresh: getSwitch(supportLoginAuto)
};
const res = await fetch('/api/tickets/support-login', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
if (supportLoginStatus) supportLoginStatus.textContent = res.ok ? 'Gespeichert' : 'Fehler';
showToast(res.ok ? 'Support-Login gespeichert' : 'Speichern fehlgeschlagen', !res.ok);
if (res.ok) {
hideModal();
await loadSupportLogin();
}
}
async function loadEvents() {
if (!currentGuild || modulesCache['eventsEnabled'] === false) {
eventsCache = [];
if (eventList) eventList.innerHTML = '<div class="muted">Events deaktiviert.</div>';
return;
}
const res = await fetch('/api/events?guildId=' + encodeURIComponent(currentGuild));
if (!res.ok) return;
const data = await res.json();
eventsCache = data.events || [];
renderEvents();
}
function renderEvents() {
if (!eventList) return;
eventList.innerHTML = '';
if (!eventsCache.length) {
eventList.innerHTML = '<div class="ticket-empty">Keine Events</div>';
return;
}
eventsCache.forEach((ev) => {
const row = document.createElement('div');
row.className = 'ticket-list-item';
row.innerHTML =
'<div class="ticket-item-top"><div class="ticket-title">' +
(ev.title || 'Event') +
'</div><span class="ticket-status-badge">' +
(ev.repeatType || 'none') +
'</span></div><div class="ticket-meta">Start: ' +
formatDate(ev.startTime) +
' · Channel: ' +
(ev.channelId || '-') +
' · Anmeldungen: ' +
(ev._count?.signups ?? 0) +
'</div>';
const actions = document.createElement('div');
actions.className = 'row';
const editBtn = document.createElement('button');
editBtn.className = 'secondary-btn';
editBtn.textContent = 'Bearbeiten';
editBtn.style.padding = '8px 10px';
editBtn.addEventListener('click', () => fillEventForm(ev));
const delBtn = document.createElement('button');
delBtn.className = 'danger-btn';
delBtn.textContent = 'Loeschen';
delBtn.style.padding = '8px 10px';
delBtn.addEventListener('click', () => deleteEvent(ev.id));
actions.appendChild(editBtn);
actions.appendChild(delBtn);
row.appendChild(actions);
eventList.appendChild(row);
});
}
function fillEventForm(ev) {
if (eventId) eventId.value = ev.id || '';
if (eventTitle) eventTitle.value = ev.title || '';
if (eventDescription) eventDescription.value = ev.description || '';
if (eventChannel) eventChannel.value = ev.channelId || '';
const dt = new Date(ev.startTime);
if (eventDate) eventDate.value = dt.toISOString().slice(0, 10);
if (eventTime) eventTime.value = dt.toISOString().slice(11, 16);
if (eventRepeat) eventRepeat.value = ev.repeatType || 'none';
if (eventWeeklyDays) eventWeeklyDays.value = Array.isArray(ev.repeatConfig?.days) ? ev.repeatConfig.days.join(',') : '';
if (eventMonthlyDay) eventMonthlyDay.value = ev.repeatConfig?.day || '';
if (eventReminder) eventReminder.value = ev.reminderOffsetMinutes ?? '';
if (eventRole) eventRole.value = ev.roleId || '';
toggleRepeatRows();
showModal(eventModal);
}
function toggleRepeatRows() {
const val = eventRepeat?.value || 'none';
if (eventWeeklyRow) eventWeeklyRow.style.display = val === 'weekly' ? 'block' : 'none';
if (eventMonthlyRow) eventMonthlyRow.style.display = val === 'monthly' ? 'block' : 'none';
}
async function saveEvent() {
if (!currentGuild) return;
const startIso = (eventDate?.value || '') + 'T' + (eventTime?.value || '00:00');
const payload = {
id: eventId?.value || undefined,
guildId: currentGuild,
title: eventTitle?.value || '',
description: eventDescription?.value || '',
channelId: eventChannel?.value || '',
startTime: startIso,
repeatType: eventRepeat?.value || 'none',
repeatConfig: {
days: (eventWeeklyDays?.value || '')
.split(',')
.map((s) => Number(s.trim()))
.filter((n) => !isNaN(n)),
day: eventMonthlyDay?.value ? Number(eventMonthlyDay.value) : undefined
},
reminderOffsetMinutes: eventReminder?.value ? Number(eventReminder.value) : 60,
roleId: eventRole?.value || undefined
};
const res = await fetch('/api/events', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
if (eventStatus) eventStatus.textContent = res.ok ? 'Gespeichert' : 'Fehler';
showToast(res.ok ? 'Event gespeichert' : 'Event speichern fehlgeschlagen', !res.ok);
if (res.ok) {
hideModal();
await loadEvents();
}
}
async function deleteEvent(id) {
if (!currentGuild) return;
const res = await fetch('/api/events/' + id, { method:'DELETE', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ guildId: currentGuild }) });
showToast(res.ok ? 'Event geloescht' : 'Event loeschen fehlgeschlagen', !res.ok);
if (res.ok) loadEvents();
}
async function loadBirthday() {
if (!currentGuild) return;
const res = await fetch('/api/birthday?guildId=' + encodeURIComponent(currentGuild));
if (!res.ok) return;
const data = await res.json();
birthdayCache = data || {};
const cfg = data.config || {};
setSwitch(birthdayToggle, cfg.enabled !== false);
if (birthdayChannel) birthdayChannel.value = cfg.channelId || '';
if (birthdayHour) birthdayHour.value = cfg.sendHour ?? '';
if (birthdayTemplate) birthdayTemplate.value = cfg.messageTemplate || 'Alles Gute zum Geburtstag, {user}!';
renderBirthdays();
}
function renderBirthdays() {
if (!birthdayList) return;
const entries = birthdayCache.birthdays || [];
birthdayList.innerHTML = '';
if (!entries.length) {
birthdayList.innerHTML = '<div class="muted">Keine Geburtstage hinterlegt.</div>';
return;
}
entries.forEach((b) => {
const row = document.createElement('div');
row.className = 'module-item';
const meta = document.createElement('div');
meta.className = 'module-meta';
const date = (b.birthDate || '').replace(/^--/, '');
meta.innerHTML = '<div class="module-title">' + (date || '-') + '</div><div class="module-desc">User: ' + b.userId + '</div>';
row.appendChild(meta);
birthdayList.appendChild(row);
});
}
async function saveBirthdayConfig() {
if (!currentGuild) return;
const payload = {
guildId: currentGuild,
enabled: getSwitch(birthdayToggle),
channelId: birthdayChannel?.value || '',
sendHour: birthdayHour?.value,
messageTemplate: birthdayTemplate?.value
};
const res = await fetch('/api/birthday', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
if (birthdayStatus) birthdayStatus.textContent = res.ok ? 'Gespeichert' : 'Fehler';
showToast(res.ok ? 'Birthday gespeichert' : 'Birthday Fehler', !res.ok);
if (res.ok) await loadBirthday();
}
function parseReactionRoleEntries(raw) {
return (raw || '')
.split('\\n')
.map((l) => l.trim())
.filter(Boolean)
.map((line) => {
const parts = line.split('|').map((p) => p.trim());
return { emoji: parts[0], roleId: parts[1], label: parts[2] || undefined, description: parts[3] || undefined };
})
.filter((e) => e.emoji && e.roleId);
}
function resetReactionRoleForm() {
editingReactionRole = null;
if (reactionRoleId) reactionRoleId.value = '';
if (reactionRoleChannel) reactionRoleChannel.value = '';
if (reactionRoleMessageId) reactionRoleMessageId.value = '';
if (reactionRoleTitle) reactionRoleTitle.value = '';
if (reactionRoleDescription) reactionRoleDescription.value = '';
if (reactionRoleEntries) reactionRoleEntries.value = '';
if (reactionRoleStatus) reactionRoleStatus.textContent = '';
}
function resetEventForm() {
if (eventId) eventId.value = '';
if (eventTitle) eventTitle.value = '';
if (eventDescription) eventDescription.value = '';
if (eventChannel) eventChannel.value = '';
if (eventDate) eventDate.value = '';
if (eventTime) eventTime.value = '';
if (eventRepeat) eventRepeat.value = 'none';
if (eventWeeklyDays) eventWeeklyDays.value = '';
if (eventMonthlyDay) eventMonthlyDay.value = '';
if (eventReminder) eventReminder.value = '120';
if (eventRole) eventRole.value = '';
if (eventStatus) eventStatus.textContent = '';
toggleRepeatRows();
}
function fillReactionRoleForm(set) {
editingReactionRole = set.id;
if (reactionRoleId) reactionRoleId.value = set.id || '';
if (reactionRoleChannel) reactionRoleChannel.value = set.channelId || '';
if (reactionRoleMessageId) reactionRoleMessageId.value = set.messageId || '';
if (reactionRoleTitle) reactionRoleTitle.value = set.title || '';
if (reactionRoleDescription) reactionRoleDescription.value = set.description || '';
if (reactionRoleEntries) {
const lines = (set.entries || []).map((e) => [e.emoji, e.roleId, e.label || '', e.description || ''].join(' | '));
reactionRoleEntries.value = lines.join('\\n');
}
if (reactionRoleStatus) reactionRoleStatus.textContent = 'Bearbeitung aktiv';
}
async function loadReactionRoles() {
if (!currentGuild) return;
resetReactionRoleForm();
if (modulesCache['reactionRolesEnabled'] === false) {
reactionRoleCache = [];
if (reactionRoleList) reactionRoleList.innerHTML = '<div class="muted">Modul deaktiviert.</div>';
return;
}
const res = await fetch('/api/reactionroles?guildId=' + encodeURIComponent(currentGuild));
if (!res.ok) return;
const data = await res.json();
reactionRoleCache = data.sets || [];
renderReactionRoleSets();
}
function renderReactionRoleSets() {
if (!reactionRoleList) return;
reactionRoleList.innerHTML = '';
if (!reactionRoleCache.length) {
reactionRoleList.innerHTML = '<div class="muted">Keine Reaction Roles angelegt.</div>';
return;
}
reactionRoleCache.forEach((set) => {
const row = document.createElement('div');
row.className = 'module-item';
const meta = document.createElement('div');
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>';
const actions = document.createElement('div');
actions.className = 'row';
const editBtn = document.createElement('button');
editBtn.className = 'secondary-btn';
editBtn.style.padding = '8px 10px';
editBtn.style.fontSize = '12px';
editBtn.textContent = 'Bearbeiten';
editBtn.addEventListener('click', () => fillReactionRoleForm(set));
const syncBtn = document.createElement('button');
syncBtn.className = 'secondary-btn';
syncBtn.style.padding = '8px 10px';
syncBtn.style.fontSize = '12px';
syncBtn.textContent = 'Sync';
syncBtn.addEventListener('click', () => syncReactionRole(set.id));
const delBtn = document.createElement('button');
delBtn.className = 'danger-btn';
delBtn.style.padding = '8px 10px';
delBtn.style.fontSize = '12px';
delBtn.textContent = 'Loeschen';
delBtn.addEventListener('click', () => deleteReactionRole(set.id));
actions.appendChild(editBtn);
actions.appendChild(syncBtn);
actions.appendChild(delBtn);
row.appendChild(meta);
row.appendChild(actions);
reactionRoleList.appendChild(row);
});
}
async function syncReactionRole(id) {
const set = reactionRoleCache.find((s) => s.id === id);
if (!set) return;
const payload = {
guildId: currentGuild,
channelId: set.channelId,
messageId: set.messageId,
title: set.title,
description: set.description,
entries: set.entries || []
};
const res = await fetch('/api/reactionroles/' + id, { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
showToast(res.ok ? 'Reaction Role aktualisiert' : 'Sync fehlgeschlagen', !res.ok);
if (res.ok) loadReactionRoles();
}
async function deleteReactionRole(id) {
if (!currentGuild) return;
const res = await fetch('/api/reactionroles/' + id, { method:'DELETE', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ guildId: currentGuild }) });
showToast(res.ok ? 'Geloescht' : 'Fehler beim Loeschen', !res.ok);
if (res.ok) {
if (editingReactionRole === id) resetReactionRoleForm();
loadReactionRoles();
}
}
// TODO: MODULE: Liste um Musik/Forum/Automod-Konfiguration ergaenzen.
// - Module-Status inkl. Direktlinks zu Detailseiten (Automod/Welcome/Musik) rendern.
// - Module-Flags aus BotModuleService spiegeln statt doppeltem Fetch.
async function loadModules() {
if (!currentGuild) return;
const res = await fetch('/api/modules?guildId=' + encodeURIComponent(currentGuild));
if (!res.ok) return;
const data = await res.json();
const list = document.getElementById('moduleList');
list.innerHTML = '';
let ticketsActive = false;
let statuspageActive = false;
let serverStatsActive = false;
let birthdayActive = false;
let reactionRolesActive = false;
let eventsActive = false;
(data.modules || []).forEach((m) => {
modulesCache[m.key] = !!m.enabled;
const row = document.createElement('div');
row.className = 'module-item';
const meta = document.createElement('div');
meta.className = 'module-meta';
meta.innerHTML = '<div class="module-title">' + m.name + '</div><div class="module-desc">' + (m.description || '') + '</div>';
const toggle = document.createElement('div');
toggle.className = 'toggle' + (m.enabled ? ' active' : '');
toggle.dataset.key = m.key;
toggle.addEventListener('click', async () => {
const willEnable = !toggle.classList.contains('active');
const ok = await saveModuleToggle(m.key, willEnable);
if (ok) {
toggle.classList.toggle('active', willEnable);
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 === '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);
}
});
row.appendChild(meta);
row.appendChild(toggle);
list.appendChild(row);
if (m.key === 'ticketsEnabled' && m.enabled) ticketsActive = true;
if (m.key === 'automodEnabled') modulesCache['automodEnabled'] = !!m.enabled;
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;
});
applyNavVisibility();
applyTicketsVisibility(ticketsActive);
if (statuspageActive) loadStatuspage();
if (serverStatsActive) loadServerStats();
if (birthdayActive) loadBirthday();
if (reactionRolesActive) loadReactionRoles();
if (eventsActive) loadEvents();
}
async function saveModuleToggle(key, enabled) {
if (!currentGuild) return false;
const payload = { guildId: currentGuild };
['ticketsEnabled', 'automodEnabled', 'welcomeEnabled', 'musicEnabled', 'levelingEnabled', 'statuspageEnabled', 'serverStatsEnabled', 'birthdayEnabled', 'reactionRolesEnabled', 'eventsEnabled'].forEach((k) => {
if (modulesCache[k] !== undefined) payload[k] = modulesCache[k];
});
payload['dynamicVoiceEnabled'] = modulesCache['dynamicVoiceEnabled'];
payload[key] = enabled;
const res = await fetch('/api/settings', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
return res.ok;
}
function applyTicketsVisibility(active) {
const ticketsNav = document.querySelector('.nav a[data-target="tickets"]');
const ticketsSection = document.querySelector('.section[data-section="tickets"]');
if (ticketsNav && ticketsSection) {
ticketsNav.classList.toggle('hidden', !active);
ticketsSection.classList.toggle('hidden', !active);
if (!active && location.hash.replace('#','') === 'tickets') {
activateSection('overview');
history.replaceState(null, '', '#overview');
}
}
}
const settingsForm = document.getElementById('settingsForm');
if (settingsForm) {
settingsForm.addEventListener('submit', async (e) => {
e.preventDefault();
if (!currentGuild) return;
const form = new FormData(e.currentTarget);
const payload = Object.fromEntries(form.entries());
payload.supportRoleId = payload.supportRoleId || undefined;
payload.guildId = currentGuild;
const res = await fetch('/api/settings', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
const saveStatus = document.getElementById('saveStatus');
if (saveStatus) saveStatus.textContent = res.ok ? 'Gespeichert' : 'Fehler';
if (res.ok) {
await loadSettings(currentGuild);
await loadModules();
}
});
}
const openPanelModalBtn = document.getElementById('openPanelModal');
if (openPanelModalBtn) {
openPanelModalBtn.addEventListener('click', () => {
const panelStatus = document.getElementById('panelStatus');
if (panelStatus) panelStatus.textContent = '';
showModal(panelModal);
});
}
if (openSupportLogin) openSupportLogin.addEventListener('click', () => { showModal(supportLoginModal); });
if (supportLoginSave) supportLoginSave.addEventListener('click', saveSupportLogin);
if (refreshSupportStatus) refreshSupportStatus.addEventListener('click', loadSupportLogin);
const panelSubmit = document.getElementById('panelSubmit');
if (panelSubmit) {
panelSubmit.addEventListener('click', async () => {
if (!currentGuild) return;
const formEl = document.getElementById('panelForm');
const form = formEl ? new FormData(formEl) : null;
const channelId = form?.get('channelId');
const panelTitle = form?.get('panelTitle');
const panelDescription = form?.get('panelDescription');
const panelCategories = form?.get('panelCategories');
const status = document.getElementById('panelStatus');
const ok = await createPanel(channelId, panelTitle, panelDescription, panelCategories);
if (status) status.textContent = ok ? 'Panel gesendet' : 'Fehler beim Senden';
showToast(ok ? 'Ticket-Panel gesendet' : 'Ticket-Panel Fehler', !ok);
if (ok) hideModal();
});
}
document.querySelectorAll('[data-close-modal]').forEach((btn) => btn.addEventListener('click', hideModal));
modalBackdrop.addEventListener('click', hideModal);
document.getElementById('closeTicketAction').addEventListener('click', async () => {
if (!selectedTicket) return;
const res = await fetch('/api/tickets/' + selectedTicket.id + '/close', { method: 'POST' });
showToast(res.ok ? 'Ticket geschlossen' : 'Ticket schliessen fehlgeschlagen', !res.ok);
if (res.ok) {
hideModal();
await loadTickets();
}
});
guildSelect.addEventListener('change', async (e) => {
currentGuild = e.target.value;
await loadSettings(currentGuild);
await loadOverview();
await loadTickets();
await loadPipeline();
await loadSla();
await loadAutomations();
await loadKb();
await loadModules();
await loadStatuspage();
});
navLinks.forEach((link) => {
link.addEventListener('click', (e) => {
e.preventDefault();
const target = link.dataset.target || 'overview';
activateSection(target);
history.replaceState(null, '', '#' + target);
if (target === 'admin' && isAdmin) loadAdminAll();
});
});
document.querySelectorAll('.ticket-tab-btn').forEach((btn) => {
btn.addEventListener('click', async () => {
document.querySelectorAll('.ticket-tab-btn').forEach((b) => b.classList.remove('active'));
btn.classList.add('active');
const tab = btn.dataset.tab;
document.querySelectorAll('.ticket-tab').forEach((t) => t.classList.remove('active'));
const target = document.getElementById('ticketTab' + (tab ? tab.charAt(0).toUpperCase() + tab.slice(1) : ''));
if (target) target.classList.add('active');
if (tab === 'pipeline') await loadPipeline();
if (tab === 'sla') await loadSla();
if (tab === 'automations') await loadAutomations();
if (tab === 'kb') await loadKb();
});
});
const slaRange = document.getElementById('slaRange');
if (slaRange) slaRange.addEventListener('change', loadSla);
const automationForm = document.getElementById('automationForm');
if (automationForm) {
automationForm.addEventListener('submit', async (e) => {
e.preventDefault();
if (!currentGuild) return;
const id = document.getElementById('automationId')?.value || '';
const name = document.getElementById('automationName')?.value || '';
const condType = document.getElementById('automationConditionType')?.value;
const condVal = document.getElementById('automationConditionValue')?.value;
const actionType = document.getElementById('automationActionType')?.value;
const actionVal = document.getElementById('automationActionValue')?.value;
const active = getSwitch(document.getElementById('automationActive'));
const condition = {};
if (condType === 'category') condition.category = condVal;
if (condType === 'status') condition.status = condVal;
if (condType === 'age') condition.minHours = Number(condVal || 0);
const action = { type: actionType };
if (actionType === 'pingRole') action.roleId = actionVal;
if (actionType === 'reminder') action.message = actionVal;
if (actionType === 'flag') action.status = actionVal;
const payload = { guildId: currentGuild, name, condition, action, active };
const url = id ? '/api/automations/' + id : '/api/automations';
const method = id ? 'PUT' : 'POST';
const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
showToast(res.ok ? 'Regel gespeichert' : 'Fehler', !res.ok);
if (res.ok) {
document.getElementById('automationId').value = '';
automationForm.reset();
setSwitch(document.getElementById('automationActive'), true);
await loadAutomations();
}
});
}
const automationReset = document.getElementById('automationReset');
if (automationReset) automationReset.addEventListener('click', () => {
document.getElementById('automationId').value = '';
if (automationForm) automationForm.reset();
setSwitch(document.getElementById('automationActive'), true);
});
const addAutomation = document.getElementById('addAutomation');
if (addAutomation) addAutomation.addEventListener('click', () => {
document.getElementById('automationId').value = '';
if (automationForm) automationForm.reset();
setSwitch(document.getElementById('automationActive'), true);
});
const kbForm = document.getElementById('kbForm');
if (kbForm) {
kbForm.addEventListener('submit', async (e) => {
e.preventDefault();
if (!currentGuild) return;
const id = document.getElementById('kbId')?.value || '';
const title = document.getElementById('kbTitle')?.value || '';
const keywords = document.getElementById('kbKeywords')?.value || '';
const content = document.getElementById('kbContent')?.value || '';
const payload = { guildId: currentGuild, title, keywords, content };
const url = id ? '/api/kb/' + id : '/api/kb';
const method = id ? 'PUT' : 'POST';
const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
showToast(res.ok ? 'Artikel gespeichert' : 'Fehler', !res.ok);
if (res.ok) {
document.getElementById('kbId').value = '';
kbForm.reset();
await loadKb();
}
});
}
const kbReset = document.getElementById('kbReset');
if (kbReset) kbReset.addEventListener('click', () => {
document.getElementById('kbId').value = '';
if (kbForm) kbForm.reset();
});
const addKb = document.getElementById('addKb');
if (addKb) addKb.addEventListener('click', () => {
document.getElementById('kbId').value = '';
if (kbForm) kbForm.reset();
});
document.getElementById('logoutBtn').addEventListener('click', () => window.location.href = BASE_AUTH + '/logout');
[automodToggle, badWordToggle, linkFilterToggle, spamFilterToggle, capsFilterToggle, logJoinLeave, logMsgEdit, logMsgDelete, logAutomod, logTickets, logMusic, dynamicVoiceToggle, supportLoginAuto].forEach((el) => {
if (el) el.addEventListener('click', () => el.classList.toggle('on'));
});
if (logSystem) logSystem.addEventListener('click', () => logSystem.classList.toggle('on'));
if (welcomeToggle) welcomeToggle.addEventListener('click', () => welcomeToggle.classList.toggle('on'));
if (birthdayToggle) birthdayToggle.addEventListener('click', () => birthdayToggle.classList.toggle('on'));
if (statuspageToggle) statuspageToggle.addEventListener('click', async () => { statuspageToggle.classList.toggle('on'); await saveStatuspageConfig(); });
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);
});
if (welcomeThumbnailFile) welcomeThumbnailFile.addEventListener('change', () => readFileToDataUrl(welcomeThumbnailFile, (data) => { welcomeThumbData = data; }));
if (welcomeImageFile) welcomeImageFile.addEventListener('change', () => readFileToDataUrl(welcomeImageFile, (data) => { welcomeImageData = data; updateWelcomePreview(); }));
if (welcomeImage) welcomeImage.addEventListener('input', updateWelcomePreview);
if (birthdaySave) birthdaySave.addEventListener('click', saveBirthdayConfig);
if (reactionRoleReset) reactionRoleReset.addEventListener('click', (e) => { e.preventDefault(); resetReactionRoleForm(); });
if (reactionRoleForm) reactionRoleForm.addEventListener('submit', async (e) => {
e.preventDefault();
if (!currentGuild) return;
const entries = parseReactionRoleEntries(reactionRoleEntries?.value || '');
if (!reactionRoleChannel?.value) {
if (reactionRoleStatus) reactionRoleStatus.textContent = 'Channel erforderlich';
return;
}
if (!entries.length) {
if (reactionRoleStatus) reactionRoleStatus.textContent = 'Mindestens ein Eintrag erforderlich';
return;
}
const payload = {
guildId: currentGuild,
channelId: (reactionRoleChannel?.value || '').trim(),
messageId: reactionRoleMessageId?.value ? reactionRoleMessageId.value.trim() : undefined,
title: reactionRoleTitle?.value?.trim() || undefined,
description: reactionRoleDescription?.value?.trim() || undefined,
entries
};
const url = editingReactionRole ? '/api/reactionroles/' + editingReactionRole : '/api/reactionroles';
const method = editingReactionRole ? 'PUT' : 'POST';
const res = await fetch(url, { method, headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
if (reactionRoleStatus) reactionRoleStatus.textContent = res.ok ? 'Gespeichert' : 'Fehler';
showToast(res.ok ? 'Reaction Role gespeichert' : 'Speichern fehlgeschlagen', !res.ok);
if (res.ok) {
resetReactionRoleForm();
await loadReactionRoles();
}
});
if (openEventModal) openEventModal.addEventListener('click', () => { resetEventForm(); showModal(eventModal); });
if (eventRepeat) eventRepeat.addEventListener('change', toggleRepeatRows);
if (eventSave) eventSave.addEventListener('click', saveEvent);
if (dynamicVoiceToggle) dynamicVoiceToggle.addEventListener('click', () => dynamicVoiceToggle.classList.toggle('on'));
document.getElementById('dynamicVoiceForm')?.addEventListener('submit', async (e) => {
e.preventDefault();
if (!currentGuild) return;
const payload = {
guildId: currentGuild,
dynamicVoiceEnabled: getSwitch(dynamicVoiceToggle),
dynamicVoiceConfig: {
lobbyChannelId: dynamicVoiceLobby?.value || undefined,
categoryId: dynamicVoiceCategory?.value || undefined,
template: dynamicVoiceTemplate?.value || undefined,
userLimit: dynamicVoiceUserLimit?.value ? Number(dynamicVoiceUserLimit.value) : undefined,
bitrate: dynamicVoiceBitrate?.value ? Number(dynamicVoiceBitrate.value) : undefined
}
};
const res = await fetch('/api/settings', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
if (dynamicVoiceStatus) dynamicVoiceStatus.textContent = res.ok ? 'Gespeichert' : 'Fehler beim Speichern';
showToast(res.ok ? 'Dynamic Voice gespeichert' : 'Dynamic Voice Fehler', !res.ok);
if (res.ok) await loadDynamicVoiceSettings(currentGuild);
});
if (automodForm) {
automodForm.addEventListener('submit', async (e) => {
e.preventDefault();
if (!currentGuild) return;
const config = {
badWordFilter: getSwitch(badWordToggle),
linkFilter: getSwitch(linkFilterToggle),
spamFilter: getSwitch(spamFilterToggle),
capsFilter: getSwitch(capsFilterToggle),
logChannelId: automodLogChannel?.value || undefined,
sensitivity: automodSensitivity?.value || 'medium',
linkWhitelist: (automodWhitelist?.value || '')
.split(',')
.map((s) => s.trim())
.filter(Boolean),
customBadwords: parseList(automodBadwords?.value),
whitelistRoles: parseList(automodWhitelistRoles?.value)
};
const payload = {
guildId: currentGuild,
automodEnabled: getSwitch(automodToggle),
automodConfig: config
};
const res = await fetch('/api/settings', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
if (automodStatus) automodStatus.textContent = res.ok ? 'Gespeichert' : 'Fehler beim Speichern';
showToast(res.ok ? 'Automod gespeichert' : 'Automod Fehler', !res.ok);
if (res.ok) await loadAutomodSettings(currentGuild);
});
}
document.getElementById('loggingSave')?.addEventListener('click', async () => {
if (!currentGuild) return;
const payload = {
guildId: currentGuild,
loggingConfig: {
logChannelId: loggingChannel?.value || undefined,
categories: {
joinLeave: getSwitch(logJoinLeave),
messageEdit: getSwitch(logMsgEdit),
messageDelete: getSwitch(logMsgDelete),
automodActions: getSwitch(logAutomod),
ticketActions: getSwitch(logTickets),
musicEvents: getSwitch(logMusic),
system: getSwitch(logSystem)
}
}
};
const res = await fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
if (loggingStatus) loggingStatus.textContent = res.ok ? 'Logging gespeichert' : 'Fehler beim Speichern';
showToast(res.ok ? 'Logging gespeichert' : 'Logging Fehler', !res.ok);
if (res.ok) await loadLoggingSettings(currentGuild);
});
if (welcomeForm) {
welcomeForm.addEventListener('submit', async (e) => {
e.preventDefault();
if (!currentGuild) return;
const payload = {
guildId: currentGuild,
welcomeConfig: {
enabled: getSwitch(welcomeToggle),
channelId: welcomeChannel?.value || undefined,
embedTitle: welcomeTitle?.value || undefined,
embedDescription: welcomeDescription?.value || undefined,
embedColor: (welcomeColor?.value || '').replace('#', ''),
embedFooter: welcomeFooter?.value || undefined,
embedThumbnail: welcomeThumbnail?.value || undefined,
embedImage: welcomeImage?.value || undefined,
embedThumbnailData: welcomeThumbData || undefined,
embedImageData: welcomeImageData || undefined
},
welcomeEnabled: getSwitch(welcomeToggle)
};
const res = await fetch('/api/settings', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
if (welcomeStatus) welcomeStatus.textContent = res.ok ? 'Gespeichert' : 'Fehler beim Speichern';
showToast(res.ok ? 'Welcome gespeichert' : 'Welcome Fehler', !res.ok);
if (res.ok) await loadWelcomeSettings(currentGuild);
});
}
const initialSection = location.hash.replace('#', '') || 'overview';
activateSection(initialSection);
ensureAuth().then(() => {
loadGuilds();
if (initialSection === 'admin' && isAdmin) loadAdminAll();
});
setInterval(loadOverview, 10000);
setInterval(loadTickets, 12000);
setInterval(() => { if (isAdmin) loadAdminAll(); }, 20000);
</script>
</body>
</html>
`);
});
router.get('/settings', (_req, res) => {
res.json({ settings: [] });
});
export default router;