Files
Papo/src/web/routes/dashboard.ts
2025-12-04 18:10:07 +01:00

3025 lines
160 KiB
TypeScript

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 auswhlen, 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 fr 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 fr 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">
<label for="badWordFilter">Bad-Word-Filter</label>
<div id="badWordFilter" class="switch"></div>
</div>
<div class="option-card">
<label for="linkFilter">Link-/Invite-Filter</label>
<div id="linkFilter" class="switch"></div>
</div>
<div class="option-card">
<label for="spamFilter">Spam-/Flood-Erkennung</label>
<div id="spamFilter" class="switch"></div>
</div>
<div class="option-card">
<label for="capsFilter">Capslock-Filter</label>
<div id="capsFilter" class="switch"></div>
</div>
</div>
<div class="form-row">
<div class="form-field">
<label class="form-label">Log Channel ID</label>
<input id="automodLogChannel" placeholder="123456789012345678" />
</div>
<div class="form-field">
<label class="form-label">Empfindlichkeit</label>
<select id="automodSensitivity">
<option value="low">niedrig</option>
<option value="medium">mittel</option>
<option value="high">hoch</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-field">
<label class="form-label">Whitelist-Links (kommagetrennt)</label>
<input id="automodWhitelist" placeholder="example.com, partner.de" />
</div>
</div>
<div class="form-row">
<div class="form-field">
<label class="form-label">Custom Badwords (kommagetrennt oder zeilenweise)</label>
<textarea id="automodBadwords" rows="3" placeholder="badword1, badword2"></textarea>
<p class="muted">Diese Woerter werden zusaetzlich zum Standard-Filter geblockt.</p>
</div>
<div class="form-field">
<label class="form-label">Whitelist-Rollen (IDs, Kommagetrennt)</label>
<textarea id="automodWhitelistRoles" rows="3" placeholder="123,456"></textarea>
<p class="muted">Nachrichten von diesen Rollen werden nicht gefiltert.</p>
</div>
</div>
<div style="display:flex; justify-content:flex-end; gap:10px;">
<button type="submit">Speichern</button>
</div>
<p class="muted" id="automodStatus"></p>
</form>
</section>
</div>
<div class="section" data-section="welcome">
<section class="card">
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap;">
<div>
<p class="section-title">Willkommensnachrichten</p>
<p class="section-sub">Embed konfigurieren und Feature aktivieren.</p>
</div>
<div class="inline">
<span class="form-label">Aktivieren</span>
<div id="welcomeToggle" class="switch"></div>
</div>
</div>
<form id="welcomeForm" style="display:flex; flex-direction:column; gap:12px; margin-top:12px;">
<div class="form-row">
<div class="form-field">
<label class="form-label">Channel ID</label>
<input id="welcomeChannel" placeholder="123456789012345678" />
</div>
<div class="form-field">
<label class="form-label">Embed-Farbe</label>
<input id="welcomeColor" type="color" value="#f97316" />
</div>
</div>
<div class="form-row">
<div class="form-field">
<label class="form-label">Embed Titel</label>
<input id="welcomeTitle" placeholder="Willkommen!" />
</div>
<div class="form-field">
<label class="form-label">Embed Footer</label>
<input id="welcomeFooter" placeholder="Schön, dass du da bist!" />
</div>
</div>
<div class="form-field">
<label class="form-label">Embed Beschreibung</label>
<textarea id="welcomeDescription" rows="3" placeholder="Du kannst {user} verwenden."></textarea>
</div>
<div class="form-row">
<div class="form-field">
<label class="form-label">Thumbnail URL oder Upload</label>
<input id="welcomeThumbnail" placeholder="https://..." />
<input type="file" id="welcomeThumbnailFile" accept="image/*" />
</div>
<div class="form-field">
<label class="form-label">Bild URL oder Upload (GIFs erlaubt)</label>
<input id="welcomeImage" placeholder="https://..." />
<input type="file" id="welcomeImageFile" accept="image/*" />
</div>
</div>
<div class="embed-preview" id="welcomePreview">
<div class="embed-color" id="welcomePreviewColor"></div>
<div class="embed-body">
<div class="embed-title" id="welcomePreviewTitle">Willkommen!</div>
<div class="embed-desc" id="welcomePreviewDesc">Schön, dass du da bist.</div>
<div class="embed-footer" id="welcomePreviewFooter">Footer</div>
<img id="welcomePreviewImage" class="embed-image" style="display:none;" />
</div>
</div>
<div style="display:flex; justify-content:flex-end; gap:10px;">
<button type="submit">Speichern</button>
</div>
<p class="muted" id="welcomeStatus"></p>
</form>
</section>
</div>
<div class="section" data-section="dynamicvoice">
<section class="card">
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap;">
<div>
<p class="section-title">Dynamic Voice</p>
<p class="section-sub">Lobby waehlen, Channel-Namen & Limits setzen.</p>
</div>
<div class="inline">
<span class="form-label">Aktivieren</span>
<div id="dynamicVoiceToggle" class="switch"></div>
</div>
</div>
<form id="dynamicVoiceForm" style="display:flex; flex-direction:column; gap:12px; margin-top:12px;">
<div class="grid">
<div class="form-field">
<label class="form-label">Lobby Channel ID (Voice)</label>
<input id="dynamicVoiceLobby" placeholder="123456789012345678" />
</div>
<div class="form-field">
<label class="form-label">Kategorie ID (optional)</label>
<input id="dynamicVoiceCategory" placeholder="123456789012345678" />
</div>
<div class="form-field">
<label class="form-label">Name-Template</label>
<input id="dynamicVoiceTemplate" placeholder="{user}s Channel" />
<p class="muted">{user} wird durch den Mitgliedsnamen ersetzt.</p>
</div>
<div class="form-field">
<label class="form-label">Userlimit (optional)</label>
<input id="dynamicVoiceUserLimit" type="number" min="0" placeholder="0 = kein Limit" />
</div>
<div class="form-field">
<label class="form-label">Bitrate (optional)</label>
<input id="dynamicVoiceBitrate" type="number" min="8000" placeholder="z.B. 64000" />
</div>
</div>
<div style="display:flex; justify-content:flex-end; gap:10px;">
<button type="submit">Speichern</button>
</div>
<p class="muted" id="dynamicVoiceStatus"></p>
</form>
</section>
</div>
<div class="section" data-section="modules">
<section class="card">
<p class="label">Module</p>
<div id="moduleList" class="module-list"></div>
</section>
</div>
<div class="section" data-section="birthday">
<section class="card">
<div class="row" style="justify-content:space-between; align-items:flex-start; gap:12px;">
<div>
<p class="section-title">Birthday</p>
<p class="section-sub">Automatische Glueckwuensche je Guild.</p>
</div>
<div class="row" style="align-items:center; gap:10px; flex-wrap:wrap; justify-content:flex-end;">
<label class="form-label">Sendezeit (Stunde)</label>
<input id="birthdayHour" type="number" min="0" max="23" placeholder="9" style="width:80px;" />
<label class="form-label">Channel ID</label>
<input id="birthdayChannel" placeholder="123456789012345678" style="width:180px;" />
<div id="birthdayToggle" class="switch"></div>
</div>
</div>
<div class="form-field">
<label class="form-label">Template</label>
<textarea id="birthdayTemplate" rows="3" placeholder="Alles Gute zum Geburtstag, {user}!"></textarea>
<p class="muted">Nutze {user} als Platzhalter.</p>
</div>
<div style="display:flex; justify-content:flex-end; gap:10px;">
<button id="birthdaySave" type="button">Speichern</button>
</div>
<p class="muted" id="birthdayStatus"></p>
</section>
<section class="card">
<div class="row" style="justify-content:space-between; align-items:center;">
<div>
<p class="section-title">Gespeicherte Geburtstage</p>
<p class="section-sub">Eintraege werden per /birthday angelegt.</p>
</div>
</div>
<div id="birthdayList" class="module-list"></div>
</section>
</div>
<div class="section" data-section="reactionroles">
<section class="card">
<div class="row" style="justify-content:space-between; align-items:flex-start; gap:12px;">
<div>
<p class="section-title">Reaction Roles</p>
<p class="section-sub">Reaktionen verteilen oder entfernen Rollen.</p>
</div>
<div class="muted" id="reactionRoleStatus"></div>
</div>
<form id="reactionRoleForm" style="display:flex; flex-direction:column; gap:12px; margin-top:8px;">
<input type="hidden" id="reactionRoleId" />
<div class="grid">
<div class="form-field">
<label class="form-label">Channel ID</label>
<input id="reactionRoleChannel" placeholder="123456789012345678" />
</div>
<div class="form-field">
<label class="form-label">Message ID (optional)</label>
<input id="reactionRoleMessageId" placeholder="Bestehende Nachricht verwenden" />
</div>
<div class="form-field">
<label class="form-label">Titel</label>
<input id="reactionRoleTitle" placeholder="Rollenwahl" />
</div>
</div>
<div class="form-field">
<label class="form-label">Beschreibung</label>
<textarea id="reactionRoleDescription" rows="2" placeholder="Kurze Beschreibung fuer das Embed"></textarea>
</div>
<div class="form-field">
<label class="form-label">Eintraege (Emoji | Role ID | Label | Beschreibung)</label>
<textarea id="reactionRoleEntries" rows="4" placeholder="😀 | 123456789 | Freunde | Erhaelt Freunde Rolle"></textarea>
<p class="muted">Eine Zeile pro Zuordnung. Label/Beschreibung optional.</p>
</div>
<div style="display:flex; justify-content:flex-end; gap:10px;">
<button type="button" class="secondary-btn" id="reactionRoleReset">Reset</button>
<button type="submit" id="reactionRoleSubmit">Speichern &amp; senden</button>
</div>
</form>
</section>
<section class="card">
<div class="row" style="justify-content:space-between; align-items:center;">
<div>
<p class="section-title">Reaction Role Nachrichten</p>
<p class="section-sub">Bestehende Setups bearbeiten oder syncen.</p>
</div>
</div>
<div id="reactionRoleList" class="module-list"></div>
</section>
</div>
<div class="section" data-section="statuspage">
<section class="card">
<div class="row" style="justify-content:space-between;">
<div>
<p class="section-title">Statuspage</p>
<p class="section-sub">Services verwalten und Checks steuern.</p>
</div>
<div class="row">
<label class="form-label">Intervall (ms)</label>
<input id="statuspageInterval" type="number" min="30000" step="5000" placeholder="60000" />
<label class="form-label">Channel ID</label>
<input id="statuspageChannel" placeholder="Optional" />
<div id="statuspageToggle" class="switch"></div>
</div>
</div>
</section>
<section class="card">
<div class="row" style="justify-content:space-between;align-items:center;">
<div>
<p class="section-title">Services</p>
<p class="section-sub">Status, Uptime, letzter Check</p>
</div>
<button class="icon-button" id="statuspageAddService">+</button>
</div>
<div id="statuspageServices" class="module-list"></div>
</section>
</div>
<div class="section" data-section="serverstats">
<section class="card">
<div class="row" style="justify-content:space-between; align-items:center; gap:10px; flex-wrap:wrap;">
<div>
<p class="section-title">Server Stats</p>
<p class="section-sub">Kategorie und Counter verwalten.</p>
</div>
<div class="row" style="gap:8px; align-items:center;">
<label class="form-label">Kategorie-Name</label>
<input id="statsCategoryName" placeholder="Server Stats" />
<label class="form-label">Refresh (Min)</label>
<input id="statsRefresh" type="number" min="1" step="1" placeholder="10" />
<div id="statsToggle" class="switch"></div>
</div>
</div>
</section>
<section class="card">
<div class="row" style="justify-content:space-between; align-items:center;">
<div>
<p class="section-title">Statistiken</p>
<p class="section-sub">Counter und Format anpassen.</p>
</div>
<button class="icon-button" id="statsAddItem">+</button>
</div>
<div id="statsItems" class="module-list"></div>
</section>
</div>
<div class="section" data-section="events">
<section class="card">
<div class="row" style="justify-content:space-between; align-items:center;">
<div>
<p class="section-title">Events</p>
<p class="section-sub">Termine planen und Erinnerungen senden.</p>
</div>
<button class="icon-button" id="openEventModal">+</button>
</div>
<div id="eventList" class="ticket-list-pane"></div>
</section>
</div>
<div class="section" data-section="admin">
<div class="grid">
<section class="card">
<p class="section-title">Guilds</p>
<p class="stat" id="adminGuilds">-</p>
<p class="label">aktiv (24h): <span id="adminActiveGuilds">-</span></p>
</section>
<section class="card">
<p class="section-title">Uptime</p>
<p class="stat" id="adminUptime">-</p>
<p class="label">Start: <span id="adminStart">-</span></p>
</section>
</div>
<section class="card">
<div class="row" style="justify-content:space-between;">
<div>
<p class="section-title">Aktivität (letzte 24h)</p>
<p class="section-sub">Events/Commands pro Stunde</p>
</div>
</div>
<div class="activity-bars" id="adminActivity"></div>
</section>
<section class="card">
<div class="row" style="justify-content:space-between;">
<div>
<p class="section-title">Logs</p>
<p class="section-sub">Neueste Einträge</p>
</div>
</div>
<ul class="log-list" id="adminLogs"></ul>
</section>
</div>
<div class="section" data-section="settings">
<section class="card">
<p class="label">Einstellungen speichern</p>
<form id="settingsForm">
<input name="welcomeChannelId" placeholder="Welcome Channel ID" />
<input name="logChannelId" placeholder="Log Channel ID" />
<input name="supportRoleId" placeholder="Support Role ID (optional)" />
<button type="submit">Speichern</button>
<p class="muted" id="saveStatus"></p>
</form>
</section>
<section class="card">
<p class="label">Logging</p>
<div class="form-row">
<div class="form-field">
<label class="form-label">Log Channel ID (Logging)</label>
<input id="loggingChannel" placeholder="123456789012345678" />
<p class="muted">Falls leer, wird der allgemeine Log Channel genutzt.</p>
</div>
</div>
<div class="option-grid">
<div class="option-card"><span>👋 User Join / Leave</span><div id="logJoinLeave" class="switch on"></div></div>
<div class="option-card"><span>✏️ Message Edit</span><div id="logMsgEdit" class="switch on"></div></div>
<div class="option-card"><span>🗑️ Message Delete</span><div id="logMsgDelete" class="switch on"></div></div>
<div class="option-card"><span>🛡️ Automod Actions</span><div id="logAutomod" class="switch on"></div></div>
<div class="option-card"><span>🎫 Ticket Actions</span><div id="logTickets" class="switch on"></div></div>
<div class="option-card"><span>🎵 Musik-Events</span><div id="logMusic" class="switch on"></div></div>
<div class="option-card"><span>⚙️ System / Channels</span><div id="logSystem" class="switch on"></div></div>
</div>
<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 welcomeImage = document.getElementById('welcomeImage');
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 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 serverStatsCache = { items: [] };
let dynamicVoiceCache = {};
let isAdmin = false;
let statuspageCache = { services: [] };
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>';
}
const STAT_LABELS = {
members_total: 'Mitglieder (gesamt)',
members_humans: 'Mitglieder (ohne Bots)',
members_bots: 'Bots',
boosts: 'Server Boosts',
text_channels: 'Text Channels',
voice_channels: 'Voice Channels',
roles: 'Rollen'
};
async function loadServerStats() {
if (!currentGuild) return;
const res = await fetch('/api/server-stats?guildId=' + encodeURIComponent(currentGuild));
if (!res.ok) return;
const data = await res.json();
serverStatsCache = data.config || { items: [] };
setSwitch(statsToggle, serverStatsCache.enabled !== false);
if (statsCategoryName) statsCategoryName.value = serverStatsCache.categoryName || 'Server Stats';
if (statsRefresh) statsRefresh.value = serverStatsCache.refreshMinutes ?? 10;
renderServerStats();
}
function renderServerStats() {
if (!statsItems) return;
statsItems.innerHTML = '';
(serverStatsCache.items || []).forEach((item) => {
const row = document.createElement('div');
row.className = 'module-item';
const meta = document.createElement('div');
meta.className = 'module-meta';
const label = STAT_LABELS[item.type] || item.type;
meta.innerHTML =
'<div class="module-title">' + label + '</div><div class="module-desc">' + (item.label || label) + ' - ' + (item.format || '{label}: {value}') + '</div>';
const buttons = document.createElement('div');
buttons.className = 'row';
const edit = document.createElement('button');
edit.className = 'secondary-btn';
edit.textContent = 'Bearbeiten';
edit.addEventListener('click', () => editServerStat(item));
const del = document.createElement('button');
del.className = 'danger-btn';
del.textContent = 'Loeschen';
del.addEventListener('click', () => {
serverStatsCache.items = (serverStatsCache.items || []).filter((x) => x !== item);
renderServerStats();
saveServerStats();
});
buttons.appendChild(edit);
buttons.appendChild(del);
row.appendChild(meta);
row.appendChild(buttons);
statsItems.appendChild(row);
});
if (!(serverStatsCache.items || []).length) statsItems.innerHTML = '<div class="muted">Keine Statistiken</div>';
}
function editServerStat(item) {
const typeKeys = Object.keys(STAT_LABELS);
const nextType = prompt('Typ (' + typeKeys.join(', ') + ')', item?.type || 'members_total');
if (!nextType || !STAT_LABELS[nextType]) return;
const nextLabel = prompt('Label', item?.label || STAT_LABELS[nextType]) || STAT_LABELS[nextType];
const nextFormat = prompt('Format ({label} / {value})', item?.format || '{label}: {value}') || '{label}: {value}';
if (item) {
item.type = nextType;
item.label = nextLabel;
item.format = nextFormat;
} else {
(serverStatsCache.items = serverStatsCache.items || []).push({
id: (crypto.randomUUID && crypto.randomUUID()) || String(Date.now()),
type: nextType,
label: nextLabel,
format: nextFormat
});
}
renderServerStats();
saveServerStats();
}
async function saveServerStats() {
if (!currentGuild) return;
const payload = {
guildId: currentGuild,
config: {
enabled: getSwitch(statsToggle),
categoryName: statsCategoryName?.value || undefined,
refreshMinutes: statsRefresh?.value ? Number(statsRefresh.value) : undefined,
items: serverStatsCache.items || []
}
};
const res = await fetch('/api/server-stats', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!res.ok) showToast('Server Stats speichern fehlgeschlagen', true);
}
async function loadStatuspage() {
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();
}
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 = 'Lschen';
del.addEventListener('click', async () => {
const res = await fetch('/api/automations/' + r.id, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ guildId: currentGuild })
});
showToast(res.ok ? 'Regel gelscht' : 'Lschen 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 = 'Lschen';
del.addEventListener('click', async () => {
const res = await fetch('/api/kb/' + a.id, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ guildId: currentGuild })
});
showToast(res.ok ? 'Artikel gelscht' : 'Lschen 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 || {};
document.querySelector('[name="welcomeChannelId"]').value = cfg.welcomeChannelId || '';
document.querySelector('[name="logChannelId"]').value = cfg.logChannelId || '';
document.querySelector('[name="supportRoleId"]').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['serverStatsEnabled'] = cfg.serverStatsEnabled === true || cfg.serverStatsConfig?.enabled === true;
modulesCache['birthdayEnabled'] = cfg.birthdayEnabled !== false && cfg.birthdayConfig?.enabled !== false;
modulesCache['reactionRolesEnabled'] = cfg.reactionRolesEnabled !== false && cfg.reactionRolesConfig?.enabled !== false;
modulesCache['eventsEnabled'] = cfg.eventsEnabled !== false;
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();
} 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');
}
}
}
document.getElementById('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) });
document.getElementById('saveStatus').textContent = res.ok ? 'Gespeichert' : 'Fehler';
if (res.ok) {
await loadSettings(currentGuild);
await loadModules();
}
});
document.getElementById('openPanelModal').addEventListener('click', () => {
document.getElementById('panelStatus').textContent = '';
showModal(panelModal);
});
if (openSupportLogin) openSupportLogin.addEventListener('click', () => { showModal(supportLoginModal); });
if (supportLoginSave) supportLoginSave.addEventListener('click', saveSupportLogin);
if (refreshSupportStatus) refreshSupportStatus.addEventListener('click', loadSupportLogin);
document.getElementById('panelSubmit').addEventListener('click', async () => {
if (!currentGuild) return;
const form = new FormData(document.getElementById('panelForm'));
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;