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
+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>
)}