diff --git a/README.md b/README.md
index 99c72e2..9b52866 100644
--- a/README.md
+++ b/README.md
@@ -76,8 +76,8 @@ auf nichts mehr: keine Posts, keine Hintergrund-Prüfungen.
| 🎉 **Giveaways** | `/giveaway` (Admin): Teilnahme-Button, automatische Ziehung; am Gewinner-Post 🔁 Neu auslosen + 👥 Teilnehmerliste |
| 🎂 **Geburtstage** | `/geburtstag` zum Eintragen; morgens Gratulation im Kanal + Tages-Rolle |
| 📅 **Events** | Neue Discord-Events werden angekündigt + öffentliche Events-Seite |
-| 🧪 **Playtester-Programm** | `/playtester-setup` postet den Bewerbungs-Button; Rolle + Liste im Panel |
-| 🎟️ **Alpha-Keys** | Key-Pool im Panel; Ein-Klick-Verteilung an alle Playtester per DM |
+| 🧪 **Playtester-Programm** | `/playtester-setup` postet den Bewerbungs-Button; eigener Panel-Bereich mit Liste, Rolle und dem Schlüssel-Vorrat — Playtester lassen sich dort auch wieder entfernen (die Rolle geht mit) |
+| 🎟️ **Alpha-Keys** | Schlüssel-Vorrat im Panel: nachlegen, gezielt an eine Person schicken, an alle Wartenden verteilen, zurückziehen oder löschen. Zustellung per Direktnachricht — kommt sie nicht an, bleibt der Schlüssel frei |
| 🎭 **Rollen-Menüs** | Menüs im Webinterface bauen (Emoji, Label, Rolle, optional exklusiv), Bot postet Button-Embeds; Posts jederzeit editierbar |
| 📋 **Bewerbungen** | Formulare (bis 5 Fragen) als Discord-Modal, Review mit ✅/❌ im Staff-Kanal, Rolle + DM bei Annahme — z. B. FiveM-Whitelist |
| 🪪 **Verknüpfte Rollen** | Discords *Linked Roles*: Level, Playtester-Status und Dabei-seit werden als geprüfte Kennzahlen an Discord gemeldet — Rollen vergibt dann Discord selbst |
@@ -169,14 +169,14 @@ selbst gehostet).
## Config-Seite (`/settings`)
-**16 Bereiche**, in der Seitenleiste nach Themen gruppiert:
+**17 Bereiche**, in der Seitenleiste nach Themen gruppiert:
| Gruppe | Bereiche |
|---|---|
| Überblick | Status · Module · Texte · Werte |
-| Auftritt | Brand · Seiten |
+| Auftritt | Brand (samt Willkommens-Karte) · Seiten |
| Inhalte | Feeds · Composer |
-| Community | Community · Rollen · Support · Bewerbungen |
+| Community | Community · Rollen · Support · Playtester · Bewerbungen |
| Technik | Server · System |
| Zugang | API · Team |
diff --git a/frontend/src/components/Dialog.jsx b/frontend/src/components/Dialog.jsx
new file mode 100644
index 0000000..34e7df3
--- /dev/null
+++ b/frontend/src/components/Dialog.jsx
@@ -0,0 +1,38 @@
+// Kleiner Dialog fürs Panel — für Dinge, die den Bereich sonst zumüllen:
+// Schlüssel nachlegen, jemanden auswählen, etwas bestätigen.
+//
+// Rendert per Portal direkt in
, damit Kopfzeile und Scanlines nicht
+// durchscheinen — dieselbe Bauart wie die Lightbox.
+import { useEffect } from 'react';
+import { createPortal } from 'react-dom';
+import { IconX } from '../icons.jsx';
+
+export default function Dialog({ title, onClose, children, footer, weit = false }) {
+ useEffect(() => {
+ const onKey = (e) => { if (e.key === 'Escape') onClose(); };
+ document.addEventListener('keydown', onKey);
+ // Hintergrund festhalten, sonst scrollt die Seite unter dem Dialog weg
+ document.body.style.overflow = 'hidden';
+ return () => {
+ document.removeEventListener('keydown', onKey);
+ document.body.style.overflow = '';
+ };
+ }, [onClose]);
+
+ return createPortal(
+
+ {/* Klicks im Kasten dürfen nicht bis zum Hintergrund durchfallen */}
+
e.stopPropagation()}>
+
+
{title}
+
+
+
{children}
+ {footer &&
{footer}
}
+
+
,
+ document.body
+ );
+}
diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx
index 55a5726..a99ab19 100644
--- a/frontend/src/pages/Settings.jsx
+++ b/frontend/src/pages/Settings.jsx
@@ -6,11 +6,12 @@ import {
IconApplications, IconComposer, IconServer, IconSystem, IconApi, IconTeam,
IconLock, IconBot, IconUpload, IconChevronDown, IconKey, IconPages, IconX,
IconHome as IconLayers, IconWarning, IconCheck, IconMessage, IconRefresh,
- IconSearch, IconSystem as IconTune, IconStar,
+ IconSearch, IconSystem as IconTune, IconStar, IconGame, IconTrash,
} from '../icons.jsx';
import { moduleIcon } from '../module-icons.jsx';
import ModulePage from '../components/ModulePage.jsx';
import UptimeTrack from '../components/UptimeTrack.jsx';
+import Dialog from '../components/Dialog.jsx';
import { findSettings, SETTING_INDEX } from '../setting-index.js';
import { Markdown } from '../markdown.jsx';
@@ -59,6 +60,8 @@ const TABS = [
find: 'rollen menü selbstbedienung buttons reaction roles' },
{ id: 'support', Icon: IconSupport, label: 'Support', section: 'community',
find: 'ticket modmail willkommen protokoll modlog moderation warnung' },
+ { id: 'playtester', Icon: IconGame, label: 'Playtester', section: 'community',
+ find: 'tester alpha keys schlüssel vergeben pool testen einladen' },
{ id: 'bewerbungen', Icon: IconApplications, label: 'Bewerbungen', section: 'community',
find: 'formular fragen bewerbung prüfen annehmen ablehnen' },
@@ -88,6 +91,7 @@ const TAB_SCOPES = {
community: 'community',
rollen: 'rollen',
support: 'settings',
+ playtester: 'community',
bewerbungen: 'bewerbungen',
composer: 'content',
seiten: 'content',
@@ -298,7 +302,6 @@ export default function Settings({ me }) {
const [error, setError] = useState(null);
const [feedback, setFeedback] = useState(null);
// Community
- const [playtesters, setPlaytesters] = useState([]);
// Rollen-Menüs
const emptyMenu = {
id: null, title: '', description: '', channel_id: '', exclusive: false,
@@ -312,6 +315,9 @@ export default function Settings({ me }) {
const [twitchCreds, setTwitchCreds] = useState({ id: '', secret: '' });
// Alpha-Keys
const [alphaKeys, setAlphaKeys] = useState(null);
+ // Offene Dialoge: Schlüssel nachlegen bzw. jemanden auswählen
+ const [keyDialog, setKeyDialog] = useState(false);
+ const [keyEmpfaenger, setKeyEmpfaenger] = useState(null);
const [keyUpload, setKeyUpload] = useState('');
// Bewerbungs-Formulare
const emptyForm = {
@@ -459,7 +465,6 @@ export default function Settings({ me }) {
})
.catch((e) => setError(e.status));
apiGet('/api/apikeys').then((d) => setKeys(d.keys)).catch(() => {});
- apiGet('/api/playtesters').then((d) => setPlaytesters(d.playtesters)).catch(() => {});
apiGet('/api/rolemenus').then((d) => setMenus(d.menus)).catch(() => {});
apiGet('/api/tags').then((d) => setTags(d.tags)).catch(() => {});
apiGet('/api/scheduled').then((d) => setScheduled(d.posts)).catch(() => {});
@@ -820,6 +825,48 @@ export default function Settings({ me }) {
}
}
+ /** Einen Schlüssel an eine ausgewählte Person schicken */
+ async function keyGeben(userId, name) {
+ try {
+ const res = await apiPost('/api/alphakeys/assign', { user_id: userId });
+ setAlphaKeys(res);
+ setKeyEmpfaenger(null);
+ flash(`✓ Schlüssel an ${name} verschickt`);
+ } catch (e) {
+ flash(`✗ ${e.body?.error ?? 'Vergeben fehlgeschlagen'}`);
+ }
+ }
+
+ /** Vergebenen Schlüssel zurück in den Vorrat */
+ async function keyZurueck(key) {
+ if (!window.confirm(`Schlüssel ${key} wieder freigeben? Die Person behält die Nachricht mit dem Schlüssel.`)) return;
+ try {
+ setAlphaKeys(await apiPost('/api/alphakeys/revoke', { key }));
+ } catch {
+ flash('✗ Zurückziehen fehlgeschlagen');
+ }
+ }
+
+ /** Freien Schlüssel löschen — z. B. einen vertippten */
+ async function keyLoeschen(key) {
+ try {
+ setAlphaKeys(await apiDelete(`/api/alphakeys/${encodeURIComponent(key)}`));
+ } catch (e) {
+ flash(`✗ ${e.body?.error ?? 'Löschen fehlgeschlagen'}`);
+ }
+ }
+
+ async function playtesterEntfernen(userId, name) {
+ if (!window.confirm(`${name} aus dem Playtester-Programm nehmen? Die Rolle wird mit entfernt.`)) return;
+ try {
+ const res = await apiDelete(`/api/playtesters/${userId}`);
+ setAlphaKeys(await apiGet('/api/alphakeys'));
+ flash(res.rolleWeg ? `✓ ${name} entfernt` : `✓ ${name} entfernt — Rolle blieb (Rechte prüfen)`);
+ } catch {
+ flash('✗ Entfernen fehlgeschlagen');
+ }
+ }
+
async function distributeKeys() {
if (!window.confirm(`Alpha-Keys jetzt per DM an ${alphaKeys?.playtestersWithout?.length ?? 0} Playtester verteilen?`)) return;
try {
@@ -1400,6 +1447,106 @@ export default function Settings({ me }) {
);
const statusDot = { online: '#23a55a', idle: '#f0b232', dnd: '#f23f43' }[form.bot_presence_status] ?? '#23a55a';
+ // Willkommens-Karte: eigener Block, damit sie im Brand-Bereich stehen
+ // kann, wo das uebrige Aussehen gepflegt wird — inhaltlich unveraendert.
+ const willkommensKarte = (
+ <>
+
+ Das Bild, das neue Mitglieder im Willkommens-Kanal begrüßt. Die Vorschau
+ zeigt sofort, wie es aussieht — gespeichert wird erst mit dem Knopf unten.
+
- Das Bild, das neue Mitglieder im Willkommens-Kanal begrüßt. Die Vorschau
- zeigt sofort, wie es aussieht — gespeichert wird erst mit dem Knopf unten.
-