From f7b261c648b9dba30b6859d28695c7402bac7740 Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Thu, 30 Jul 2026 00:40:08 +0200 Subject: [PATCH] =?UTF-8?q?Branding-Tab:=20Name/Farben/Bot-Status/Avatar/B?= =?UTF-8?q?anner=20+=20Button-Farben=20f=C3=BCr=20Rollen-Men=C3=BCs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Alle Embeds nutzen jetzt brandName/brandColor/brandColor2/brandFooter aus den Settings (Codemod über 20 Module) — Farbe & Name serverweit per Klick änderbar - Bot-Status (Presence): Typ (Spielt/Schaut/Hört/Status) + Text, sofort angewendet; Server-Monitor nutzt die Presence nur noch, wenn kein eigener Status gesetzt ist - Avatar-/Banner-Upload direkt aufs Bot-Profil (Base64, 8-MB-Limit, Discord-Rate-Limit sauber gemeldet) - Env-Verlagerung: GITEA_API_TOKEN (write-only Setting) und DISCORD_GUILD_ID jetzt auch über den Brand-Tab pflegbar — weniger Redeploys - Rollen-Menüs: Button-Farbe pro Eintrag wählbar (Grau/Blau/Grün/Rot) Co-Authored-By: Claude Fable 5 --- README.md | 7 +- frontend/src/pages/Settings.jsx | 115 ++++++++++++++++++++++++++- frontend/src/style.css | 35 ++++++++ src/bot/client.js | 15 ++-- src/bot/commands/bug.js | 6 +- src/bot/commands/giveaway.js | 5 +- src/bot/commands/playtester-setup.js | 6 +- src/bot/commands/purge.js | 4 +- src/bot/commands/rank.js | 6 +- src/bot/commands/ticket-setup.js | 6 +- src/bot/commands/timeout.js | 4 +- src/bot/commands/warn.js | 4 +- src/bot/commands/warns.js | 3 +- src/bot/commands/wunsch.js | 6 +- src/bot/community.js | 5 +- src/bot/giveaways.js | 3 +- src/bot/mod-tools.js | 23 +++--- src/bot/presence.js | 29 +++++++ src/bot/release-feed.js | 7 +- src/bot/role-menus.js | 16 +++- src/bot/server-monitor.js | 22 ++--- src/bot/weekly-recap.js | 7 +- src/gitea-api.js | 8 +- src/runtime-settings.js | 46 +++++++++++ src/web/api.js | 86 ++++++++++++++++++-- src/web/devlog-endpoint.js | 9 +-- src/web/scheduled-posts.js | 5 +- 27 files changed, 395 insertions(+), 93 deletions(-) create mode 100644 src/bot/presence.js diff --git a/README.md b/README.md index e49edbe..667c735 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ SQLite (better-sqlite3, FTS5), Docker Multi-Stage — deploybar als Portainer-St | 📈 **Level-System** | XP pro Nachricht (60s-Cooldown, MEE6-Formel), `/rank`, Level-Up-Announce, Rollen-Belohnungen, öffentliche Bestenliste auf `/level` | | 🏷️ **Tags** | `/tag ` (mit Autocomplete) postet gespeicherte Text-Bausteine — verwaltet im Composer-Tab | | ⏰ **Geplante Posts** | Einmalig, täglich oder wöchentlich — im Composer-Tab geplant, vom Bot gepostet (bei Carl Premium) | +| 🎨 **Branding** | Brand-Tab: Name + Embed-Farben für alle Bot-Posts, Bot-Status (Presence), Avatar-/Banner-Upload, Guild-ID & Gitea-Token ohne Env-Redeploy | | 👋 **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** | FiveM-kompatible Game-Server (/dynamic.json): Live-Status-Embed + Spielerzahl in der Bot-Presence | @@ -58,8 +59,8 @@ Design: D4RKST3R-Brand (Neon-Gelb/Orange auf Schwarz, Bebas Neue + Barlow Conden Share Tech Mono, selbst gehostet). ### Setup-Seite (`/settings`) -Aufgeteilt in **Tabs mit Sidebar-Navigation** (Status · Feeds · Community · Rollen · -Support · Composer · System · API). Alles zur Laufzeit änderbar — gespeichert in +Aufgeteilt in **Tabs mit Sidebar-Navigation** (Status · Brand · Feeds · Community · +Rollen · Support · Composer · System · API). Alles zur Laufzeit änderbar — gespeichert in SQLite, Env-Variablen sind nur Fallback, kein Redeploy nötig: - **Allgemein:** Öffentliche URL, Gitea-URL @@ -116,7 +117,7 @@ in Portainer als Stack-Environment-Variables. | `GITEA_WEBHOOK_SECRET` | ✅ | HMAC-Prüfung der Gitea-Webhooks¹ | | `DEVLOG_POST_SECRET` | ✅ | Secret im Devlog-Endpoint-Pfad¹ | | `DISCORD_GUILD_ID` | optional | Guild-Commands sofort statt global (bis 1h) | -| `GITEA_API_TOKEN` | optional | Für `/bug` → Issues (Scope `write:issue`); ohne Token ist `/bug` aus | +| `GITEA_API_TOKEN` | optional | Für `/bug` → Issues (Scope `write:issue`); alternativ im Brand-Tab hinterlegen | | `GITEA_URL` | optional | Default `https://git.d4rkst3r.de` | | `PUBLIC_URL` | optional | Default `https://bot.d4rkst3r.de` | | `COMMIT_CHANNEL_ID` / `DEVLOG_CHANNEL_ID` | optional | Fallbacks — Kanäle kommen normal von der Setup-Seite | diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index a65ebb1..46bb722 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -10,6 +10,7 @@ const API_SCOPES = [ const TABS = [ { id: 'status', icon: '📊', label: 'Status' }, + { id: 'brand', icon: '🎨', label: 'Brand' }, { id: 'feeds', icon: '📔', label: 'Feeds' }, { id: 'community', icon: '🙌', label: 'Community' }, { id: 'rollen', icon: '🎭', label: 'Rollen' }, @@ -43,12 +44,14 @@ export default function Settings({ me }) { // Rollen-Menüs const emptyMenu = { id: null, title: '', description: '', channel_id: '', exclusive: false, - entries: [{ emoji: '', label: '', role_id: '' }], + entries: [{ emoji: '', label: '', role_id: '', style: 'secondary' }], }; const [menus, setMenus] = useState([]); const [menuDraft, setMenuDraft] = useState(emptyMenu); // Composer const [composer, setComposer] = useState({ channel: '', title: '', text: '', messageId: '' }); + // Branding + const [giteaToken, setGiteaToken] = useState(''); // Tags + geplante Posts const [tags, setTags] = useState([]); const [tagDraft, setTagDraft] = useState({ name: '', content: '' }); @@ -78,6 +81,33 @@ export default function Settings({ me }) { apiGet('/api/scheduled').then((d) => setScheduled(d.posts)).catch(() => {}); }, [me.admin]); + async function uploadImage(target, file) { + if (!file) return; + const reader = new FileReader(); + reader.onload = async () => { + try { + await apiPost(`/api/branding/${target}`, { data: reader.result }); + flash(`✓ ${target === 'avatar' ? 'Avatar' : 'Banner'} gesetzt`); + } catch { + flash('✗ Discord lehnt ab (Rate-Limit? ~2 Änderungen / 10 min)'); + } + }; + reader.readAsDataURL(file); + } + + async function saveBrand() { + try { + const body = { ...form }; + if (giteaToken.trim()) body.gitea_api_token = giteaToken.trim(); + const res = await apiPut('/api/settings', body); + setForm(res.settings); + setGiteaToken(''); + flash('✓ Gespeichert & angewendet'); + } catch { + flash('✗ Speichern fehlgeschlagen (Farben als #rrggbb?)'); + } + } + async function saveTagDraft() { if (!tagDraft.name.trim() || !tagDraft.content.trim()) { flash('✗ Name und Inhalt nötig'); @@ -223,7 +253,9 @@ export default function Settings({ me }) { setMenuDraft({ id: m.id, title: m.title, description: m.description, channel_id: m.channel_id ?? '', exclusive: m.exclusive, - entries: m.entries.length ? m.entries : [{ emoji: '', label: '', role_id: '' }], + entries: m.entries.length + ? m.entries.map((e) => ({ style: 'secondary', ...e })) + : [{ emoji: '', label: '', role_id: '', style: 'secondary' }], }); const setMenuEntry = (i, patch) => @@ -291,6 +323,73 @@ export default function Settings({ me }) { ); + const tabBrand = ( + <> +
+

// Identität

+
+ + setForm({ ...form, brand_name: e.target.value })} /> +
+
+ +
+ setForm({ ...form, brand_color: e.target.value })} /> + setForm({ ...form, brand_color2: e.target.value })} /> + {form.brand_color} · {form.brand_color2} +
+
+
+ +
+

// Bot-Status

+
+ +
+ + setForm({ ...form, bot_status_text: e.target.value })} /> +
+
+
+ +
+

// Avatar & Banner

+

+ Wird direkt beim Bot-Account gesetzt. Discord erlaubt nur ~2 Änderungen pro 10 Minuten. +

+
+ + uploadImage('avatar', e.target.files?.[0])} /> +
+
+ + uploadImage('banner', e.target.files?.[0])} /> +
+
+ +
+

// Aus der Env verlagert

+
+ + setForm({ ...form, discord_guild_id: e.target.value })} /> +
+
+ + setGiteaToken(e.target.value)} /> +
+
+ +
+ +
+ + ); + const tabFeeds = ( <>
@@ -533,6 +632,17 @@ export default function Settings({ me }) { {roleOptions} +