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

+

+
+
+

Nutzer (ca.)

+

+
+
+
+ +
+
+
+

Module

+

Funktionen, die Nexumi pro Server bereitstellt.

+
+ +
+

+
+ +
+

+

+
+ +
+
+ +
+
+
+ + +
+ +