Host-Routing: Bot-Produktseite und Community-Hub getrennt
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

Eine Anwendung, zwei Gesichter — der Server erkennt an der Domain, welche
Seite gefragt ist, und schreibt das als data-site an den <body>. Das
Frontend rendert daraufhin entweder den Community-Hub wie bisher oder die
neue Produktseite.

- Bot-Produktseite (BotApp): Landing mit Feature-Übersicht, Befehls-
  referenz, Dashboard unter /dashboard, Orange als Leitfarbe
- Weiterleitungen: Community-Routen auf der Bot-Domain und umgekehrt
  werden dauerhaft (301) auf die richtige Adresse geschickt — geteilte
  Devlog-Permalinks laufen also nicht ins Leere. Rechtstexte bleiben auf
  beiden erreichbar.
- Open-Graph-Tags je Domain, inklusive eigener Vorschau für frei
  angelegte Seiten (Entwürfe bekommen bewusst keine)
- Login: neue Einstellung für die Cookie-Domain, damit die Anmeldung auf
  beiden Seiten gilt; nach dem Discord-Login landet man wieder auf der
  Seite, von der man gestartet ist (vorher immer auf der Hauptadresse)
- Alles greift erst, wenn beide Adressen im Setup eingetragen sind —
  bis dahin verhält sich die Anwendung unverändert

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 15:20:53 +02:00
co-authored by Claude Fable 5
parent e8158ce90b
commit 079c75b184
13 changed files with 515 additions and 14 deletions
+74
View File
@@ -0,0 +1,74 @@
// Übersicht aller Slash-Befehle des Bots.
import { useEffect } from 'react';
import { IconLock } from '../icons.jsx';
const GROUPS = [
['Für alle', [
['/rank', 'Zeigt deinen Rang, deine XP und den Fortschritt zum nächsten Level.'],
['/wunsch idee:…', 'Reicht einen Feature-Wunsch ein — landet im Voting-Kanal und auf der Roadmap.'],
['/bug titel:… beschreibung:…', 'Meldet einen Fehler als Issue im Repository. Wird er geschlossen, bekommst du eine Nachricht.'],
['/geburtstag setzen tag:… monat:…', 'Trägt deinen Geburtstag ein. Der Bot gratuliert morgens und vergibt eine Rolle für den Tag.'],
['/geburtstag entfernen', 'Trägt ihn wieder aus.'],
['/remind dauer:… text:…', 'Erinnert dich per Direktnachricht — etwa „2h" oder „30m".'],
['/tag name:…', 'Ruft einen gespeicherten Textbaustein ab, mit Vorschlägen beim Tippen.'],
['/ping', 'Lebenszeichen des Bots.'],
]],
['Moderation', [
['/warn user:… grund:…', 'Verwarnt jemanden, schickt eine Nachricht und schreibt es ins Protokoll.'],
['/warns user:…', 'Zeigt die Verwarnungs-Historie.'],
['/timeout user:… dauer:…', 'Setzt eine Auszeit.'],
['/purge anzahl:…', 'Löscht mehrere Nachrichten auf einmal.'],
]],
['Einrichtung', [
['/giveaway preis:… dauer:… gewinner:…', 'Startet eine Verlosung mit Teilnahme-Knopf und automatischer Ziehung.'],
['/ticket-setup', 'Postet den Knopf, über den Support-Tickets als private Threads entstehen.'],
['/playtester-setup', 'Postet den Bewerbungs-Knopf für das Playtester-Programm.'],
['/devlog-backfill', 'Holt ältere Devlog-Nachrichten nachträglich ins Archiv.'],
['/galerie-backfill', 'Dasselbe für den Screenshot-Kanal.'],
]],
];
export default function BotCommands() {
useEffect(() => { window.scrollTo(0, 0); }, []);
return (
<>
<section className="page-header">
<div className="page-bg-text">BEFEHLE</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">// Referenz</div>
<h1 className="page-title">Befehle</h1>
<p className="page-subtitle">
Alle Slash-Befehle im Überblick. Vieles läuft zusätzlich über Knöpfe
direkt in den Nachrichten.
</p>
</div>
</section>
<section className="content">
{GROUPS.map(([group, commands]) => (
<div className="roadmap-group" key={group}>
<h2 className="settings-title">
// {group}
{group === 'Moderation' && <IconLock size={13} />}
{group === 'Einrichtung' && <IconLock size={13} />}
</h2>
<div className="cmd-list">
{commands.map(([name, desc]) => (
<div className="cmd-row" key={name}>
<code className="cmd-name">{name}</code>
<span className="cmd-desc">{desc}</span>
</div>
))}
</div>
</div>
))}
<p className="notice">
Moderations- und Einrichtungs-Befehle sind Mitgliedern mit den passenden
Discord-Rechten vorbehalten.
</p>
</section>
</>
);
}
+119
View File
@@ -0,0 +1,119 @@
// Produktseite des Bots: was er kann, wie er aussieht, wo es weitergeht.
// Bewusst ohne „Einladen"-Button — der Bot ist auf einen Server ausgelegt.
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { apiGet } from '../api.js';
import {
IconDevlog, IconLevel, IconRoles, IconServer, IconSupport, IconGiveaway,
IconBirthday, IconShield, IconMagic, IconKey, IconArrowRight, IconBot,
} from '../icons.jsx';
const HIGHLIGHTS = [
[IconDevlog, 'Devlogs & Feeds', 'Entwicklungs-Berichte landen als gebrandetes Embed im Discord und werden gleichzeitig durchsuchbar archiviert. Commits und Releases aus Gitea genauso.'],
[IconServer, 'Server-Monitor', 'Über 300 Spiele per gamedig, dazu FiveM und einfache Erreichbarkeits-Checks. Ein Live-Embed pro Server, Alarm bei Ausfall, Spielerzahl in der Bot-Präsenz.'],
[IconLevel, 'Level & Aktivität', 'XP fürs Mitreden mit Rollen-Belohnungen, öffentliche Bestenliste und Aktivitäts-Statistik des Servers.'],
[IconRoles, 'Rollen-Menüs', 'Selbstbedienung per Knopfdruck — im Discord und im Browser. Farben pro Knopf, wahlweise exklusiv.'],
[IconSupport, 'Support & Kontakt', 'Tickets als private Threads mit Gesprächsprotokoll, dazu Modmail: eine Direktnachricht an den Bot landet beim Team.'],
[IconShield, 'Moderation', 'Verwarnungen mit Verlauf, Auszeiten, Aufräumen — alles im Mod-Log, dazu Protokoll für Beitritte, Namens- und Rollenwechsel.'],
[IconGiveaway, 'Verlosungen', 'Teilnahme per Knopf, automatische Ziehung, Neuauslosung wenn sich niemand meldet.'],
[IconBirthday, 'Geburtstage', 'Wer mag, trägt seinen Tag ein — der Bot gratuliert morgens und vergibt eine Rolle für den Tag.'],
[IconMagic, 'Willkommens-Karten', 'Neue Mitglieder bekommen ein gerendertes Bild mit Avatar und Mitglieds-Nummer.'],
];
export default function BotLanding() {
const [status, setStatus] = useState(null);
const [hub, setHub] = useState(null);
useEffect(() => {
apiGet('/api/legal').then((d) => setHub(d)).catch(() => {});
apiGet('/api/serverstats').then(setStatus).catch(() => {});
window.scrollTo(0, 0);
}, []);
return (
<>
<section className="page-header bot-hero">
<div className="page-bg-text">BOT</div>
<div className="page-header-grid" aria-hidden="true" />
<div className="page-header-content">
<div className="page-tag">// Selbst gebaut, keine Paywall</div>
<h1 className="page-title">
Ein Discord-Bot,<br />der alles macht
</h1>
<p className="page-subtitle">
Devlogs, Level, Moderation, Server-Monitoring, Verlosungen, Tickets
was sonst drei Bots mit Abo-Modell erledigen, läuft hier in einem,
mit eigenem Webinterface.
</p>
<div className="home-cta">
<Link className="btn btn-save with-icon" to="/features">
Alle Funktionen <IconArrowRight size={15} />
</Link>
<Link className="btn btn-ghost with-icon" to="/commands">
<IconBot /> Befehle ansehen
</Link>
</div>
</div>
</section>
<section className="content">
<div className="srv-summary">
<div className="srv-sum-item">
<span className="srv-sum-n">16</span>
<span className="srv-sum-label">Slash-Befehle</span>
</div>
<div className="srv-sum-item">
<span className="srv-sum-n">300<span className="srv-sum-of">+</span></span>
<span className="srv-sum-label">Spiele im Monitor</span>
</div>
<div className="srv-sum-item">
<span className="srv-sum-n">0<span className="srv-sum-of"> </span></span>
<span className="srv-sum-label">Kosten, keine Premium-Stufen</span>
</div>
</div>
<h2 className="settings-title" style={{ marginTop: '2.5rem' }}>// Was er kann</h2>
<div className="home-tiles">
{HIGHLIGHTS.map(([Icon, title, text]) => (
<div className="card home-tile" key={title}>
<span className="home-tile-icon"><Icon size={24} /></span>
<span className="home-tile-title">{title}</span>
<span className="home-tile-desc">{text}</span>
</div>
))}
</div>
<div className="roadmap-group" style={{ marginTop: '2.5rem' }}>
<h2 className="settings-title">// Für Entwickler</h2>
<div className="home-tiles">
<div className="card home-tile">
<span className="home-tile-icon"><IconKey size={24} /></span>
<span className="home-tile-title">API &amp; Single Sign-On</span>
<span className="home-tile-desc">
Eigene Skripte posten über die API v1 mit Bearer-Schlüsseln und Bereichs-Rechten.
Andere Dienste nutzen den Discord-Login des Bots mit und bekommen die Rollen
gleich mitgeliefert.
</span>
</div>
<div className="card home-tile">
<span className="home-tile-icon"><IconServer size={24} /></span>
<span className="home-tile-title">Läuft überall</span>
<span className="home-tile-desc">
Node und SQLite in einem Container, ein Volume für die Daten.
Nächtliche Sicherungen inklusive von der Datenbank und allen Repositories.
</span>
</div>
</div>
</div>
{hub?.publicUrl && (
<p className="notice" style={{ marginTop: '2rem' }}>
Der Bot läuft für die D4RKST3R-Community
{status?.members ? ` mit ${status.members} Mitgliedern` : ''}
{' '}<a href={hub.publicUrl} style={{ textDecoration: 'underline' }}>zum Community-Hub</a>.
</p>
)}
</section>
</>
);
}
+22
View File
@@ -1311,6 +1311,28 @@ export default function Settings({ me }) {
<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>
</div>
<div className="settings-section">
<h2 className="settings-title">// Zwei Seiten</h2>
<p className="section-intro">
Community-Hub und Bot-Produktseite auf getrennte Adressen legen. Solange
beide Felder leer sind, läuft alles unverändert auf der öffentlichen URL.
Sind sie gesetzt, leitet der Server falsch aufgerufene Seiten automatisch
auf die richtige Adresse um.
</p>
<div className="field">
<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">
<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">
<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">
<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 })} />