Config-Suche findet einzelne Einstellungen
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

Die Seitenleisten-Suche kannte nur Bereiche: „starboard" führte zu
Community, aber nicht zum Schwellwert-Feld. Bei rund 60 Einstellungen
heißt das raten, in welchem Bereich etwas liegt.

setting-index.js hält jetzt fest, wo jede Einstellung wohnt, mit
Suchwörtern und Synonymen. Die Suche zeigt Treffer als eigene Gruppe unter
den Bereichen; ein Klick wechselt den Bereich, scrollt zum Feld und hebt es
kurz hervor. Enter nimmt den ersten Treffer, wobei Einstellungen Vorrang
vor Bereichen haben — wer „schwellwert" tippt, will das Feld.

Die Felder tragen dafür data-setting="<schlüssel>"; das Attribut wurde
skriptgesteuert ergänzt, ohne die Struktur anzufassen. Fünf Einstellungen
teilen sich ein Feld mit einer anderen (etwa die beiden Markenfarben) —
die zeigen über `target` auf den Nachbarn.

Textfelder werden beim Sprung fokussiert, Kanal-Auswahlen bewusst nicht:
deren Fokus klappt die Liste auf und verdeckt alles darunter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 18:22:35 +02:00
co-authored by Claude Fable 5
parent 1c3fda8d64
commit 0240225165
4 changed files with 241 additions and 62 deletions
+8 -2
View File
@@ -128,8 +128,14 @@ Code, die Datenbank enthält nur echte Abweichungen.
| 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.
Das Suchfeld über der Leiste findet **Bereiche und einzelne Einstellungen**:
„geburtstag" führt zu Community, „schwellwert" direkt aufs Starboard-Feld, das
kurz hervorgehoben wird. Enter nimmt den ersten Treffer. Welche Einstellung wo
liegt, steht in [`frontend/src/setting-index.js`](frontend/src/setting-index.js).
Sobald sich etwas vom gespeicherten Stand unterscheidet, erscheint unten eine
Leiste mit Anzahl, *Verwerfen* und *Speichern*; beim Verlassen mit offenen
Änderungen fragt der Browser nach.
Der aktive Bereich steht in der Adresse (`/settings#texte`), Links und Neuladen
landen also wieder dort.
+102 -60
View File
@@ -9,6 +9,7 @@ import {
IconSearch, IconSystem as IconTune,
} from '../icons.jsx';
import { moduleIcon } from '../module-icons.jsx';
import { findSettings } from '../setting-index.js';
import { Markdown } from '../markdown.jsx';
const API_SCOPES = [
@@ -339,6 +340,27 @@ export default function Settings({ me }) {
const foundTabs = query
? visibleTabs.filter((t) => `${t.label} ${t.find}`.toLowerCase().includes(query))
: visibleTabs;
// … und zusätzlich auf einzelne Einstellungen, mit Sprung aufs Feld
const foundSettings = query ? findSettings(query, canTab) : [];
/** Bereich wechseln, zum Feld scrollen und es kurz hervorheben */
function jumpToSetting(entry) {
setTab(entry.tab);
setTabSearch('');
const key = entry.target ?? entry.id;
// Zwei Bilder warten, bis der neue Bereich wirklich im Dokument steht
requestAnimationFrame(() => requestAnimationFrame(() => {
const el = document.querySelector(`[data-setting="${key}"]`);
if (!el) return;
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
el.classList.add('field-found');
setTimeout(() => el.classList.remove('field-found'), 2200);
// Nur Textfelder fokussieren: bei der Kanal-Auswahl würde der Fokus
// die Liste aufklappen und alles darunter verdecken.
const feld = el.querySelector('input, textarea');
if (feld && !feld.closest('.picksel')) feld.focus({ preventScroll: true });
}));
}
// Tags + geplante Posts
const [tags, setTags] = useState([]);
const [tagDraft, setTagDraft] = useState({ name: '', content: '' });
@@ -1039,7 +1061,7 @@ export default function Settings({ me }) {
</label>
</div>
<div className="bot-card-right">
<div className="field">
<div className="field" data-setting="bot_name">
<label>Bot-Name <span className="field-hint">Discord erlaubt ~2 Umbenennungen pro Stunde</span></label>
<input type="text" maxLength={32} value={botName} onChange={(e) => setBotName(e.target.value)} />
</div>
@@ -1047,7 +1069,7 @@ export default function Settings({ me }) {
<label>Beschreibung <span className="field-hint">Über mich" im Bot-Profil (max 400 Zeichen)</span></label>
<textarea rows={3} maxLength={400} placeholder="z. B. Community-Bot für EcoGame Devlogs, Level, Tickets & mehr. 🤖" value={botDesc} onChange={(e) => setBotDesc(e.target.value)} />
</div>
<div className="field">
<div className="field" data-setting="bot_presence_status">
<label>Bot-Status</label>
<select value={form.bot_presence_status} onChange={(e) => setForm({ ...form, bot_presence_status: e.target.value })}>
<option value="online">🟢 Online</option>
@@ -1055,7 +1077,7 @@ export default function Settings({ me }) {
<option value="dnd">⛔ Bitte nicht stören</option>
</select>
</div>
<div className="field">
<div className="field" data-setting="bot_status_text">
<label>Aktivität <span className="field-hint">leer = Server-Monitor darf die Spielerzahl anzeigen</span></label>
<div className="field-row">
<select style={{ flex: 'none', width: '9rem' }} value={form.bot_status_type} onChange={(e) => setForm({ ...form, bot_status_type: e.target.value })}>
@@ -1072,11 +1094,11 @@ export default function Settings({ me }) {
<div className="settings-section">
<h2 className="settings-title">// Embeds</h2>
<div className="field">
<div className="field" data-setting="brand_name">
<label>Brand-Name <span className="field-hint">erscheint in allen Embed-Footern („NAME // DEVLOG")</span></label>
<input type="text" placeholder="D4RKST3R" value={form.brand_name} onChange={(e) => setForm({ ...form, brand_name: e.target.value })} />
</div>
<div className="field">
<div className="field" data-setting="brand_color">
<label>Embed-Farben <span className="field-hint">primär (Embeds) · sekundär (Releases, Mod-Aktionen)</span></label>
<div className="field-row">
<input type="color" className="color-pick" value={form.brand_color} onChange={(e) => setForm({ ...form, brand_color: e.target.value })} />
@@ -1088,7 +1110,7 @@ export default function Settings({ me }) {
<div className="settings-section">
<h2 className="settings-title">// Aus der Env verlagert</h2>
<div className="field">
<div className="field" data-setting="discord_guild_id">
<label>Guild-ID <span className="field-hint">für sofortige Slash-Commands; greift beim nächsten Neustart</span></label>
<input type="text" placeholder="Server-ID" value={form.discord_guild_id} onChange={(e) => setForm({ ...form, discord_guild_id: e.target.value })} />
</div>
@@ -1108,7 +1130,7 @@ export default function Settings({ me }) {
<>
<div className="settings-section">
<h2 className="settings-title">// Kanäle</h2>
<div className="field">
<div className="field" data-setting="devlog_channel_id">
<label>Devlog-Kanal <span className="field-hint">öffentlich hier postet der Bot die Devlogs</span></label>
<div className="field-row">
<PickSelect
@@ -1120,7 +1142,7 @@ export default function Settings({ me }) {
<button className="btn" onClick={() => sendTest('devlog')}>Test</button>
</div>
</div>
<div className="field">
<div className="field" data-setting="commit_channel_id">
<label>Commit-Kanal <span className="field-hint">privat hier landen die Push-Embeds</span></label>
<div className="field-row">
<PickSelect
@@ -1132,7 +1154,7 @@ export default function Settings({ me }) {
<button className="btn" onClick={() => sendTest('commit')}>Test</button>
</div>
</div>
<div className="field">
<div className="field" data-setting="release_channel_id">
<label>Release-Kanal <span className="field-hint">Ankündigungen bei neuen Gitea-Releases leer = aus</span></label>
<div className="field-row">
<PickSelect
@@ -1148,7 +1170,7 @@ export default function Settings({ me }) {
<div className="settings-section">
<h2 className="settings-title">// Devlog</h2>
<div className="field">
<div className="field" data-setting="devlog_ping_role_id">
<label>Ping-Rolle <span className="field-hint">wird bei jedem Devlog erwähnt; 🔔-Button am Post leer = kein Ping</span></label>
<PickSelect
items={roleItems}
@@ -1157,14 +1179,14 @@ export default function Settings({ me }) {
empty="— kein Ping —"
/>
</div>
<div className="field">
<div className="field" data-setting="weekly_recap_enabled">
<label className="toggle">
<input type="checkbox" checked={form.weekly_recap_enabled} onChange={(e) => setForm({ ...form, weekly_recap_enabled: e.target.checked })} />
<span>Wochen-Rückblick (sonntags 20:00)</span>
<span className="field-hint">Commits pro Tag, Devlogs & Top-Projekte</span>
</label>
</div>
<div className="field">
<div className="field" data-setting="devlog_threads_enabled">
<label className="toggle">
<input type="checkbox" checked={form.devlog_threads_enabled} onChange={(e) => setForm({ ...form, devlog_threads_enabled: e.target.checked })} />
<span>Diskussions-Thread unter jedem Devlog</span>
@@ -1178,18 +1200,18 @@ export default function Settings({ me }) {
<div className="settings-section">
<h2 className="settings-title">// Commit-Feed</h2>
<div className="field">
<div className="field" data-setting="commit_feed_enabled">
<label className="toggle">
<input type="checkbox" checked={form.commit_feed_enabled} onChange={(e) => setForm({ ...form, commit_feed_enabled: e.target.checked })} />
<span>Push-Embeds posten</span>
<span className="field-hint">aus = weiter archivieren, nur nicht posten</span>
</label>
</div>
<div className="field">
<div className="field" data-setting="commit_branch_filter">
<label>Branch-Filter <span className="field-hint">kommagetrennt, leer = alle</span></label>
<input type="text" placeholder="z. B. main, release" value={form.commit_branch_filter} onChange={(e) => setForm({ ...form, commit_branch_filter: e.target.value })} />
</div>
<div className="field">
<div className="field" data-setting="ignored_repos">
<label>Ignorierte Repos <span className="field-hint">kommagetrennt, volle Namen</span></label>
<input type="text" placeholder="z. B. D4rkst3r/d4rkbot" value={form.ignored_repos} onChange={(e) => setForm({ ...form, ignored_repos: e.target.value })} />
</div>
@@ -1201,7 +1223,7 @@ export default function Settings({ me }) {
<>
<div className="settings-section">
<h2 className="settings-title">// Geburtstage</h2>
<div className="field">
<div className="field" data-setting="birthday_channel_id">
<label>Geburtstags-Kanal <span className="field-hint">Member tragen sich mit /geburtstag ein; Gratulation morgens ab 09:00 — leer = aus</span></label>
<PickSelect
items={channelItems}
@@ -1210,7 +1232,7 @@ export default function Settings({ me }) {
empty=" deaktiviert "
/>
</div>
<div className="field">
<div className="field" data-setting="birthday_role_id">
<label>Geburtstags-Rolle <span className="field-hint">gibt's für den Tag, wird am nächsten Morgen wieder entfernt — leer = keine</span></label>
<PickSelect
items={roleItems}
@@ -1223,7 +1245,7 @@ export default function Settings({ me }) {
<div className="settings-section">
<h2 className="settings-title">// Playtester</h2>
<div className="field">
<div className="field" data-setting="playtester_role_id">
<label>Playtester-Rolle <span className="field-hint">Bewerbungs-Post via /playtester-setup; leer = aus</span></label>
<PickSelect
items={roleItems}
@@ -1279,7 +1301,7 @@ export default function Settings({ me }) {
<div className="settings-section">
<h2 className="settings-title">// Starboard & Galerie</h2>
<div className="field">
<div className="field" data-setting="starboard_channel_id">
<label>Starboard-Kanal <span className="field-hint">Nachrichten mit genug ⭐ landen hier</span></label>
<div className="field-row">
<PickSelect
@@ -1291,7 +1313,7 @@ export default function Settings({ me }) {
<input type="text" style={{ width: '6rem', flex: 'none' }} title="Schwellwert (Anzahl )" value={form.starboard_threshold} onChange={(e) => setForm({ ...form, starboard_threshold: e.target.value })} />
</div>
</div>
<div className="field">
<div className="field" data-setting="screenshot_channel_id">
<label>Screenshot-Kanal <span className="field-hint">Bilder → öffentliche Galerie; Historie: /galerie-backfill</span></label>
<PickSelect
items={channelItems}
@@ -1300,7 +1322,7 @@ export default function Settings({ me }) {
empty=" deaktiviert "
/>
</div>
<div className="field">
<div className="field" data-setting="voting_channel_id">
<label>Voting-Kanal <span className="field-hint">/wunsch postet hier; Rangliste auf /roadmap</span></label>
<PickSelect
items={channelItems}
@@ -1313,21 +1335,21 @@ export default function Settings({ me }) {
<div className="settings-section">
<h2 className="settings-title">// Level-System</h2>
<div className="field">
<div className="field" data-setting="levels_enabled">
<label className="toggle">
<input type="checkbox" checked={form.levels_enabled} onChange={(e) => setForm({ ...form, levels_enabled: e.target.checked })} />
<span>XP fürs Schreiben vergeben</span>
<span className="field-hint">1525 XP pro Nachricht, 60s Cooldown; /rank + Bestenliste auf /level</span>
</label>
</div>
<div className="field">
<div className="field" data-setting="levels_announce">
<label className="toggle">
<input type="checkbox" checked={form.levels_announce} onChange={(e) => setForm({ ...form, levels_announce: e.target.checked })} />
<span>Level-Up im Kanal feiern</span>
<span className="field-hint">„🎉 Name ist jetzt Level 5!"</span>
</label>
</div>
<div className="field">
<div className="field" data-setting="level_rewards">
<label>Rollen-Belohnungen <span className="field-hint">Format: Level=Rollen-ID, kommagetrennt z. B. 5=1234, 10=5678</span></label>
<input type="text" placeholder="5=RollenID, 10=RollenID" value={form.level_rewards} onChange={(e) => setForm({ ...form, level_rewards: e.target.value })} />
</div>
@@ -1339,7 +1361,7 @@ export default function Settings({ me }) {
<>
<div className="settings-section">
<h2 className="settings-title">// Rollen bei Join</h2>
<div className="field">
<div className="field" data-setting="autorole_id">
<label>Auto-Rolle <span className="field-hint">bekommt jeder neue Member automatisch leer = aus</span></label>
<PickSelect
items={roleItems}
@@ -1348,7 +1370,7 @@ export default function Settings({ me }) {
empty="— deaktiviert —"
/>
</div>
<div className="field">
<div className="field" data-setting="sticky_roles_enabled">
<label className="toggle">
<input type="checkbox" checked={form.sticky_roles_enabled} onChange={(e) => setForm({ ...form, sticky_roles_enabled: e.target.checked })} />
<span>Sticky-Roles</span>
@@ -1461,7 +1483,7 @@ export default function Settings({ me }) {
<>
<div className="settings-section">
<h2 className="settings-title">// Moderation &amp; Kontakt</h2>
<div className="field">
<div className="field" data-setting="modmail_channel_id">
<label>Modmail-Kanal <span className="field-hint">PRIVATER Staff-Kanal DMs an den Bot landen hier als Threads</span></label>
<PickSelect
items={channelItems}
@@ -1470,7 +1492,7 @@ export default function Settings({ me }) {
empty="— deaktiviert —"
/>
</div>
<div className="field">
<div className="field" data-setting="ticket_channel_id">
<label>Ticket-Kanal <span className="field-hint">private Ticket-Threads; Post via /ticket-setup</span></label>
<PickSelect
items={channelItems}
@@ -1479,7 +1501,7 @@ export default function Settings({ me }) {
empty="— deaktiviert —"
/>
</div>
<div className="field">
<div className="field" data-setting="welcome_channel_id">
<label>Willkommens-Kanal <span className="field-hint">Begrüßungs-Embed für neue Member</span></label>
<PickSelect
items={channelItems}
@@ -1488,7 +1510,7 @@ export default function Settings({ me }) {
empty="— deaktiviert —"
/>
</div>
<div className="field">
<div className="field" data-setting="modlog_channel_id">
<label>Mod-Log-Kanal <span className="field-hint">PRIVAT gelöschte/bearbeitete Nachrichten</span></label>
<PickSelect
items={channelItems}
@@ -1506,7 +1528,7 @@ export default function Settings({ me }) {
zeigt sofort, wie es aussieht gespeichert wird erst mit dem Knopf unten.
</p>
<div className="field">
<div className="field" data-setting="welcome_card_enabled">
<label className="toggle">
<input
type="checkbox"
@@ -1541,7 +1563,7 @@ export default function Settings({ me }) {
))}
</div>
<div className="field">
<div className="field" data-setting="welcome_card_kicker">
<label>Kleine Zeile <span className="field-hint">über dem Namen leer = weglassen</span></label>
<input
type="text" maxLength={24} placeholder="WILLKOMMEN"
@@ -1549,7 +1571,7 @@ export default function Settings({ me }) {
onChange={(e) => setForm({ ...form, welcome_card_kicker: e.target.value })}
/>
</div>
<div className="field">
<div className="field" data-setting="welcome_card_sub">
<label>Zeile darunter <span className="field-hint">{'{count}'} wird zur Mitglieds-Nummer leer = weglassen</span></label>
<input
type="text" maxLength={60} placeholder="Member #{count}"
@@ -1557,7 +1579,7 @@ export default function Settings({ me }) {
onChange={(e) => setForm({ ...form, welcome_card_sub: e.target.value })}
/>
</div>
<div className="field">
<div className="field" data-setting="welcome_card_image">
<label>Hintergrundbild <span className="field-hint">https-Adresse, 900×300 passt genau leer = nur Farbe mit Raster</span></label>
<input
type="text" placeholder="https://cdn.d4rkst3r.de/welcome.png"
@@ -1565,7 +1587,7 @@ export default function Settings({ me }) {
onChange={(e) => setForm({ ...form, welcome_card_image: e.target.value })}
/>
</div>
<div className="field">
<div className="field" data-setting="welcome_card_watermark">
<label className="toggle">
<input
type="checkbox"
@@ -1783,11 +1805,11 @@ export default function Settings({ me }) {
<>
<div className="settings-section">
<h2 className="settings-title">// Allgemein</h2>
<div className="field">
<div className="field" data-setting="public_url">
<label>Öffentliche URL <span className="field-hint">Basis für Buttons, RSS, OAuth-Redirect</span></label>
<input type="text" placeholder="https://bot.d4rkst3r.de" value={form.public_url} onChange={(e) => setForm({ ...form, public_url: e.target.value })} />
</div>
<div className="field">
<div className="field" data-setting="gitea_url">
<label>Gitea-URL</label>
<input type="text" placeholder="https://git.d4rkst3r.de" value={form.gitea_url} onChange={(e) => setForm({ ...form, gitea_url: e.target.value })} />
</div>
@@ -1801,23 +1823,23 @@ export default function Settings({ me }) {
Sind sie gesetzt, leitet der Server falsch aufgerufene Seiten automatisch
auf die richtige Adresse um.
</p>
<div className="field">
<div className="field" data-setting="hub_url">
<label>Community-Hub <span className="field-hint">Devlogs, Roadmap, Server, Profile Ziel für Devlog-Buttons, RSS und Link-Vorschauen</span></label>
<input type="text" placeholder="https://hub.d4rkst3r.de" value={form.hub_url ?? ''} onChange={(e) => setForm({ ...form, hub_url: e.target.value })} />
</div>
<div className="field">
<div className="field" data-setting="bot_url">
<label>Bot-Produktseite <span className="field-hint">Funktionen, Befehle und dieses Dashboard</span></label>
<input type="text" placeholder="https://bot.d4rkst3r.de" value={form.bot_url ?? ''} onChange={(e) => setForm({ ...form, bot_url: e.target.value })} />
</div>
<div className="field">
<div className="field" data-setting="cookie_domain">
<label>Login-Domain <span className="field-hint">z. B. <code>.d4rkst3r.de</code> dann gilt die Anmeldung auf beiden Seiten; leer = getrennte Logins</span></label>
<input type="text" placeholder=".d4rkst3r.de" value={form.cookie_domain ?? ''} onChange={(e) => setForm({ ...form, cookie_domain: e.target.value })} />
</div>
<div className="field">
<div className="field" data-setting="bug_report_repo">
<label>Bug-Repo <span className="field-hint">Ziel für /bug-Issues{data.status.giteaTokenConfigured ? '' : ' — ⚠️ kein GITEA_API_TOKEN, /bug ist aus'}</span></label>
<input type="text" placeholder="D4rkst3r/EcoGame" value={form.bug_report_repo} onChange={(e) => setForm({ ...form, bug_report_repo: e.target.value })} />
</div>
<div className="field">
<div className="field" data-setting="roadmap_repo">
<label>Roadmap-Repo <span className="field-hint">Milestones öffentliche /roadmap</span></label>
<input type="text" placeholder="D4rkst3r/EcoGame" value={form.roadmap_repo} onChange={(e) => setForm({ ...form, roadmap_repo: e.target.value })} />
</div>
@@ -1867,15 +1889,15 @@ export default function Settings({ me }) {
ist eine ladungsfähige Anschrift nötig (kein Postfach) nur der Owner sieht
und ändert diese Felder.
</p>
<div className="field">
<div className="field" data-setting="legal_name">
<label>Name / Verantwortlicher</label>
<input type="text" placeholder="Vorname Nachname" value={form.legal_name ?? ''} onChange={(e) => setForm({ ...form, legal_name: e.target.value })} />
</div>
<div className="field">
<div className="field" data-setting="legal_address">
<label>Anschrift <span className="field-hint">mehrzeilig: Straße, PLZ Ort, Land</span></label>
<textarea rows={3} placeholder={'Musterstraße 1\n12345 Musterstadt\nDeutschland'} value={form.legal_address ?? ''} onChange={(e) => setForm({ ...form, legal_address: e.target.value })} />
</div>
<div className="field">
<div className="field" data-setting="legal_email">
<label>Kontakt-E-Mail <span className="field-hint">Pflichtangabe muss erreichbar sein</span></label>
<input type="email" placeholder="kontakt@d4rkst3r.de" value={form.legal_email ?? ''} onChange={(e) => setForm({ ...form, legal_email: e.target.value })} />
</div>
@@ -1887,14 +1909,14 @@ export default function Settings({ me }) {
<div className="settings-section">
<h2 className="settings-title">// Web-Login (Member)</h2>
<div className="field">
<div className="field" data-setting="member_gate_enabled">
<label className="toggle">
<input type="checkbox" checked={form.member_gate_enabled} onChange={(e) => setForm({ ...form, member_gate_enabled: e.target.checked })} />
<span>Login nur für Discord-Member</span>
<span className="field-hint">Nicht-Member werden nach dem Discord-Login abgewiesen (braucht gesetzte Guild-ID)</span>
</label>
</div>
<div className="field">
<div className="field" data-setting="discord_invite_url">
<label>Discord-Invite-Link <span className="field-hint">wird Abgewiesenen als Zum Discord"-Button angezeigt</span></label>
<input type="text" placeholder="https://discord.gg/…" value={form.discord_invite_url} onChange={(e) => setForm({ ...form, discord_invite_url: e.target.value })} />
</div>
@@ -1902,7 +1924,7 @@ export default function Settings({ me }) {
<div className="settings-section">
<h2 className="settings-title">// Events, Social &amp; Voice</h2>
<div className="field">
<div className="field" data-setting="events_announce_channel_id">
<label>Event-Ankündigungs-Kanal <span className="field-hint">neue Discord-Events werden hier angekündigt + Events-Seite</span></label>
<PickSelect
items={channelItems}
@@ -1911,7 +1933,7 @@ export default function Settings({ me }) {
empty="— deaktiviert —"
/>
</div>
<div className="field">
<div className="field" data-setting="social_announce_channel_id">
<label>Social-Kanal <span className="field-hint">🔴 Twitch-Live + neue YouTube-Videos</span></label>
<PickSelect
items={channelItems}
@@ -1920,7 +1942,7 @@ export default function Settings({ me }) {
empty="— deaktiviert —"
/>
</div>
<div className="field">
<div className="field" data-setting="youtube_channel_id">
<div className="field-row">
<input type="text" placeholder="YouTube-Kanal-ID (UC…)" value={form.youtube_channel_id} onChange={(e) => setForm({ ...form, youtube_channel_id: e.target.value })} />
<input type="text" placeholder="Twitch-Login (z. B. d4rkst3r)" value={form.twitch_channel} onChange={(e) => setForm({ ...form, twitch_channel: e.target.value })} />
@@ -1933,7 +1955,7 @@ export default function Settings({ me }) {
<input type="password" placeholder="Client-Secret" value={twitchCreds.secret} onChange={(e) => setTwitchCreds({ ...twitchCreds, secret: e.target.value })} />
</div>
</div>
<div className="field">
<div className="field" data-setting="tempvoice_channel_id">
<label>Temp-Voice-Hub <span className="field-hint">Join to Create": Beitritt erzeugt eigenen Voice-Kanal (Bot braucht Kanäle verwalten + Member verschieben)</span></label>
<PickSelect
items={channelItems}
@@ -1946,14 +1968,14 @@ export default function Settings({ me }) {
<div className="settings-section">
<h2 className="settings-title">// Backups &amp; Watchdog</h2>
<div className="field">
<div className="field" data-setting="backup_enabled">
<label className="toggle">
<input type="checkbox" checked={form.backup_enabled} onChange={(e) => setForm({ ...form, backup_enabled: e.target.checked })} />
<span>Nächtliches DB-Backup (03:00)</span>
<span className="field-hint">gzip, 14 Tage Rotation</span>
</label>
</div>
<div className="field">
<div className="field" data-setting="backup_channel_id">
<label>Backup-Upload-Kanal <span className="field-hint">Offsite-Kopie in PRIVATEN Kanal — leer = nur lokal</span></label>
<div className="field-row">
<PickSelect
@@ -1965,7 +1987,7 @@ export default function Settings({ me }) {
<button className="btn" onClick={() => sendTest('backup')}>Backup jetzt</button>
</div>
</div>
<div className="field">
<div className="field" data-setting="repo_backup_enabled">
<label className="toggle">
<input type="checkbox" checked={form.repo_backup_enabled} onChange={(e) => setForm({ ...form, repo_backup_enabled: e.target.checked })} />
<span>Repo-Backups (04:00)</span>
@@ -1975,7 +1997,7 @@ export default function Settings({ me }) {
<button className="btn" onClick={() => sendTest('repobackup')}>Repos jetzt sichern</button>
</div>
</div>
<div className="field">
<div className="field" data-setting="watchdog_urls">
<label>Watchdog-URLs <span className="field-hint">kommagetrennt; DM bei Ausfall — leer = aus</span></label>
<input type="text" placeholder="https://git.d4rkst3r.de, https://bot.d4rkst3r.de/health" value={form.watchdog_urls} onChange={(e) => setForm({ ...form, watchdog_urls: e.target.value })} />
</div>
@@ -2089,7 +2111,7 @@ export default function Settings({ me }) {
Jeder Server bekommt sein eigenes Live-Status-Embed im Status-Kanal (🟢/🔴,
Spieler-Balken, Map, Adresse) aktualisiert alle 2 Minuten, DiscordGSM-Style.
</p>
<div className="field">
<div className="field" data-setting="status_channel_id">
<div className="field-row">
<div style={{ flex: 1 }}>
<label>Status-Kanal</label>
@@ -2666,9 +2688,11 @@ export default function Settings({ me }) {
aria-label="Bereich suchen"
onChange={(e) => setTabSearch(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && foundTabs[0]) {
setTab(foundTabs[0].id);
setTabSearch('');
// Einstellungen zuerst: wer „schwellwert" tippt,
// will das Feld, nicht den Bereich
if (e.key === 'Enter') {
if (foundSettings[0]) jumpToSetting(foundSettings[0]);
else if (foundTabs[0]) { setTab(foundTabs[0].id); setTabSearch(''); }
}
if (e.key === 'Escape') setTabSearch('');
}}
@@ -2680,7 +2704,25 @@ export default function Settings({ me }) {
)}
</div>
{foundTabs.length === 0 && (
{foundSettings.length > 0 && (
<div className="settings-nav-group">
<span className="settings-nav-label">Einstellungen</span>
{foundSettings.map((s) => (
<button
key={s.id}
className="settings-hit"
onClick={() => jumpToSetting(s)}
>
<span className="settings-hit-label">{s.label}</span>
<span className="settings-hit-tab">
{TABS.find((t) => t.id === s.tab)?.label ?? s.tab}
</span>
</button>
))}
</div>
)}
{foundTabs.length === 0 && foundSettings.length === 0 && (
<p className="settings-nav-empty">Nichts gefunden.</p>
)}
+103
View File
@@ -0,0 +1,103 @@
// Register aller Einstellungen für die Suche in der Config-Seitenleiste.
//
// Die Seitenleisten-Suche fand bisher nur Bereiche: „starboard" führte zu
// Community, aber nicht zum Schwellwert-Feld. Hier steht, wo jede Einstellung
// wohnt — daraus wird ein Sprung direkt aufs Feld.
//
// `id` — Schlüssel wie im Formular; im JSX steht dazu data-setting="<id>"
// `target` — nur nötig, wenn sich zwei Einstellungen ein Feld teilen (etwa die
// beiden Markenfarben): dann wird das Feld des Nachbarn angesprungen
// `tab` — Bereich, in dem das Feld liegt
// `label` — was in der Trefferliste steht
// `find` — zusätzliche Suchwörter (Synonyme, Fachbegriffe, alte Namen)
//
// Werte und Textvorlagen haben eigene Bereiche mit eigener Struktur; sie sind
// hier mit ihrem Bereich vertreten, nicht Feld für Feld.
export const SETTING_INDEX = [
/* ── Kanäle ─────────────────────────────────────── */
{ id: 'devlog_channel_id', tab: 'feeds', label: 'Devlog-Kanal', find: 'devlog posten kanal' },
{ id: 'commit_channel_id', tab: 'feeds', label: 'Commit-Kanal', find: 'gitea push commits' },
{ id: 'release_channel_id', tab: 'feeds', label: 'Release-Kanal', find: 'ankündigung version changelog' },
{ id: 'starboard_channel_id', tab: 'community', label: 'Starboard-Kanal', find: 'stern best-of highlights' },
{ id: 'screenshot_channel_id', tab: 'community', label: 'Screenshot-Kanal', find: 'galerie bilder' },
{ id: 'voting_channel_id', tab: 'community', label: 'Voting-Kanal', find: 'wunsch abstimmen ideen' },
{ id: 'birthday_channel_id', tab: 'community', label: 'Geburtstags-Kanal', find: 'geburtstag gratulation' },
{ id: 'modmail_channel_id', tab: 'support', label: 'Modmail-Kanal', find: 'direktnachricht dm staff' },
{ id: 'ticket_channel_id', tab: 'support', label: 'Ticket-Kanal', find: 'support anfrage thread' },
{ id: 'welcome_channel_id', tab: 'support', label: 'Willkommens-Kanal', find: 'begrüßung neue member' },
{ id: 'modlog_channel_id', tab: 'support', label: 'Protokoll-Kanal', find: 'modlog logging gelöscht' },
{ id: 'events_announce_channel_id', tab: 'system', label: 'Event-Kanal', find: 'events ankündigung' },
{ id: 'social_announce_channel_id', tab: 'system', label: 'Social-Kanal', find: 'twitch youtube live stream' },
{ id: 'tempvoice_channel_id', tab: 'system', label: 'Sprachkanal-Hub', find: 'temp voice join to create' },
{ id: 'backup_channel_id', tab: 'system', label: 'Backup-Kanal', find: 'sicherung upload' },
{ id: 'status_channel_id', tab: 'server', label: 'Server-Status-Kanal', find: 'monitor game embed' },
{ id: 'server_alert_channel_id', target: 'status_channel_id', tab: 'server', label: 'Alarm-Kanal', find: 'down ausfall alert' },
/* ── Rollen ─────────────────────────────────────── */
{ id: 'devlog_ping_role_id', tab: 'feeds', label: 'Devlog-Ping-Rolle', find: 'benachrichtigung abo erwähnen' },
{ id: 'playtester_role_id', tab: 'community', label: 'Playtester-Rolle', find: 'alpha tester' },
{ id: 'birthday_role_id', tab: 'community', label: 'Geburtstags-Rolle', find: 'tagesrolle' },
{ id: 'autorole_id', tab: 'community', label: 'Auto-Rolle', find: 'startrolle beitritt automatisch' },
/* ── Schalter & Werte in den Bereichen ──────────── */
{ id: 'commit_feed_enabled', tab: 'feeds', label: 'Push-Embeds posten', find: 'commit feed an aus' },
{ id: 'commit_branch_filter', tab: 'feeds', label: 'Branch-Filter', find: 'main release zweig' },
{ id: 'ignored_repos', tab: 'feeds', label: 'Ignorierte Repos', find: 'ausschließen repository' },
{ id: 'weekly_recap_enabled', tab: 'feeds', label: 'Wochen-Rückblick', find: 'sonntag zusammenfassung' },
{ id: 'devlog_threads_enabled', tab: 'feeds', label: 'Devlog-Threads', find: 'diskussion kommentare' },
{ id: 'starboard_threshold', target: 'starboard_channel_id', tab: 'community', label: 'Starboard-Schwellwert', find: 'sterne anzahl schwelle' },
{ id: 'levels_enabled', tab: 'community', label: 'Level-System', find: 'xp rang erfahrung' },
{ id: 'levels_announce', tab: 'community', label: 'Level-Up ankündigen', find: 'aufstieg nachricht' },
{ id: 'level_rewards', tab: 'community', label: 'Level-Belohnungen', find: 'rollen ab level' },
{ id: 'sticky_roles_enabled', tab: 'community', label: 'Rollen merken', find: 'sticky rejoin wiederherstellen' },
/* ── Willkommens-Karte ──────────────────────────── */
{ id: 'welcome_card_enabled', tab: 'support', label: 'Willkommens-Karte rendern', find: 'bild karte begrüßung' },
{ id: 'welcome_card_kicker', tab: 'support', label: 'Willkommens-Karte: kleine Zeile', find: 'überschrift text karte' },
{ id: 'welcome_card_sub', tab: 'support', label: 'Willkommens-Karte: Zeile darunter', find: 'member nummer text' },
{ id: 'welcome_card_image', tab: 'support', label: 'Willkommens-Karte: Hintergrundbild', find: 'grafik bild hintergrund' },
{ id: 'welcome_card_watermark', tab: 'support', label: 'Willkommens-Karte: Markenname', find: 'wasserzeichen hintergrund' },
/* ── Marke ──────────────────────────────────────── */
{ id: 'brand_name', tab: 'brand', label: 'Markenname', find: 'footer embed name' },
{ id: 'brand_color', tab: 'brand', label: 'Primärfarbe', find: 'farbe embed hex' },
{ id: 'brand_color2', target: 'brand_color', tab: 'brand', label: 'Sekundärfarbe', find: 'farbe zweite hex' },
{ id: 'bot_name', tab: 'brand', label: 'Bot-Name', find: 'umbenennen username' },
{ id: 'bot_presence_status', tab: 'brand', label: 'Bot-Status', find: 'online abwesend nicht stören' },
{ id: 'bot_status_text', tab: 'brand', label: 'Bot-Aktivität', find: 'spielt schaut status text' },
/* ── Technik & Zugang ───────────────────────────── */
{ id: 'bug_report_repo', tab: 'system', label: 'Repo für Fehlermeldungen', find: 'bug issue gitea' },
{ id: 'roadmap_repo', tab: 'system', label: 'Repo für die Roadmap', find: 'milestones meilensteine' },
{ id: 'watchdog_urls', tab: 'system', label: 'Zu prüfende Adressen', find: 'watchdog erreichbarkeit uptime' },
{ id: 'twitch_channel', target: 'youtube_channel_id', tab: 'system', label: 'Twitch-Kanal', find: 'stream live' },
{ id: 'youtube_channel_id', tab: 'system', label: 'YouTube-Kanal', find: 'video rss' },
{ id: 'backup_enabled', tab: 'system', label: 'Datenbank-Sicherung', find: 'backup nächtlich' },
{ id: 'repo_backup_enabled', tab: 'system', label: 'Repository-Sicherung', find: 'gitea bundle backup' },
{ id: 'member_gate_enabled', tab: 'system', label: 'Login nur für Mitglieder', find: 'gate zugang sperren' },
{ id: 'discord_invite_url', tab: 'system', label: 'Einladungs-Link', find: 'invite discord' },
{ id: 'public_url', tab: 'system', label: 'Öffentliche Adresse', find: 'url domain' },
{ id: 'hub_url', tab: 'system', label: 'Hub-Adresse', find: 'domain zwei seiten community' },
{ id: 'bot_url', tab: 'system', label: 'Bot-Adresse', find: 'domain zwei seiten produktseite' },
{ id: 'cookie_domain', tab: 'system', label: 'Cookie-Domain', find: 'login beide domains' },
{ id: 'gitea_url', tab: 'system', label: 'Gitea-Adresse', find: 'git server url' },
{ id: 'discord_guild_id', tab: 'brand', label: 'Server-ID', find: 'guild id discord' },
{ id: 'legal_name', tab: 'system', label: 'Impressum: Name', find: 'betreiber rechtstext' },
{ id: 'legal_address', tab: 'system', label: 'Impressum: Anschrift', find: 'adresse rechtstext' },
{ id: 'legal_email', tab: 'system', label: 'Impressum: E-Mail', find: 'kontakt rechtstext' },
];
/**
* Einstellungen zu einer Sucheingabe.
* @param {string} query
* @param {(tab: string) => boolean} allowed — Rechte-Prüfung je Bereich
*/
export function findSettings(query, allowed) {
const q = query.trim().toLowerCase();
if (q.length < 2) return [];
return SETTING_INDEX
.filter((s) => allowed(s.tab))
.filter((s) => `${s.label} ${s.find} ${s.id}`.toLowerCase().includes(q))
.slice(0, 8);
}
+28
View File
@@ -679,6 +679,34 @@ a.sha:hover { border-color: var(--neon); background: rgba(245, 197, 24, .07); }
color: var(--muted2);
padding: .3rem .9rem .25rem;
}
/* Treffer der Einstellungs-Suche */
.settings-hit {
display: flex; flex-direction: column; gap: .1rem;
width: 100%; text-align: left;
background: none; border: 0; border-left: 2px solid transparent;
border-radius: 9px;
padding: .4rem .9rem;
cursor: pointer;
transition: background .15s, border-color .15s;
}
.settings-hit:hover { background: rgba(255, 255, 255, .04); border-left-color: var(--neon); }
.settings-hit-label { color: var(--text); font-size: .88rem; line-height: 1.3; }
.settings-hit-tab {
font-family: var(--mono); font-size: .55rem;
letter-spacing: .14em; text-transform: uppercase; color: var(--muted2);
}
.settings-hit:hover .settings-hit-tab { color: var(--neon); }
/* Angesprungenes Feld kurz hervorheben, damit man es im Formular findet */
.field-found {
animation: fieldFound 2.2s ease both;
border-radius: 8px;
}
@keyframes fieldFound {
0%, 55% { box-shadow: 0 0 0 2px var(--neon), 0 0 18px rgba(245, 197, 24, .25); }
100% { box-shadow: 0 0 0 2px transparent, 0 0 0 transparent; }
}
.settings-nav-empty {
font-family: var(--mono); font-size: .66rem; letter-spacing: .1em;
color: var(--muted2); padding: .6rem .9rem;