3025 lines
160 KiB
TypeScript
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 & 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>×</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>×</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>×</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 & 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>×</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;
|
|
|
|
|