Ticket-System, Web-Composer und Nachrichten-Bearbeitung

- Tickets: /ticket-setup postet 🎫-Button; Klick erstellt privaten Thread
  (ein offenes Ticket pro User), Schließen-Button sperrt + archiviert
- Composer auf der Setup-Seite: Nachricht/Embed als Bot in beliebigen Kanal
  senden oder per Message-ID bearbeiten (nur eigene Bot-Posts)
- API v1: PATCH /api/v1/message — Skripte können ihre Bot-Posts aktualisieren
- Setting ticket_channel_id; README ergänzt

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 13:43:27 +02:00
co-authored by Claude Opus 4.8
parent f631c0e2ab
commit 4d1b0a956d
9 changed files with 310 additions and 4 deletions
+74
View File
@@ -28,6 +28,8 @@ export default function Settings({ me }) {
const [feedback, setFeedback] = useState(null);
// Community
const [playtesters, setPlaytesters] = useState([]);
// Composer
const [composer, setComposer] = useState({ channel: '', title: '', text: '', messageId: '' });
// API-Keys
const [keys, setKeys] = useState([]);
const [newKeyName, setNewKeyName] = useState('');
@@ -80,6 +82,25 @@ export default function Settings({ me }) {
}
}
async function sendCompose() {
if (!composer.channel || !composer.text.trim()) {
flash('✗ Kanal und Text nötig');
return;
}
try {
const res = await apiPost('/api/compose', {
channel_id: composer.channel,
message_id: composer.messageId.trim() || undefined,
embed_title: composer.title.trim() || undefined,
content: composer.text,
});
flash(res.edited ? '✓ Aktualisiert' : `✓ Gesendet (ID ${res.message_id})`);
if (!res.edited) setComposer({ ...composer, messageId: res.message_id });
} catch {
flash('✗ Senden fehlgeschlagen');
}
}
async function sendTest(target) {
try {
await apiPost(`/api/settings/test/${target}`);
@@ -400,6 +421,59 @@ export default function Settings({ me }) {
{channelOptions}
</select>
</div>
<div className="field">
<label>Ticket-Kanal <span className="field-hint">hier entstehen die privaten Ticket-Threads; Post via /ticket-setup</span></label>
<select
value={form.ticket_channel_id ?? ''}
onChange={(e) => setForm({ ...form, ticket_channel_id: e.target.value })}
>
<option value=""> deaktiviert </option>
{channelOptions}
</select>
</div>
</div>
{/* Composer */}
<div className="settings-section">
<h2 className="settings-title">// Post verfassen</h2>
<div className="field">
<label>Kanal</label>
<select value={composer.channel} onChange={(e) => setComposer({ ...composer, channel: e.target.value })}>
<option value="" disabled> Kanal wählen </option>
{channelOptions}
</select>
</div>
<div className="field">
<label>Embed-Titel <span className="field-hint">leer = normale Nachricht statt Embed</span></label>
<input
type="text"
placeholder="z. B. 📣 Ankündigung"
value={composer.title}
onChange={(e) => setComposer({ ...composer, title: e.target.value })}
/>
</div>
<div className="field">
<label>Text <span className="field-hint">Discord-Markdown (##, **, Listen )</span></label>
<textarea
rows={6}
value={composer.text}
onChange={(e) => setComposer({ ...composer, text: e.target.value })}
/>
</div>
<div className="field">
<label>Message-ID <span className="field-hint">optional vorhandenen Bot-Post bearbeiten statt neu senden</span></label>
<div className="field-row">
<input
type="text"
placeholder="leer = neue Nachricht"
value={composer.messageId}
onChange={(e) => setComposer({ ...composer, messageId: e.target.value })}
/>
<button className="btn" onClick={sendCompose}>
{composer.messageId.trim() ? 'Aktualisieren' : 'Senden'}
</button>
</div>
</div>
</div>
{/* Game-Server */}