diff --git a/public/styles/dashboard.css b/public/styles/dashboard.css
new file mode 100644
index 0000000..07ea185
--- /dev/null
+++ b/public/styles/dashboard.css
@@ -0,0 +1,336 @@
+:root {
+ --bg: #080c15;
+ --card: rgba(16, 19, 28, 0.65);
+ --surface: rgba(12, 15, 22, 0.75);
+ --text: #f7fafc;
+ --accent: #f97316;
+ --accent-strong: #ff9b3d;
+ --muted: #a8b2c5;
+ --border: rgba(255, 255, 255, 0.08);
+ --danger: #ef4444;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+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;
+}
+
+#app {
+ display: flex;
+ min-height: 100vh;
+ width: 100%;
+}
+
+.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;
+ 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);
+}
+
+.nav a.hidden {
+ display: none;
+}
+
+.content {
+ flex: 1;
+ padding: 28px 34px 56px;
+ background: linear-gradient(145deg, rgba(255, 153, 73, 0.05) 0%, rgba(255, 255, 255, 0) 32%);
+}
+
+.page-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 16px;
+}
+
+h1 {
+ margin: 0;
+ font-size: 26px;
+ letter-spacing: 0.6px;
+ font-weight: 800;
+}
+
+.muted {
+ color: var(--muted);
+ font-size: 13px;
+}
+
+.header-actions {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+}
+
+.select,
+select {
+ min-height: 44px;
+ padding: 10px 12px;
+ border-radius: 14px;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ background: rgba(255, 255, 255, 0.06);
+ color: var(--text);
+}
+
+.btn {
+ 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;
+}
+
+.btn:hover {
+ transform: translateY(-1px);
+ filter: brightness(1.05);
+ box-shadow: 0 18px 40px rgba(249, 115, 22, 0.4);
+}
+
+.btn.secondary {
+ 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);
+}
+
+.btn.danger {
+ background: linear-gradient(135deg, #ef4444, #b91c1c);
+ box-shadow: 0 12px 28px rgba(239, 68, 68, 0.32);
+ border-color: rgba(239, 68, 68, 0.5);
+}
+
+main .section {
+ margin-top: 22px;
+ display: none;
+}
+
+main .section.active {
+ display: block;
+}
+
+.grid {
+ display: grid;
+ gap: 14px;
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
+}
+
+.cards-grid {
+ margin-top: 16px;
+}
+
+.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);
+}
+
+.card.clickable {
+ cursor: pointer;
+ transition: transform 150ms ease, border-color 150ms ease, box-shadow 150ms ease, background 150ms ease;
+}
+
+.card.clickable: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;
+}
+
+.stat {
+ font-size: 34px;
+ font-weight: 800;
+ margin: 0;
+}
+
+.label {
+ color: var(--muted);
+ margin: 4px 0 0 0;
+ letter-spacing: 0.2px;
+}
+
+.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);
+}
+
+.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);
+}
+
+.modal-backdrop {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.6);
+ display: none;
+ align-items: center;
+ justify-content: center;
+ z-index: 1200;
+ backdrop-filter: blur(4px);
+}
+
+.modal-backdrop.show {
+ display: flex;
+}
+
+.modal {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 16px;
+ padding: 18px;
+ width: min(640px, 94vw);
+ box-shadow: 0 20px 48px rgba(0, 0, 0, 0.45);
+}
+
+.form-field {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ margin-bottom: 10px;
+}
+
+.form-label {
+ font-size: 13px;
+ color: var(--muted);
+ letter-spacing: 0.2px;
+ font-weight: 600;
+}
+
+input,
+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);
+}
+
+.section-title {
+ margin: 0 0 10px 0;
+ font-size: 20px;
+ font-weight: 800;
+}
+
+.empty-state {
+ 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);
+}
+
+.hidden {
+ display: none !important;
+}
diff --git a/public/styles/dashboard.sections.css b/public/styles/dashboard.sections.css
new file mode 100644
index 0000000..4059d62
--- /dev/null
+++ b/public/styles/dashboard.sections.css
@@ -0,0 +1,106 @@
+.tickets-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 18px;
+}
+
+.ticket-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.ticket-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;
+ box-shadow: 0 14px 32px rgba(0, 0, 0, 0.3);
+}
+
+.ticket-status {
+ 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-closed {
+ background: rgba(239, 68, 68, 0.16);
+ color: #f87171;
+ border-color: rgba(239, 68, 68, 0.42);
+}
+
+.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);
+}
+
+.module-meta {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.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.3);
+ border-color: rgba(249, 115, 22, 0.6);
+ box-shadow: 0 8px 20px rgba(249, 115, 22, 0.24);
+}
+
+.switch.on::after {
+ transform: translateX(22px);
+ background: #ffd9b3;
+ box-shadow: 0 6px 14px rgba(249, 115, 22, 0.35);
+}
diff --git a/public/ts-build/app.js b/public/ts-build/app.js
new file mode 100644
index 0000000..2eca8c0
--- /dev/null
+++ b/public/ts-build/app.js
@@ -0,0 +1,3 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+require("./core/app.js");
diff --git a/public/ts-build/components/admin/index.js b/public/ts-build/components/admin/index.js
new file mode 100644
index 0000000..d9d1b48
--- /dev/null
+++ b/public/ts-build/components/admin/index.js
@@ -0,0 +1,26 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.initAdminSection = initAdminSection;
+const api_js_1 = require("../../services/api.js");
+const toast_js_1 = require("../../ui/toast.js");
+async function initAdminSection(guildId) {
+ const section = document.getElementById('section-admin');
+ if (!section)
+ return;
+ section.innerHTML = '
Lade Admin-Daten...
';
+ try {
+ const data = await api_js_1.api.settings(guildId);
+ section.innerHTML = `
+ Admin
+
+
Rohdaten (nur Admin):
+
${JSON.stringify(data, null, 2)}
+
+ `;
+ }
+ catch (err) {
+ console.error(err);
+ section.innerHTML = 'Admin-Daten konnten nicht geladen werden.
';
+ (0, toast_js_1.showToast)('Fehler beim Laden der Admin-Daten', true);
+ }
+}
diff --git a/public/ts-build/components/dashboard.js b/public/ts-build/components/dashboard.js
new file mode 100644
index 0000000..6b36a84
--- /dev/null
+++ b/public/ts-build/components/dashboard.js
@@ -0,0 +1,93 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.initDashboardView = initDashboardView;
+const api_js_1 = require("../services/api.js");
+const store_js_1 = require("../state/store.js");
+const toast_js_1 = require("../ui/toast.js");
+const overview_js_1 = require("./overview.js");
+const index_js_1 = require("./tickets/index.js");
+const index_js_2 = require("./modules/index.js");
+const index_js_3 = require("./events/index.js");
+const index_js_4 = require("./admin/index.js");
+const settings_js_1 = require("./settings.js");
+let overviewInterval = null;
+let ticketsInterval = null;
+async function populateGuildSelect() {
+ const select = document.getElementById('guildSelect');
+ const cfg = (0, store_js_1.getConfig)();
+ if (!select || !cfg)
+ return;
+ select.innerHTML = ``;
+ try {
+ const data = await api_js_1.api.guilds();
+ select.innerHTML = '';
+ data.guilds.forEach((g) => {
+ const opt = document.createElement('option');
+ opt.value = g.id;
+ opt.textContent = g.name;
+ if (g.id === cfg.initialGuildId)
+ opt.selected = true;
+ select.appendChild(opt);
+ });
+ const current = select.value || cfg.initialGuildId || data.guilds[0]?.id;
+ (0, store_js_1.setState)({ guildId: current || undefined });
+ select.value = current || '';
+ }
+ catch (err) {
+ console.error(err);
+ (0, toast_js_1.showToast)('Guilds konnten nicht geladen werden', true);
+ }
+}
+function registerGuildChange() {
+ const select = document.getElementById('guildSelect');
+ if (!select)
+ return;
+ select.addEventListener('change', () => {
+ const guildId = select.value;
+ (0, store_js_1.setState)({ guildId });
+ refreshSections();
+ });
+}
+async function refreshSections() {
+ const { guildId } = (0, store_js_1.getState)();
+ if (!guildId)
+ return;
+ await (0, overview_js_1.renderOverview)(guildId);
+ await (0, index_js_1.initTicketsSection)(guildId);
+ await (0, index_js_2.initModulesSection)(guildId);
+ await (0, settings_js_1.renderSettingsSection)(guildId);
+ await (0, index_js_3.initEventsSection)(guildId);
+ const cfg = (0, store_js_1.getConfig)();
+ if (cfg?.isAdmin) {
+ await (0, index_js_4.initAdminSection)(guildId);
+ }
+}
+function setupPolling() {
+ const { guildId } = (0, store_js_1.getState)();
+ if (overviewInterval)
+ window.clearInterval(overviewInterval);
+ if (ticketsInterval)
+ window.clearInterval(ticketsInterval);
+ overviewInterval = window.setInterval(() => {
+ const current = (0, store_js_1.getState)().guildId;
+ if (current)
+ (0, overview_js_1.renderOverview)(current);
+ }, 10000);
+ ticketsInterval = window.setInterval(() => {
+ const current = (0, store_js_1.getState)().guildId;
+ if (current)
+ (0, index_js_1.initTicketsSection)(current);
+ }, 12000);
+}
+function initDashboardView() {
+ const cfg = (0, store_js_1.getConfig)();
+ const logoutBtn = document.getElementById('logoutBtn');
+ if (logoutBtn && cfg) {
+ logoutBtn.addEventListener('click', () => (window.location.href = `${cfg.baseAuth}/logout`));
+ }
+ populateGuildSelect().then(() => {
+ registerGuildChange();
+ refreshSections();
+ setupPolling();
+ });
+}
diff --git a/public/ts-build/components/events/index.js b/public/ts-build/components/events/index.js
new file mode 100644
index 0000000..0ecc8bf
--- /dev/null
+++ b/public/ts-build/components/events/index.js
@@ -0,0 +1,43 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.initEventsSection = initEventsSection;
+const api_js_1 = require("../../services/api.js");
+const toast_js_1 = require("../../ui/toast.js");
+async function initEventsSection(guildId) {
+ const section = document.getElementById('section-events');
+ if (!section)
+ return;
+ section.innerHTML = 'Lade Events...
';
+ try {
+ const data = await api_js_1.api.events(guildId);
+ const events = data?.events || data || [];
+ section.innerHTML = 'Events
';
+ if (!events.length) {
+ section.innerHTML += 'Keine Events geplant.
';
+ return;
+ }
+ const list = document.createElement('div');
+ list.className = 'ticket-list';
+ events.forEach((ev) => {
+ const item = document.createElement('div');
+ item.className = 'ticket-item';
+ item.innerHTML = `
+
+
+
${ev.title || 'Event'}
+
${ev.date || ''}
+
+
${ev.status || 'open'}
+
+ ${ev.description || ''}
+ `;
+ list.appendChild(item);
+ });
+ section.appendChild(list);
+ }
+ catch (err) {
+ console.error(err);
+ section.innerHTML = 'Events konnten nicht geladen werden.
';
+ (0, toast_js_1.showToast)('Fehler beim Laden der Events', true);
+ }
+}
diff --git a/public/ts-build/components/guildSelect.js b/public/ts-build/components/guildSelect.js
new file mode 100644
index 0000000..a8566fa
--- /dev/null
+++ b/public/ts-build/components/guildSelect.js
@@ -0,0 +1,59 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.initSelectionView = initSelectionView;
+const api_js_1 = require("../services/api.js");
+const store_js_1 = require("../state/store.js");
+const toast_js_1 = require("../ui/toast.js");
+async function initSelectionView() {
+ const cfg = (0, store_js_1.getConfig)();
+ const grid = document.getElementById('guildGrid');
+ const logoutBtn = document.getElementById('logoutBtn');
+ const userInfo = document.getElementById('userInfo');
+ if (logoutBtn && cfg) {
+ logoutBtn.addEventListener('click', () => {
+ window.location.href = `${cfg.baseAuth}/logout`;
+ });
+ }
+ try {
+ const me = await api_js_1.api.me();
+ if (userInfo && me?.user)
+ userInfo.textContent = `${me.user.username}#${me.user.discriminator}`;
+ }
+ catch {
+ // ignore
+ }
+ if (!grid || !cfg)
+ return;
+ grid.innerHTML = 'Lade Guilds...
';
+ try {
+ const data = await api_js_1.api.guilds();
+ grid.innerHTML = '';
+ (data.guilds || []).forEach((g) => {
+ const card = document.createElement('div');
+ card.className = 'card clickable';
+ card.innerHTML = `
+
+

+
+
${g.name}
+
ID: ${g.id}
+
+
+ Zum Dashboard
+ `;
+ card.addEventListener('click', () => {
+ const qs = g.id ? `?guildId=${encodeURIComponent(g.id)}` : '';
+ window.location.href = `${cfg.baseDashboard}${qs}`;
+ });
+ grid.appendChild(card);
+ });
+ if (!data.guilds?.length) {
+ grid.innerHTML = 'Bot ist in keiner Guild. Bitte Bot einladen.
';
+ }
+ }
+ catch (err) {
+ console.error(err);
+ grid.innerHTML = 'Fehler beim Laden der Guilds
';
+ (0, toast_js_1.showToast)('Guilds konnten nicht geladen werden', true);
+ }
+}
diff --git a/public/ts-build/components/modules/dynamicVoice.js b/public/ts-build/components/modules/dynamicVoice.js
new file mode 100644
index 0000000..d658ab5
--- /dev/null
+++ b/public/ts-build/components/modules/dynamicVoice.js
@@ -0,0 +1,26 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.renderDynamicVoiceModule = renderDynamicVoiceModule;
+const api_js_1 = require("../../services/api.js");
+const toast_js_1 = require("../../ui/toast.js");
+async function renderDynamicVoiceModule(guildId) {
+ const container = document.getElementById('module-dynamicvoice');
+ if (!container)
+ return;
+ container.innerHTML = 'Lade Dynamic Voice...
';
+ try {
+ const data = await api_js_1.api.dynamicVoice(guildId);
+ const cfg = data?.config || data?.dynamicVoiceConfig || {};
+ container.innerHTML = `
+ Dynamic Voice
+ Lobby: ${cfg.lobbyChannelId || '-'}
+ Template: ${cfg.template || '-'}
+ User-Limit: ${cfg.userLimit ?? '-'}
+ `;
+ }
+ catch (err) {
+ console.error(err);
+ container.innerHTML = 'Dynamic Voice konnte nicht geladen werden.
';
+ (0, toast_js_1.showToast)('Fehler beim Laden von Dynamic Voice', true);
+ }
+}
diff --git a/public/ts-build/components/modules/index.js b/public/ts-build/components/modules/index.js
new file mode 100644
index 0000000..f164ab0
--- /dev/null
+++ b/public/ts-build/components/modules/index.js
@@ -0,0 +1,104 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.initModulesSection = initModulesSection;
+const api_js_1 = require("../../services/api.js");
+const toast_js_1 = require("../../ui/toast.js");
+const switch_js_1 = require("../../ui/switch.js");
+const welcome_js_1 = require("./welcome.js");
+const logging_js_1 = require("./logging.js");
+const reactionRoles_js_1 = require("./reactionRoles.js");
+const dynamicVoice_js_1 = require("./dynamicVoice.js");
+const statuspage_js_1 = require("./statuspage.js");
+const serverstats_js_1 = require("./serverstats.js");
+async function initModulesSection(guildId) {
+ const section = document.getElementById('section-modules');
+ if (!section)
+ return;
+ section.innerHTML = `
+ Module
+
+
+ `;
+ await Promise.all([
+ renderModuleToggles(guildId),
+ (0, welcome_js_1.renderWelcomeModule)(guildId),
+ (0, logging_js_1.renderLoggingModule)(guildId),
+ (0, reactionRoles_js_1.renderReactionRolesModule)(guildId),
+ (0, dynamicVoice_js_1.renderDynamicVoiceModule)(guildId),
+ (0, statuspage_js_1.renderStatuspageModule)(guildId),
+ (0, serverstats_js_1.renderServerStatsModule)(guildId)
+ ]);
+}
+async function renderModuleToggles(guildId) {
+ const container = document.getElementById('module-toggles');
+ if (!container)
+ return;
+ container.innerHTML = 'Lade Module...
';
+ try {
+ const data = await api_js_1.api.modules(guildId);
+ const modules = data?.modules || data || {};
+ container.innerHTML = '';
+ const entries = [
+ { key: 'ticketsEnabled', label: 'Tickets', desc: 'Ticket-System aktivieren' },
+ { key: 'automodEnabled', label: 'Automod', desc: 'Moderations-Filter' },
+ { key: 'welcomeEnabled', label: 'Welcome', desc: 'Begrueßungsnachrichten' },
+ { key: 'musicEnabled', label: 'Musik', desc: 'Musiksteuerung' },
+ { key: 'levelingEnabled', label: 'Leveling', desc: 'XP/Level System' },
+ { key: 'statuspageEnabled', label: 'Statuspage', desc: 'Statusberichte' },
+ { key: 'serverStatsEnabled', label: 'Server Stats', desc: 'Stat-Channel' },
+ { key: 'birthdayEnabled', label: 'Birthday', desc: 'Geburtstagsmodul' },
+ { key: 'reactionRolesEnabled', label: 'Reaction Roles', desc: 'Selbstzuweisbare Rollen' },
+ { key: 'eventsEnabled', label: 'Events', desc: 'Event-Planung' },
+ { key: 'dynamicVoiceEnabled', label: 'Dynamic Voice', desc: 'Dynamische Voice Channels' }
+ ];
+ entries.forEach((entry) => {
+ const row = document.createElement('div');
+ row.className = 'module-item';
+ row.innerHTML = `
+
+
+ `;
+ const toggle = row.querySelector('.switch');
+ toggle.addEventListener('click', async () => {
+ (0, switch_js_1.setSwitch)(toggle, !(0, switch_js_1.getSwitch)(toggle));
+ await saveModules(guildId);
+ });
+ container.appendChild(row);
+ });
+ }
+ catch (err) {
+ console.error(err);
+ container.innerHTML = 'Module konnten nicht geladen werden.
';
+ (0, toast_js_1.showToast)('Fehler beim Laden der Module', true);
+ }
+}
+async function saveModules(guildId) {
+ const toggles = Array.from(document.querySelectorAll('#module-toggles .switch'));
+ const payload = { guildId };
+ toggles.forEach((t) => {
+ const key = t.dataset.key;
+ if (!key)
+ return;
+ payload[key] = t.classList.contains('on');
+ });
+ try {
+ await api_js_1.api.saveSettings(payload);
+ (0, toast_js_1.showToast)('Module gespeichert');
+ }
+ catch (err) {
+ console.error(err);
+ (0, toast_js_1.showToast)('Module speichern fehlgeschlagen', true);
+ }
+}
diff --git a/public/ts-build/components/modules/logging.js b/public/ts-build/components/modules/logging.js
new file mode 100644
index 0000000..7faf5f1
--- /dev/null
+++ b/public/ts-build/components/modules/logging.js
@@ -0,0 +1,26 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.renderLoggingModule = renderLoggingModule;
+const api_js_1 = require("../../services/api.js");
+const toast_js_1 = require("../../ui/toast.js");
+async function renderLoggingModule(guildId) {
+ const container = document.getElementById('module-logging');
+ if (!container)
+ return;
+ container.innerHTML = 'Lade Logging...
';
+ try {
+ const data = await api_js_1.api.settings(guildId);
+ const cfg = data?.settings?.loggingConfig || data?.loggingConfig || {};
+ container.innerHTML = `
+ Logging
+ Channel: ${cfg.logChannelId || '-'}
+ Join/Leave: ${cfg.categories?.joinLeave ? 'an' : 'aus'}
+ System: ${cfg.categories?.system ? 'an' : 'aus'}
+ `;
+ }
+ catch (err) {
+ console.error(err);
+ container.innerHTML = 'Logging konnte nicht geladen werden.
';
+ (0, toast_js_1.showToast)('Fehler beim Laden von Logging', true);
+ }
+}
diff --git a/public/ts-build/components/modules/reactionRoles.js b/public/ts-build/components/modules/reactionRoles.js
new file mode 100644
index 0000000..160df16
--- /dev/null
+++ b/public/ts-build/components/modules/reactionRoles.js
@@ -0,0 +1,37 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.renderReactionRolesModule = renderReactionRolesModule;
+const api_js_1 = require("../../services/api.js");
+const toast_js_1 = require("../../ui/toast.js");
+async function renderReactionRolesModule(guildId) {
+ const container = document.getElementById('module-reactionroles');
+ if (!container)
+ return;
+ container.innerHTML = 'Lade Reaction Roles...
';
+ try {
+ const data = await api_js_1.api.reactionRoles(guildId);
+ const entries = data?.entries || data?.reactionRoles || [];
+ container.innerHTML = 'Reaction Roles
';
+ if (!entries.length) {
+ container.innerHTML += 'Keine Reaction Roles.
';
+ return;
+ }
+ const list = document.createElement('div');
+ list.className = 'ticket-list';
+ entries.slice(0, 3).forEach((e) => {
+ const item = document.createElement('div');
+ item.className = 'ticket-item';
+ item.innerHTML = `
+ ${e.title || e.messageId || 'Eintrag'}
+ ${e.channelId || ''}
+ `;
+ list.appendChild(item);
+ });
+ container.appendChild(list);
+ }
+ catch (err) {
+ console.error(err);
+ container.innerHTML = 'Reaction Roles konnten nicht geladen werden.
';
+ (0, toast_js_1.showToast)('Fehler beim Laden der Reaction Roles', true);
+ }
+}
diff --git a/public/ts-build/components/modules/serverstats.js b/public/ts-build/components/modules/serverstats.js
new file mode 100644
index 0000000..b41a47b
--- /dev/null
+++ b/public/ts-build/components/modules/serverstats.js
@@ -0,0 +1,27 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.renderServerStatsModule = renderServerStatsModule;
+const api_js_1 = require("../../services/api.js");
+const toast_js_1 = require("../../ui/toast.js");
+async function renderServerStatsModule(guildId) {
+ const container = document.getElementById('module-serverstats');
+ if (!container)
+ return;
+ container.innerHTML = 'Lade Server Stats...
';
+ try {
+ const data = await api_js_1.api.serverStats(guildId);
+ const cfg = data?.config || data || {};
+ const items = cfg.items || [];
+ container.innerHTML = `
+ Server Stats
+ Kategorie: ${cfg.categoryId || '-'}
+ Refresh: ${cfg.refresh || '-'}m
+ Items: ${items.length}
+ `;
+ }
+ catch (err) {
+ console.error(err);
+ container.innerHTML = 'Server Stats konnten nicht geladen werden.
';
+ (0, toast_js_1.showToast)('Fehler beim Laden der Server Stats', true);
+ }
+}
diff --git a/public/ts-build/components/modules/statuspage.js b/public/ts-build/components/modules/statuspage.js
new file mode 100644
index 0000000..e620860
--- /dev/null
+++ b/public/ts-build/components/modules/statuspage.js
@@ -0,0 +1,27 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.renderStatuspageModule = renderStatuspageModule;
+const api_js_1 = require("../../services/api.js");
+const toast_js_1 = require("../../ui/toast.js");
+async function renderStatuspageModule(guildId) {
+ const container = document.getElementById('module-statuspage');
+ if (!container)
+ return;
+ container.innerHTML = 'Lade Statuspage...
';
+ try {
+ const data = await api_js_1.api.statuspage(guildId);
+ const cfg = data?.config || data || {};
+ const services = cfg.services || [];
+ container.innerHTML = `
+ Statuspage
+ Channel: ${cfg.channelId || '-'}
+ Intervall: ${cfg.interval || '-'}m
+ Services: ${services.length}
+ `;
+ }
+ catch (err) {
+ console.error(err);
+ container.innerHTML = 'Statuspage konnte nicht geladen werden.
';
+ (0, toast_js_1.showToast)('Fehler beim Laden der Statuspage', true);
+ }
+}
diff --git a/public/ts-build/components/modules/welcome.js b/public/ts-build/components/modules/welcome.js
new file mode 100644
index 0000000..e5aa2ed
--- /dev/null
+++ b/public/ts-build/components/modules/welcome.js
@@ -0,0 +1,26 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.renderWelcomeModule = renderWelcomeModule;
+const api_js_1 = require("../../services/api.js");
+const toast_js_1 = require("../../ui/toast.js");
+async function renderWelcomeModule(guildId) {
+ const container = document.getElementById('module-welcome');
+ if (!container)
+ return;
+ container.innerHTML = 'Lade Welcome...
';
+ try {
+ const data = await api_js_1.api.settings(guildId);
+ const cfg = data?.settings?.welcomeConfig || data?.welcomeConfig || {};
+ container.innerHTML = `
+ Welcome
+ Channel: ${cfg.channelId || '-'}
+ Embed Titel: ${cfg.embedTitle || '-'}
+ Status: ${data?.settings?.welcomeEnabled ? 'aktiv' : 'inaktiv'}
+ `;
+ }
+ catch (err) {
+ console.error(err);
+ container.innerHTML = 'Welcome konnte nicht geladen werden.
';
+ (0, toast_js_1.showToast)('Fehler beim Laden von Welcome', true);
+ }
+}
diff --git a/public/ts-build/components/overview.js b/public/ts-build/components/overview.js
new file mode 100644
index 0000000..685229d
--- /dev/null
+++ b/public/ts-build/components/overview.js
@@ -0,0 +1,37 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.renderOverview = renderOverview;
+const api_js_1 = require("../services/api.js");
+const toast_js_1 = require("../ui/toast.js");
+async function renderOverview(guildId) {
+ const section = document.getElementById('section-overview');
+ if (!section)
+ return;
+ section.innerHTML = 'Lade Uebersicht...
';
+ try {
+ const data = await api_js_1.api.overview(guildId);
+ const stats = data?.stats || {};
+ section.innerHTML = `
+ Uebersicht
+
+
+
Tickets offen
+
${stats.openTickets ?? '-'}
+
+
+
Module aktiv
+
${stats.activeModules ?? '-'}
+
+
+
Events geplant
+
${stats.events ?? '-'}
+
+
+ `;
+ }
+ catch (err) {
+ console.error(err);
+ section.innerHTML = 'Uebersicht konnte nicht geladen werden.
';
+ (0, toast_js_1.showToast)('Fehler beim Laden der Uebersicht', true);
+ }
+}
diff --git a/public/ts-build/components/settings.js b/public/ts-build/components/settings.js
new file mode 100644
index 0000000..fac4e9e
--- /dev/null
+++ b/public/ts-build/components/settings.js
@@ -0,0 +1,26 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.renderSettingsSection = renderSettingsSection;
+const api_js_1 = require("../services/api.js");
+const toast_js_1 = require("../ui/toast.js");
+async function renderSettingsSection(guildId) {
+ const section = document.getElementById('section-settings');
+ if (!section)
+ return;
+ section.innerHTML = 'Lade Einstellungen...
';
+ try {
+ const data = await api_js_1.api.settings(guildId);
+ const settings = data?.settings || {};
+ section.innerHTML = `
+ Einstellungen
+
+
${JSON.stringify(settings, null, 2)}
+
+ `;
+ }
+ catch (err) {
+ console.error(err);
+ section.innerHTML = 'Einstellungen konnten nicht geladen werden.
';
+ (0, toast_js_1.showToast)('Fehler beim Laden der Einstellungen', true);
+ }
+}
diff --git a/public/ts-build/components/tickets/automations.js b/public/ts-build/components/tickets/automations.js
new file mode 100644
index 0000000..0726330
--- /dev/null
+++ b/public/ts-build/components/tickets/automations.js
@@ -0,0 +1,42 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.renderAutomations = renderAutomations;
+const api_js_1 = require("../../services/api.js");
+const toast_js_1 = require("../../ui/toast.js");
+async function renderAutomations(guildId) {
+ const container = document.getElementById('tickets-automations');
+ if (!container)
+ return;
+ container.innerHTML = 'Lade Automationen...
';
+ try {
+ const data = await api_js_1.api.automations(guildId);
+ const rules = data?.rules || data || [];
+ if (!rules.length) {
+ container.innerHTML = 'Keine Regeln angelegt.
';
+ return;
+ }
+ const list = document.createElement('div');
+ list.className = 'ticket-list';
+ rules.forEach((r) => {
+ const item = document.createElement('div');
+ item.className = 'ticket-item';
+ item.innerHTML = `
+
+
+
${r.name || 'Regel'}
+
${r.condition?.type || r.condition?.status || ''}
+
+
${r.active ? 'aktiv' : 'inaktiv'}
+
+ `;
+ list.appendChild(item);
+ });
+ container.innerHTML = 'Automationen
';
+ container.appendChild(list);
+ }
+ catch (err) {
+ console.error(err);
+ container.innerHTML = 'Automationen konnten nicht geladen werden.
';
+ (0, toast_js_1.showToast)('Fehler beim Laden der Automationen', true);
+ }
+}
diff --git a/public/ts-build/components/tickets/index.js b/public/ts-build/components/tickets/index.js
new file mode 100644
index 0000000..f38c463
--- /dev/null
+++ b/public/ts-build/components/tickets/index.js
@@ -0,0 +1,32 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.initTicketsSection = initTicketsSection;
+const list_js_1 = require("./list.js");
+const pipeline_js_1 = require("./pipeline.js");
+const sla_js_1 = require("./sla.js");
+const automations_js_1 = require("./automations.js");
+const kb_js_1 = require("./kb.js");
+async function initTicketsSection(guildId) {
+ const section = document.getElementById('section-tickets');
+ if (!section)
+ return;
+ section.innerHTML = `
+ Tickets
+
+
+ `;
+ await Promise.all([
+ (0, list_js_1.renderTicketList)(guildId),
+ (0, pipeline_js_1.renderPipeline)(guildId),
+ (0, sla_js_1.renderSla)(guildId),
+ (0, automations_js_1.renderAutomations)(guildId),
+ (0, kb_js_1.renderKb)(guildId)
+ ]);
+}
diff --git a/public/ts-build/components/tickets/kb.js b/public/ts-build/components/tickets/kb.js
new file mode 100644
index 0000000..8369db2
--- /dev/null
+++ b/public/ts-build/components/tickets/kb.js
@@ -0,0 +1,37 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.renderKb = renderKb;
+const api_js_1 = require("../../services/api.js");
+const toast_js_1 = require("../../ui/toast.js");
+async function renderKb(guildId) {
+ const container = document.getElementById('tickets-kb');
+ if (!container)
+ return;
+ container.innerHTML = 'Lade Knowledge Base...
';
+ try {
+ const data = await api_js_1.api.kb(guildId);
+ const entries = data?.articles || data?.kb || [];
+ if (!entries.length) {
+ container.innerHTML = 'Keine KB-Eintraege.
';
+ return;
+ }
+ const list = document.createElement('div');
+ list.className = 'ticket-list';
+ entries.slice(0, 4).forEach((k) => {
+ const item = document.createElement('div');
+ item.className = 'ticket-item';
+ item.innerHTML = `
+ ${k.title || 'Artikel'}
+ ${k.keywords || ''}
+ `;
+ list.appendChild(item);
+ });
+ container.innerHTML = 'Knowledge Base
';
+ container.appendChild(list);
+ }
+ catch (err) {
+ console.error(err);
+ container.innerHTML = 'KB konnte nicht geladen werden.
';
+ (0, toast_js_1.showToast)('Fehler beim Laden der KB', true);
+ }
+}
diff --git a/public/ts-build/components/tickets/list.js b/public/ts-build/components/tickets/list.js
new file mode 100644
index 0000000..7a72c58
--- /dev/null
+++ b/public/ts-build/components/tickets/list.js
@@ -0,0 +1,43 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.renderTicketList = renderTicketList;
+const api_js_1 = require("../../services/api.js");
+const toast_js_1 = require("../../ui/toast.js");
+async function renderTicketList(guildId) {
+ const container = document.getElementById('tickets-list');
+ if (!container)
+ return;
+ container.innerHTML = 'Lade Tickets...
';
+ try {
+ const data = await api_js_1.api.tickets(guildId);
+ const tickets = data?.tickets || [];
+ if (!tickets.length) {
+ container.innerHTML = 'Keine Tickets
';
+ return;
+ }
+ const list = document.createElement('div');
+ list.className = 'ticket-list';
+ tickets.slice(0, 5).forEach((t) => {
+ const item = document.createElement('div');
+ item.className = 'ticket-item';
+ item.innerHTML = `
+
+
+
${t.title || t.id}
+
${t.user || ''}
+
+
${t.status || 'open'}
+
+ ${t.description || ''}
+ `;
+ list.appendChild(item);
+ });
+ container.innerHTML = 'Aktuelle Tickets
';
+ container.appendChild(list);
+ }
+ catch (err) {
+ console.error(err);
+ container.innerHTML = 'Tickets konnten nicht geladen werden.
';
+ (0, toast_js_1.showToast)('Fehler beim Laden der Tickets', true);
+ }
+}
diff --git a/public/ts-build/components/tickets/pipeline.js b/public/ts-build/components/tickets/pipeline.js
new file mode 100644
index 0000000..5cdfcbf
--- /dev/null
+++ b/public/ts-build/components/tickets/pipeline.js
@@ -0,0 +1,37 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.renderPipeline = renderPipeline;
+const api_js_1 = require("../../services/api.js");
+const toast_js_1 = require("../../ui/toast.js");
+async function renderPipeline(guildId) {
+ const container = document.getElementById('tickets-pipeline');
+ if (!container)
+ return;
+ container.innerHTML = 'Lade Pipeline...
';
+ try {
+ const data = await api_js_1.api.pipeline(guildId);
+ const lanes = data?.lanes || [];
+ container.innerHTML = 'Pipeline
';
+ if (!lanes.length) {
+ container.innerHTML += 'Keine Pipeline-Daten
';
+ return;
+ }
+ const grid = document.createElement('div');
+ grid.className = 'grid';
+ lanes.forEach((lane) => {
+ const card = document.createElement('div');
+ card.className = 'card';
+ card.innerHTML = `
+ ${lane.name || 'Lane'}
+ ${lane.count ?? 0}
+ `;
+ grid.appendChild(card);
+ });
+ container.appendChild(grid);
+ }
+ catch (err) {
+ console.error(err);
+ container.innerHTML = 'Pipeline konnte nicht geladen werden.
';
+ (0, toast_js_1.showToast)('Fehler beim Laden der Pipeline', true);
+ }
+}
diff --git a/public/ts-build/components/tickets/sla.js b/public/ts-build/components/tickets/sla.js
new file mode 100644
index 0000000..ee42a37
--- /dev/null
+++ b/public/ts-build/components/tickets/sla.js
@@ -0,0 +1,25 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.renderSla = renderSla;
+const api_js_1 = require("../../services/api.js");
+const toast_js_1 = require("../../ui/toast.js");
+async function renderSla(guildId) {
+ const container = document.getElementById('tickets-sla');
+ if (!container)
+ return;
+ container.innerHTML = 'Lade SLA...
';
+ try {
+ const data = await api_js_1.api.sla(guildId);
+ const stats = data?.stats || {};
+ container.innerHTML = `
+ SLA
+ ${stats.averageResponse ?? '-'}m
+ Durchschnittliche Antwortzeit
+ `;
+ }
+ catch (err) {
+ console.error(err);
+ container.innerHTML = 'SLA konnte nicht geladen werden.
';
+ (0, toast_js_1.showToast)('Fehler beim Laden der SLA', true);
+ }
+}
diff --git a/public/ts-build/core/app.js b/public/ts-build/core/app.js
new file mode 100644
index 0000000..a89217e
--- /dev/null
+++ b/public/ts-build/core/app.js
@@ -0,0 +1,69 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const api_js_1 = require("../services/api.js");
+const store_js_1 = require("../state/store.js");
+const navigation_js_1 = require("../ui/navigation.js");
+const toast_js_1 = require("../ui/toast.js");
+const guildSelect_js_1 = require("../components/guildSelect.js");
+const dashboard_js_1 = require("../components/dashboard.js");
+function readConfig() {
+ const root = document.getElementById('app');
+ if (!root)
+ throw new Error('App-Container fehlt');
+ const view = root.dataset.view || 'selection';
+ const baseRoot = root.dataset.baseRoot || '/ucp';
+ const baseDashboard = root.dataset.baseDashboard || `${baseRoot}/dashboard`;
+ const baseAuth = root.dataset.baseAuth || `${baseRoot}/auth`;
+ const baseApi = root.dataset.baseApi || `${baseRoot}/api`;
+ const initialGuildId = root.dataset.guildId || undefined;
+ const isAdmin = root.dataset.userAdmin === 'true';
+ const userLabel = root.dataset.userName
+ ? `${root.dataset.userName}${root.dataset.userDisc ? '#' + root.dataset.userDisc : ''}`
+ : undefined;
+ (0, store_js_1.initConfig)({ baseRoot, baseDashboard, baseAuth, baseApi, view, initialGuildId, isAdmin, userLabel });
+}
+async function ensureAuth() {
+ try {
+ const me = await api_js_1.api.me();
+ if (!me?.user) {
+ const cfg = (0, store_js_1.getConfig)();
+ window.location.href = (cfg?.baseAuth || '/auth') + '/discord';
+ return null;
+ }
+ const userInfo = document.getElementById('userInfo');
+ if (userInfo && me.user)
+ userInfo.textContent = `${me.user.username}#${me.user.discriminator}`;
+ (0, store_js_1.setState)({ isAdmin: !!me.user?.isAdmin, userLabel: me.user ? `${me.user.username}#${me.user.discriminator}` : undefined });
+ return me;
+ }
+ catch (err) {
+ console.error(err);
+ (0, toast_js_1.showToast)('Authentifizierung fehlgeschlagen', true);
+ return null;
+ }
+}
+async function bootstrap() {
+ readConfig();
+ const cfg = (0, store_js_1.getConfig)();
+ if (!cfg)
+ return;
+ const sidebarRoot = document.getElementById('sidebar-root');
+ if (sidebarRoot)
+ (0, navigation_js_1.renderSidebar)(sidebarRoot, !!cfg.isAdmin);
+ if (cfg.view === 'selection') {
+ (0, guildSelect_js_1.initSelectionView)();
+ }
+ else {
+ await ensureAuth();
+ (0, dashboard_js_1.initDashboardView)();
+ (0, navigation_js_1.initNavigation)((section) => {
+ // Sections werden innerhalb der jeweiligen Komponenten bedient
+ if (section === 'admin' && !cfg.isAdmin)
+ (0, toast_js_1.showToast)('Kein Admin-Recht', true);
+ });
+ }
+}
+bootstrap().catch((err) => {
+ console.error(err);
+ (0, toast_js_1.showToast)('Fehler beim Laden', true);
+});
diff --git a/public/ts-build/services/api.js b/public/ts-build/services/api.js
new file mode 100644
index 0000000..b52bb74
--- /dev/null
+++ b/public/ts-build/services/api.js
@@ -0,0 +1,74 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.api = void 0;
+const store_js_1 = require("../state/store.js");
+function buildUrl(path, query) {
+ const cfg = (0, store_js_1.getConfig)();
+ const base = cfg?.baseApi || '';
+ const url = new URL(path.startsWith('http') ? path : `${base}${path}`, window.location.origin);
+ if (query) {
+ Object.entries(query).forEach(([k, v]) => {
+ if (v === undefined || v === null)
+ return;
+ url.searchParams.set(k, String(v));
+ });
+ }
+ return url.toString();
+}
+async function request(path, options = {}) {
+ const { query, headers, ...rest } = options;
+ const url = buildUrl(path, query);
+ const res = await fetch(url, {
+ ...rest,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(headers || {})
+ }
+ });
+ if (!res.ok) {
+ const text = await res.text().catch(() => res.statusText);
+ throw new Error(`Request failed: ${res.status} ${text}`);
+ }
+ const contentType = res.headers.get('content-type');
+ if (contentType && contentType.includes('application/json')) {
+ return (await res.json());
+ }
+ return (await res.text());
+}
+exports.api = {
+ me: () => request('/me'),
+ guilds: () => request('/guilds'),
+ overview: (guildId) => request(`/overview`, { query: { guildId } }),
+ settings: (guildId) => request(`/settings`, { query: { guildId } }),
+ saveSettings: (payload) => request('/settings', { method: 'POST', body: JSON.stringify(payload) }),
+ modules: (guildId) => request(`/modules`, { query: { guildId } }),
+ tickets: (guildId) => request(`/tickets`, { query: { guildId } }),
+ pipeline: (guildId, filter) => request(`/tickets/pipeline`, { query: { guildId, filter } }),
+ sla: (guildId, range) => request(`/tickets/sla`, { query: { guildId, range } }),
+ automations: (guildId) => request(`/automations`, { query: { guildId } }),
+ saveAutomation: (payload) => request(payload['id'] ? `/automations/${payload['id']}` : '/automations', {
+ method: payload['id'] ? 'PUT' : 'POST',
+ body: JSON.stringify(payload)
+ }),
+ kb: (guildId) => request(`/kb`, { query: { guildId } }),
+ saveKb: (payload) => request(payload['id'] ? `/kb/${payload['id']}` : '/kb', {
+ method: payload['id'] ? 'PUT' : 'POST',
+ body: JSON.stringify(payload)
+ }),
+ reactionRoles: (guildId) => request(`/reactionroles`, { query: { guildId } }),
+ saveReactionRole: (payload) => request(payload.id ? `/reactionroles/${payload.id}` : '/reactionroles', {
+ method: payload.id ? 'PUT' : 'POST',
+ body: JSON.stringify(payload)
+ }),
+ events: (guildId) => request(`/events`, { query: { guildId } }),
+ saveEvent: (payload) => request(payload.id ? `/events/${payload.id}` : '/events', {
+ method: payload.id ? 'PUT' : 'POST',
+ body: JSON.stringify(payload)
+ }),
+ statuspage: (guildId) => request(`/statuspage`, { query: { guildId } }),
+ saveStatuspage: (payload) => request('/statuspage', { method: 'POST', body: JSON.stringify(payload) }),
+ serverStats: (guildId) => request(`/serverstats`, { query: { guildId } }),
+ saveServerStats: (payload) => request('/serverstats', { method: 'POST', body: JSON.stringify(payload) }),
+ dynamicVoice: (guildId) => request(`/dynamicvoice`, { query: { guildId } }),
+ saveDynamicVoice: (payload) => request('/dynamicvoice', { method: 'POST', body: JSON.stringify(payload) })
+};
diff --git a/public/ts-build/state/store.js b/public/ts-build/state/store.js
new file mode 100644
index 0000000..a1c6943
--- /dev/null
+++ b/public/ts-build/state/store.js
@@ -0,0 +1,32 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.initConfig = initConfig;
+exports.getConfig = getConfig;
+exports.getState = getState;
+exports.setState = setState;
+exports.subscribe = subscribe;
+let config = null;
+let state = {};
+const listeners = new Set();
+function initConfig(next) {
+ config = next;
+ state = {
+ guildId: next.initialGuildId,
+ isAdmin: next.isAdmin,
+ userLabel: next.userLabel
+ };
+}
+function getConfig() {
+ return config;
+}
+function getState() {
+ return state;
+}
+function setState(partial) {
+ state = { ...state, ...partial };
+ listeners.forEach((l) => l(state));
+}
+function subscribe(listener) {
+ listeners.add(listener);
+ return () => listeners.delete(listener);
+}
diff --git a/public/ts-build/ui/modal.js b/public/ts-build/ui/modal.js
new file mode 100644
index 0000000..c7dc843
--- /dev/null
+++ b/public/ts-build/ui/modal.js
@@ -0,0 +1,27 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.showModal = showModal;
+exports.hideModal = hideModal;
+let activeModal = null;
+let backdrop = null;
+function ensureBackdrop() {
+ if (backdrop)
+ return backdrop;
+ backdrop = document.createElement('div');
+ backdrop.className = 'modal-backdrop';
+ backdrop.addEventListener('click', hideModal);
+ document.body.appendChild(backdrop);
+ return backdrop;
+}
+function showModal(content) {
+ const bd = ensureBackdrop();
+ if (!content.parentElement)
+ bd.appendChild(content);
+ activeModal = content;
+ bd.classList.add('show');
+}
+function hideModal() {
+ if (backdrop)
+ backdrop.classList.remove('show');
+ activeModal = null;
+}
diff --git a/public/ts-build/ui/navigation.js b/public/ts-build/ui/navigation.js
new file mode 100644
index 0000000..2369c27
--- /dev/null
+++ b/public/ts-build/ui/navigation.js
@@ -0,0 +1,57 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.renderSidebar = renderSidebar;
+exports.initNavigation = initNavigation;
+const store_js_1 = require("../state/store.js");
+const defaultNav = [
+ { id: 'overview', label: 'Uebersicht', icon: '[*]' },
+ { id: 'tickets', label: 'Ticketsystem', icon: '[*]' },
+ { id: 'modules', label: 'Module', icon: '[*]' },
+ { id: 'settings', label: 'Einstellungen', icon: '[*]' },
+ { id: 'events', label: 'Events', icon: '[*]' },
+ { id: 'admin', label: 'Admin', icon: '[*]', requiresAdmin: true }
+];
+function renderSidebar(container, isAdmin) {
+ container.innerHTML = '';
+ const brand = document.createElement('div');
+ brand.className = 'brand';
+ brand.textContent = 'Papo Control';
+ const nav = document.createElement('div');
+ nav.className = 'nav';
+ defaultNav.forEach((item) => {
+ if (item.requiresAdmin && !isAdmin)
+ return;
+ const a = document.createElement('a');
+ a.href = `#${item.id}`;
+ a.dataset.target = item.id;
+ a.innerHTML = `${item.icon || ''}${item.label}`;
+ nav.appendChild(a);
+ });
+ container.appendChild(brand);
+ container.appendChild(nav);
+}
+function initNavigation(onChange) {
+ const navLinks = Array.from(document.querySelectorAll('.nav a'));
+ const activate = (section) => {
+ navLinks.forEach((link) => link.classList.toggle('active', link.dataset.target === section));
+ document.querySelectorAll('.section').forEach((sec) => {
+ sec.classList.toggle('active', sec.id === `section-${section}`);
+ });
+ (0, store_js_1.setState)({}); // trigger listeners for potential observers
+ onChange(section);
+ };
+ navLinks.forEach((link) => {
+ link.addEventListener('click', (e) => {
+ e.preventDefault();
+ const target = link.dataset.target || 'overview';
+ history.replaceState(null, '', `#${target}`);
+ activate(target);
+ });
+ });
+ const initial = (location.hash || '#overview').replace('#', '');
+ activate(initial);
+ window.addEventListener('hashchange', () => {
+ const section = (location.hash || '#overview').replace('#', '');
+ activate(section);
+ });
+}
diff --git a/public/ts-build/ui/switch.js b/public/ts-build/ui/switch.js
new file mode 100644
index 0000000..46b01ba
--- /dev/null
+++ b/public/ts-build/ui/switch.js
@@ -0,0 +1,19 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.toggleSwitch = toggleSwitch;
+exports.getSwitch = getSwitch;
+exports.setSwitch = setSwitch;
+function toggleSwitch(el, force) {
+ if (!el)
+ return;
+ const next = force === undefined ? !el.classList.contains('on') : force;
+ el.classList.toggle('on', next);
+}
+function getSwitch(el) {
+ return el?.classList.contains('on') ?? false;
+}
+function setSwitch(el, value) {
+ if (!el)
+ return;
+ el.classList.toggle('on', value);
+}
diff --git a/public/ts-build/ui/toast.js b/public/ts-build/ui/toast.js
new file mode 100644
index 0000000..dbdf5b8
--- /dev/null
+++ b/public/ts-build/ui/toast.js
@@ -0,0 +1,27 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.showToast = showToast;
+exports.hideToast = hideToast;
+let currentTimeout = null;
+function showToast(message, isError = false, duration = 2500) {
+ let toast = document.getElementById('toast-root');
+ if (!toast) {
+ toast = document.createElement('div');
+ toast.id = 'toast-root';
+ document.body.appendChild(toast);
+ }
+ toast.className = `toast ${isError ? 'error' : ''}`;
+ toast.textContent = message;
+ requestAnimationFrame(() => {
+ toast?.classList.add('show');
+ });
+ if (currentTimeout)
+ window.clearTimeout(currentTimeout);
+ currentTimeout = window.setTimeout(() => hideToast(), duration);
+}
+function hideToast() {
+ const toast = document.getElementById('toast-root');
+ if (!toast)
+ return;
+ toast.classList.remove('show');
+}
diff --git a/public/ts/app.ts b/public/ts/app.ts
new file mode 100644
index 0000000..c2c03c1
--- /dev/null
+++ b/public/ts/app.ts
@@ -0,0 +1 @@
+import './core/app.js';
diff --git a/public/ts/components/admin/index.ts b/public/ts/components/admin/index.ts
new file mode 100644
index 0000000..3874230
--- /dev/null
+++ b/public/ts/components/admin/index.ts
@@ -0,0 +1,22 @@
+import { api } from '../../services/api.js';
+import { showToast } from '../../ui/toast.js';
+
+export async function initAdminSection(guildId: string) {
+ const section = document.getElementById('section-admin');
+ if (!section) return;
+ section.innerHTML = 'Lade Admin-Daten...
';
+ try {
+ const data: any = await api.settings(guildId);
+ section.innerHTML = `
+ Admin
+
+
Rohdaten (nur Admin):
+
${JSON.stringify(data, null, 2)}
+
+ `;
+ } catch (err) {
+ console.error(err);
+ section.innerHTML = 'Admin-Daten konnten nicht geladen werden.
';
+ showToast('Fehler beim Laden der Admin-Daten', true);
+ }
+}
diff --git a/public/ts/components/dashboard.ts b/public/ts/components/dashboard.ts
new file mode 100644
index 0000000..e9c9bc4
--- /dev/null
+++ b/public/ts/components/dashboard.ts
@@ -0,0 +1,88 @@
+import { api } from '../services/api.js';
+import { getConfig, getState, setState } from '../state/store.js';
+import { showToast } from '../ui/toast.js';
+import { renderOverview } from './overview.js';
+import { initTicketsSection } from './tickets/index.js';
+import { initModulesSection } from './modules/index.js';
+import { initEventsSection } from './events/index.js';
+import { initAdminSection } from './admin/index.js';
+import { renderSettingsSection } from './settings.js';
+
+let overviewInterval: number | null = null;
+let ticketsInterval: number | null = null;
+
+async function populateGuildSelect() {
+ const select = document.getElementById('guildSelect') as HTMLSelectElement | null;
+ const cfg = getConfig();
+ if (!select || !cfg) return;
+ select.innerHTML = ``;
+ try {
+ const data = await api.guilds();
+ select.innerHTML = '';
+ data.guilds.forEach((g) => {
+ const opt = document.createElement('option');
+ opt.value = g.id;
+ opt.textContent = g.name;
+ if (g.id === cfg.initialGuildId) opt.selected = true;
+ select.appendChild(opt);
+ });
+ const current = select.value || cfg.initialGuildId || data.guilds[0]?.id;
+ setState({ guildId: current || undefined });
+ select.value = current || '';
+ } catch (err) {
+ console.error(err);
+ showToast('Guilds konnten nicht geladen werden', true);
+ }
+}
+
+function registerGuildChange() {
+ const select = document.getElementById('guildSelect') as HTMLSelectElement | null;
+ if (!select) return;
+ select.addEventListener('change', () => {
+ const guildId = select.value;
+ setState({ guildId });
+ refreshSections();
+ });
+}
+
+async function refreshSections() {
+ const { guildId } = getState();
+ if (!guildId) return;
+ await renderOverview(guildId);
+ await initTicketsSection(guildId);
+ await initModulesSection(guildId);
+ await renderSettingsSection(guildId);
+ await initEventsSection(guildId);
+ const cfg = getConfig();
+ if (cfg?.isAdmin) {
+ await initAdminSection(guildId);
+ }
+}
+
+function setupPolling() {
+ const { guildId } = getState();
+ if (overviewInterval) window.clearInterval(overviewInterval);
+ if (ticketsInterval) window.clearInterval(ticketsInterval);
+ overviewInterval = window.setInterval(() => {
+ const current = getState().guildId;
+ if (current) renderOverview(current);
+ }, 10000);
+ ticketsInterval = window.setInterval(() => {
+ const current = getState().guildId;
+ if (current) initTicketsSection(current);
+ }, 12000);
+}
+
+export function initDashboardView() {
+ const cfg = getConfig();
+ const logoutBtn = document.getElementById('logoutBtn');
+ if (logoutBtn && cfg) {
+ logoutBtn.addEventListener('click', () => (window.location.href = `${cfg.baseAuth}/logout`));
+ }
+
+ populateGuildSelect().then(() => {
+ registerGuildChange();
+ refreshSections();
+ setupPolling();
+ });
+}
diff --git a/public/ts/components/events/index.ts b/public/ts/components/events/index.ts
new file mode 100644
index 0000000..42470a8
--- /dev/null
+++ b/public/ts/components/events/index.ts
@@ -0,0 +1,39 @@
+import { api } from '../../services/api.js';
+import { showToast } from '../../ui/toast.js';
+
+export async function initEventsSection(guildId: string) {
+ const section = document.getElementById('section-events');
+ if (!section) return;
+ section.innerHTML = 'Lade Events...
';
+ try {
+ const data: any = await api.events(guildId);
+ const events = data?.events || data || [];
+ section.innerHTML = 'Events
';
+ if (!events.length) {
+ section.innerHTML += 'Keine Events geplant.
';
+ return;
+ }
+ const list = document.createElement('div');
+ list.className = 'ticket-list';
+ events.forEach((ev: any) => {
+ const item = document.createElement('div');
+ item.className = 'ticket-item';
+ item.innerHTML = `
+
+
+
${ev.title || 'Event'}
+
${ev.date || ''}
+
+
${ev.status || 'open'}
+
+ ${ev.description || ''}
+ `;
+ list.appendChild(item);
+ });
+ section.appendChild(list);
+ } catch (err) {
+ console.error(err);
+ section.innerHTML = 'Events konnten nicht geladen werden.
';
+ showToast('Fehler beim Laden der Events', true);
+ }
+}
diff --git a/public/ts/components/guildSelect.ts b/public/ts/components/guildSelect.ts
new file mode 100644
index 0000000..6286152
--- /dev/null
+++ b/public/ts/components/guildSelect.ts
@@ -0,0 +1,56 @@
+import { api } from '../services/api.js';
+import { getConfig } from '../state/store.js';
+import { showToast } from '../ui/toast.js';
+
+export async function initSelectionView() {
+ const cfg = getConfig();
+ const grid = document.getElementById('guildGrid');
+ const logoutBtn = document.getElementById('logoutBtn');
+ const userInfo = document.getElementById('userInfo');
+
+ if (logoutBtn && cfg) {
+ logoutBtn.addEventListener('click', () => {
+ window.location.href = `${cfg.baseAuth}/logout`;
+ });
+ }
+
+ try {
+ const me = await api.me();
+ if (userInfo && me?.user) userInfo.textContent = `${me.user.username}#${me.user.discriminator}`;
+ } catch {
+ // ignore
+ }
+
+ if (!grid || !cfg) return;
+ grid.innerHTML = 'Lade Guilds...
';
+ try {
+ const data = await api.guilds();
+ grid.innerHTML = '';
+ (data.guilds || []).forEach((g) => {
+ const card = document.createElement('div');
+ card.className = 'card clickable';
+ card.innerHTML = `
+
+

+
+
${g.name}
+
ID: ${g.id}
+
+
+ Zum Dashboard
+ `;
+ card.addEventListener('click', () => {
+ const qs = g.id ? `?guildId=${encodeURIComponent(g.id)}` : '';
+ window.location.href = `${cfg.baseDashboard}${qs}`;
+ });
+ grid.appendChild(card);
+ });
+ if (!data.guilds?.length) {
+ grid.innerHTML = 'Bot ist in keiner Guild. Bitte Bot einladen.
';
+ }
+ } catch (err) {
+ console.error(err);
+ grid.innerHTML = 'Fehler beim Laden der Guilds
';
+ showToast('Guilds konnten nicht geladen werden', true);
+ }
+}
diff --git a/public/ts/components/modules/dynamicVoice.ts b/public/ts/components/modules/dynamicVoice.ts
new file mode 100644
index 0000000..ac84ce8
--- /dev/null
+++ b/public/ts/components/modules/dynamicVoice.ts
@@ -0,0 +1,22 @@
+import { api } from '../../services/api.js';
+import { showToast } from '../../ui/toast.js';
+
+export async function renderDynamicVoiceModule(guildId: string) {
+ const container = document.getElementById('module-dynamicvoice');
+ if (!container) return;
+ container.innerHTML = 'Lade Dynamic Voice...
';
+ try {
+ const data: any = await api.dynamicVoice(guildId);
+ const cfg = data?.config || data?.dynamicVoiceConfig || {};
+ container.innerHTML = `
+ Dynamic Voice
+ Lobby: ${cfg.lobbyChannelId || '-'}
+ Template: ${cfg.template || '-'}
+ User-Limit: ${cfg.userLimit ?? '-'}
+ `;
+ } catch (err) {
+ console.error(err);
+ container.innerHTML = 'Dynamic Voice konnte nicht geladen werden.
';
+ showToast('Fehler beim Laden von Dynamic Voice', true);
+ }
+}
diff --git a/public/ts/components/modules/index.ts b/public/ts/components/modules/index.ts
new file mode 100644
index 0000000..cd7efec
--- /dev/null
+++ b/public/ts/components/modules/index.ts
@@ -0,0 +1,99 @@
+import { api } from '../../services/api.js';
+import { showToast } from '../../ui/toast.js';
+import { setSwitch, getSwitch } from '../../ui/switch.js';
+import { renderWelcomeModule } from './welcome.js';
+import { renderLoggingModule } from './logging.js';
+import { renderReactionRolesModule } from './reactionRoles.js';
+import { renderDynamicVoiceModule } from './dynamicVoice.js';
+import { renderStatuspageModule } from './statuspage.js';
+import { renderServerStatsModule } from './serverstats.js';
+
+export async function initModulesSection(guildId: string) {
+ const section = document.getElementById('section-modules');
+ if (!section) return;
+ section.innerHTML = `
+ Module
+
+
+ `;
+ await Promise.all([
+ renderModuleToggles(guildId),
+ renderWelcomeModule(guildId),
+ renderLoggingModule(guildId),
+ renderReactionRolesModule(guildId),
+ renderDynamicVoiceModule(guildId),
+ renderStatuspageModule(guildId),
+ renderServerStatsModule(guildId)
+ ]);
+}
+
+async function renderModuleToggles(guildId: string) {
+ const container = document.getElementById('module-toggles');
+ if (!container) return;
+ container.innerHTML = 'Lade Module...
';
+ try {
+ const data: any = await api.modules(guildId);
+ const modules = data?.modules || data || {};
+ container.innerHTML = '';
+ const entries: Array<{ key: string; label: string; desc: string }> = [
+ { key: 'ticketsEnabled', label: 'Tickets', desc: 'Ticket-System aktivieren' },
+ { key: 'automodEnabled', label: 'Automod', desc: 'Moderations-Filter' },
+ { key: 'welcomeEnabled', label: 'Welcome', desc: 'Begrueßungsnachrichten' },
+ { key: 'musicEnabled', label: 'Musik', desc: 'Musiksteuerung' },
+ { key: 'levelingEnabled', label: 'Leveling', desc: 'XP/Level System' },
+ { key: 'statuspageEnabled', label: 'Statuspage', desc: 'Statusberichte' },
+ { key: 'serverStatsEnabled', label: 'Server Stats', desc: 'Stat-Channel' },
+ { key: 'birthdayEnabled', label: 'Birthday', desc: 'Geburtstagsmodul' },
+ { key: 'reactionRolesEnabled', label: 'Reaction Roles', desc: 'Selbstzuweisbare Rollen' },
+ { key: 'eventsEnabled', label: 'Events', desc: 'Event-Planung' },
+ { key: 'dynamicVoiceEnabled', label: 'Dynamic Voice', desc: 'Dynamische Voice Channels' }
+ ];
+ entries.forEach((entry) => {
+ const row = document.createElement('div');
+ row.className = 'module-item';
+ row.innerHTML = `
+
+
+ `;
+ const toggle = row.querySelector('.switch') as HTMLElement;
+ toggle.addEventListener('click', async () => {
+ setSwitch(toggle, !getSwitch(toggle));
+ await saveModules(guildId);
+ });
+ container.appendChild(row);
+ });
+ } catch (err) {
+ console.error(err);
+ container.innerHTML = 'Module konnten nicht geladen werden.
';
+ showToast('Fehler beim Laden der Module', true);
+ }
+}
+
+async function saveModules(guildId: string) {
+ const toggles = Array.from(document.querySelectorAll('#module-toggles .switch'));
+ const payload: Record = { guildId };
+ toggles.forEach((t) => {
+ const key = t.dataset.key;
+ if (!key) return;
+ payload[key] = t.classList.contains('on');
+ });
+ try {
+ await api.saveSettings(payload);
+ showToast('Module gespeichert');
+ } catch (err) {
+ console.error(err);
+ showToast('Module speichern fehlgeschlagen', true);
+ }
+}
diff --git a/public/ts/components/modules/logging.ts b/public/ts/components/modules/logging.ts
new file mode 100644
index 0000000..eeff4a0
--- /dev/null
+++ b/public/ts/components/modules/logging.ts
@@ -0,0 +1,22 @@
+import { api } from '../../services/api.js';
+import { showToast } from '../../ui/toast.js';
+
+export async function renderLoggingModule(guildId: string) {
+ const container = document.getElementById('module-logging');
+ if (!container) return;
+ container.innerHTML = 'Lade Logging...
';
+ try {
+ const data: any = await api.settings(guildId);
+ const cfg = data?.settings?.loggingConfig || data?.loggingConfig || {};
+ container.innerHTML = `
+ Logging
+ Channel: ${cfg.logChannelId || '-'}
+ Join/Leave: ${cfg.categories?.joinLeave ? 'an' : 'aus'}
+ System: ${cfg.categories?.system ? 'an' : 'aus'}
+ `;
+ } catch (err) {
+ console.error(err);
+ container.innerHTML = 'Logging konnte nicht geladen werden.
';
+ showToast('Fehler beim Laden von Logging', true);
+ }
+}
diff --git a/public/ts/components/modules/reactionRoles.ts b/public/ts/components/modules/reactionRoles.ts
new file mode 100644
index 0000000..b5c89dc
--- /dev/null
+++ b/public/ts/components/modules/reactionRoles.ts
@@ -0,0 +1,33 @@
+import { api } from '../../services/api.js';
+import { showToast } from '../../ui/toast.js';
+
+export async function renderReactionRolesModule(guildId: string) {
+ const container = document.getElementById('module-reactionroles');
+ if (!container) return;
+ container.innerHTML = 'Lade Reaction Roles...
';
+ try {
+ const data: any = await api.reactionRoles(guildId);
+ const entries = data?.entries || data?.reactionRoles || [];
+ container.innerHTML = 'Reaction Roles
';
+ if (!entries.length) {
+ container.innerHTML += 'Keine Reaction Roles.
';
+ return;
+ }
+ const list = document.createElement('div');
+ list.className = 'ticket-list';
+ entries.slice(0, 3).forEach((e: any) => {
+ const item = document.createElement('div');
+ item.className = 'ticket-item';
+ item.innerHTML = `
+ ${e.title || e.messageId || 'Eintrag'}
+ ${e.channelId || ''}
+ `;
+ list.appendChild(item);
+ });
+ container.appendChild(list);
+ } catch (err) {
+ console.error(err);
+ container.innerHTML = 'Reaction Roles konnten nicht geladen werden.
';
+ showToast('Fehler beim Laden der Reaction Roles', true);
+ }
+}
diff --git a/public/ts/components/modules/serverstats.ts b/public/ts/components/modules/serverstats.ts
new file mode 100644
index 0000000..78191df
--- /dev/null
+++ b/public/ts/components/modules/serverstats.ts
@@ -0,0 +1,23 @@
+import { api } from '../../services/api.js';
+import { showToast } from '../../ui/toast.js';
+
+export async function renderServerStatsModule(guildId: string) {
+ const container = document.getElementById('module-serverstats');
+ if (!container) return;
+ container.innerHTML = 'Lade Server Stats...
';
+ try {
+ const data: any = await api.serverStats(guildId);
+ const cfg = data?.config || data || {};
+ const items = cfg.items || [];
+ container.innerHTML = `
+ Server Stats
+ Kategorie: ${cfg.categoryId || '-'}
+ Refresh: ${cfg.refresh || '-'}m
+ Items: ${items.length}
+ `;
+ } catch (err) {
+ console.error(err);
+ container.innerHTML = 'Server Stats konnten nicht geladen werden.
';
+ showToast('Fehler beim Laden der Server Stats', true);
+ }
+}
diff --git a/public/ts/components/modules/statuspage.ts b/public/ts/components/modules/statuspage.ts
new file mode 100644
index 0000000..ec804f0
--- /dev/null
+++ b/public/ts/components/modules/statuspage.ts
@@ -0,0 +1,23 @@
+import { api } from '../../services/api.js';
+import { showToast } from '../../ui/toast.js';
+
+export async function renderStatuspageModule(guildId: string) {
+ const container = document.getElementById('module-statuspage');
+ if (!container) return;
+ container.innerHTML = 'Lade Statuspage...
';
+ try {
+ const data: any = await api.statuspage(guildId);
+ const cfg = data?.config || data || {};
+ const services = cfg.services || [];
+ container.innerHTML = `
+ Statuspage
+ Channel: ${cfg.channelId || '-'}
+ Intervall: ${cfg.interval || '-'}m
+ Services: ${services.length}
+ `;
+ } catch (err) {
+ console.error(err);
+ container.innerHTML = 'Statuspage konnte nicht geladen werden.
';
+ showToast('Fehler beim Laden der Statuspage', true);
+ }
+}
diff --git a/public/ts/components/modules/welcome.ts b/public/ts/components/modules/welcome.ts
new file mode 100644
index 0000000..435c9e3
--- /dev/null
+++ b/public/ts/components/modules/welcome.ts
@@ -0,0 +1,22 @@
+import { api } from '../../services/api.js';
+import { showToast } from '../../ui/toast.js';
+
+export async function renderWelcomeModule(guildId: string) {
+ const container = document.getElementById('module-welcome');
+ if (!container) return;
+ container.innerHTML = 'Lade Welcome...
';
+ try {
+ const data: any = await api.settings(guildId);
+ const cfg = data?.settings?.welcomeConfig || data?.welcomeConfig || {};
+ container.innerHTML = `
+ Welcome
+ Channel: ${cfg.channelId || '-'}
+ Embed Titel: ${cfg.embedTitle || '-'}
+ Status: ${data?.settings?.welcomeEnabled ? 'aktiv' : 'inaktiv'}
+ `;
+ } catch (err) {
+ console.error(err);
+ container.innerHTML = 'Welcome konnte nicht geladen werden.
';
+ showToast('Fehler beim Laden von Welcome', true);
+ }
+}
diff --git a/public/ts/components/overview.ts b/public/ts/components/overview.ts
new file mode 100644
index 0000000..a25f04a
--- /dev/null
+++ b/public/ts/components/overview.ts
@@ -0,0 +1,33 @@
+import { api } from '../services/api.js';
+import { showToast } from '../ui/toast.js';
+
+export async function renderOverview(guildId: string) {
+ const section = document.getElementById('section-overview');
+ if (!section) return;
+ section.innerHTML = 'Lade Uebersicht...
';
+ try {
+ const data: any = await api.overview(guildId);
+ const stats = data?.stats || {};
+ section.innerHTML = `
+ Uebersicht
+
+
+
Tickets offen
+
${stats.openTickets ?? '-'}
+
+
+
Module aktiv
+
${stats.activeModules ?? '-'}
+
+
+
Events geplant
+
${stats.events ?? '-'}
+
+
+ `;
+ } catch (err) {
+ console.error(err);
+ section.innerHTML = 'Uebersicht konnte nicht geladen werden.
';
+ showToast('Fehler beim Laden der Uebersicht', true);
+ }
+}
diff --git a/public/ts/components/settings.ts b/public/ts/components/settings.ts
new file mode 100644
index 0000000..8d0eefe
--- /dev/null
+++ b/public/ts/components/settings.ts
@@ -0,0 +1,22 @@
+import { api } from '../services/api.js';
+import { showToast } from '../ui/toast.js';
+
+export async function renderSettingsSection(guildId: string) {
+ const section = document.getElementById('section-settings');
+ if (!section) return;
+ section.innerHTML = 'Lade Einstellungen...
';
+ try {
+ const data: any = await api.settings(guildId);
+ const settings = data?.settings || {};
+ section.innerHTML = `
+ Einstellungen
+
+
${JSON.stringify(settings, null, 2)}
+
+ `;
+ } catch (err) {
+ console.error(err);
+ section.innerHTML = 'Einstellungen konnten nicht geladen werden.
';
+ showToast('Fehler beim Laden der Einstellungen', true);
+ }
+}
diff --git a/public/ts/components/tickets/automations.ts b/public/ts/components/tickets/automations.ts
new file mode 100644
index 0000000..404dab3
--- /dev/null
+++ b/public/ts/components/tickets/automations.ts
@@ -0,0 +1,38 @@
+import { api } from '../../services/api.js';
+import { showToast } from '../../ui/toast.js';
+
+export async function renderAutomations(guildId: string) {
+ const container = document.getElementById('tickets-automations');
+ if (!container) return;
+ container.innerHTML = 'Lade Automationen...
';
+ try {
+ const data: any = await api.automations(guildId);
+ const rules = data?.rules || data || [];
+ if (!rules.length) {
+ container.innerHTML = 'Keine Regeln angelegt.
';
+ return;
+ }
+ const list = document.createElement('div');
+ list.className = 'ticket-list';
+ rules.forEach((r: any) => {
+ const item = document.createElement('div');
+ item.className = 'ticket-item';
+ item.innerHTML = `
+
+
+
${r.name || 'Regel'}
+
${r.condition?.type || r.condition?.status || ''}
+
+
${r.active ? 'aktiv' : 'inaktiv'}
+
+ `;
+ list.appendChild(item);
+ });
+ container.innerHTML = 'Automationen
';
+ container.appendChild(list);
+ } catch (err) {
+ console.error(err);
+ container.innerHTML = 'Automationen konnten nicht geladen werden.
';
+ showToast('Fehler beim Laden der Automationen', true);
+ }
+}
diff --git a/public/ts/components/tickets/index.ts b/public/ts/components/tickets/index.ts
new file mode 100644
index 0000000..cbd0b4d
--- /dev/null
+++ b/public/ts/components/tickets/index.ts
@@ -0,0 +1,29 @@
+import { renderTicketList } from './list.js';
+import { renderPipeline } from './pipeline.js';
+import { renderSla } from './sla.js';
+import { renderAutomations } from './automations.js';
+import { renderKb } from './kb.js';
+
+export async function initTicketsSection(guildId: string) {
+ const section = document.getElementById('section-tickets');
+ if (!section) return;
+ section.innerHTML = `
+ Tickets
+
+
+ `;
+ await Promise.all([
+ renderTicketList(guildId),
+ renderPipeline(guildId),
+ renderSla(guildId),
+ renderAutomations(guildId),
+ renderKb(guildId)
+ ]);
+}
diff --git a/public/ts/components/tickets/kb.ts b/public/ts/components/tickets/kb.ts
new file mode 100644
index 0000000..ba0dc44
--- /dev/null
+++ b/public/ts/components/tickets/kb.ts
@@ -0,0 +1,33 @@
+import { api } from '../../services/api.js';
+import { showToast } from '../../ui/toast.js';
+
+export async function renderKb(guildId: string) {
+ const container = document.getElementById('tickets-kb');
+ if (!container) return;
+ container.innerHTML = 'Lade Knowledge Base...
';
+ try {
+ const data: any = await api.kb(guildId);
+ const entries = data?.articles || data?.kb || [];
+ if (!entries.length) {
+ container.innerHTML = 'Keine KB-Eintraege.
';
+ return;
+ }
+ const list = document.createElement('div');
+ list.className = 'ticket-list';
+ entries.slice(0, 4).forEach((k: any) => {
+ const item = document.createElement('div');
+ item.className = 'ticket-item';
+ item.innerHTML = `
+ ${k.title || 'Artikel'}
+ ${k.keywords || ''}
+ `;
+ list.appendChild(item);
+ });
+ container.innerHTML = 'Knowledge Base
';
+ container.appendChild(list);
+ } catch (err) {
+ console.error(err);
+ container.innerHTML = 'KB konnte nicht geladen werden.
';
+ showToast('Fehler beim Laden der KB', true);
+ }
+}
diff --git a/public/ts/components/tickets/list.ts b/public/ts/components/tickets/list.ts
new file mode 100644
index 0000000..70a7589
--- /dev/null
+++ b/public/ts/components/tickets/list.ts
@@ -0,0 +1,39 @@
+import { api } from '../../services/api.js';
+import { showToast } from '../../ui/toast.js';
+
+export async function renderTicketList(guildId: string) {
+ const container = document.getElementById('tickets-list');
+ if (!container) return;
+ container.innerHTML = 'Lade Tickets...
';
+ try {
+ const data: any = await api.tickets(guildId);
+ const tickets = data?.tickets || [];
+ if (!tickets.length) {
+ container.innerHTML = 'Keine Tickets
';
+ return;
+ }
+ const list = document.createElement('div');
+ list.className = 'ticket-list';
+ tickets.slice(0, 5).forEach((t: any) => {
+ const item = document.createElement('div');
+ item.className = 'ticket-item';
+ item.innerHTML = `
+
+
+
${t.title || t.id}
+
${t.user || ''}
+
+
${t.status || 'open'}
+
+ ${t.description || ''}
+ `;
+ list.appendChild(item);
+ });
+ container.innerHTML = 'Aktuelle Tickets
';
+ container.appendChild(list);
+ } catch (err) {
+ console.error(err);
+ container.innerHTML = 'Tickets konnten nicht geladen werden.
';
+ showToast('Fehler beim Laden der Tickets', true);
+ }
+}
diff --git a/public/ts/components/tickets/pipeline.ts b/public/ts/components/tickets/pipeline.ts
new file mode 100644
index 0000000..9c4bc6e
--- /dev/null
+++ b/public/ts/components/tickets/pipeline.ts
@@ -0,0 +1,33 @@
+import { api } from '../../services/api.js';
+import { showToast } from '../../ui/toast.js';
+
+export async function renderPipeline(guildId: string) {
+ const container = document.getElementById('tickets-pipeline');
+ if (!container) return;
+ container.innerHTML = 'Lade Pipeline...
';
+ try {
+ const data: any = await api.pipeline(guildId);
+ const lanes = data?.lanes || [];
+ container.innerHTML = 'Pipeline
';
+ if (!lanes.length) {
+ container.innerHTML += 'Keine Pipeline-Daten
';
+ return;
+ }
+ const grid = document.createElement('div');
+ grid.className = 'grid';
+ lanes.forEach((lane: any) => {
+ const card = document.createElement('div');
+ card.className = 'card';
+ card.innerHTML = `
+ ${lane.name || 'Lane'}
+ ${lane.count ?? 0}
+ `;
+ grid.appendChild(card);
+ });
+ container.appendChild(grid);
+ } catch (err) {
+ console.error(err);
+ container.innerHTML = 'Pipeline konnte nicht geladen werden.
';
+ showToast('Fehler beim Laden der Pipeline', true);
+ }
+}
diff --git a/public/ts/components/tickets/sla.ts b/public/ts/components/tickets/sla.ts
new file mode 100644
index 0000000..6d1050e
--- /dev/null
+++ b/public/ts/components/tickets/sla.ts
@@ -0,0 +1,21 @@
+import { api } from '../../services/api.js';
+import { showToast } from '../../ui/toast.js';
+
+export async function renderSla(guildId: string) {
+ const container = document.getElementById('tickets-sla');
+ if (!container) return;
+ container.innerHTML = 'Lade SLA...
';
+ try {
+ const data: any = await api.sla(guildId);
+ const stats = data?.stats || {};
+ container.innerHTML = `
+ SLA
+ ${stats.averageResponse ?? '-'}m
+ Durchschnittliche Antwortzeit
+ `;
+ } catch (err) {
+ console.error(err);
+ container.innerHTML = 'SLA konnte nicht geladen werden.
';
+ showToast('Fehler beim Laden der SLA', true);
+ }
+}
diff --git a/public/ts/core/app.ts b/public/ts/core/app.ts
new file mode 100644
index 0000000..22dfb3f
--- /dev/null
+++ b/public/ts/core/app.ts
@@ -0,0 +1,67 @@
+import { api } from '../services/api.js';
+import { initConfig, setState, getConfig } from '../state/store.js';
+import { renderSidebar, initNavigation } from '../ui/navigation.js';
+import { showToast } from '../ui/toast.js';
+import { initSelectionView } from '../components/guildSelect.js';
+import { initDashboardView } from '../components/dashboard.js';
+
+function readConfig(): void {
+ const root = document.getElementById('app');
+ if (!root) throw new Error('App-Container fehlt');
+ const view = (root.dataset.view as 'selection' | 'dashboard') || 'selection';
+ const baseRoot = root.dataset.baseRoot || '/ucp';
+ const baseDashboard = root.dataset.baseDashboard || `${baseRoot}/dashboard`;
+ const baseAuth = root.dataset.baseAuth || `${baseRoot}/auth`;
+ const baseApi = root.dataset.baseApi || `${baseRoot}/api`;
+ const initialGuildId = root.dataset.guildId || undefined;
+ const isAdmin = root.dataset.userAdmin === 'true';
+ const userLabel = root.dataset.userName
+ ? `${root.dataset.userName}${root.dataset.userDisc ? '#' + root.dataset.userDisc : ''}`
+ : undefined;
+
+ initConfig({ baseRoot, baseDashboard, baseAuth, baseApi, view, initialGuildId, isAdmin, userLabel });
+}
+
+async function ensureAuth() {
+ try {
+ const me = await api.me();
+ if (!me?.user) {
+ const cfg = getConfig();
+ window.location.href = (cfg?.baseAuth || '/auth') + '/discord';
+ return null;
+ }
+ const userInfo = document.getElementById('userInfo');
+ if (userInfo && me.user) userInfo.textContent = `${me.user.username}#${me.user.discriminator}`;
+ setState({ isAdmin: !!me.user?.isAdmin, userLabel: me.user ? `${me.user.username}#${me.user.discriminator}` : undefined });
+ return me;
+ } catch (err) {
+ console.error(err);
+ showToast('Authentifizierung fehlgeschlagen', true);
+ return null;
+ }
+}
+
+async function bootstrap() {
+ readConfig();
+ const cfg = getConfig();
+ if (!cfg) return;
+
+ const sidebarRoot = document.getElementById('sidebar-root');
+ if (sidebarRoot) renderSidebar(sidebarRoot, !!cfg.isAdmin);
+
+ if (cfg.view === 'selection') {
+ initSelectionView();
+ } else {
+ await ensureAuth();
+ initDashboardView();
+ initNavigation((section) => {
+ // Sections werden innerhalb der jeweiligen Komponenten bedient
+ if (section === 'admin' && !cfg.isAdmin) showToast('Kein Admin-Recht', true);
+ });
+ }
+}
+
+bootstrap().catch((err) => {
+ console.error(err);
+ showToast('Fehler beim Laden', true);
+});
diff --git a/public/ts/services/api.ts b/public/ts/services/api.ts
new file mode 100644
index 0000000..7a672cb
--- /dev/null
+++ b/public/ts/services/api.ts
@@ -0,0 +1,79 @@
+import { getConfig } from '../state/store.js';
+
+type FetchOptions = RequestInit & { query?: Record };
+
+function buildUrl(path: string, query?: Record) {
+ const cfg = getConfig();
+ const base = cfg?.baseApi || '';
+ const url = new URL(path.startsWith('http') ? path : `${base}${path}`, window.location.origin);
+ if (query) {
+ Object.entries(query).forEach(([k, v]) => {
+ if (v === undefined || v === null) return;
+ url.searchParams.set(k, String(v));
+ });
+ }
+ return url.toString();
+}
+
+async function request(path: string, options: FetchOptions = {}): Promise {
+ const { query, headers, ...rest } = options;
+ const url = buildUrl(path, query);
+ const res = await fetch(url, {
+ ...rest,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(headers || {})
+ }
+ });
+ if (!res.ok) {
+ const text = await res.text().catch(() => res.statusText);
+ throw new Error(`Request failed: ${res.status} ${text}`);
+ }
+ const contentType = res.headers.get('content-type');
+ if (contentType && contentType.includes('application/json')) {
+ return (await res.json()) as T;
+ }
+ return (await res.text()) as unknown as T;
+}
+
+export const api = {
+ me: () => request<{ user?: { username: string; discriminator: string; isAdmin?: boolean } }>('/me'),
+ guilds: () => request<{ guilds: Array<{ id: string; name: string; icon?: string }> }>('/guilds'),
+ overview: (guildId: string) => request(`/overview`, { query: { guildId } }),
+ settings: (guildId: string) => request(`/settings`, { query: { guildId } }),
+ saveSettings: (payload: Record) => request('/settings', { method: 'POST', body: JSON.stringify(payload) }),
+ modules: (guildId: string) => request(`/modules`, { query: { guildId } }),
+ tickets: (guildId: string) => request(`/tickets`, { query: { guildId } }),
+ pipeline: (guildId: string, filter?: string) => request(`/tickets/pipeline`, { query: { guildId, filter } }),
+ sla: (guildId: string, range?: number) => request(`/tickets/sla`, { query: { guildId, range } }),
+ automations: (guildId: string) => request(`/automations`, { query: { guildId } }),
+ saveAutomation: (payload: Record) =>
+ request(payload['id'] ? `/automations/${payload['id']}` : '/automations', {
+ method: payload['id'] ? 'PUT' : 'POST',
+ body: JSON.stringify(payload)
+ }),
+ kb: (guildId: string) => request(`/kb`, { query: { guildId } }),
+ saveKb: (payload: Record) =>
+ request(payload['id'] ? `/kb/${payload['id']}` : '/kb', {
+ method: payload['id'] ? 'PUT' : 'POST',
+ body: JSON.stringify(payload)
+ }),
+ reactionRoles: (guildId: string) => request(`/reactionroles`, { query: { guildId } }),
+ saveReactionRole: (payload: Record & { id?: string }) =>
+ request(payload.id ? `/reactionroles/${payload.id}` : '/reactionroles', {
+ method: payload.id ? 'PUT' : 'POST',
+ body: JSON.stringify(payload)
+ }),
+ events: (guildId: string) => request(`/events`, { query: { guildId } }),
+ saveEvent: (payload: Record & { id?: string }) =>
+ request(payload.id ? `/events/${payload.id}` : '/events', {
+ method: payload.id ? 'PUT' : 'POST',
+ body: JSON.stringify(payload)
+ }),
+ statuspage: (guildId: string) => request(`/statuspage`, { query: { guildId } }),
+ saveStatuspage: (payload: Record) => request('/statuspage', { method: 'POST', body: JSON.stringify(payload) }),
+ serverStats: (guildId: string) => request(`/serverstats`, { query: { guildId } }),
+ saveServerStats: (payload: Record) => request('/serverstats', { method: 'POST', body: JSON.stringify(payload) }),
+ dynamicVoice: (guildId: string) => request(`/dynamicvoice`, { query: { guildId } }),
+ saveDynamicVoice: (payload: Record) => request('/dynamicvoice', { method: 'POST', body: JSON.stringify(payload) })
+};
diff --git a/public/ts/state/store.ts b/public/ts/state/store.ts
new file mode 100644
index 0000000..4c809db
--- /dev/null
+++ b/public/ts/state/store.ts
@@ -0,0 +1,49 @@
+export interface AppConfig {
+ baseRoot: string;
+ baseDashboard: string;
+ baseAuth: string;
+ baseApi: string;
+ view: 'selection' | 'dashboard';
+ initialGuildId?: string;
+ isAdmin?: boolean;
+ userLabel?: string;
+}
+
+export interface AppState {
+ guildId?: string;
+ isAdmin?: boolean;
+ userLabel?: string;
+}
+
+type Listener = (state: AppState) => void;
+
+let config: AppConfig | null = null;
+let state: AppState = {};
+const listeners = new Set();
+
+export function initConfig(next: AppConfig) {
+ config = next;
+ state = {
+ guildId: next.initialGuildId,
+ isAdmin: next.isAdmin,
+ userLabel: next.userLabel
+ };
+}
+
+export function getConfig() {
+ return config;
+}
+
+export function getState() {
+ return state;
+}
+
+export function setState(partial: Partial) {
+ state = { ...state, ...partial };
+ listeners.forEach((l) => l(state));
+}
+
+export function subscribe(listener: Listener) {
+ listeners.add(listener);
+ return () => listeners.delete(listener);
+}
diff --git a/public/ts/ui/modal.ts b/public/ts/ui/modal.ts
new file mode 100644
index 0000000..e6292a5
--- /dev/null
+++ b/public/ts/ui/modal.ts
@@ -0,0 +1,23 @@
+let activeModal: HTMLElement | null = null;
+let backdrop: HTMLElement | null = null;
+
+function ensureBackdrop() {
+ if (backdrop) return backdrop;
+ backdrop = document.createElement('div');
+ backdrop.className = 'modal-backdrop';
+ backdrop.addEventListener('click', hideModal);
+ document.body.appendChild(backdrop);
+ return backdrop;
+}
+
+export function showModal(content: HTMLElement) {
+ const bd = ensureBackdrop();
+ if (!content.parentElement) bd.appendChild(content);
+ activeModal = content;
+ bd.classList.add('show');
+}
+
+export function hideModal() {
+ if (backdrop) backdrop.classList.remove('show');
+ activeModal = null;
+}
diff --git a/public/ts/ui/navigation.ts b/public/ts/ui/navigation.ts
new file mode 100644
index 0000000..06a9594
--- /dev/null
+++ b/public/ts/ui/navigation.ts
@@ -0,0 +1,67 @@
+import { getState, setState } from '../state/store.js';
+
+export interface NavItem {
+ id: string;
+ label: string;
+ icon?: string;
+ requiresAdmin?: boolean;
+}
+
+const defaultNav: NavItem[] = [
+ { id: 'overview', label: 'Uebersicht', icon: '[*]' },
+ { id: 'tickets', label: 'Ticketsystem', icon: '[*]' },
+ { id: 'modules', label: 'Module', icon: '[*]' },
+ { id: 'settings', label: 'Einstellungen', icon: '[*]' },
+ { id: 'events', label: 'Events', icon: '[*]' },
+ { id: 'admin', label: 'Admin', icon: '[*]', requiresAdmin: true }
+];
+
+export function renderSidebar(container: HTMLElement, isAdmin: boolean) {
+ container.innerHTML = '';
+ const brand = document.createElement('div');
+ brand.className = 'brand';
+ brand.textContent = 'Papo Control';
+ const nav = document.createElement('div');
+ nav.className = 'nav';
+
+ defaultNav.forEach((item) => {
+ if (item.requiresAdmin && !isAdmin) return;
+ const a = document.createElement('a');
+ a.href = `#${item.id}`;
+ a.dataset.target = item.id;
+ a.innerHTML = `${item.icon || ''}${item.label}`;
+ nav.appendChild(a);
+ });
+
+ container.appendChild(brand);
+ container.appendChild(nav);
+}
+
+export function initNavigation(onChange: (section: string) => void) {
+ const navLinks = Array.from(document.querySelectorAll('.nav a'));
+ const activate = (section: string) => {
+ navLinks.forEach((link) => link.classList.toggle('active', link.dataset.target === section));
+ document.querySelectorAll('.section').forEach((sec) => {
+ sec.classList.toggle('active', sec.id === `section-${section}`);
+ });
+ setState({}); // trigger listeners for potential observers
+ onChange(section);
+ };
+
+ navLinks.forEach((link) => {
+ link.addEventListener('click', (e) => {
+ e.preventDefault();
+ const target = link.dataset.target || 'overview';
+ history.replaceState(null, '', `#${target}`);
+ activate(target);
+ });
+ });
+
+ const initial = (location.hash || '#overview').replace('#', '');
+ activate(initial);
+
+ window.addEventListener('hashchange', () => {
+ const section = (location.hash || '#overview').replace('#', '');
+ activate(section);
+ });
+}
diff --git a/public/ts/ui/switch.ts b/public/ts/ui/switch.ts
new file mode 100644
index 0000000..d3430a9
--- /dev/null
+++ b/public/ts/ui/switch.ts
@@ -0,0 +1,14 @@
+export function toggleSwitch(el: HTMLElement | null, force?: boolean) {
+ if (!el) return;
+ const next = force === undefined ? !el.classList.contains('on') : force;
+ el.classList.toggle('on', next);
+}
+
+export function getSwitch(el: HTMLElement | null) {
+ return el?.classList.contains('on') ?? false;
+}
+
+export function setSwitch(el: HTMLElement | null, value: boolean) {
+ if (!el) return;
+ el.classList.toggle('on', value);
+}
diff --git a/public/ts/ui/toast.ts b/public/ts/ui/toast.ts
new file mode 100644
index 0000000..68dcf39
--- /dev/null
+++ b/public/ts/ui/toast.ts
@@ -0,0 +1,23 @@
+let currentTimeout: number | null = null;
+
+export function showToast(message: string, isError = false, duration = 2500) {
+ let toast = document.getElementById('toast-root') as HTMLElement | null;
+ if (!toast) {
+ toast = document.createElement('div');
+ toast.id = 'toast-root';
+ document.body.appendChild(toast);
+ }
+ toast.className = `toast ${isError ? 'error' : ''}`;
+ toast.textContent = message;
+ requestAnimationFrame(() => {
+ toast?.classList.add('show');
+ });
+ if (currentTimeout) window.clearTimeout(currentTimeout);
+ currentTimeout = window.setTimeout(() => hideToast(), duration);
+}
+
+export function hideToast() {
+ const toast = document.getElementById('toast-root');
+ if (!toast) return;
+ toast.classList.remove('show');
+}
diff --git a/src/web/routes/dashboard.ts b/src/web/routes/dashboard.ts
index 7fbfc1b..fdbedf8 100644
--- a/src/web/routes/dashboard.ts
+++ b/src/web/routes/dashboard.ts
@@ -8,3011 +8,29 @@ router.get('/', (req, res) => {
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 = `
-
- `;
+ const guildId = typeof req.query.guildId === 'string' ? req.query.guildId : '';
if (!guildId) {
- res.send(`
-
-
-
-
-
- Papo Dashboard - Auswahl
-
-
-
-
- ${sidebar}
-
-
Waehle eine Guild aus
-
Nur Guilds, auf denen der Bot ist.
-
-
-
-
-
-
- `);
- return;
+ return res.render('dashboardSelection', {
+ baseRoot,
+ baseDashboard,
+ baseAuth,
+ baseApi
+ });
}
- res.send(`
-
-
-
-
-
- Papo Dashboard
-
-
-
-
- ${sidebar}
-
-
-
-
Guild Dashboard
-
Dashboard & Guild-Config
-
-
-
-
-
-
-
-
- Bot aktiv
-
-
-
-
-
-
-
Tickets
-
bersicht, Pipeline, SLA, Automationen, Knowledge-Base.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Ticketliste
-
Links auswhlen, Details im Modal. Plus ffnet Panel-Erstellung.
-
-
-
-
-
-
-
-
-
-
-
-
Support-Login Status
-
Aktive Supporter und letzte Sessions.
-
-
-
-
-
-
-
-
-
-
-
-
Status-Pipeline
-
Tickets nach Phase. Status per Dropdown ndern.
-
-
-
-
-
-
-
-
-
-
-
SLA / Response-Zeiten
-
Average Time to Claim / First Response.
-
-
-
-
-
-
SLA pro Supporter
-
- | Supporter | Tickets | TTC | TTFR |
-
-
-
-
-
SLA pro Tag
-
- | Datum | Tickets | TTC | TTFR |
-
-
-
-
-
-
-
-
-
-
-
-
Automationen
-
Regeln fr Ticket-Aktionen.
-
-
-
-
-
-
-
-
-
-
-
-
-
Knowledge-Base
-
Artikel fr Self-Service.
-
-
-
-
-
-
-
-
-
-
-
-
Automod Einstellungen
-
Automod pro Guild aktivieren und Regeln konfigurieren.
-
-
-
-
-
-
-
-
-
-
-
Willkommensnachrichten
-
Embed konfigurieren und Feature aktivieren.
-
-
-
-
-
-
-
-
-
-
-
Dynamic Voice
-
Lobby waehlen, Channel-Namen & Limits setzen.
-
-
-
-
-
-
-
-
-
-
-
-
Birthday
-
Automatische Glueckwuensche je Guild.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Gespeicherte Geburtstage
-
Eintraege werden per /birthday angelegt.
-
-
-
-
-
-
-
-
-
-
Reaction Roles
-
Reaktionen verteilen oder entfernen Rollen.
-
-
-
-
-
-
-
-
-
Reaction Role Nachrichten
-
Bestehende Setups bearbeiten oder syncen.
-
-
-
-
-
-
-
-
-
-
Statuspage
-
Services verwalten und Checks steuern.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Services
-
Status, Uptime, letzter Check
-
-
-
-
-
-
-
-
-
-
-
Server Stats
-
Kategorie und Counter verwalten.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Statistiken
-
Counter und Format anpassen.
-
-
-
-
-
-
-
-
-
-
-
Events
-
Termine planen und Erinnerungen senden.
-
-
-
-
-
-
-
-
-
- Guilds
- -
- aktiv (24h): -
-
-
- Uptime
- -
- Start: -
-
-
-
-
-
-
Aktivität (letzte 24h)
-
Events/Commands pro Stunde
-
-
-
-
-
-
-
-
Logs
-
Neueste Einträge
-
-
-
-
-
-
-
-
- Logging
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `);
+ return res.render('dashboard', {
+ baseRoot,
+ baseDashboard,
+ baseAuth,
+ baseApi,
+ guildId,
+ user: req.session.user
+ });
});
router.get('/settings', (_req, res) => {
@@ -3020,5 +38,3 @@ router.get('/settings', (_req, res) => {
});
export default router;
-
-
diff --git a/tsconfig.frontend.json b/tsconfig.frontend.json
new file mode 100644
index 0000000..3e29cb2
--- /dev/null
+++ b/tsconfig.frontend.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "outDir": "public/ts-build",
+ "rootDir": "public/ts",
+ "skipLibCheck": true,
+ "noEmitOnError": false
+ },
+ "include": ["public/ts/**/*"]
+}
diff --git a/views/dashboard.ejs b/views/dashboard.ejs
new file mode 100644
index 0000000..2924c03
--- /dev/null
+++ b/views/dashboard.ejs
@@ -0,0 +1,46 @@
+
+
+
+
+
+ Papo Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/views/dashboardSelection.ejs b/views/dashboardSelection.ejs
new file mode 100644
index 0000000..106cc29
--- /dev/null
+++ b/views/dashboardSelection.ejs
@@ -0,0 +1,36 @@
+
+
+
+
+
+ Papo Dashboard - Auswahl
+
+
+
+
+
+
+
+