Server-Monitor v3, Composer v2, Temp-Voice-Panel und Embed-Polish
- Server-Monitor v3: gamedig-Integration (300+ Spiele), GSM-Stil-Embeds (Spieler-Balken mit %, Map, Spieler-Liste, klickbarer Connect-Link, Ping im Footer), Down/Up-Alerts (2 Fails → 🚨, Recovery → ✅ mit Downtime) in konfigurierbaren Alert-Kanal; Server-Tab mit 19 Spiel-Presets, Host/Port bzw. Query-URL und Connect-URL - Composer v2: visueller Embed-Builder mit Live-Discord-Preview (Autor, Felder, Bilder, Farbe, Footer, Timestamp), speicherbare Vorlagen (/api/templates), /api/compose versteht volle Embeds - Temp-Voice: Control-Panel im erstellten Kanal (Umbenennen, Sperren, Limit, Löschen) — nur der Kanal-Besitzer darf bedienen - Embed-Polish: brandEmbed-Factory (src/embeds.js), Footer überall mit Bot-Avatar-Icon Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { apiGet, apiPut, apiPost, apiDelete } from '../api.js';
|
||||
import EmbedComposer from '../components/EmbedComposer.jsx';
|
||||
|
||||
const API_SCOPES = [
|
||||
{ id: 'message', label: 'message', hint: 'Nachrichten/Embeds in Kanäle posten' },
|
||||
@@ -77,8 +78,6 @@ export default function Settings({ me }) {
|
||||
};
|
||||
const [menus, setMenus] = useState([]);
|
||||
const [menuDraft, setMenuDraft] = useState(emptyMenu);
|
||||
// Composer
|
||||
const [composer, setComposer] = useState({ channel: '', title: '', text: '', messageId: '' });
|
||||
// Branding
|
||||
const [giteaToken, setGiteaToken] = useState('');
|
||||
const [botName, setBotName] = useState('');
|
||||
@@ -97,7 +96,17 @@ export default function Settings({ me }) {
|
||||
const [triggers, setTriggers] = useState([]);
|
||||
const [triggerDraft, setTriggerDraft] = useState({ keyword: '', reply: '' });
|
||||
// Game-Server
|
||||
const emptyServer = { id: null, name: '', type: 'fivem', query_url: '', address: '' };
|
||||
const GAME_PRESETS = [
|
||||
['fivem', '🚗 FiveM'], ['http', '🌐 HTTP-Check'], ['minecraft', '⛏️ Minecraft (Java)'],
|
||||
['minecraftbe', '⛏️ Minecraft (Bedrock)'], ['rust', '🪓 Rust'], ['valheim', '🛡️ Valheim'],
|
||||
['palworld', '🐑 Palworld'], ['arkse', '🦖 ARK: Survival Evolved'], ['dayz', '🧟 DayZ'],
|
||||
['projectzomboid', '🧟 Project Zomboid'], ['sevendaystodie', '🧟 7 Days to Die'],
|
||||
['teamfortress2', '🎩 Team Fortress 2'], ['counterstrike2', '🔫 Counter-Strike 2'],
|
||||
['garrysmod', '🔧 Garry’s Mod'], ['satisfactory', '🏭 Satisfactory'],
|
||||
['terraria', '🌳 Terraria'], ['arma3', '🪖 Arma 3'], ['squad', '🪖 Squad'],
|
||||
['unturned', '🏝️ Unturned'],
|
||||
];
|
||||
const emptyServer = { id: null, name: '', type: 'fivem', query_url: '', address: '', host: '', port: '', connect_url: '' };
|
||||
const [servers, setServers] = useState([]);
|
||||
const [serverDraft, setServerDraft] = useState(emptyServer);
|
||||
// Bot-Beschreibung + Team
|
||||
@@ -177,9 +186,10 @@ export default function Settings({ me }) {
|
||||
if (res) setTeam(res.admins);
|
||||
}
|
||||
|
||||
const serverNeedsUrl = ['fivem', 'http'].includes(serverDraft.type);
|
||||
async function saveServer() {
|
||||
if (!serverDraft.name.trim() || !serverDraft.query_url.trim()) {
|
||||
flash('✗ Name und Query-URL nötig');
|
||||
if (!serverDraft.name.trim() || (serverNeedsUrl ? !serverDraft.query_url.trim() : !serverDraft.host.trim())) {
|
||||
flash(serverNeedsUrl ? '✗ Name und Query-URL nötig' : '✗ Name und Host nötig');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -388,25 +398,6 @@ export default function Settings({ me }) {
|
||||
if (createdKey?.id === id) setCreatedKey(null);
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
const reloadMenus = async () => setMenus((await apiGet('/api/rolemenus')).menus);
|
||||
|
||||
async function saveMenu(publish = false) {
|
||||
@@ -1024,33 +1015,7 @@ export default function Settings({ me }) {
|
||||
|
||||
const tabComposer = (
|
||||
<>
|
||||
<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={8} 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</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 btn-save" onClick={sendCompose}>
|
||||
{composer.messageId.trim() ? 'Aktualisieren' : 'Senden'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EmbedComposer channelOptions={channelOptions} flash={flash} />
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Geplante Posts</h2>
|
||||
@@ -1311,14 +1276,25 @@ export default function Settings({ me }) {
|
||||
Spieler-Balken, Map, Adresse) — aktualisiert alle 2 Minuten, DiscordGSM-Style.
|
||||
</p>
|
||||
<div className="field">
|
||||
<label>Status-Kanal</label>
|
||||
<select value={form.status_channel_id ?? ''} onChange={(e) => setForm({ ...form, status_channel_id: e.target.value })}>
|
||||
<option value="">— deaktiviert —</option>
|
||||
{channelOptions}
|
||||
</select>
|
||||
<div className="field-row">
|
||||
<div style={{ flex: 1 }}>
|
||||
<label>Status-Kanal</label>
|
||||
<select value={form.status_channel_id ?? ''} onChange={(e) => setForm({ ...form, status_channel_id: e.target.value })}>
|
||||
<option value="">— deaktiviert —</option>
|
||||
{channelOptions}
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label>Alert-Kanal <span className="field-hint">🚨 down / ✅ online</span></label>
|
||||
<select value={form.server_alert_channel_id ?? ''} onChange={(e) => setForm({ ...form, server_alert_channel_id: e.target.value })}>
|
||||
<option value="">— keine Alerts —</option>
|
||||
{channelOptions}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-actions" style={{ marginBottom: '1.4rem' }}>
|
||||
<button className="btn btn-save" onClick={save}>Kanal speichern</button>
|
||||
<button className="btn btn-save" onClick={save}>Kanäle speichern</button>
|
||||
</div>
|
||||
|
||||
{servers.length > 0 && (
|
||||
@@ -1329,7 +1305,8 @@ export default function Settings({ me }) {
|
||||
<span className="key-scopes">{s.type}</span>
|
||||
<span className="key-used" title={s.query_url}>{s.query_url}</span>
|
||||
<button className="btn btn-mini" onClick={() => setServerDraft({
|
||||
id: s.id, name: s.name, type: s.type, query_url: s.query_url, address: s.address ?? '',
|
||||
id: s.id, name: s.name, type: s.type, query_url: s.query_url ?? '',
|
||||
address: s.address ?? '', host: s.host ?? '', port: s.port ?? '', connect_url: s.connect_url ?? '',
|
||||
})}>Bearbeiten</button>
|
||||
<button className="card-delete" title="Server + Embed entfernen" onClick={() => removeServer(s.id)}>✕</button>
|
||||
</div>
|
||||
@@ -1341,17 +1318,35 @@ export default function Settings({ me }) {
|
||||
<label>{serverDraft.id ? `Server #${serverDraft.id} bearbeiten` : 'Neuer Server'}</label>
|
||||
<div className="field-row">
|
||||
<input type="text" style={{ flex: 'none', width: '11rem' }} placeholder="Name, z. B. FiveM" value={serverDraft.name} onChange={(e) => setServerDraft({ ...serverDraft, name: e.target.value })} />
|
||||
<select style={{ flex: 'none', width: '8rem' }} value={serverDraft.type} onChange={(e) => setServerDraft({ ...serverDraft, type: e.target.value })}>
|
||||
<option value="fivem">FiveM</option>
|
||||
<option value="http">HTTP-Check</option>
|
||||
</select>
|
||||
<input type="text" placeholder={serverDraft.type === 'fivem' ? 'http://ip:30120' : 'https://dienst.example/health'} value={serverDraft.query_url} onChange={(e) => setServerDraft({ ...serverDraft, query_url: e.target.value })} />
|
||||
<input
|
||||
type="text" list="game-presets" style={{ flex: 'none', width: '13rem' }}
|
||||
placeholder="Spiel (gamedig-ID)" title="Vorschläge oder beliebige gamedig-Spiel-ID"
|
||||
value={serverDraft.type}
|
||||
onChange={(e) => setServerDraft({ ...serverDraft, type: e.target.value })}
|
||||
/>
|
||||
<datalist id="game-presets">
|
||||
{GAME_PRESETS.map(([id, label]) => <option key={id} value={id}>{label}</option>)}
|
||||
</datalist>
|
||||
{serverNeedsUrl ? (
|
||||
<input type="text" placeholder={serverDraft.type === 'fivem' ? 'http://ip:30120' : 'https://dienst.example/health'} value={serverDraft.query_url} onChange={(e) => setServerDraft({ ...serverDraft, query_url: e.target.value })} />
|
||||
) : (
|
||||
<>
|
||||
<input type="text" placeholder="Host / IP" value={serverDraft.host} onChange={(e) => setServerDraft({ ...serverDraft, host: e.target.value })} />
|
||||
<input type="text" style={{ flex: 'none', width: '6rem' }} placeholder="Port" value={serverDraft.port} onChange={(e) => setServerDraft({ ...serverDraft, port: e.target.value })} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<p className="field-hint" style={{ marginTop: '.3rem' }}>
|
||||
Neben FiveM/HTTP funktioniert jede <a href="https://github.com/gamedig/node-gamedig#games-list" target="_blank" rel="noreferrer" style={{ textDecoration: 'underline' }}>gamedig-Spiel-ID</a> (300+ Spiele) — Host + Query-Port eintragen.
|
||||
</p>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Anzeige-Adresse <span className="field-hint">optional — steht als Copy-Paste-Block im Embed (z. B. connect play.d4rkst3r.de)</span></label>
|
||||
<label>Connect-Link + Anzeige-Adresse <span className="field-hint">Link ist im Embed klickbar (auch steam:// / fivem://); Adresse als Copy-Paste-Block</span></label>
|
||||
<div className="field-row">
|
||||
<input type="text" placeholder="fivem://connect/play.d4rkst3r.de" value={serverDraft.connect_url} onChange={(e) => setServerDraft({ ...serverDraft, connect_url: e.target.value })} />
|
||||
<input type="text" placeholder="connect play.d4rkst3r.de" value={serverDraft.address} onChange={(e) => setServerDraft({ ...serverDraft, address: e.target.value })} />
|
||||
</div>
|
||||
<div className="field-row" style={{ marginTop: '.6rem' }}>
|
||||
<button className="btn btn-save" onClick={saveServer}>{serverDraft.id ? 'Aktualisieren' : 'Hinzufügen'}</button>
|
||||
{serverDraft.id && <button className="btn btn-ghost" onClick={() => setServerDraft(emptyServer)}>Abbrechen</button>}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user