import { Avatar, Button, Card, Chip, Input, Spinner, Switch, Tab, Tabs, TextArea } from '@heroui/react'; import { Activity, AudioLines, Bot, CalendarDays, Cable, ChevronRight, Command, Home, LifeBuoy, Logs, MessageSquare, Puzzle, RadioTower, ScanSearch, Settings, Shield, Sparkles, Tag, Ticket, UserRound, Users, Wrench } from 'lucide-react'; import { useEffect, useState } from 'react'; type AppConfig = { baseRoot?: string; baseApi?: string; baseAuth?: string; baseDashboard?: string; initialGuildId?: string; }; type User = { username: string; discriminator?: string; isAdmin?: boolean; }; type Guild = { id: string; name: string; icon?: string; }; type NavKey = | 'overview' | 'tickets' | 'automod' | 'welcome' | 'dynamicvoice' | 'birthday' | 'reactionroles' | 'statuspage' | 'serverstats' | 'settings' | 'modules' | 'events' | 'admin'; type TicketRecord = { id: string; topic?: string; status?: string; category?: string; priority?: string; createdAt?: string; claimedById?: string | null; }; type StatusService = { id: string; name?: string; url?: string; status?: string; uptimePct?: number; lastCheckedAt?: string; }; type EventItem = { id: string; title: string; description?: string; startsAt?: string; reminderMinutes?: number; channelId?: string; }; type ReactionRoleSet = { id: string; title?: string; description?: string; channelId?: string; messageId?: string; entries?: Array<{ emoji: string; roleId: string; label?: string; description?: string }>; }; type ModuleItem = { key: string; name: string; description?: string; enabled: boolean; }; type LogEntry = { level?: string; category?: string; message?: string; timestamp?: string; }; type SettingsState = Record; const appConfig: AppConfig = (window as any).__PAPO__ || {}; const navItems: Array<{ key: NavKey; label: string; icon: JSX.Element }> = [ { key: 'overview', label: 'Uebersicht', icon: }, { key: 'tickets', label: 'Ticketsystem', icon: }, { key: 'automod', label: 'Automod', icon: }, { key: 'welcome', label: 'Willkommen', icon: }, { key: 'dynamicvoice', label: 'Dynamic Voice', icon: }, { key: 'birthday', label: 'Birthday', icon: }, { key: 'reactionroles', label: 'Reaction Roles', icon: }, { key: 'statuspage', label: 'Statuspage', icon: }, { key: 'serverstats', label: 'Server Stats', icon: }, { key: 'settings', label: 'Einstellungen', icon: }, { key: 'modules', label: 'Module', icon: }, { key: 'events', label: 'Events', icon: }, { key: 'admin', label: 'Admin', icon: } ]; function apiUrl(path: string) { const baseApi = appConfig.baseApi || '/api'; return `${baseApi}${path}`; } async function apiFetch(path: string, init?: RequestInit): Promise { const response = await fetch(apiUrl(path), { ...init, headers: { 'Content-Type': 'application/json', ...(init?.headers || {}) } }); if (response.status === 401) { window.location.href = `${appConfig.baseAuth || '/auth'}/discord`; throw new Error('unauthorized'); } if (!response.ok) { throw new Error(`request failed: ${response.status}`); } return response.json() as Promise; } function formatDate(value?: string | number | null) { if (!value) return '-'; const date = new Date(value); if (Number.isNaN(date.getTime())) return String(value); return `${date.toLocaleDateString('de-DE')} ${date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}`; } function guildIconUrl(guild?: Guild | null) { if (!guild) return undefined; if (guild.icon) return `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png`; return undefined; } function sparkPath(kind: 'messages' | 'commands' | 'automod') { if (kind === 'messages') return 'M4 26 C16 12, 24 10, 34 18 S54 30, 64 22 S80 6, 92 14 S108 20, 116 18'; if (kind === 'commands') return 'M4 22 C14 10, 22 12, 34 20 S54 28, 66 16 S82 8, 92 14 S108 24, 116 22'; return 'M4 28 C14 24, 22 10, 34 12 S54 30, 66 22 S84 8, 96 14 S110 18, 116 16'; } function chartColor(kind: 'messages' | 'commands' | 'automod') { if (kind === 'messages') return '#f2b36f'; if (kind === 'commands') return '#b26cff'; return '#48d883'; } function App() { const [loading, setLoading] = useState(true); const [user, setUser] = useState(null); const [guilds, setGuilds] = useState([]); const [currentGuildId, setCurrentGuildId] = useState(appConfig.initialGuildId || ''); const [section, setSection] = useState('overview'); const [guildInfo, setGuildInfo] = useState(null); const [overview, setOverview] = useState(null); const [activity, setActivity] = useState(null); const [logs, setLogs] = useState([]); const [tickets, setTickets] = useState([]); const [pipeline, setPipeline] = useState>({}); const [sla, setSla] = useState({ supporters: [], days: [] }); const [automations, setAutomations] = useState([]); const [kbArticles, setKbArticles] = useState([]); const [settings, setSettings] = useState({}); const [modules, setModules] = useState([]); const [birthday, setBirthday] = useState({ config: {}, birthdays: [] }); const [reactionRoles, setReactionRoles] = useState([]); const [statuspage, setStatuspage] = useState({ services: [] }); const [serverStats, setServerStats] = useState({ items: [] }); const [events, setEvents] = useState([]); const [admin, setAdmin] = useState({ overview: null, activity: null, logs: [] }); const [statusMessage, setStatusMessage] = useState(''); const [ticketTab, setTicketTab] = useState('overview'); const [automationDraft, setAutomationDraft] = useState({ name: '', conditionValue: '', actionValue: '' }); const [kbDraft, setKbDraft] = useState({ title: '', keywords: '', content: '' }); const [eventDraft, setEventDraft] = useState({ title: '', description: '', channelId: '', startsAt: '' }); const [statusDraft, setStatusDraft] = useState(null); const [statsDraft, setStatsDraft] = useState(null); const [reactionDraft, setReactionDraft] = useState({ title: '', channelId: '', entries: '' }); useEffect(() => { const hash = window.location.hash.replace('#', '') as NavKey; if (navItems.some((item) => item.key === hash)) setSection(hash); }, []); useEffect(() => { window.location.hash = section; }, [section]); useEffect(() => { void bootstrap(); }, []); useEffect(() => { if (!currentGuildId) return; void loadGuildData(currentGuildId); }, [currentGuildId]); async function bootstrap() { try { const me = await apiFetch<{ user: User }>('/me'); const guildRes = await apiFetch<{ guilds: Guild[] }>('/guilds'); setUser(me.user); setGuilds(guildRes.guilds || []); if (!currentGuildId && guildRes.guilds?.length) { setCurrentGuildId(guildRes.guilds[0].id); } } finally { setLoading(false); } } async function loadGuildData(guildId: string) { setStatusMessage('Lade Daten...'); try { const [ guildInfoRes, overviewRes, activityRes, logsRes, settingsRes, modulesRes, birthdayRes, reactionRes, statusRes, statsRes, eventsRes ] = await Promise.all([ apiFetch(`/guild/info?guildId=${encodeURIComponent(guildId)}`), apiFetch(`/overview?guildId=${encodeURIComponent(guildId)}`), apiFetch(`/guild/activity?guildId=${encodeURIComponent(guildId)}`), apiFetch(`/guild/logs?guildId=${encodeURIComponent(guildId)}`), apiFetch(`/settings?guildId=${encodeURIComponent(guildId)}`), apiFetch(`/modules?guildId=${encodeURIComponent(guildId)}`), apiFetch(`/birthday?guildId=${encodeURIComponent(guildId)}`), apiFetch(`/reactionroles?guildId=${encodeURIComponent(guildId)}`), apiFetch(`/statuspage?guildId=${encodeURIComponent(guildId)}`), apiFetch(`/server-stats?guildId=${encodeURIComponent(guildId)}`), apiFetch(`/events?guildId=${encodeURIComponent(guildId)}`) ]); setGuildInfo(guildInfoRes.guild || null); setOverview(overviewRes); setActivity(activityRes.activity || {}); setLogs(logsRes.logs || []); setSettings(settingsRes.settings || {}); setModules(modulesRes.modules || []); setBirthday(birthdayRes); setReactionRoles(reactionRes.sets || []); setStatuspage(statusRes.config || { services: [] }); setServerStats(statsRes.config || { items: [] }); setStatsDraft(statsRes.config || { items: [] }); setStatusDraft(statusRes.config || { services: [] }); setEvents(eventsRes.events || []); setReactionDraft({ title: '', channelId: '', entries: '' }); await Promise.all([loadTicketData(guildId), loadAdminData()]); setStatusMessage(''); } catch { setStatusMessage('Daten konnten nicht geladen werden'); } } async function loadTicketData(guildId: string) { const [ticketRes, pipelineRes, slaRes, automationRes, kbRes] = await Promise.all([ apiFetch(`/tickets?guildId=${encodeURIComponent(guildId)}`), apiFetch(`/tickets/pipeline?guildId=${encodeURIComponent(guildId)}`), apiFetch(`/tickets/sla?guildId=${encodeURIComponent(guildId)}&range=30`), apiFetch(`/automations?guildId=${encodeURIComponent(guildId)}`), apiFetch(`/kb?guildId=${encodeURIComponent(guildId)}`) ]); setTickets(ticketRes.tickets || []); setPipeline(pipelineRes.pipeline || {}); setSla(slaRes || { supporters: [], days: [] }); setAutomations(automationRes.rules || []); setKbArticles(kbRes.articles || []); } async function loadAdminData() { if (!user?.isAdmin) return; const [overviewRes, activityRes, logsRes] = await Promise.all([ apiFetch('/admin/overview'), apiFetch('/admin/activity'), apiFetch('/admin/logs') ]); setAdmin({ overview: overviewRes, activity: activityRes, logs: logsRes.logs || [] }); } async function saveSettingsPayload(payload: Record, okMessage: string) { if (!currentGuildId) return; await apiFetch('/settings', { method: 'POST', body: JSON.stringify({ guildId: currentGuildId, ...payload }) }); setStatusMessage(okMessage); await loadGuildData(currentGuildId); } async function saveBirthday() { await apiFetch('/birthday', { method: 'POST', body: JSON.stringify({ guildId: currentGuildId, enabled: birthday.config?.enabled ?? true, channelId: birthday.config?.channelId || '', sendHour: birthday.config?.sendHour || 9, messageTemplate: birthday.config?.messageTemplate || '' }) }); setStatusMessage('Birthday gespeichert'); await loadGuildData(currentGuildId); } async function saveStatuspage() { await apiFetch('/statuspage', { method: 'POST', body: JSON.stringify({ guildId: currentGuildId, config: statusDraft }) }); setStatusMessage('Statuspage gespeichert'); await loadGuildData(currentGuildId); } async function saveServerStats() { await apiFetch('/server-stats', { method: 'POST', body: JSON.stringify({ guildId: currentGuildId, config: statsDraft }) }); setStatusMessage('Server Stats gespeichert'); await loadGuildData(currentGuildId); } async function saveEvent() { if (!eventDraft.title) return; await apiFetch('/events', { method: 'POST', body: JSON.stringify({ guildId: currentGuildId, title: eventDraft.title, description: eventDraft.description, channelId: eventDraft.channelId || undefined, startsAt: eventDraft.startsAt || undefined }) }); setEventDraft({ title: '', description: '', channelId: '', startsAt: '' }); await loadGuildData(currentGuildId); setStatusMessage('Event gespeichert'); } async function deleteEvent(id: string) { await apiFetch(`/events/${id}`, { method: 'DELETE', body: JSON.stringify({ guildId: currentGuildId }) }); await loadGuildData(currentGuildId); } async function saveReactionRole() { const entries = reactionDraft.entries .split('\n') .map((line) => line.trim()) .filter(Boolean) .map((line) => { const parts = line.split('|').map((part) => part.trim()); return { emoji: parts[0], roleId: parts[1], label: parts[2] || undefined, description: parts[3] || undefined }; }) .filter((entry) => entry.emoji && entry.roleId); await apiFetch('/reactionroles', { method: 'POST', body: JSON.stringify({ guildId: currentGuildId, channelId: reactionDraft.channelId, title: reactionDraft.title, entries }) }); await loadGuildData(currentGuildId); setStatusMessage('Reaction Role gespeichert'); } async function toggleModule(key: string, enabled: boolean) { await saveSettingsPayload({ [key]: enabled }, `${key} aktualisiert`); } async function saveAutomation() { await apiFetch('/automations', { method: 'POST', body: JSON.stringify({ guildId: currentGuildId, name: automationDraft.name || 'Automation', condition: { category: automationDraft.conditionValue }, action: { type: 'reminder', message: automationDraft.actionValue || 'Reminder' }, active: true }) }); setAutomationDraft({ name: '', conditionValue: '', actionValue: '' }); await loadTicketData(currentGuildId); } async function saveKbArticle() { await apiFetch('/kb', { method: 'POST', body: JSON.stringify({ guildId: currentGuildId, title: kbDraft.title || 'Artikel', keywords: kbDraft.keywords, content: kbDraft.content }) }); setKbDraft({ title: '', keywords: '', content: '' }); await loadTicketData(currentGuildId); } const selectedGuild = guilds.find((guild) => guild.id === currentGuildId) || null; const moduleFlags = guildInfo?.modules || {}; if (loading) { return (
); } if (!selectedGuild) { return (

Waehle einen Server

Die Guild-Auswahl laeuft jetzt ueber die neue HeroUI-Oberflaeche.

{guilds.map((guild) => ( setCurrentGuildId(guild.id)} >
{guild.name}
ID: {guild.id}
))}
); } return (

Guild Dashboard

Komplettes HeroUI-Rework fuer dein Bot-Dashboard

{statusMessage ?
{statusMessage}
: null}
{section === 'overview' && (
{guildInfo?.name || selectedGuild.name}
ID: {guildInfo?.id || selectedGuild.id}
{Object.entries(moduleFlags).map(([key, enabled]) => ( {key.replace('Enabled', '').toUpperCase()} ))}
} variant="dot" > Bot aktiv
setSection('serverstats')} items={[ { icon: , label: 'Owner', value: guildInfo?.owner?.tag || '-' }, { icon: , label: 'Erstellt', value: formatDate(guildInfo?.createdAt) }, { icon: , label: 'Member', value: String(guildInfo?.memberCount ?? 0) }, { icon: , label: 'Channels', value: `${guildInfo?.textCount || 0} Text / ${guildInfo?.voiceCount || 0} Voice` }, { icon: , label: 'Tickets offen', value: String(overview?.tickets?.open ?? 0) }, { icon: , label: 'Tickets IP / Closed', value: `${overview?.tickets?.inProgress ?? 0} / ${overview?.tickets?.closed ?? 0}` } ]} />

Activity

Live-Statistiken aus deinem Bot

} kind="messages" label="Messages (24h)" value={activity?.messages24h ?? 0} /> } kind="commands" label="Commands (24h)" value={activity?.commands24h ?? 0} /> } kind="automod" label="Automod (24h)" value={activity?.automod24h ?? 0} />

Guild Logs

Neueste Ereignisse

{logs.length ? ( logs.map((log, index) => (
{(log.level || 'info').toUpperCase()} {formatDate(log.timestamp)}
{log.category ? `[${log.category}] ` : ''}{log.message || '-'}
)) ) : (
Keine Logs
)}

Schnellzugriff

Die wichtigsten Bereiche deines Dashboards

{navItems .filter((item) => !['overview', 'admin'].includes(item.key)) .slice(0, 6) .map((item) => ( setSection(item.key)}>
{item.icon}
{item.label}
HeroUI-Komponenten fuer den Bereich {item.label}
))}
)} {section === 'tickets' && ( setTicketTab(String(key))} > {ticketTab === 'overview' && (
`${ticket.topic || 'Ticket'} · ${ticket.status || 'open'}`)} /> `${ticket.category || 'Allgemein'} · ${formatDate(ticket.createdAt)}`)} />
)} {ticketTab === 'pipeline' && (
{['neu', 'in_bearbeitung', 'warten_auf_user', 'erledigt'].map((state) => ( ticket.topic || ticket.id)} /> ))}
)} {ticketTab === 'sla' && (
`${row.supporter || '-'} · ${row.tickets || 0} Tickets · ${row.ttc || '-'} TTC · ${row.ttfr || '-'} TTFR`)} /> `${row.date || '-'} · ${row.tickets || 0} Tickets · ${row.ttc || '-'} TTC · ${row.ttfr || '-'} TTFR`)} />
)} {ticketTab === 'automations' && (
`${rule.name || 'Automation'} · ${rule.active === false ? 'inaktiv' : 'aktiv'}`)} /> setAutomationDraft((state) => ({ ...state, name: value }))} /> setAutomationDraft((state) => ({ ...state, conditionValue: value }))} />