From 00249f9d8b524f3bb1768c5e6776ff095c923edd Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Thu, 30 Jul 2026 02:17:30 +0200 Subject: [PATCH] Server-Monitor v3, Composer v2, Temp-Voice-Panel und Embed-Polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- README.md | 14 +- frontend/src/components/EmbedComposer.jsx | 262 +++++++++ frontend/src/pages/Settings.jsx | 123 +++-- frontend/src/style.css | 82 +++ package-lock.json | 613 +++++++++++++++++++++- package.json | 3 +- src/bot/extras.js | 113 +++- src/bot/giveaways.js | 2 +- src/bot/mod-tools.js | 4 +- src/bot/release-feed.js | 2 +- src/bot/server-monitor.js | 130 ++++- src/bot/weekly-recap.js | 2 +- src/db.js | 39 +- src/embeds.js | 20 + src/web/api-v1.js | 7 +- src/web/api.js | 78 ++- src/web/devlog-endpoint.js | 1 + 17 files changed, 1379 insertions(+), 116 deletions(-) create mode 100644 frontend/src/components/EmbedComposer.jsx create mode 100644 src/embeds.js diff --git a/README.md b/README.md index 45e74ad..27f00c7 100644 --- a/README.md +++ b/README.md @@ -40,11 +40,11 @@ SQLite (better-sqlite3, FTS5), Docker Multi-Stage — deploybar als Portainer-St | 💬 **Triggers** | Auto-Antworten auf Schlüsselwörter (30s-Cooldown), verwaltet im Composer-Tab | | ⏰ **/remind** | Erinnerungen per DM (`/remind dauer:2h text:…`) | | 📣 **Twitch/YouTube** | 🔴-Live- und ▶️-Neues-Video-Announcements (YouTube ohne Key via RSS, Twitch mit App-Credentials) | -| 🔊 **Temp-Voice** | „Join to Create": Hub-Kanal betreten → eigener Voice-Kanal, löscht sich wenn leer | +| 🔊 **Temp-Voice** | „Join to Create": Hub-Kanal betreten → eigener Voice-Kanal mit Control-Panel (Umbenennen, Sperren, Limit, Löschen — nur für den Besitzer), löscht sich wenn leer | | 📊 **Server-Stats** | Nachrichten/Joins/Leaves pro Tag → Aktivitäts-Chart auf der Level-Seite | | 👋 **Willkommens-Embed** | Begrüßung neuer Member im Brand-Look (braucht Server-Members-Intent) | | 📋 **Mod-Log** | Gelöschte/bearbeitete Nachrichten in einen privaten Log-Kanal | -| 🎮 **Server-Monitor** | DiscordGSM-Stil: eigener Setup-Tab, pro Server ein Live-Embed (🟢/🔴, Spieler-Balken, Map, Copy-Paste-Adresse); Typen FiveM + HTTP-Check; Spielerzahl in der Presence | +| 🎮 **Server-Monitor** | DiscordGSM-Stil: eigener Setup-Tab, pro Server ein Live-Embed (🟢/🔴, Spieler-Balken mit %, Map, Spieler-Liste, klickbarer Connect-Link, Ping); **300+ Spiele via gamedig** (Minecraft, Rust, CS2, Valheim, ARK …) + FiveM + HTTP-Check; Down/Up-Alerts (🚨 nach 2 Fehlversuchen, ✅ mit Downtime bei Recovery) in eigenen Alert-Kanal; Spielerzahl in der Presence | | 👥 **Team-Rechte** | Team-Mitglieder bekommen gezielten Web-Zugriff (Composer, Bewerbungen, Rollen, Server, …) — Brand, System, API-Keys bleiben Owner-only | | 💡 **Feature-Voting** | `/wunsch` → Voting-Post mit 👍; Top-Wünsche öffentlich auf der Roadmap-Seite | | 🎉 **Giveaways** | `/giveaway` (Admin): Teilnahme-Button, automatische Ziehung nach Ablauf | @@ -82,7 +82,10 @@ SQLite, Env-Variablen sind nur Fallback, kein Redeploy nötig: - **Community:** Playtester-Rolle (+ Liste), Starboard-Kanal + ⭐-Schwellwert, Screenshot-Kanal, Voting-Kanal - **Moderation & Kontakt:** Modmail-, Willkommens-, Mod-Log-Kanal -- **Game-Server:** Status-Kanal + Server-Liste (`Name=URL`, FiveM-kompatibel) +- **Game-Server:** eigener Tab — Spiel-Auswahl (19 Presets + freie gamedig-ID), Host/Port + oder Query-URL, Connect-URL, Status- & Alert-Kanal +- **Composer:** visueller **Embed-Builder mit Live-Discord-Preview** (Autor, Felder, + Bilder, Farbe, Footer), Nachrichten senden & nachträglich bearbeiten, speicherbare Vorlagen - **Bug-Reports / Roadmap:** Ziel-Repos - **Backups:** an/aus, Upload-Kanal, „Backup jetzt" - **Watchdog:** überwachte URLs @@ -258,6 +261,7 @@ urllib.request.urlopen(req) | `POST/GET /api/v1/*` | API-Key (Bearer) | Externe Skripte — siehe „API v1" oben | | `GET/POST/DELETE /api/apikeys` | Admin | API-Key-Verwaltung | | `GET/POST/PUT/DELETE /api/rolemenus`, `POST /api/rolemenus/:id/publish` | Admin | Rollen-Menüs | +| `POST /api/compose` · `GET/PUT/DELETE /api/templates` | Team (content) | Embed-Composer senden/bearbeiten · Vorlagen | | `GET/PUT /api/settings`, `POST /api/settings/test/:target` | Admin | Setup-Seite | | `GET /api/commits?page=` · `DELETE /api/devlogs/:id` | Admin | Commit-Archiv · Devlog löschen | | `GET /auth/login` · `/auth/callback` · `/auth/logout` | — | Discord-OAuth2 | @@ -278,6 +282,7 @@ d4rkbot/ │ ├── runtime-settings.js # Effektive Settings (DB vor Env) │ ├── gitea-api.js # Gitea-REST (Issues + Assets für /bug) │ ├── backup.js # Nächtliche DB-Backups (gzip, Rotation, Upload) +│ ├── embeds.js # brandEmbed-Factory (Farbe, Footer, Bot-Icon) │ ├── bot/ │ │ ├── client.js # Discord-Client, Commands, Listener, Button-Handler │ │ ├── commit-feed.js # Push → Embed @@ -285,7 +290,7 @@ d4rkbot/ │ │ ├── devlog-archive.js # Nachricht → SQLite (+ Bild-Download) │ │ ├── community.js # Starboard + Screenshot-Galerie │ │ ├── mod-tools.js # Modmail, Willkommens-Embed, Mod-Log -│ │ ├── server-monitor.js # Game-Server-Status (FiveM-kompatibel) +│ │ ├── server-monitor.js # Game-Server-Status (gamedig 300+ Spiele, Alerts) │ │ ├── giveaways.js # Giveaway-Ziehung (Scheduler) │ │ ├── weekly-recap.js # Sonntags-Zusammenfassung + Scheduler │ │ ├── watchdog.js # URL-Checks + DM-Alarm @@ -300,6 +305,7 @@ d4rkbot/ │ └── src/ │ ├── App.jsx # Layout, Router, Nav, Login-Status │ ├── markdown.jsx # Mini-Markdown (fett/kursiv/Code/Links/Listen/---/##) +│ ├── components/ # EmbedComposer (Builder + Discord-Preview) │ └── pages/ # Devlogs, Roadmap, Galerie, Changelog, Commits, Settings ├── tools/test-webhook.mjs # Lokaler Commit-Feed-Test ├── .env.example # Vorlage für alle Env-Variablen diff --git a/frontend/src/components/EmbedComposer.jsx b/frontend/src/components/EmbedComposer.jsx new file mode 100644 index 0000000..f1696dd --- /dev/null +++ b/frontend/src/components/EmbedComposer.jsx @@ -0,0 +1,262 @@ +// Composer v2 (Vorbild: message.style / embed-generator): voller Embed-Builder +// mit Live-Vorschau im Discord-Look + speicherbaren Vorlagen. +import { useEffect, useState } from 'react'; +import { apiGet, apiPost, apiPut, apiDelete } from '../api.js'; +import { Markdown } from '../markdown.jsx'; + +const EMPTY_EMBED = { + author_name: '', author_icon: '', title: '', url: '', description: '', + color: '#f5c518', thumbnail: '', image: '', footer: '', timestamp: true, + fields: [], +}; + +/** Builder-State → Discord-Embed-Objekt (nur gesetzte Felder) */ +function toApiEmbed(e) { + const out = {}; + if (e.author_name.trim()) { + out.author = { name: e.author_name.trim() }; + if (e.author_icon.trim()) out.author.icon_url = e.author_icon.trim(); + } + if (e.title.trim()) out.title = e.title.trim(); + if (e.url.trim()) out.url = e.url.trim(); + if (e.description.trim()) out.description = e.description; + out.color = parseInt(e.color.replace('#', ''), 16) || 0xf5c518; + if (e.thumbnail.trim()) out.thumbnail = { url: e.thumbnail.trim() }; + if (e.image.trim()) out.image = { url: e.image.trim() }; + if (e.footer.trim()) out.footer = { text: e.footer.trim() }; + if (e.timestamp) out.timestamp = new Date().toISOString(); + const fields = e.fields.filter((f) => f.name.trim() && f.value.trim()); + if (fields.length) out.fields = fields; + return out; +} + +/** Discord-Embed-Objekt (aus Vorlage) → Builder-State */ +function fromApiEmbed(embed) { + if (!embed) return { ...EMPTY_EMBED, fields: [] }; + return { + author_name: embed.author?.name ?? '', + author_icon: embed.author?.icon_url ?? '', + title: embed.title ?? '', + url: embed.url ?? '', + description: embed.description ?? '', + color: `#${Number(embed.color ?? 0xf5c518).toString(16).padStart(6, '0')}`, + thumbnail: embed.thumbnail?.url ?? '', + image: embed.image?.url ?? '', + footer: embed.footer?.text ?? '', + timestamp: Boolean(embed.timestamp), + fields: (embed.fields ?? []).map((f) => ({ name: f.name, value: f.value, inline: Boolean(f.inline) })), + }; +} + +/** Live-Vorschau im Discord-Look */ +function DiscordPreview({ content, embed, useEmbed }) { + const hasEmbed = useEmbed && (embed.title || embed.description || embed.author_name || embed.fields.some((f) => f.name)); + return ( +
+
+
🤖
+
+
d4rkbot APP heute um {new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })} Uhr
+ {content.trim() &&
} + {hasEmbed && ( +
+
+ {embed.author_name && ( +
+ {embed.author_icon && } + {embed.author_name} +
+ )} + {embed.title &&
{embed.title}
} + {embed.description &&
} + {embed.fields.filter((f) => f.name && f.value).length > 0 && ( +
+ {embed.fields.filter((f) => f.name && f.value).map((f, i) => ( +
+
{f.name}
+
+
+ ))} +
+ )} + {embed.image && } + {(embed.footer || embed.timestamp) && ( +
+ {embed.footer}{embed.footer && embed.timestamp ? ' • ' : ''}{embed.timestamp ? 'heute' : ''} +
+ )} +
+ {embed.thumbnail && } +
+ )} +
+
+
+ ); +} + +export default function EmbedComposer({ channelOptions, flash }) { + const [channel, setChannel] = useState(''); + const [messageId, setMessageId] = useState(''); + const [content, setContent] = useState(''); + const [useEmbed, setUseEmbed] = useState(true); + const [embed, setEmbed] = useState({ ...EMPTY_EMBED, fields: [] }); + const [templates, setTemplates] = useState([]); + const [templateName, setTemplateName] = useState(''); + + useEffect(() => { + apiGet('/api/templates').then((d) => setTemplates(d.templates)).catch(() => {}); + }, []); + + const setE = (patch) => setEmbed({ ...embed, ...patch }); + const setField = (i, patch) => + setE({ fields: embed.fields.map((f, j) => (j === i ? { ...f, ...patch } : f)) }); + + async function send() { + if (!channel) return flash('✗ Kanal wählen'); + const apiEmbed = useEmbed ? toApiEmbed(embed) : undefined; + if (!content.trim() && !useEmbed) return flash('✗ Text oder Embed nötig'); + try { + const res = await apiPost('/api/compose', { + channel_id: channel, + message_id: messageId.trim() || undefined, + content, + embed: apiEmbed, + }); + flash(res.edited ? '✓ Aktualisiert' : `✓ Gesendet (ID ${res.message_id})`); + if (!res.edited) setMessageId(res.message_id); + } catch { + flash('✗ Senden fehlgeschlagen'); + } + } + + async function saveTemplate() { + if (!templateName.trim()) return flash('✗ Vorlagen-Name nötig'); + try { + const res = await apiPut(`/api/templates/${encodeURIComponent(templateName.trim())}`, { + content, + embed: useEmbed ? toApiEmbed(embed) : null, + }); + setTemplates(res.templates); + flash('✓ Vorlage gespeichert'); + } catch { + flash('✗ Vorlage fehlgeschlagen'); + } + } + + function loadTemplate(name) { + const t = templates.find((x) => x.name === name); + if (!t) return; + setContent(t.payload.content ?? ''); + setUseEmbed(Boolean(t.payload.embed)); + setEmbed(fromApiEmbed(t.payload.embed)); + setTemplateName(name); + } + + async function removeTemplate() { + if (!templateName.trim() || !window.confirm(`Vorlage „${templateName}" löschen?`)) return; + const res = await apiDelete(`/api/templates/${encodeURIComponent(templateName.trim())}`).catch(() => null); + if (res) { setTemplates(res.templates); setTemplateName(''); flash('✓ Vorlage gelöscht'); } + } + + return ( +
+

// Post verfassen

+ +
+
+ + setMessageId(e.target.value)} /> +
+
+
+
+ + setTemplateName(e.target.value)} /> + + +
+
+ +
+
+
+ +