Config statt Setup: Bereiche gruppiert, Suche, Status als Einstieg
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

Die Seite hieß Setup und listete 15 Bereiche flach untereinander. Wer
nicht wusste, dass Geburtstage unter „Community" stecken und Willkommens-
Texte unter „Support", hat geklickt bis er es fand.

Die Seitenleiste gruppiert jetzt nach Überblick, Auftritt, Inhalte,
Community, Technik und Zugang. Darüber steht ein Suchfeld, das nicht nur
Beschriftungen durchsucht, sondern auch Stichwörter je Bereich — „geburtstag"
führt zu Community, „ticket" zu Support. Enter springt zum ersten Treffer.

Der aktive Bereich steht in der Adresse (/settings#texte). Damit überlebt er
das Neuladen, und man kann jemandem einen Link auf genau die Stelle schicken.

Der Status-Bereich war die leerste Seite im ganzen Panel: sechs Zahlen. Er ist
jetzt der Einstieg und beantwortet die Frage, die man beim Öffnen wirklich hat
— was läuft noch nicht? Eingeschaltete Module, denen ein Kanal oder eine Rolle
fehlt, stehen dort mit einem Knopf, der direkt an die richtige Stelle springt.
Ist alles eingerichtet, sagt die Seite genau das.

Umbenannt in „Config", weil „Setup" nach einmaliger Einrichtung klingt — die
Seite ist aber der Ort, an dem man dauerhaft alles einstellt.

Schmale Bildschirme: die Leiste wird zur umbrechenden Zeile ohne
Gruppen-Überschriften, das Suchfeld nimmt die volle Breite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 16:13:33 +02:00
co-authored by Claude Fable 5
parent dadd75d452
commit 2ce274707a
15 changed files with 280 additions and 78 deletions
+33 -15
View File
@@ -2,7 +2,7 @@
Discord-Bot + Webinterface: Devlog-Tagebuch, Commit-Feed, Changelog und
Community-Tools für die EcoGame-Entwicklung — alles gebrandet, alles über
die Setup-Seite steuerbar.
die Config-Seite steuerbar.
**Stack:** Node.js 20+ (ESM), discord.js v14, Fastify 5, React 19 + Vite,
SQLite (better-sqlite3, FTS5), Docker Multi-Stage — deploybar als Portainer-Stack.
@@ -22,7 +22,7 @@ SQLite (better-sqlite3, FTS5), Docker Multi-Stage — deploybar als Portainer-St
| 📊 **Wochen-Rückblick** | Sonntags 20:00 automatisch: Commits pro Tag als Balken, Devlogs, Top-Projekte |
| 🐛 **`/bug`** | Member melden Bugs → Gitea-Issue (inkl. Screenshot-Upload); Issue geschlossen → DM an den Reporter |
| 🚨 **Watchdog** | Prüft eigene Dienste alle 2 min, DM-Alarm bei Ausfall + Entwarnung |
| 🧪 **Playtester-Programm** | `/playtester-setup` postet den Bewerbungs-Button; Rolle + Liste (Setup-Seite); Alpha-Keys später per DM über die API |
| 🧪 **Playtester-Programm** | `/playtester-setup` postet den Bewerbungs-Button; Rolle + Liste (Config-Seite); Alpha-Keys später per DM über die API |
| ⭐ **Starboard** | Nachrichten mit genug ⭐-Reaktionen landen automatisch im Best-of-Kanal |
| 📸 **Screenshot-Galerie** | Bilder aus dem Screenshot-Kanal → öffentliche `/galerie` (lokal gespeichert); Historie via `/galerie-backfill` |
| 📬 **Modmail** | DM an den Bot → Thread im privaten Staff-Kanal; Antworten im Thread gehen als DM zurück |
@@ -47,14 +47,14 @@ SQLite (better-sqlite3, FTS5), Docker Multi-Stage — deploybar als Portainer-St
| 📢 **Auto-Publish** | Devlog-/Release-/Composer-Posts in Ankündigungs-Kanälen werden automatisch veröffentlicht (Follower bekommen sie) |
| 🔗 **Link-Vorschau** | Jedes Devlog hat einen Permalink (`/devlogs/:id`); geteilte Links zeigen überall gebrandete Open-Graph-Vorschau mit Bild |
| 📋 **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 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 |
| 🎮 **Server-Monitor** | DiscordGSM-Stil: eigener Config-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; **Audit-Log** im Team-Tab zeigt dem Owner die letzten Aktionen |
| 🔑 **Single Sign-On** | Andere Dienste (Kanban, Platform …) nutzen den Discord-Login des Bots mit und bekommen die Discord-Rollen gleich mitgeliefert — [Anleitung](docs/sso.md) |
| 🎨 **Geteiltes Design** | `/brand.css` + `/brand-nav.js` geben jeder anderen App den D4RKST3R-Look samt Navigation — Farben kommen live aus dem Brand-Tab; dazu ein [Gitea-Theme](docs/gitea-theme/) im selben Look |
| 💾 **Repo-Backups** | Nachts werden alle Gitea-Repos als git-Bundle gesichert (komplette Historie, direkt wieder klonbar) |
| ⚡ **Auto-Deploy** | Push auf `main` → Portainer rollt neu aus; optional mit Code-Prüfung über Gitea Actions — [Anleitung](docs/auto-deploy.md) |
| 🏠 **Portal** | Dienste (Gitea, Kanban, Cloud …) im Setup pflegen → Kacheln auf der Startseite und Links in der geteilten Navigation |
| ⚖️ **Rechtstexte** | `/impressum` + `/datenschutz` aus dem Setup gepflegt; Member können ihre Daten selbst löschen (DSGVO Art. 17) |
| 🏠 **Portal** | Dienste (Gitea, Kanban, Cloud …) in der Config pflegen → Kacheln auf der Startseite und Links in der geteilten Navigation |
| ⚖️ **Rechtstexte** | `/impressum` + `/datenschutz` in der Config gepflegt; Member können ihre Daten selbst löschen (DSGVO Art. 17) |
| 💡 **Feature-Voting** | `/wunsch` → Voting-Post mit 👍; Top-Wünsche öffentlich auf der Roadmap-Seite |
| 🎉 **Giveaways** | `/giveaway` (Admin): Teilnahme-Button, automatische Ziehung nach Ablauf; am Gewinner-Post: 🔁 Neu auslosen (Admin) + 👥 Teilnehmerliste |
| 📈 **Contribution-Heatmap** | GitHub-Style-Jahreskalender aus dem Commit-Archiv auf der Roadmap-Seite |
@@ -72,12 +72,12 @@ SQLite (better-sqlite3, FTS5), Docker Multi-Stage — deploybar als Portainer-St
| `/changelog` | öffentlich | Alle Releases mit Notes, Tag- und Pre-Release-Chips |
| `/feed.xml` | öffentlich | RSS-Feed der Devlogs |
| `/profil` | Member | Eigenes Profil: Rang, XP, Rollen (togglebar), Alpha-Key, DSGVO-Löschung |
| `/impressum` · `/datenschutz` | öffentlich | Rechtstexte — Betreiber-Angaben aus dem Setup (Owner-only) |
| `/impressum` · `/datenschutz` | öffentlich | Rechtstexte — Betreiber-Angaben aus der Config (Owner-only) |
| `/<kürzel>` | öffentlich | **Frei angelegte Seiten** (Regeln, Über uns, FAQ …) — im Seiten-Tab gepflegt |
| `/` | öffentlich | Startseite: Hero, Live-Zahlen (Member/Server/Spieler), Bereichs-Kacheln, neuestes Devlog |
| `/server` | öffentlich | Live-Status aller Game-Server: Logo, Spieler-Balken, 24h-Verlauf, Uptime & Peak, Copy-Adresse |
| `/commits` | nur Admin | Archivierte Commits aller Repos (SHA → Gitea-Link) |
| `/settings` | nur Admin | **Setup-Seite** — siehe unten |
| `/settings` | nur Admin | **Config-Seite** — siehe unten |
Login via Discord-OAuth2 (identify-Scope, signierte Session-Cookies, keine Token-Speicherung).
Design: D4RKST3R-Brand (Neon-Gelb/Orange auf Schwarz, Bebas Neue + Barlow Condensed +
@@ -105,11 +105,29 @@ Ein leeres Feld stellt den Standardtext wieder her.
Die Vorlagen stehen in [`src/templates.js`](src/templates.js). Der Standard lebt im
Code, die Datenbank enthält nur echte Abweichungen.
### Setup-Seite (`/settings`)
Aufgeteilt in **Tabs mit Sidebar-Navigation** (Status · Brand · Feeds · Community ·
Rollen · Support · Bewerbungen · Composer · Server · System · API · Team) —
Team-Mitglieder sehen nur die Tabs ihrer Bereiche. Alles zur Laufzeit änderbar — gespeichert in
SQLite, Env-Variablen sind nur Fallback, kein Redeploy nötig:
### Config-Seite (`/settings`)
15 Bereiche, in der Seitenleiste nach Themen gruppiert:
| Gruppe | Bereiche |
|---|---|
| Überblick | Status · Module · Texte |
| Auftritt | Brand · Seiten |
| Inhalte | Feeds · Composer |
| Community | Community · Rollen · Support · Bewerbungen |
| Technik | Server · System |
| Zugang | API · Team |
Das Suchfeld über der Leiste findet Bereiche auch über Stichwörter — „geburtstag"
führt zu Community, „willkommen" zu Support. Enter springt zum ersten Treffer.
Der aktive Bereich steht in der Adresse (`/settings#texte`), Links und Neuladen
landen also wieder dort.
Der Status-Bereich ist der Einstieg: Kennzahlen plus **„Noch einzurichten"** —
alle eingeschalteten Module, denen noch ein Kanal oder eine Rolle fehlt, mit
Sprung direkt zur richtigen Stelle.
Team-Mitglieder sehen nur die Bereiche ihrer Rechte. Alles zur Laufzeit änderbar —
gespeichert in SQLite, Env-Variablen sind nur Fallback, kein Redeploy nötig:
- **Allgemein:** Öffentliche URL, Gitea-URL
- **Kanäle** (Devlog / Commit / Release) als Dropdown + „Test senden"-Button je Kanal
@@ -172,7 +190,7 @@ in Portainer als Stack-Environment-Variables.
| `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 |
| `COMMIT_CHANNEL_ID` / `DEVLOG_CHANNEL_ID` | optional | Fallbacks — Kanäle kommen normal von der Config-Seite |
| `TZ` | optional | Default `Europe/Berlin` (Wochen-Rückblick, Datumsformate) |
¹ Generieren: `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`
@@ -255,7 +273,7 @@ node tools/test-webhook.mjs
Der Bot ist die zentrale Discord-Brücke der Infrastruktur: devlog.py, Platform,
FiveM-Server, CI-Jobs … reden alle mit einer API statt mit zig Discord-Webhooks.
**Auth:** API-Keys auf der Setup-Seite erstellen (Name + Scopes, Key wird einmalig
**Auth:** API-Keys auf der Config-Seite erstellen (Name + Scopes, Key wird einmalig
angezeigt, Widerruf jederzeit). Jeder Request:
```
Authorization: Bearer d4rk_<key>
@@ -305,7 +323,7 @@ urllib.request.urlopen(req)
| `GET /brand.css` · `/brand-nav.js` | — | Design-Tokens + Navigation für andere Apps (CORS offen) |
| `GET /api/services` | — | Dienste-Kacheln fürs Portal (Pflege: Scope `settings`) |
| `GET /api/legal` · `DELETE /api/profile` | — / Member | Impressums-Angaben · DSGVO-Löschung |
| `GET/PUT /api/settings`, `POST /api/settings/test/:target` | Admin | Setup-Seite |
| `GET/PUT /api/settings`, `POST /api/settings/test/:target` | Admin | Config-Seite |
| `GET /api/commits?page=` · `DELETE /api/devlogs/:id` | Admin | Commit-Archiv · Devlog löschen |
| `GET /auth/login` · `/auth/callback` · `/auth/logout` | — | Discord-OAuth2 |
+5 -5
View File
@@ -1,11 +1,11 @@
# Konzept: Bot-Produktseite und Community-Hub trennen
**Stand:** 31.07.2026 · Umgesetzt. Zum Aktivieren müssen nur noch die beiden
Adressen im Setup eingetragen werden (System → Zwei Seiten).
Adressen in der Config eingetragen werden (System → Zwei Seiten).
## Aktivieren
1. **Setup → System → Zwei Seiten** ausfüllen:
1. **Config → System → Zwei Seiten** ausfüllen:
* Community-Hub: `https://hub.d4rkst3r.de`
* Bot-Produktseite: `https://bot.d4rkst3r.de`
* Login-Domain: `.d4rkst3r.de` (damit die Anmeldung auf beiden gilt)
@@ -43,7 +43,7 @@ Konfigurieren.
| `/features` | Alle Funktionen ausführlich, gruppiert (Community, Moderation, Feeds, Server) |
| `/commands` | Slash-Command-Referenz, aus dem Code erzeugt |
| `/docs` | Kurzanleitungen: API v1, SSO, Webhooks, Design-System |
| `/dashboard` | die heutige Setup-Seite (Login + Berechtigung nötig) |
| `/dashboard` | die heutige Config-Seite (Login + Berechtigung nötig) |
| `/impressum`, `/datenschutz` | Rechtstexte (auf beiden Domains identisch) |
Die 16 Slash-Commands, die es aktuell gibt: `ping`, `bug`, `wunsch`,
@@ -83,7 +83,7 @@ Zeile ändern, ohne dass ein Commit nötig wird.
Deshalb ein schlanker Seiten-Editor statt eines Forums:
* Seiten werden im Setup angelegt: Titel, Adress-Kürzel, Inhalt in Markdown
* Seiten werden in der Config angelegt: Titel, Adress-Kürzel, Inhalt in Markdown
* Live-Vorschau wie im Composer, gerendert mit dem vorhandenen Markdown-Parser
* pro Seite wählbar: veröffentlicht oder Entwurf, im Menü sichtbar oder nur
über den Direktlink erreichbar, Reihenfolge im Menü
@@ -185,7 +185,7 @@ Login-Umleitung und Dashboard-Verweise nutzen `botUrl()`.
4. Frontend in `shared/`, `hub/` und `bot/` aufteilen, zwei Builds einrichten
5. Host-Routing in Backend und Frontend (siehe oben)
6. Bot-Landing, Feature- und Command-Seite bauen
7. Setup-Seite nach `/dashboard` umhängen
7. Config-Seite nach `/dashboard` umhängen
8. Weiterleitungen für alte Links
9. Cookie-Domain umstellen
10. `hub_url`/`bot_url` in den Einstellungen ergänzen und alle Verwendungen umziehen
+3 -3
View File
@@ -6,7 +6,7 @@ und bekommst dabei gleich die **Discord-Rollen** des Users mitgeliefert.
## 1. App registrieren
Setup**API***Single Sign-On*:
Config**API***Single Sign-On*:
| Feld | Beispiel | Bedeutung |
|---|---|---|
@@ -116,7 +116,7 @@ Brand-Tokens und eine fertige Navigation aus:
Die Farben kommen live aus dem Brand-Tab — änderst du sie dort, ändern sie sich
überall mit (Cache: 5 Minuten).
* **`/brand-nav.js`** — fügt oben die D4RKST3R-Leiste ein, mit Links zurück zum Hub
und zu allen im Setup gepflegten Diensten. Die Links stecken fertig im Skript,
und zu allen in der Config gepflegten Diensten. Die Links stecken fertig im Skript,
es sind also keine weiteren Anfragen und kein CORS nötig.
Die Schriften installierst du in der App selbst, damit sie lokal ausgeliefert werden
@@ -134,4 +134,4 @@ npm i @fontsource/bebas-neue @fontsource/barlow-condensed @fontsource/share-tech
* Rollen werden bei jedem Login frisch von Discord geholt. Wer eine Rolle verliert,
verliert die Rechte beim nächsten Login — für sofortige Wirkung die Sitzung in der
App kurz halten oder periodisch neu prüfen.
* Wird eine App im Setup gelöscht, funktioniert ihr Login sofort nicht mehr.
* Wird eine App in der Config gelöscht, funktioniert ihr Login sofort nicht mehr.
+2 -2
View File
@@ -136,7 +136,7 @@ export default function App() {
<NavLink to={`/${p.slug}`} key={p.slug}>{p.title}</NavLink>
))}
</NavDropdown>
{(me.admin || me.scopes?.length > 0) && <NavLink to="/settings">Setup</NavLink>}
{(me.admin || me.scopes?.length > 0) && <NavLink to="/settings">Config</NavLink>}
</nav>
<div className="auth">
@@ -178,7 +178,7 @@ export default function App() {
))}
{me.user && <NavLink to="/profil">Profil</NavLink>}
{me.admin && <NavLink to="/commits">Commits</NavLink>}
{(me.admin || me.scopes?.length > 0) && <NavLink to="/settings">Setup</NavLink>}
{(me.admin || me.scopes?.length > 0) && <NavLink to="/settings">Config</NavLink>}
{me.user
? <a href="/auth/logout">Logout</a>
: <a href="/auth/login">Login mit Discord</a>}
+1 -1
View File
@@ -30,7 +30,7 @@ export default function Impressum() {
{incomplete && (
<p className="notice">
Die Impressums-Angaben sind noch nicht vollständig hinterlegt
(Setup System Impressum).
(Config System Impressum).
</p>
)}
+168 -40
View File
@@ -6,6 +6,7 @@ import {
IconApplications, IconComposer, IconServer, IconSystem, IconApi, IconTeam,
IconLock, IconBot, IconUpload, IconChevronDown, IconKey, IconPages, IconX,
IconHome as IconLayers, IconWarning, IconCheck, IconMessage, IconRefresh,
IconSearch,
} from '../icons.jsx';
import { Markdown } from '../markdown.jsx';
@@ -16,22 +17,54 @@ const API_SCOPES = [
{ id: 'read', label: 'read', hint: 'Member-Infos & Statistiken lesen' },
];
// Bereiche der Seitenleiste — bei 15 Tabs findet man ohne Gruppierung nichts mehr.
const TAB_SECTIONS = [
{ id: 'ueberblick', label: 'Überblick' },
{ id: 'auftritt', label: 'Auftritt' },
{ id: 'inhalte', label: 'Inhalte' },
{ id: 'community', label: 'Community' },
{ id: 'technik', label: 'Technik' },
{ id: 'zugang', label: 'Zugang' },
];
// `find` sind zusätzliche Suchbegriffe: was jemand tippt, wenn er den Tab-Namen
// nicht kennt („geburtstag" → Community, „willkommen" → Support).
const TABS = [
{ id: 'status', Icon: IconStatus, label: 'Status' },
{ id: 'module', Icon: IconLayers, label: 'Module' },
{ id: 'texte', Icon: IconMessage, label: 'Texte' },
{ id: 'brand', Icon: IconBrand, label: 'Brand' },
{ id: 'feeds', Icon: IconFeeds, label: 'Feeds' },
{ id: 'community', Icon: IconCommunity, label: 'Community' },
{ id: 'rollen', Icon: IconRoles, label: 'Rollen' },
{ id: 'support', Icon: IconSupport, label: 'Support' },
{ id: 'bewerbungen', Icon: IconApplications, label: 'Bewerbungen' },
{ id: 'composer', Icon: IconComposer, label: 'Composer' },
{ id: 'seiten', Icon: IconPages, label: 'Seiten' },
{ id: 'server', Icon: IconServer, label: 'Server' },
{ id: 'system', Icon: IconSystem, label: 'System' },
{ id: 'api', Icon: IconApi, label: 'API' },
{ id: 'team', Icon: IconTeam, label: 'Team' },
{ id: 'status', Icon: IconStatus, label: 'Status', section: 'ueberblick',
find: 'uptime laufzeit datenbank backup version zahlen übersicht' },
{ id: 'module', Icon: IconLayers, label: 'Module', section: 'ueberblick',
find: 'funktionen an aus schalter aktivieren deaktivieren' },
{ id: 'texte', Icon: IconMessage, label: 'Texte', section: 'ueberblick',
find: 'nachrichten vorlagen platzhalter begrüßung wortlaut' },
{ id: 'brand', Icon: IconBrand, label: 'Brand', section: 'auftritt',
find: 'avatar banner farbe logo name footer aussehen embed' },
{ id: 'seiten', Icon: IconPages, label: 'Seiten', section: 'auftritt',
find: 'regeln über uns faq markdown menü impressum' },
{ id: 'feeds', Icon: IconFeeds, label: 'Feeds', section: 'inhalte',
find: 'devlog commit release changelog rückblick threads ping' },
{ id: 'composer', Icon: IconComposer, label: 'Composer', section: 'inhalte',
find: 'nachricht senden embed bauen tags trigger geplant zeitplan' },
{ id: 'community', Icon: IconCommunity, label: 'Community', section: 'community',
find: 'level xp starboard galerie geburtstag playtester alpha keys voting wünsche autorolle' },
{ id: 'rollen', Icon: IconRoles, label: 'Rollen', section: 'community',
find: 'rollen menü selbstbedienung buttons reaction roles' },
{ id: 'support', Icon: IconSupport, label: 'Support', section: 'community',
find: 'ticket modmail willkommen protokoll modlog moderation warnung' },
{ id: 'bewerbungen', Icon: IconApplications, label: 'Bewerbungen', section: 'community',
find: 'formular fragen bewerbung prüfen annehmen ablehnen' },
{ id: 'server', Icon: IconServer, label: 'Server', section: 'technik',
find: 'game server monitor fivem minecraft status spieler gamedig' },
{ id: 'system', Icon: IconSystem, label: 'System', section: 'technik',
find: 'backup gitea watchdog twitch youtube events sprachkanäle impressum datenschutz' },
{ id: 'api', Icon: IconApi, label: 'API', section: 'zugang',
find: 'schlüssel key token zugriff sso webhook' },
{ id: 'team', Icon: IconTeam, label: 'Team', section: 'zugang',
find: 'mitarbeiter rechte bereiche protokoll audit zugriff' },
];
// Welcher Team-Scope schaltet welchen Tab frei (null = nur Owner)
@@ -125,7 +158,15 @@ function fmtBytes(b) {
}
export default function Settings({ me }) {
const [tab, setTab] = useState('status');
// Tab steht in der Adresse (#texte), damit Neuladen und Links den Bereich behalten
const [tab, setTabState] = useState(
() => TABS.find((t) => t.id === window.location.hash.slice(1))?.id ?? 'status'
);
const setTab = (id) => {
setTabState(id);
window.history.replaceState(null, '', `#${id}`);
};
const [tabSearch, setTabSearch] = useState('');
const [data, setData] = useState(null);
const [form, setForm] = useState(null);
const [error, setError] = useState(null);
@@ -202,6 +243,12 @@ export default function Settings({ me }) {
return need ? me.scopes?.includes(need) : false;
};
const visibleTabs = TABS.filter((t) => canTab(t.id));
// Suche greift auf Beschriftung und Stichwörter — wer „geburtstag" tippt,
// landet bei Community, ohne den Tab-Namen zu kennen.
const query = tabSearch.trim().toLowerCase();
const foundTabs = query
? visibleTabs.filter((t) => `${t.label} ${t.find}`.toLowerCase().includes(query))
: visibleTabs;
// Tags + geplante Posts
const [tags, setTags] = useState([]);
const [tagDraft, setTagDraft] = useState({ name: '', content: '' });
@@ -666,11 +713,11 @@ export default function Settings({ me }) {
const header = (
<section className="page-header">
<div className="page-bg-text">SETUP</div>
<div className="page-bg-text">CONFIG</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">// Admin</div>
<h1 className="page-title">Setup</h1>
<h1 className="page-title">Config</h1>
<p className="page-subtitle">Alles an einem Ort Änderungen greifen sofort.</p>
</div>
</section>
@@ -706,21 +753,66 @@ export default function Settings({ me }) {
/* ── Tab-Inhalte ───────────────────────────────── */
// Eingeschaltete Module, denen noch etwas fehlt — die tun bis dahin nichts
const activeModules = modules.filter((m) => m.enabled);
const unfinished = activeModules.filter((m) => m.missing.length > 0);
const tabStatus = (
<div className="settings-section">
<h2 className="settings-title">// Status</h2>
<div className="stats">
<div className="stat"><span className="stat-value">{data.status.botTag ?? '—'}</span><span className="stat-label">Bot</span></div>
<div className="stat"><span className="stat-value">{fmtUptime(data.status.uptimeSeconds)}</span><span className="stat-label">Uptime</span></div>
<div className="stat"><span className="stat-value">{data.status.devlogs}</span><span className="stat-label">Devlogs</span></div>
<div className="stat"><span className="stat-value">{data.status.commits}</span><span className="stat-label">Commits</span></div>
<div className="stat"><span className="stat-value">{fmtBytes(data.status.dbSizeBytes)}</span><span className="stat-label">Datenbank</span></div>
<div className="stat">
<span className="stat-value">{data.status.lastBackup ? data.status.lastBackup.slice(0, 10) : '—'}</span>
<span className="stat-label">Letztes Backup</span>
<>
<div className="settings-section">
<h2 className="settings-title">// Status</h2>
<div className="stats">
<div className="stat"><span className="stat-value">{data.status.botTag ?? '—'}</span><span className="stat-label">Bot</span></div>
<div className="stat"><span className="stat-value">{fmtUptime(data.status.uptimeSeconds)}</span><span className="stat-label">Uptime</span></div>
{modules.length > 0 && (
<div className="stat">
<span className="stat-value">{activeModules.length}<span className="stat-of">/{modules.length}</span></span>
<span className="stat-label">Module an</span>
</div>
)}
<div className="stat"><span className="stat-value">{data.status.devlogs}</span><span className="stat-label">Devlogs</span></div>
<div className="stat"><span className="stat-value">{data.status.commits}</span><span className="stat-label">Commits</span></div>
<div className="stat"><span className="stat-value">{fmtBytes(data.status.dbSizeBytes)}</span><span className="stat-label">Datenbank</span></div>
<div className="stat">
<span className="stat-value">{data.status.lastBackup ? data.status.lastBackup.slice(0, 10) : '—'}</span>
<span className="stat-label">Letztes Backup</span>
</div>
</div>
</div>
</div>
{modules.length > 0 && (
<div className="settings-section">
<h2 className="settings-title">// Noch einzurichten</h2>
{unfinished.length === 0 ? (
<p className="section-intro todo-done with-icon">
<IconCheck size={15} /> Alles eingerichtet jedes eingeschaltete Modul hat, was es braucht.
</p>
) : (
<>
<p className="section-intro">
Diese Module sind eingeschaltet, aber es fehlt ihnen noch etwas
bis dahin tun sie nichts.
</p>
<div className="key-list">
{unfinished.map((m) => (
<div className="key-row" key={m.id}>
<span className="key-name">{m.name}</span>
<span className="todo-missing with-icon">
<IconWarning size={13} /> {m.missing.join(', ')}
</span>
{m.tab && (
<button className="btn-mini" onClick={() => setTab(m.tab)}>
Einrichten
</button>
)}
</div>
))}
</div>
</>
)}
</div>
)}
</>
);
const statusDot = { online: '#23a55a', idle: '#f0b232', dnd: '#f23f43' }[form.bot_presence_status] ?? '#23a55a';
@@ -2089,16 +2181,52 @@ export default function Settings({ me }) {
<section className="content content-wide">
<div className="settings-layout">
<nav className="settings-nav">
{visibleTabs.map((t) => (
<button
key={t.id}
className={`settings-nav-item ${tab === t.id ? 'active' : ''}`}
onClick={() => setTab(t.id)}
>
<span className="settings-nav-icon"><t.Icon /></span>
{t.label}
</button>
))}
<div className="settings-search">
<IconSearch size={15} />
<input
type="text"
value={tabSearch}
placeholder="Suchen "
aria-label="Bereich suchen"
onChange={(e) => setTabSearch(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && foundTabs[0]) {
setTab(foundTabs[0].id);
setTabSearch('');
}
if (e.key === 'Escape') setTabSearch('');
}}
/>
{tabSearch && (
<button className="settings-search-clear" aria-label="Suche leeren" onClick={() => setTabSearch('')}>
<IconX size={13} />
</button>
)}
</div>
{foundTabs.length === 0 && (
<p className="settings-nav-empty">Nichts gefunden.</p>
)}
{TAB_SECTIONS.map((section) => {
const items = foundTabs.filter((t) => t.section === section.id);
if (items.length === 0) return null;
return (
<div className="settings-nav-group" key={section.id}>
<span className="settings-nav-label">{section.label}</span>
{items.map((t) => (
<button
key={t.id}
className={`settings-nav-item ${tab === t.id ? 'active' : ''}`}
onClick={() => setTab(t.id)}
>
<span className="settings-nav-icon"><t.Icon /></span>
{t.label}
</button>
))}
</div>
);
})}
</nav>
<div className="settings-content">
+60 -4
View File
@@ -635,9 +635,59 @@ a.sha:hover { border-color: var(--neon); background: rgba(245, 197, 24, .07); }
gap: .25rem;
position: sticky;
top: 5rem;
min-width: 11rem;
width: 12.5rem;
flex-shrink: 0;
/* Bei 15 Bereichen wird die Leiste höher als der Bildschirm — dann scrollt sie selbst */
max-height: calc(100vh - 6.5rem);
overflow-y: auto;
padding-right: .3rem;
}
.settings-nav::-webkit-scrollbar { width: 5px; }
.settings-nav::-webkit-scrollbar-thumb { background: var(--border); border-radius: 99px; }
/* Suche über den Bereichen */
.settings-search {
display: flex; align-items: center; gap: .5rem;
background: var(--bg2); border: 1px solid var(--border);
border-radius: 9px; padding: .45rem .6rem;
margin-bottom: .5rem; color: var(--muted2);
transition: border-color .15s;
}
.settings-search:focus-within { border-color: var(--neon); }
.settings-search input {
flex: 1; min-width: 0;
background: none; border: 0; outline: none; padding: 0;
font-family: var(--mono); font-size: .72rem; letter-spacing: .08em;
color: var(--text);
}
.settings-search input::placeholder { color: var(--muted2); letter-spacing: .12em; }
.settings-search-clear {
background: none; border: 0; padding: 0; display: inline-flex;
color: var(--muted2); cursor: pointer;
}
.settings-search-clear:hover { color: var(--neon); }
/* Bereichs-Gruppen */
.settings-nav-group { display: flex; flex-direction: column; gap: .2rem; margin-bottom: .7rem; }
.settings-nav-label {
font-family: var(--mono);
font-size: .55rem; letter-spacing: .22em; text-transform: uppercase;
color: var(--muted2);
padding: .3rem .9rem .25rem;
}
.settings-nav-empty {
font-family: var(--mono); font-size: .66rem; letter-spacing: .1em;
color: var(--muted2); padding: .6rem .9rem;
}
/* Status-Seite: „Noch einzurichten" */
.stat-of { color: var(--muted2); font-size: .6em; }
.todo-missing {
font-family: var(--mono); font-size: .62rem;
letter-spacing: .1em; text-transform: uppercase;
color: var(--neon2); flex: 1;
}
.todo-done { color: var(--success); }
.settings-nav-item {
display: flex; align-items: center; gap: .7rem;
font-family: var(--mono);
@@ -712,15 +762,21 @@ a.sha:hover { border-color: var(--neon); background: rgba(245, 197, 24, .07); }
@media (max-width: 760px) {
.settings-layout { flex-direction: column; }
/* Schmal wird aus der Leiste eine Zeile: Suche oben, Bereiche daneben scrollend.
Die Gruppen-Überschriften fallen weg — waagerecht bringen sie nichts. */
.settings-nav {
position: static;
flex-direction: row;
overflow-x: auto;
flex-wrap: wrap;
overflow: visible;
max-height: none;
width: 100%;
min-width: 0;
gap: .35rem;
padding-bottom: .35rem;
padding: 0 0 .35rem;
}
.settings-search { width: 100%; margin-bottom: .2rem; }
.settings-nav-group { flex-direction: row; flex-wrap: wrap; margin-bottom: 0; gap: .35rem; }
.settings-nav-label { display: none; }
.settings-nav-item {
border-left: none;
border-bottom: 2px solid transparent;
+1 -1
View File
@@ -13,7 +13,7 @@ export async function execute(interaction) {
const channelId = screenshotChannelId();
if (!channelId) {
await interaction.editReply('❌ Erst auf der Setup-Seite einen Screenshot-Kanal wählen.');
await interaction.editReply('❌ Erst auf der Config-Seite einen Screenshot-Kanal wählen.');
return;
}
const channel = await interaction.client.channels.fetch(channelId);
+1 -1
View File
@@ -14,7 +14,7 @@ export const data = new SlashCommandBuilder()
export async function execute(interaction) {
if (!playtesterRoleId()) {
await interaction.reply({
content: '❌ Erst auf der Setup-Seite eine Playtester-Rolle wählen.',
content: '❌ Erst auf der Config-Seite eine Playtester-Rolle wählen.',
flags: MessageFlags.Ephemeral,
});
return;
+1 -1
View File
@@ -1,4 +1,4 @@
// /tag — gespeicherte Text-Bausteine abrufen (verwaltet über die Setup-Seite)
// /tag — gespeicherte Text-Bausteine abrufen (verwaltet über die Config-Seite)
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
import { getTag, listTags } from '../../db.js';
+1 -1
View File
@@ -14,7 +14,7 @@ export const data = new SlashCommandBuilder()
export async function execute(interaction) {
if (!ticketChannelId()) {
await interaction.reply({
content: '❌ Erst auf der Setup-Seite einen Ticket-Kanal wählen.',
content: '❌ Erst auf der Config-Seite einen Ticket-Kanal wählen.',
flags: MessageFlags.Ephemeral,
});
return;
+1 -1
View File
@@ -1,4 +1,4 @@
// Release-Ankündigungen: großes Embed in den konfigurierten Kanal (Setup-Seite)
// Release-Ankündigungen: großes Embed in den konfigurierten Kanal (Config-Seite)
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js';
import { releaseChannelId, publicUrl, brandColor2, brandFooter } from '../runtime-settings.js';
import { config } from '../config.js';
+1 -1
View File
@@ -1,5 +1,5 @@
// Watchdog: prüft konfigurierte URLs und schickt dem Admin eine DM bei Ausfall.
// URLs kommen aus dem Setting watchdog_urls (Setup-Seite), leer = deaktiviert.
// URLs kommen aus dem Setting watchdog_urls (Config-Seite), leer = deaktiviert.
import { getSetting } from '../db.js';
import { config } from '../config.js';
import { moduleEnabled } from '../modules.js';
+1 -1
View File
@@ -75,7 +75,7 @@ export function discordGuildId() {
return getSetting('discord_guild_id') || config.discordGuildId || null;
}
/* ── Branding (Setup → Brand-Tab) ── */
/* ── Branding (Config → Brand-Tab) ── */
/** Brand-Name für Footer/Embeds (Default: D4RKST3R) */
export function brandName() {
+1 -1
View File
@@ -1,5 +1,5 @@
// Öffentliche v1-API für eigene Skripte/Dienste (devlog.py, Platform, FiveM, CI …)
// Auth: Authorization: Bearer d4rk_<key> — Keys + Scopes über die Setup-Seite.
// Auth: Authorization: Bearer d4rk_<key> — Keys + Scopes über die Config-Seite.
import { verifyApiKey, archiveStats } from '../db.js';
/** Bearer-Key prüfen; bei Erfolg Key-Info zurückgeben, sonst Fehler-Antwort senden */