diff --git a/README.md b/README.md
index f609680..b47f9ae 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Nexumi
-Nexumi is a self-hosted public Discord bot with dashboard architecture, built as a TypeScript monorepo.
+Nexumi is a public Discord bot with dashboard architecture, built as a TypeScript monorepo.
## Phase 1 scope
diff --git a/apps/webui/src/app/layout.tsx b/apps/webui/src/app/layout.tsx
index 1ce16e5..36df855 100644
--- a/apps/webui/src/app/layout.tsx
+++ b/apps/webui/src/app/layout.tsx
@@ -11,7 +11,7 @@ const inter = Inter({ subsets: ['latin'], variable: '--font-inter', display: 'sw
export const metadata: Metadata = {
title: 'Nexumi — Discord Bot',
- description: 'Self-hosted Discord bot with dashboard, moderation, and community modules'
+ description: 'Discord bot with dashboard, moderation, and community modules'
};
export default async function RootLayout({ children }: { children: ReactNode }) {
diff --git a/apps/webui/src/messages/de.json b/apps/webui/src/messages/de.json
index 7b808c1..3044768 100644
--- a/apps/webui/src/messages/de.json
+++ b/apps/webui/src/messages/de.json
@@ -737,7 +737,7 @@
},
"landing": {
"hero": {
- "subtitle": "Selbst gehosteter Discord-Bot mit Moderations-, Community- und Integrationsmodulen sowie Web-Dashboard.",
+ "subtitle": "Discord-Bot mit Moderations-, Community- und Integrationsmodulen sowie Web-Dashboard.",
"invite": "Bot einladen",
"login": "Zum Dashboard",
"dashboard": "Dashboard öffnen",
@@ -789,13 +789,13 @@
"title": "Datenschutzerklärung",
"p1": "Nexumi verarbeitet Discord-Daten, soweit dies für den Betrieb des Bots und des Dashboards erforderlich ist (z. B. Server-IDs, Nutzer-IDs, Moderationsfälle, Modul-Einstellungen).",
"p2": "Die Anmeldung am Dashboard erfolgt über Discord OAuth2 (Scopes identify und guilds). Es werden Sitzungsdaten in Redis gespeichert.",
- "p3": "Daten werden in der selbst gehosteten PostgreSQL-Datenbank gespeichert. Aufbewahrungsdauern sind über Modul-Einstellungen bzw. Löschanfragen steuerbar.",
+ "p3": "Daten werden in unserer PostgreSQL-Datenbank gespeichert. Aufbewahrungsdauern sind über Modul-Einstellungen bzw. Löschanfragen steuerbar.",
"p4": "Es findet keine Weitergabe an Dritte statt, außer soweit für den Betrieb technisch erforderlich (z. B. Discord API, optional Sentry).",
"p5": "Betroffene können Auskunft, Berichtigung und Löschung verlangen. Kontakt über die im Impressum genannte Adresse oder den Bot-Command /gdpr sofern verfügbar."
},
"terms": {
"title": "Nutzungsbedingungen",
- "p1": "Nexumi wird als selbst gehosteter Discord-Bot bereitgestellt. Die Nutzung erfolgt auf eigene Verantwortung.",
+ "p1": "Nexumi wird als Discord-Bot bereitgestellt. Die Nutzung erfolgt auf eigene Verantwortung.",
"p2": "Missbrauch, Spam oder Verstöße gegen die Discord Terms of Service können zum Ausschluss (Blacklist) führen.",
"p3": "Wir übernehmen keine Gewähr für unterbrechungsfreien Betrieb. Wartungsfenster werden nach Möglichkeit angekündigt.",
"p4": "Es gilt das Recht der Bundesrepublik Deutschland, soweit zwingende Verbraucherschutzvorschriften nichts anderes vorsehen."
diff --git a/apps/webui/src/messages/en.json b/apps/webui/src/messages/en.json
index ce282a9..4426149 100644
--- a/apps/webui/src/messages/en.json
+++ b/apps/webui/src/messages/en.json
@@ -737,7 +737,7 @@
},
"landing": {
"hero": {
- "subtitle": "Self-hosted Discord bot with moderation, community, and integration modules plus a web dashboard.",
+ "subtitle": "Discord bot with moderation, community, and integration modules plus a web dashboard.",
"invite": "Invite bot",
"login": "Open dashboard",
"dashboard": "Open dashboard",
@@ -789,13 +789,13 @@
"title": "Privacy policy",
"p1": "Nexumi processes Discord data as required to operate the bot and dashboard (e.g. guild IDs, user IDs, moderation cases, module settings).",
"p2": "Dashboard login uses Discord OAuth2 (identify and guilds scopes). Session data is stored in Redis.",
- "p3": "Data is stored in the self-hosted PostgreSQL database. Retention can be configured per module or via deletion requests.",
+ "p3": "Data is stored in our PostgreSQL database. Retention can be configured per module or via deletion requests.",
"p4": "Data is not shared with third parties except where technically required (e.g. Discord API, optional Sentry).",
"p5": "Data subjects may request access, correction, and deletion via the contact in the legal notice or bot commands where available."
},
"terms": {
"title": "Terms of service",
- "p1": "Nexumi is provided as a self-hosted Discord bot. Use is at your own risk.",
+ "p1": "Nexumi is provided as a Discord bot. Use is at your own risk.",
"p2": "Abuse, spam, or violations of Discord Terms of Service may result in blacklisting.",
"p3": "We do not guarantee uninterrupted service. Maintenance windows are announced when possible.",
"p4": "German law applies unless mandatory consumer protection rules require otherwise."
diff --git a/docs/SPEC.md b/docs/SPEC.md
index 224993d..ed40b92 100644
--- a/docs/SPEC.md
+++ b/docs/SPEC.md
@@ -1,6 +1,6 @@
# Nexumi – Vollständige Feature-Spezifikation (Cursor-Prompt)
-Baue „Nexumi", einen vollwertigen, selbst gehosteten, **öffentlichen** Discord-Bot mit Web-Dashboard. Domain: `https://nexumi.de`. Repository: `https://git.hexahost.dev/smueller/Nexumi`. Der Name „Nexumi" wird konsistent verwendet: Bot-Präsenz, WebUI-Branding, Embed-Footer, Docker-Image-Namen, Dokumentation. Halte dich an diese Spezifikation. Frage nach, bevor du Module weglässt oder den Stack änderst.
+Baue „Nexumi", einen vollwertigen, **öffentlichen** Discord-Bot mit Web-Dashboard. Domain: `https://nexumi.de`. Repository: `https://git.hexahost.dev/smueller/Nexumi`. Der Name „Nexumi" wird konsistent verwendet: Bot-Präsenz, WebUI-Branding, Embed-Footer, Docker-Image-Namen, Dokumentation. Halte dich an diese Spezifikation. Frage nach, bevor du Module weglässt oder den Stack änderst.
## Tech-Stack & Architektur
diff --git a/landingpage/assets/nexumi-logo.svg b/landingpage/assets/nexumi-logo.svg
new file mode 100644
index 0000000..210c5a6
--- /dev/null
+++ b/landingpage/assets/nexumi-logo.svg
@@ -0,0 +1,37 @@
+
diff --git a/landingpage/assets/style.css b/landingpage/assets/style.css
new file mode 100644
index 0000000..afae64b
--- /dev/null
+++ b/landingpage/assets/style.css
@@ -0,0 +1,327 @@
+:root {
+ --background: #f8f8fb;
+ --foreground: #111116;
+ --card: #ffffff;
+ --muted: #f1f1f5;
+ --muted-foreground: #6b7280;
+ --primary: #6366f1;
+ --primary-foreground: #ffffff;
+ --secondary: #f1f1f5;
+ --secondary-foreground: #111116;
+ --border: #e4e4ea;
+ --radius: 0.625rem;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --background: #0a0a0d;
+ --foreground: #e6e6ea;
+ --card: #131318;
+ --muted: #1b1b22;
+ --muted-foreground: #9a9aa5;
+ --secondary: #1b1b22;
+ --secondary-foreground: #e6e6ea;
+ --border: #23232b;
+ }
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ min-height: 100vh;
+ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
+ background: var(--background);
+ color: var(--foreground);
+ line-height: 1.5;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+.shell {
+ display: flex;
+ min-height: 100vh;
+ flex-direction: column;
+}
+
+.container {
+ width: 100%;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding-left: 1.5rem;
+ padding-right: 1.5rem;
+}
+
+.border-b {
+ border-bottom: 1px solid var(--border);
+}
+
+.border-t {
+ border-top: 1px solid var(--border);
+}
+
+header .bar {
+ display: flex;
+ height: 3.5rem;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-weight: 600;
+}
+
+.brand img {
+ width: 28px;
+ height: 28px;
+}
+
+nav {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.btn {
+ display: inline-flex;
+ height: 2.25rem;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ border-radius: calc(var(--radius) - 2px);
+ padding: 0 0.75rem;
+ font-size: 0.875rem;
+ font-weight: 500;
+ border: 1px solid transparent;
+ cursor: pointer;
+ transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
+}
+
+.btn-lg {
+ height: 2.75rem;
+ padding: 0 1.25rem;
+ font-size: 1rem;
+}
+
+.btn-primary {
+ background: var(--primary);
+ color: var(--primary-foreground);
+}
+
+.btn-primary:hover {
+ filter: brightness(0.95);
+}
+
+.btn-secondary {
+ background: var(--secondary);
+ color: var(--secondary-foreground);
+}
+
+.btn-outline {
+ background: transparent;
+ border-color: var(--border);
+}
+
+.btn-ghost {
+ background: transparent;
+}
+
+.btn-ghost:hover,
+.btn-outline:hover,
+.btn-secondary:hover {
+ background: var(--muted);
+}
+
+main {
+ flex: 1;
+}
+
+.hero {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+ padding-top: 4rem;
+ padding-bottom: 4rem;
+}
+
+@media (min-width: 640px) {
+ .hero {
+ padding-top: 5rem;
+ padding-bottom: 5rem;
+ }
+}
+
+.hero-brand {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.hero-brand img {
+ width: 48px;
+ height: 48px;
+}
+
+.hero h1 {
+ margin: 0;
+ font-size: 2.25rem;
+ font-weight: 600;
+ letter-spacing: -0.025em;
+}
+
+@media (min-width: 640px) {
+ .hero h1 {
+ font-size: 3rem;
+ }
+}
+
+.hero p {
+ margin: 0;
+ max-width: 42rem;
+ font-size: 1.125rem;
+ color: var(--muted-foreground);
+}
+
+.actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+}
+
+.stats {
+ display: grid;
+ gap: 1rem;
+ padding-top: 3rem;
+ padding-bottom: 3rem;
+}
+
+@media (min-width: 640px) {
+ .stats {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+.card {
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: 1rem 1.25rem;
+}
+
+.card-label {
+ margin: 0 0 0.5rem;
+ font-size: 0.875rem;
+ font-weight: 500;
+ color: var(--muted-foreground);
+}
+
+.card-value {
+ margin: 0;
+ font-size: 1.875rem;
+ font-weight: 600;
+}
+
+.features {
+ padding-top: 3rem;
+ padding-bottom: 3rem;
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+}
+
+.features > .intro h2 {
+ margin: 0;
+ font-size: 1.5rem;
+ font-weight: 600;
+}
+
+.features > .intro p {
+ margin: 0.5rem 0 0;
+ color: var(--muted-foreground);
+}
+
+.group {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.group h3 {
+ margin: 0;
+ font-size: 0.875rem;
+ font-weight: 600;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ color: var(--muted-foreground);
+}
+
+.grid {
+ display: grid;
+ gap: 0.75rem;
+}
+
+@media (min-width: 640px) {
+ .grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (min-width: 1024px) {
+ .grid {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+}
+
+.module-title {
+ margin: 0 0 0.5rem;
+ font-size: 1rem;
+ font-weight: 600;
+}
+
+.module-desc {
+ margin: 0;
+ font-size: 0.875rem;
+ color: var(--muted-foreground);
+}
+
+footer .bar {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ padding-top: 2rem;
+ padding-bottom: 2rem;
+ font-size: 0.875rem;
+ color: var(--muted-foreground);
+}
+
+@media (min-width: 640px) {
+ footer .bar {
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ }
+}
+
+footer nav {
+ gap: 1rem;
+}
+
+footer a:hover {
+ color: var(--foreground);
+}
diff --git a/landingpage/config.php b/landingpage/config.php
new file mode 100644
index 0000000..329ec5c
--- /dev/null
+++ b/landingpage/config.php
@@ -0,0 +1,24 @@
+ '1529428118010462278',
+
+ // Dashboard (Traefik / WebUI)
+ 'dashboard_url' => 'https://dashboard.nexumi.de',
+
+ // Optional support invite
+ 'support_url' => 'https://discord.gg/RZ9JcQrPDz',
+
+ // Live stats from the WebUI public API (falls back to placeholders on failure)
+ 'stats_api_url' => 'https://dashboard.nexumi.de/api/public/stats',
+
+ // Footer legal pages (still hosted on the dashboard app)
+ 'status_url' => 'https://dashboard.nexumi.de/status',
+ 'impressum_url' => 'https://dashboard.nexumi.de/impressum',
+ 'privacy_url' => 'https://dashboard.nexumi.de/privacy',
+ 'terms_url' => 'https://dashboard.nexumi.de/terms',
+];
diff --git a/landingpage/index.php b/landingpage/index.php
new file mode 100644
index 0000000..b484b7a
--- /dev/null
+++ b/landingpage/index.php
@@ -0,0 +1,192 @@
+ $clientId,
+ 'permissions' => '8',
+ 'scope' => 'bot applications.commands',
+], '', '&', PHP_QUERY_RFC3986);
+
+$dashboardUrl = rtrim((string) $config['dashboard_url'], '/');
+$loginUrl = $dashboardUrl . '/login';
+$supportUrl = isset($config['support_url']) && $config['support_url'] !== ''
+ ? (string) $config['support_url']
+ : null;
+
+$guildCount = 0;
+$userCount = 0;
+
+$statsApi = (string) ($config['stats_api_url'] ?? '');
+if ($statsApi !== '') {
+ $ctx = stream_context_create([
+ 'http' => [
+ 'timeout' => 3,
+ 'header' => "Accept: application/json\r\n",
+ ],
+ ]);
+ $raw = @file_get_contents($statsApi, false, $ctx);
+ if ($raw !== false) {
+ $json = json_decode($raw, true);
+ if (is_array($json)) {
+ $guildCount = (int) ($json['guildCount'] ?? 0);
+ $userCount = (int) ($json['approximateUserCount'] ?? 0);
+ }
+ }
+}
+
+$year = (int) date('Y');
+
+$groups = [
+ 'moderation' => [
+ 'label' => 'Moderation',
+ 'modules' => [
+ ['label' => 'Moderation', 'description' => 'Bans, Verwarnungen, Timeouts und das Fall-System'],
+ ['label' => 'Auto-Moderation', 'description' => 'Spam-, Raid- und Nuke-Schutz'],
+ ['label' => 'Logging', 'description' => 'Log-Kanäle für Server-Events'],
+ ],
+ ],
+ 'engagement' => [
+ 'label' => 'Engagement',
+ 'modules' => [
+ ['label' => 'Willkommen & Abschied', 'description' => 'Join- und Leave-Nachrichten, Autoroles'],
+ ['label' => 'Verifizierung', 'description' => 'Button- und Captcha-Verifizierung'],
+ ['label' => 'Leveling & XP', 'description' => 'Text- und Voice-XP, Rollen-Belohnungen'],
+ ['label' => 'Economy', 'description' => 'Währung, Shop und Spiele'],
+ ['label' => 'Fun & Spiele', 'description' => 'Minispiele und Medien-Commands'],
+ ],
+ ],
+ 'community' => [
+ 'label' => 'Community',
+ 'modules' => [
+ ['label' => 'Giveaways', 'description' => 'Button-basierte Giveaways'],
+ ['label' => 'Tickets', 'description' => 'Support-Ticket-Panels und Transkripte'],
+ ['label' => 'Self Roles', 'description' => 'Reaktions-, Button- und Dropdown-Rollenpanels'],
+ ['label' => 'Starboard', 'description' => 'Beliebte Nachrichten hervorheben'],
+ ['label' => 'Vorschläge', 'description' => 'Community-Vorschläge mit Abstimmung'],
+ ['label' => 'Geburtstage', 'description' => 'Geburtstags-Ankündigungen und -Rollen'],
+ ],
+ ],
+ 'utility' => [
+ 'label' => 'Utility',
+ 'modules' => [
+ ['label' => 'Tags', 'description' => 'Custom Commands und Auto-Responder'],
+ ['label' => 'Temp-Voice', 'description' => 'Join-to-Create Voice-Kanäle'],
+ ],
+ ],
+ 'integrations' => [
+ 'label' => 'Integrationen',
+ 'modules' => [
+ ['label' => 'Server-Statistiken', 'description' => 'Automatische Statistik-Kanäle und Invites'],
+ ['label' => 'Social Feeds', 'description' => 'Twitch-, YouTube-, RSS- und Reddit-Benachrichtigungen'],
+ ['label' => 'Scheduler', 'description' => 'Geplante und wiederkehrende Ankündigungen'],
+ ['label' => 'Backups', 'description' => 'Server-Backup und -Wiederherstellung'],
+ ],
+ ],
+];
+
+function h(?string $value): string
+{
+ return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+}
+?>
+
+
+
+
+
+ Nexumi
+
+
+
+
+
+
+
+
+
+
+
+
+

+
Nexumi
+
+
Discord-Bot mit Moderations-, Community- und Integrationsmodulen sowie Web-Dashboard.
+
+
+
+
+
+
+
+
Server
+
= h(number_format($guildCount, 0, ',', '.')) ?>
+
+
+
Nutzer (ca.)
+
= h(number_format($userCount, 0, ',', '.')) ?>
+
+
+
+
+
+
+
+
Module
+
Funktionen, die Nexumi pro Server bereitstellt.
+
+
+
+
= h($group['label']) ?>
+
+
+
+
= h($module['label']) ?>
+
= h($module['description']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+