Initial commit: Gaming Hub foundation

Plugin-based Discord bot framework with web frontend:
- Core: Discord.js client, SSE broadcast, JSON persistence
- Plugin system: lifecycle hooks (init, onReady, routes, snapshot, destroy)
- Web: React 19 + Vite 6 + TypeScript, tab-based navigation
- Docker: multi-stage build (Node 24, static ffmpeg, yt-dlp)
- GitLab CI: Kaniko with LAN registry caching

Ready for plugin development.
This commit is contained in:
Claude Code 2026-03-05 22:52:13 +01:00
parent 1ae431dd2f
commit ae1c41f0ae
19 changed files with 954 additions and 0 deletions

129
web/src/App.tsx Normal file
View file

@ -0,0 +1,129 @@
import { useState, useEffect, useRef } from 'react';
interface PluginInfo {
name: string;
version: string;
description: string;
}
// Plugin tab components will be registered here
const tabComponents: Record<string, React.FC<{ data: any }>> = {};
export function registerTab(pluginName: string, component: React.FC<{ data: any }>) {
tabComponents[pluginName] = component;
}
export default function App() {
const [connected, setConnected] = useState(false);
const [plugins, setPlugins] = useState<PluginInfo[]>([]);
const [activeTab, setActiveTab] = useState<string>('');
const [pluginData, setPluginData] = useState<Record<string, any>>({});
const eventSourceRef = useRef<EventSource | null>(null);
// Fetch plugin list
useEffect(() => {
fetch('/api/plugins')
.then(r => r.json())
.then((list: PluginInfo[]) => {
setPlugins(list);
if (list.length > 0 && !activeTab) setActiveTab(list[0].name);
})
.catch(() => {});
}, []);
// SSE connection
useEffect(() => {
let es: EventSource | null = null;
let retryTimer: ReturnType<typeof setTimeout>;
function connect() {
es = new EventSource('/api/events');
eventSourceRef.current = es;
es.onopen = () => setConnected(true);
es.onmessage = (ev) => {
try {
const msg = JSON.parse(ev.data);
if (msg.type === 'snapshot') {
setPluginData(prev => ({ ...prev, ...msg }));
} else if (msg.plugin) {
setPluginData(prev => ({
...prev,
[msg.plugin]: { ...(prev[msg.plugin] || {}), ...msg },
}));
}
} catch {}
};
es.onerror = () => {
setConnected(false);
es?.close();
retryTimer = setTimeout(connect, 3000);
};
}
connect();
return () => { es?.close(); clearTimeout(retryTimer); };
}, []);
const TabComponent = activeTab ? tabComponents[activeTab] : null;
const version = (import.meta as any).env?.VITE_APP_VERSION ?? '1.0.0';
// Tab icon mapping
const tabIcons: Record<string, string> = {
soundboard: '\u{1F3B5}',
stats: '\u{1F4CA}',
events: '\u{1F4C5}',
games: '\u{1F3B2}',
gamevote: '\u{1F3AE}',
};
return (
<div className="hub-app">
<header className="hub-header">
<div className="hub-header-left">
<span className="hub-logo">{'\u{1F3AE}'}</span>
<span className="hub-title">Gaming Hub</span>
<span className={`hub-conn-dot ${connected ? 'online' : ''}`} />
</div>
<nav className="hub-tabs">
{plugins.map(p => (
<button
key={p.name}
className={`hub-tab ${activeTab === p.name ? 'active' : ''}`}
onClick={() => setActiveTab(p.name)}
title={p.description}
>
<span className="hub-tab-icon">{tabIcons[p.name] ?? '\u{1F4E6}'}</span>
<span className="hub-tab-label">{p.name}</span>
</button>
))}
</nav>
<div className="hub-header-right">
<span className="hub-version">v{version}</span>
</div>
</header>
<main className="hub-content">
{plugins.length === 0 ? (
<div className="hub-empty">
<span className="hub-empty-icon">{'\u{1F4E6}'}</span>
<h2>Keine Plugins geladen</h2>
<p>Plugins werden im Server konfiguriert.</p>
</div>
) : TabComponent ? (
<TabComponent data={pluginData[activeTab] || {}} />
) : (
<div className="hub-empty">
<span className="hub-empty-icon">{tabIcons[activeTab] ?? '\u{1F4E6}'}</span>
<h2>{activeTab}</h2>
<p>Plugin-UI wird geladen...</p>
</div>
)}
</main>
</div>
);
}