Config-Suche findet einzelne Einstellungen
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:
@@ -128,8 +128,14 @@ Code, die Datenbank enthält nur echte Abweichungen.
|
|||||||
| Technik | Server · System |
|
| Technik | Server · System |
|
||||||
| Zugang | API · Team |
|
| Zugang | API · Team |
|
||||||
|
|
||||||
Das Suchfeld über der Leiste findet Bereiche auch über Stichwörter — „geburtstag"
|
Das Suchfeld über der Leiste findet **Bereiche und einzelne Einstellungen**:
|
||||||
führt zu Community, „willkommen" zu Support. Enter springt zum ersten Treffer.
|
„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
|
Der aktive Bereich steht in der Adresse (`/settings#texte`), Links und Neuladen
|
||||||
landen also wieder dort.
|
landen also wieder dort.
|
||||||
|
|
||||||
|
|||||||
+102
-60
@@ -9,6 +9,7 @@ import {
|
|||||||
IconSearch, IconSystem as IconTune,
|
IconSearch, IconSystem as IconTune,
|
||||||
} from '../icons.jsx';
|
} from '../icons.jsx';
|
||||||
import { moduleIcon } from '../module-icons.jsx';
|
import { moduleIcon } from '../module-icons.jsx';
|
||||||
|
import { findSettings } from '../setting-index.js';
|
||||||
import { Markdown } from '../markdown.jsx';
|
import { Markdown } from '../markdown.jsx';
|
||||||
|
|
||||||
const API_SCOPES = [
|
const API_SCOPES = [
|
||||||
@@ -339,6 +340,27 @@ export default function Settings({ me }) {
|
|||||||
const foundTabs = query
|
const foundTabs = query
|
||||||
? visibleTabs.filter((t) => `${t.label} ${t.find}`.toLowerCase().includes(query))
|
? visibleTabs.filter((t) => `${t.label} ${t.find}`.toLowerCase().includes(query))
|
||||||
: visibleTabs;
|
: 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
|
// Tags + geplante Posts
|
||||||
const [tags, setTags] = useState([]);
|
const [tags, setTags] = useState([]);
|
||||||
const [tagDraft, setTagDraft] = useState({ name: '', content: '' });
|
const [tagDraft, setTagDraft] = useState({ name: '', content: '' });
|
||||||
@@ -1039,7 +1061,7 @@ export default function Settings({ me }) {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="bot-card-right">
|
<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>
|
<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)} />
|
<input type="text" maxLength={32} value={botName} onChange={(e) => setBotName(e.target.value)} />
|
||||||
</div>
|
</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>
|
<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)} />
|
<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>
|
||||||
<div className="field">
|
<div className="field" data-setting="bot_presence_status">
|
||||||
<label>Bot-Status</label>
|
<label>Bot-Status</label>
|
||||||
<select value={form.bot_presence_status} onChange={(e) => setForm({ ...form, bot_presence_status: e.target.value })}>
|
<select value={form.bot_presence_status} onChange={(e) => setForm({ ...form, bot_presence_status: e.target.value })}>
|
||||||
<option value="online">🟢 Online</option>
|
<option value="online">🟢 Online</option>
|
||||||
@@ -1055,7 +1077,7 @@ export default function Settings({ me }) {
|
|||||||
<option value="dnd">⛔ Bitte nicht stören</option>
|
<option value="dnd">⛔ Bitte nicht stören</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
<label>Aktivität <span className="field-hint">leer = Server-Monitor darf die Spielerzahl anzeigen</span></label>
|
||||||
<div className="field-row">
|
<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 })}>
|
<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">
|
<div className="settings-section">
|
||||||
<h2 className="settings-title">// Embeds</h2>
|
<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>
|
<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 })} />
|
<input type="text" placeholder="D4RKST3R" value={form.brand_name} onChange={(e) => setForm({ ...form, brand_name: e.target.value })} />
|
||||||
</div>
|
</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>
|
<label>Embed-Farben <span className="field-hint">primär (Embeds) · sekundär (Releases, Mod-Aktionen)</span></label>
|
||||||
<div className="field-row">
|
<div className="field-row">
|
||||||
<input type="color" className="color-pick" value={form.brand_color} onChange={(e) => setForm({ ...form, brand_color: e.target.value })} />
|
<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">
|
<div className="settings-section">
|
||||||
<h2 className="settings-title">// Aus der Env verlagert</h2>
|
<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>
|
<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 })} />
|
<input type="text" placeholder="Server-ID" value={form.discord_guild_id} onChange={(e) => setForm({ ...form, discord_guild_id: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
@@ -1108,7 +1130,7 @@ export default function Settings({ me }) {
|
|||||||
<>
|
<>
|
||||||
<div className="settings-section">
|
<div className="settings-section">
|
||||||
<h2 className="settings-title">// Kanäle</h2>
|
<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>
|
<label>Devlog-Kanal <span className="field-hint">öffentlich — hier postet der Bot die Devlogs</span></label>
|
||||||
<div className="field-row">
|
<div className="field-row">
|
||||||
<PickSelect
|
<PickSelect
|
||||||
@@ -1120,7 +1142,7 @@ export default function Settings({ me }) {
|
|||||||
<button className="btn" onClick={() => sendTest('devlog')}>Test</button>
|
<button className="btn" onClick={() => sendTest('devlog')}>Test</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<label>Commit-Kanal <span className="field-hint">privat — hier landen die Push-Embeds</span></label>
|
||||||
<div className="field-row">
|
<div className="field-row">
|
||||||
<PickSelect
|
<PickSelect
|
||||||
@@ -1132,7 +1154,7 @@ export default function Settings({ me }) {
|
|||||||
<button className="btn" onClick={() => sendTest('commit')}>Test</button>
|
<button className="btn" onClick={() => sendTest('commit')}>Test</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<label>Release-Kanal <span className="field-hint">Ankündigungen bei neuen Gitea-Releases — leer = aus</span></label>
|
||||||
<div className="field-row">
|
<div className="field-row">
|
||||||
<PickSelect
|
<PickSelect
|
||||||
@@ -1148,7 +1170,7 @@ export default function Settings({ me }) {
|
|||||||
|
|
||||||
<div className="settings-section">
|
<div className="settings-section">
|
||||||
<h2 className="settings-title">// Devlog</h2>
|
<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>
|
<label>Ping-Rolle <span className="field-hint">wird bei jedem Devlog erwähnt; 🔔-Button am Post — leer = kein Ping</span></label>
|
||||||
<PickSelect
|
<PickSelect
|
||||||
items={roleItems}
|
items={roleItems}
|
||||||
@@ -1157,14 +1179,14 @@ export default function Settings({ me }) {
|
|||||||
empty="— kein Ping —"
|
empty="— kein Ping —"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field" data-setting="weekly_recap_enabled">
|
||||||
<label className="toggle">
|
<label className="toggle">
|
||||||
<input type="checkbox" checked={form.weekly_recap_enabled} onChange={(e) => setForm({ ...form, weekly_recap_enabled: e.target.checked })} />
|
<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>Wochen-Rückblick (sonntags 20:00)</span>
|
||||||
<span className="field-hint">Commits pro Tag, Devlogs & Top-Projekte</span>
|
<span className="field-hint">Commits pro Tag, Devlogs & Top-Projekte</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field" data-setting="devlog_threads_enabled">
|
||||||
<label className="toggle">
|
<label className="toggle">
|
||||||
<input type="checkbox" checked={form.devlog_threads_enabled} onChange={(e) => setForm({ ...form, devlog_threads_enabled: e.target.checked })} />
|
<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>
|
<span>Diskussions-Thread unter jedem Devlog</span>
|
||||||
@@ -1178,18 +1200,18 @@ export default function Settings({ me }) {
|
|||||||
|
|
||||||
<div className="settings-section">
|
<div className="settings-section">
|
||||||
<h2 className="settings-title">// Commit-Feed</h2>
|
<h2 className="settings-title">// Commit-Feed</h2>
|
||||||
<div className="field">
|
<div className="field" data-setting="commit_feed_enabled">
|
||||||
<label className="toggle">
|
<label className="toggle">
|
||||||
<input type="checkbox" checked={form.commit_feed_enabled} onChange={(e) => setForm({ ...form, commit_feed_enabled: e.target.checked })} />
|
<input type="checkbox" checked={form.commit_feed_enabled} onChange={(e) => setForm({ ...form, commit_feed_enabled: e.target.checked })} />
|
||||||
<span>Push-Embeds posten</span>
|
<span>Push-Embeds posten</span>
|
||||||
<span className="field-hint">aus = weiter archivieren, nur nicht posten</span>
|
<span className="field-hint">aus = weiter archivieren, nur nicht posten</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field" data-setting="commit_branch_filter">
|
||||||
<label>Branch-Filter <span className="field-hint">kommagetrennt, leer = alle</span></label>
|
<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 })} />
|
<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>
|
||||||
<div className="field">
|
<div className="field" data-setting="ignored_repos">
|
||||||
<label>Ignorierte Repos <span className="field-hint">kommagetrennt, volle Namen</span></label>
|
<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 })} />
|
<input type="text" placeholder="z. B. D4rkst3r/d4rkbot" value={form.ignored_repos} onChange={(e) => setForm({ ...form, ignored_repos: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
@@ -1201,7 +1223,7 @@ export default function Settings({ me }) {
|
|||||||
<>
|
<>
|
||||||
<div className="settings-section">
|
<div className="settings-section">
|
||||||
<h2 className="settings-title">// Geburtstage</h2>
|
<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>
|
<label>Geburtstags-Kanal <span className="field-hint">Member tragen sich mit /geburtstag ein; Gratulation morgens ab 09:00 — leer = aus</span></label>
|
||||||
<PickSelect
|
<PickSelect
|
||||||
items={channelItems}
|
items={channelItems}
|
||||||
@@ -1210,7 +1232,7 @@ export default function Settings({ me }) {
|
|||||||
empty="— deaktiviert —"
|
empty="— deaktiviert —"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
<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
|
<PickSelect
|
||||||
items={roleItems}
|
items={roleItems}
|
||||||
@@ -1223,7 +1245,7 @@ export default function Settings({ me }) {
|
|||||||
|
|
||||||
<div className="settings-section">
|
<div className="settings-section">
|
||||||
<h2 className="settings-title">// Playtester</h2>
|
<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>
|
<label>Playtester-Rolle <span className="field-hint">Bewerbungs-Post via /playtester-setup; leer = aus</span></label>
|
||||||
<PickSelect
|
<PickSelect
|
||||||
items={roleItems}
|
items={roleItems}
|
||||||
@@ -1279,7 +1301,7 @@ export default function Settings({ me }) {
|
|||||||
|
|
||||||
<div className="settings-section">
|
<div className="settings-section">
|
||||||
<h2 className="settings-title">// Starboard & Galerie</h2>
|
<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>
|
<label>Starboard-Kanal <span className="field-hint">Nachrichten mit genug ⭐ landen hier</span></label>
|
||||||
<div className="field-row">
|
<div className="field-row">
|
||||||
<PickSelect
|
<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 })} />
|
<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>
|
</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>
|
<label>Screenshot-Kanal <span className="field-hint">Bilder → öffentliche Galerie; Historie: /galerie-backfill</span></label>
|
||||||
<PickSelect
|
<PickSelect
|
||||||
items={channelItems}
|
items={channelItems}
|
||||||
@@ -1300,7 +1322,7 @@ export default function Settings({ me }) {
|
|||||||
empty="— deaktiviert —"
|
empty="— deaktiviert —"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
<label>Voting-Kanal <span className="field-hint">/wunsch postet hier; Rangliste auf /roadmap</span></label>
|
||||||
<PickSelect
|
<PickSelect
|
||||||
items={channelItems}
|
items={channelItems}
|
||||||
@@ -1313,21 +1335,21 @@ export default function Settings({ me }) {
|
|||||||
|
|
||||||
<div className="settings-section">
|
<div className="settings-section">
|
||||||
<h2 className="settings-title">// Level-System</h2>
|
<h2 className="settings-title">// Level-System</h2>
|
||||||
<div className="field">
|
<div className="field" data-setting="levels_enabled">
|
||||||
<label className="toggle">
|
<label className="toggle">
|
||||||
<input type="checkbox" checked={form.levels_enabled} onChange={(e) => setForm({ ...form, levels_enabled: e.target.checked })} />
|
<input type="checkbox" checked={form.levels_enabled} onChange={(e) => setForm({ ...form, levels_enabled: e.target.checked })} />
|
||||||
<span>XP fürs Schreiben vergeben</span>
|
<span>XP fürs Schreiben vergeben</span>
|
||||||
<span className="field-hint">15–25 XP pro Nachricht, 60s Cooldown; /rank + Bestenliste auf /level</span>
|
<span className="field-hint">15–25 XP pro Nachricht, 60s Cooldown; /rank + Bestenliste auf /level</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field" data-setting="levels_announce">
|
||||||
<label className="toggle">
|
<label className="toggle">
|
||||||
<input type="checkbox" checked={form.levels_announce} onChange={(e) => setForm({ ...form, levels_announce: e.target.checked })} />
|
<input type="checkbox" checked={form.levels_announce} onChange={(e) => setForm({ ...form, levels_announce: e.target.checked })} />
|
||||||
<span>Level-Up im Kanal feiern</span>
|
<span>Level-Up im Kanal feiern</span>
|
||||||
<span className="field-hint">„🎉 Name ist jetzt Level 5!"</span>
|
<span className="field-hint">„🎉 Name ist jetzt Level 5!"</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
<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 })} />
|
<input type="text" placeholder="5=RollenID, 10=RollenID" value={form.level_rewards} onChange={(e) => setForm({ ...form, level_rewards: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
@@ -1339,7 +1361,7 @@ export default function Settings({ me }) {
|
|||||||
<>
|
<>
|
||||||
<div className="settings-section">
|
<div className="settings-section">
|
||||||
<h2 className="settings-title">// Rollen bei Join</h2>
|
<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>
|
<label>Auto-Rolle <span className="field-hint">bekommt jeder neue Member automatisch — leer = aus</span></label>
|
||||||
<PickSelect
|
<PickSelect
|
||||||
items={roleItems}
|
items={roleItems}
|
||||||
@@ -1348,7 +1370,7 @@ export default function Settings({ me }) {
|
|||||||
empty="— deaktiviert —"
|
empty="— deaktiviert —"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field" data-setting="sticky_roles_enabled">
|
||||||
<label className="toggle">
|
<label className="toggle">
|
||||||
<input type="checkbox" checked={form.sticky_roles_enabled} onChange={(e) => setForm({ ...form, sticky_roles_enabled: e.target.checked })} />
|
<input type="checkbox" checked={form.sticky_roles_enabled} onChange={(e) => setForm({ ...form, sticky_roles_enabled: e.target.checked })} />
|
||||||
<span>Sticky-Roles</span>
|
<span>Sticky-Roles</span>
|
||||||
@@ -1461,7 +1483,7 @@ export default function Settings({ me }) {
|
|||||||
<>
|
<>
|
||||||
<div className="settings-section">
|
<div className="settings-section">
|
||||||
<h2 className="settings-title">// Moderation & Kontakt</h2>
|
<h2 className="settings-title">// Moderation & 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>
|
<label>Modmail-Kanal <span className="field-hint">PRIVATER Staff-Kanal — DMs an den Bot landen hier als Threads</span></label>
|
||||||
<PickSelect
|
<PickSelect
|
||||||
items={channelItems}
|
items={channelItems}
|
||||||
@@ -1470,7 +1492,7 @@ export default function Settings({ me }) {
|
|||||||
empty="— deaktiviert —"
|
empty="— deaktiviert —"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
<label>Ticket-Kanal <span className="field-hint">private Ticket-Threads; Post via /ticket-setup</span></label>
|
||||||
<PickSelect
|
<PickSelect
|
||||||
items={channelItems}
|
items={channelItems}
|
||||||
@@ -1479,7 +1501,7 @@ export default function Settings({ me }) {
|
|||||||
empty="— deaktiviert —"
|
empty="— deaktiviert —"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
<label>Willkommens-Kanal <span className="field-hint">Begrüßungs-Embed für neue Member</span></label>
|
||||||
<PickSelect
|
<PickSelect
|
||||||
items={channelItems}
|
items={channelItems}
|
||||||
@@ -1488,7 +1510,7 @@ export default function Settings({ me }) {
|
|||||||
empty="— deaktiviert —"
|
empty="— deaktiviert —"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
<label>Mod-Log-Kanal <span className="field-hint">PRIVAT — gelöschte/bearbeitete Nachrichten</span></label>
|
||||||
<PickSelect
|
<PickSelect
|
||||||
items={channelItems}
|
items={channelItems}
|
||||||
@@ -1506,7 +1528,7 @@ export default function Settings({ me }) {
|
|||||||
zeigt sofort, wie es aussieht — gespeichert wird erst mit dem Knopf unten.
|
zeigt sofort, wie es aussieht — gespeichert wird erst mit dem Knopf unten.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="field">
|
<div className="field" data-setting="welcome_card_enabled">
|
||||||
<label className="toggle">
|
<label className="toggle">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -1541,7 +1563,7 @@ export default function Settings({ me }) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
<label>Kleine Zeile <span className="field-hint">über dem Namen — leer = weglassen</span></label>
|
||||||
<input
|
<input
|
||||||
type="text" maxLength={24} placeholder="WILLKOMMEN"
|
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 })}
|
onChange={(e) => setForm({ ...form, welcome_card_kicker: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
<label>Zeile darunter <span className="field-hint">{'{count}'} wird zur Mitglieds-Nummer — leer = weglassen</span></label>
|
||||||
<input
|
<input
|
||||||
type="text" maxLength={60} placeholder="Member #{count}"
|
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 })}
|
onChange={(e) => setForm({ ...form, welcome_card_sub: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
<label>Hintergrundbild <span className="field-hint">https-Adresse, 900×300 passt genau — leer = nur Farbe mit Raster</span></label>
|
||||||
<input
|
<input
|
||||||
type="text" placeholder="https://cdn.d4rkst3r.de/welcome.png"
|
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 })}
|
onChange={(e) => setForm({ ...form, welcome_card_image: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field" data-setting="welcome_card_watermark">
|
||||||
<label className="toggle">
|
<label className="toggle">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -1783,11 +1805,11 @@ export default function Settings({ me }) {
|
|||||||
<>
|
<>
|
||||||
<div className="settings-section">
|
<div className="settings-section">
|
||||||
<h2 className="settings-title">// Allgemein</h2>
|
<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>
|
<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 })} />
|
<input type="text" placeholder="https://bot.d4rkst3r.de" value={form.public_url} onChange={(e) => setForm({ ...form, public_url: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field" data-setting="gitea_url">
|
||||||
<label>Gitea-URL</label>
|
<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 })} />
|
<input type="text" placeholder="https://git.d4rkst3r.de" value={form.gitea_url} onChange={(e) => setForm({ ...form, gitea_url: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
@@ -1801,23 +1823,23 @@ export default function Settings({ me }) {
|
|||||||
Sind sie gesetzt, leitet der Server falsch aufgerufene Seiten automatisch
|
Sind sie gesetzt, leitet der Server falsch aufgerufene Seiten automatisch
|
||||||
auf die richtige Adresse um.
|
auf die richtige Adresse um.
|
||||||
</p>
|
</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>
|
<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 })} />
|
<input type="text" placeholder="https://hub.d4rkst3r.de" value={form.hub_url ?? ''} onChange={(e) => setForm({ ...form, hub_url: e.target.value })} />
|
||||||
</div>
|
</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>
|
<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 })} />
|
<input type="text" placeholder="https://bot.d4rkst3r.de" value={form.bot_url ?? ''} onChange={(e) => setForm({ ...form, bot_url: e.target.value })} />
|
||||||
</div>
|
</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>
|
<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 })} />
|
<input type="text" placeholder=".d4rkst3r.de" value={form.cookie_domain ?? ''} onChange={(e) => setForm({ ...form, cookie_domain: e.target.value })} />
|
||||||
</div>
|
</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>
|
<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 })} />
|
<input type="text" placeholder="D4rkst3r/EcoGame" value={form.bug_report_repo} onChange={(e) => setForm({ ...form, bug_report_repo: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field" data-setting="roadmap_repo">
|
||||||
<label>Roadmap-Repo <span className="field-hint">Milestones → öffentliche /roadmap</span></label>
|
<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 })} />
|
<input type="text" placeholder="D4rkst3r/EcoGame" value={form.roadmap_repo} onChange={(e) => setForm({ ...form, roadmap_repo: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
@@ -1867,15 +1889,15 @@ export default function Settings({ me }) {
|
|||||||
ist eine ladungsfähige Anschrift nötig (kein Postfach) — nur der Owner sieht
|
ist eine ladungsfähige Anschrift nötig (kein Postfach) — nur der Owner sieht
|
||||||
und ändert diese Felder.
|
und ändert diese Felder.
|
||||||
</p>
|
</p>
|
||||||
<div className="field">
|
<div className="field" data-setting="legal_name">
|
||||||
<label>Name / Verantwortlicher</label>
|
<label>Name / Verantwortlicher</label>
|
||||||
<input type="text" placeholder="Vorname Nachname" value={form.legal_name ?? ''} onChange={(e) => setForm({ ...form, legal_name: e.target.value })} />
|
<input type="text" placeholder="Vorname Nachname" value={form.legal_name ?? ''} onChange={(e) => setForm({ ...form, legal_name: e.target.value })} />
|
||||||
</div>
|
</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>
|
<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 })} />
|
<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>
|
||||||
<div className="field">
|
<div className="field" data-setting="legal_email">
|
||||||
<label>Kontakt-E-Mail <span className="field-hint">Pflichtangabe — muss erreichbar sein</span></label>
|
<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 })} />
|
<input type="email" placeholder="kontakt@d4rkst3r.de" value={form.legal_email ?? ''} onChange={(e) => setForm({ ...form, legal_email: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
@@ -1887,14 +1909,14 @@ export default function Settings({ me }) {
|
|||||||
|
|
||||||
<div className="settings-section">
|
<div className="settings-section">
|
||||||
<h2 className="settings-title">// Web-Login (Member)</h2>
|
<h2 className="settings-title">// Web-Login (Member)</h2>
|
||||||
<div className="field">
|
<div className="field" data-setting="member_gate_enabled">
|
||||||
<label className="toggle">
|
<label className="toggle">
|
||||||
<input type="checkbox" checked={form.member_gate_enabled} onChange={(e) => setForm({ ...form, member_gate_enabled: e.target.checked })} />
|
<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>Login nur für Discord-Member</span>
|
||||||
<span className="field-hint">Nicht-Member werden nach dem Discord-Login abgewiesen (braucht gesetzte Guild-ID)</span>
|
<span className="field-hint">Nicht-Member werden nach dem Discord-Login abgewiesen (braucht gesetzte Guild-ID)</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
<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 })} />
|
<input type="text" placeholder="https://discord.gg/…" value={form.discord_invite_url} onChange={(e) => setForm({ ...form, discord_invite_url: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
@@ -1902,7 +1924,7 @@ export default function Settings({ me }) {
|
|||||||
|
|
||||||
<div className="settings-section">
|
<div className="settings-section">
|
||||||
<h2 className="settings-title">// Events, Social & Voice</h2>
|
<h2 className="settings-title">// Events, Social & 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>
|
<label>Event-Ankündigungs-Kanal <span className="field-hint">neue Discord-Events werden hier angekündigt + Events-Seite</span></label>
|
||||||
<PickSelect
|
<PickSelect
|
||||||
items={channelItems}
|
items={channelItems}
|
||||||
@@ -1911,7 +1933,7 @@ export default function Settings({ me }) {
|
|||||||
empty="— deaktiviert —"
|
empty="— deaktiviert —"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
<label>Social-Kanal <span className="field-hint">🔴 Twitch-Live + ▶️ neue YouTube-Videos</span></label>
|
||||||
<PickSelect
|
<PickSelect
|
||||||
items={channelItems}
|
items={channelItems}
|
||||||
@@ -1920,7 +1942,7 @@ export default function Settings({ me }) {
|
|||||||
empty="— deaktiviert —"
|
empty="— deaktiviert —"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field" data-setting="youtube_channel_id">
|
||||||
<div className="field-row">
|
<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="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 })} />
|
<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 })} />
|
<input type="password" placeholder="Client-Secret" value={twitchCreds.secret} onChange={(e) => setTwitchCreds({ ...twitchCreds, secret: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<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
|
<PickSelect
|
||||||
items={channelItems}
|
items={channelItems}
|
||||||
@@ -1946,14 +1968,14 @@ export default function Settings({ me }) {
|
|||||||
|
|
||||||
<div className="settings-section">
|
<div className="settings-section">
|
||||||
<h2 className="settings-title">// Backups & Watchdog</h2>
|
<h2 className="settings-title">// Backups & Watchdog</h2>
|
||||||
<div className="field">
|
<div className="field" data-setting="backup_enabled">
|
||||||
<label className="toggle">
|
<label className="toggle">
|
||||||
<input type="checkbox" checked={form.backup_enabled} onChange={(e) => setForm({ ...form, backup_enabled: e.target.checked })} />
|
<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>Nächtliches DB-Backup (03:00)</span>
|
||||||
<span className="field-hint">gzip, 14 Tage Rotation</span>
|
<span className="field-hint">gzip, 14 Tage Rotation</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
<label>Backup-Upload-Kanal <span className="field-hint">Offsite-Kopie in PRIVATEN Kanal — leer = nur lokal</span></label>
|
||||||
<div className="field-row">
|
<div className="field-row">
|
||||||
<PickSelect
|
<PickSelect
|
||||||
@@ -1965,7 +1987,7 @@ export default function Settings({ me }) {
|
|||||||
<button className="btn" onClick={() => sendTest('backup')}>Backup jetzt</button>
|
<button className="btn" onClick={() => sendTest('backup')}>Backup jetzt</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field" data-setting="repo_backup_enabled">
|
||||||
<label className="toggle">
|
<label className="toggle">
|
||||||
<input type="checkbox" checked={form.repo_backup_enabled} onChange={(e) => setForm({ ...form, repo_backup_enabled: e.target.checked })} />
|
<input type="checkbox" checked={form.repo_backup_enabled} onChange={(e) => setForm({ ...form, repo_backup_enabled: e.target.checked })} />
|
||||||
<span>Repo-Backups (04:00)</span>
|
<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>
|
<button className="btn" onClick={() => sendTest('repobackup')}>Repos jetzt sichern</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<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 })} />
|
<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>
|
</div>
|
||||||
@@ -2089,7 +2111,7 @@ export default function Settings({ me }) {
|
|||||||
Jeder Server bekommt sein eigenes Live-Status-Embed im Status-Kanal (🟢/🔴,
|
Jeder Server bekommt sein eigenes Live-Status-Embed im Status-Kanal (🟢/🔴,
|
||||||
Spieler-Balken, Map, Adresse) — aktualisiert alle 2 Minuten, DiscordGSM-Style.
|
Spieler-Balken, Map, Adresse) — aktualisiert alle 2 Minuten, DiscordGSM-Style.
|
||||||
</p>
|
</p>
|
||||||
<div className="field">
|
<div className="field" data-setting="status_channel_id">
|
||||||
<div className="field-row">
|
<div className="field-row">
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<label>Status-Kanal</label>
|
<label>Status-Kanal</label>
|
||||||
@@ -2666,9 +2688,11 @@ export default function Settings({ me }) {
|
|||||||
aria-label="Bereich suchen"
|
aria-label="Bereich suchen"
|
||||||
onChange={(e) => setTabSearch(e.target.value)}
|
onChange={(e) => setTabSearch(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' && foundTabs[0]) {
|
// Einstellungen zuerst: wer „schwellwert" tippt,
|
||||||
setTab(foundTabs[0].id);
|
// will das Feld, nicht den Bereich
|
||||||
setTabSearch('');
|
if (e.key === 'Enter') {
|
||||||
|
if (foundSettings[0]) jumpToSetting(foundSettings[0]);
|
||||||
|
else if (foundTabs[0]) { setTab(foundTabs[0].id); setTabSearch(''); }
|
||||||
}
|
}
|
||||||
if (e.key === 'Escape') setTabSearch('');
|
if (e.key === 'Escape') setTabSearch('');
|
||||||
}}
|
}}
|
||||||
@@ -2680,7 +2704,25 @@ export default function Settings({ me }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
<p className="settings-nav-empty">Nichts gefunden.</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -679,6 +679,34 @@ a.sha:hover { border-color: var(--neon); background: rgba(245, 197, 24, .07); }
|
|||||||
color: var(--muted2);
|
color: var(--muted2);
|
||||||
padding: .3rem .9rem .25rem;
|
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 {
|
.settings-nav-empty {
|
||||||
font-family: var(--mono); font-size: .66rem; letter-spacing: .1em;
|
font-family: var(--mono); font-size: .66rem; letter-spacing: .1em;
|
||||||
color: var(--muted2); padding: .6rem .9rem;
|
color: var(--muted2); padding: .6rem .9rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user