deploy #1
@@ -1,4 +1,4 @@
|
|||||||
NODE_ENV=development
|
NODE_ENV=production
|
||||||
# Shared by apps/bot (gateway connection) and apps/webui (narrow REST calls +
|
# Shared by apps/bot (gateway connection) and apps/webui (narrow REST calls +
|
||||||
# BullMQ job enqueueing for guild backups, giveaways and the scheduler).
|
# BullMQ job enqueueing for guild backups, giveaways and the scheduler).
|
||||||
BOT_TOKEN=
|
BOT_TOKEN=
|
||||||
@@ -15,12 +15,15 @@ BACKUP_RETENTION_DAYS=14
|
|||||||
BACKUP_CRON=0 3 * * *
|
BACKUP_CRON=0 3 * * *
|
||||||
BACKUP_DIR=/backups
|
BACKUP_DIR=/backups
|
||||||
HEALTH_PORT=8080
|
HEALTH_PORT=8080
|
||||||
|
# Bot health stays internal (no published port). Override only if you expose it.
|
||||||
PUBLIC_BASE_URL=http://localhost:8080
|
PUBLIC_BASE_URL=http://localhost:8080
|
||||||
TWITCH_CLIENT_ID=
|
TWITCH_CLIENT_ID=
|
||||||
TWITCH_CLIENT_SECRET=
|
TWITCH_CLIENT_SECRET=
|
||||||
|
|
||||||
# WebUI (apps/webui)
|
# WebUI (apps/webui) – public URL behind Traefik
|
||||||
WEBUI_URL=http://10.111.0.65:3000
|
WEBUI_URL=https://dashboard.nexumi.de
|
||||||
|
# Discord Developer Portal OAuth2 redirect:
|
||||||
|
# https://dashboard.nexumi.de/api/auth/callback
|
||||||
# Must be at least 32 characters long. Generate e.g. with `openssl rand -hex 32`.
|
# Must be at least 32 characters long. Generate e.g. with `openssl rand -hex 32`.
|
||||||
SESSION_SECRET=
|
SESSION_SECRET=
|
||||||
# Comma-separated Discord user IDs treated as global bot owners (owner panel, Phase 7 Batch C).
|
# Comma-separated Discord user IDs treated as global bot owners (owner panel, Phase 7 Batch C).
|
||||||
|
|||||||
@@ -1,52 +1,12 @@
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { getLocale, t } from '@/lib/i18n';
|
|
||||||
import { getSession } from '@/lib/session';
|
|
||||||
|
|
||||||
interface LoginPageProps {
|
interface LoginPageProps {
|
||||||
searchParams: Promise<{ error?: string }>;
|
searchParams: Promise<{ error?: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const KNOWN_ERRORS = ['access_denied', 'invalid_request', 'invalid_state', 'oauth_failed'] as const;
|
/** Alias for `/` so OAuth error redirects to `/login?error=…` keep working. */
|
||||||
|
|
||||||
export default async function LoginPage({ searchParams }: LoginPageProps) {
|
export default async function LoginPage({ searchParams }: LoginPageProps) {
|
||||||
const session = await getSession();
|
|
||||||
if (session) {
|
|
||||||
redirect('/dashboard');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { error } = await searchParams;
|
const { error } = await searchParams;
|
||||||
const locale = await getLocale();
|
const target = error ? `/?error=${encodeURIComponent(error)}` : '/';
|
||||||
const errorMessage =
|
redirect(target);
|
||||||
error && (KNOWN_ERRORS as readonly string[]).includes(error)
|
|
||||||
? t(locale, `login.errors.${error}`)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="flex min-h-screen items-center justify-center px-4">
|
|
||||||
<Card className="w-full max-w-sm">
|
|
||||||
<CardHeader className="items-center text-center">
|
|
||||||
<div className="mb-2 flex size-12 items-center justify-center rounded-xl bg-primary text-lg font-semibold text-primary-foreground">
|
|
||||||
N
|
|
||||||
</div>
|
|
||||||
<CardTitle className="text-xl">{t(locale, 'login.title')}</CardTitle>
|
|
||||||
<CardDescription>{t(locale, 'login.subtitle')}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
{errorMessage && (
|
|
||||||
<p className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
|
||||||
{errorMessage}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<a
|
|
||||||
href="/api/auth/login"
|
|
||||||
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded-md bg-primary text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
|
||||||
>
|
|
||||||
{t(locale, 'login.cta')}
|
|
||||||
</a>
|
|
||||||
<p className="text-center text-xs text-muted-foreground">{t(locale, 'login.footer')}</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,111 +1,17 @@
|
|||||||
import { DASHBOARD_MODULES, type DashboardModuleGroup } from '@nexumi/shared';
|
import { redirect } from 'next/navigation';
|
||||||
import Image from 'next/image';
|
import { LoginCard } from '@/components/auth/login-card';
|
||||||
import Link from 'next/link';
|
|
||||||
import { SiteShell } from '@/components/site/site-shell';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { buildBotInviteUrl, env } from '@/lib/env';
|
|
||||||
import { getLocale, t } from '@/lib/i18n';
|
|
||||||
import { getPublicStats } from '@/lib/public-stats';
|
|
||||||
import { getSession } from '@/lib/session';
|
import { getSession } from '@/lib/session';
|
||||||
|
|
||||||
const GROUP_ORDER: DashboardModuleGroup[] = [
|
interface HomePageProps {
|
||||||
'moderation',
|
searchParams: Promise<{ error?: string }>;
|
||||||
'engagement',
|
}
|
||||||
'community',
|
|
||||||
'utility',
|
export default async function HomePage({ searchParams }: HomePageProps) {
|
||||||
'integrations'
|
const session = await getSession();
|
||||||
];
|
if (session) {
|
||||||
|
redirect('/dashboard');
|
||||||
export default async function LandingPage() {
|
}
|
||||||
const [locale, stats, session] = await Promise.all([getLocale(), getPublicStats(), getSession()]);
|
|
||||||
const inviteUrl = buildBotInviteUrl();
|
const { error } = await searchParams;
|
||||||
const isLoggedIn = Boolean(session);
|
return <LoginCard error={error} />;
|
||||||
|
|
||||||
return (
|
|
||||||
<SiteShell>
|
|
||||||
<section className="border-b border-border">
|
|
||||||
<div className="mx-auto flex w-full max-w-[1200px] flex-col gap-8 px-6 py-16 sm:py-20">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Image src="/nexumi-logo.svg" alt="" width={48} height={48} priority />
|
|
||||||
<h1 className="text-4xl font-semibold tracking-tight sm:text-5xl">Nexumi</h1>
|
|
||||||
</div>
|
|
||||||
<p className="max-w-2xl text-lg text-muted-foreground">{t(locale, 'landing.hero.subtitle')}</p>
|
|
||||||
<div className="flex flex-wrap gap-3">
|
|
||||||
<Button asChild size="lg">
|
|
||||||
<a href={inviteUrl} target="_blank" rel="noreferrer">
|
|
||||||
{t(locale, 'landing.hero.invite')}
|
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
<Button asChild size="lg" variant="secondary">
|
|
||||||
<Link href={isLoggedIn ? '/dashboard' : '/login'}>
|
|
||||||
{isLoggedIn ? t(locale, 'landing.hero.dashboard') : t(locale, 'landing.hero.login')}
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
{env.SUPPORT_SERVER_URL ? (
|
|
||||||
<Button asChild size="lg" variant="outline">
|
|
||||||
<a href={env.SUPPORT_SERVER_URL} target="_blank" rel="noreferrer">
|
|
||||||
{t(locale, 'landing.hero.support')}
|
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="border-b border-border">
|
|
||||||
<div className="mx-auto grid w-full max-w-[1200px] gap-4 px-6 py-12 sm:grid-cols-2">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
||||||
{t(locale, 'landing.stats.guilds')}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="text-3xl font-semibold">{stats.guildCount.toLocaleString(locale)}</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
||||||
{t(locale, 'landing.stats.users')}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="text-3xl font-semibold">
|
|
||||||
{stats.approximateUserCount.toLocaleString(locale)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<div className="mx-auto w-full max-w-[1200px] space-y-8 px-6 py-12">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-2xl font-semibold">{t(locale, 'landing.features.title')}</h2>
|
|
||||||
<p className="mt-2 text-muted-foreground">{t(locale, 'landing.features.subtitle')}</p>
|
|
||||||
</div>
|
|
||||||
{GROUP_ORDER.map((group) => {
|
|
||||||
const modules = DASHBOARD_MODULES.filter((module) => module.group === group);
|
|
||||||
return (
|
|
||||||
<div key={group} className="space-y-3">
|
|
||||||
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
|
||||||
{t(locale, `moduleGroups.${group}`)}
|
|
||||||
</h3>
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{modules.map((module) => (
|
|
||||||
<Card key={module.id}>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-base">{t(locale, `modules.${module.id}.label`)}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="text-sm text-muted-foreground">
|
|
||||||
{t(locale, `modules.${module.id}.description`)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</SiteShell>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
117
apps/webui/src/archived/landing-page.tsx
Normal file
117
apps/webui/src/archived/landing-page.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
/**
|
||||||
|
* Archived marketing landing page (Phase 8).
|
||||||
|
* Replaced by the login screen at `/` for the dashboard.nexumi.de deploy.
|
||||||
|
* A separate public landing will live on nexumi.de later.
|
||||||
|
* This file is not an App Router page and is not served.
|
||||||
|
*/
|
||||||
|
import { DASHBOARD_MODULES, type DashboardModuleGroup } from '@nexumi/shared';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { SiteShell } from '@/components/site/site-shell';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { buildBotInviteUrl, env } from '@/lib/env';
|
||||||
|
import { getLocale, t } from '@/lib/i18n';
|
||||||
|
import { getPublicStats } from '@/lib/public-stats';
|
||||||
|
import { getSession } from '@/lib/session';
|
||||||
|
|
||||||
|
const GROUP_ORDER: DashboardModuleGroup[] = [
|
||||||
|
'moderation',
|
||||||
|
'engagement',
|
||||||
|
'community',
|
||||||
|
'utility',
|
||||||
|
'integrations'
|
||||||
|
];
|
||||||
|
|
||||||
|
export default async function ArchivedLandingPage() {
|
||||||
|
const [locale, stats, session] = await Promise.all([getLocale(), getPublicStats(), getSession()]);
|
||||||
|
const inviteUrl = buildBotInviteUrl();
|
||||||
|
const isLoggedIn = Boolean(session);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SiteShell>
|
||||||
|
<section className="border-b border-border">
|
||||||
|
<div className="mx-auto flex w-full max-w-[1200px] flex-col gap-8 px-6 py-16 sm:py-20">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Image src="/nexumi-logo.svg" alt="" width={48} height={48} priority />
|
||||||
|
<h1 className="text-4xl font-semibold tracking-tight sm:text-5xl">Nexumi</h1>
|
||||||
|
</div>
|
||||||
|
<p className="max-w-2xl text-lg text-muted-foreground">{t(locale, 'landing.hero.subtitle')}</p>
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<Button asChild size="lg">
|
||||||
|
<a href={inviteUrl} target="_blank" rel="noreferrer">
|
||||||
|
{t(locale, 'landing.hero.invite')}
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
<Button asChild size="lg" variant="secondary">
|
||||||
|
<Link href={isLoggedIn ? '/dashboard' : '/login'}>
|
||||||
|
{isLoggedIn ? t(locale, 'landing.hero.dashboard') : t(locale, 'landing.hero.login')}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
{env.SUPPORT_SERVER_URL ? (
|
||||||
|
<Button asChild size="lg" variant="outline">
|
||||||
|
<a href={env.SUPPORT_SERVER_URL} target="_blank" rel="noreferrer">
|
||||||
|
{t(locale, 'landing.hero.support')}
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="border-b border-border">
|
||||||
|
<div className="mx-auto grid w-full max-w-[1200px] gap-4 px-6 py-12 sm:grid-cols-2">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
{t(locale, 'landing.stats.guilds')}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-3xl font-semibold">{stats.guildCount.toLocaleString(locale)}</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
{t(locale, 'landing.stats.users')}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-3xl font-semibold">
|
||||||
|
{stats.approximateUserCount.toLocaleString(locale)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div className="mx-auto w-full max-w-[1200px] space-y-8 px-6 py-12">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-semibold">{t(locale, 'landing.features.title')}</h2>
|
||||||
|
<p className="mt-2 text-muted-foreground">{t(locale, 'landing.features.subtitle')}</p>
|
||||||
|
</div>
|
||||||
|
{GROUP_ORDER.map((group) => {
|
||||||
|
const modules = DASHBOARD_MODULES.filter((module) => module.group === group);
|
||||||
|
return (
|
||||||
|
<div key={group} className="space-y-3">
|
||||||
|
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||||
|
{t(locale, `moduleGroups.${group}`)}
|
||||||
|
</h3>
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{modules.map((module) => (
|
||||||
|
<Card key={module.id}>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-base">{t(locale, `modules.${module.id}.label`)}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-sm text-muted-foreground">
|
||||||
|
{t(locale, `modules.${module.id}.description`)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</SiteShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
apps/webui/src/components/auth/login-card.tsx
Normal file
40
apps/webui/src/components/auth/login-card.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { getLocale, t } from '@/lib/i18n';
|
||||||
|
|
||||||
|
const KNOWN_ERRORS = ['access_denied', 'invalid_request', 'invalid_state', 'oauth_failed'] as const;
|
||||||
|
|
||||||
|
export async function LoginCard({ error }: { error?: string }) {
|
||||||
|
const locale = await getLocale();
|
||||||
|
const errorMessage =
|
||||||
|
error && (KNOWN_ERRORS as readonly string[]).includes(error)
|
||||||
|
? t(locale, `login.errors.${error}`)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-screen items-center justify-center px-4">
|
||||||
|
<Card className="w-full max-w-sm">
|
||||||
|
<CardHeader className="items-center text-center">
|
||||||
|
<div className="mb-2 flex size-12 items-center justify-center rounded-xl bg-primary text-lg font-semibold text-primary-foreground">
|
||||||
|
N
|
||||||
|
</div>
|
||||||
|
<CardTitle className="text-xl">{t(locale, 'login.title')}</CardTitle>
|
||||||
|
<CardDescription>{t(locale, 'login.subtitle')}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{errorMessage && (
|
||||||
|
<p className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||||
|
{errorMessage}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<a
|
||||||
|
href="/api/auth/login"
|
||||||
|
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded-md bg-primary text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||||
|
>
|
||||||
|
{t(locale, 'login.cta')}
|
||||||
|
</a>
|
||||||
|
<p className="text-center text-xs text-muted-foreground">{t(locale, 'login.footer')}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ services:
|
|||||||
POSTGRES_PASSWORD: nexumi
|
POSTGRES_PASSWORD: nexumi
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U nexumi -d nexumi"]
|
test: ["CMD-SHELL", "pg_isready -U nexumi -d nexumi"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
@@ -18,6 +20,8 @@ services:
|
|||||||
command: redis-server --appendonly yes
|
command: redis-server --appendonly yes
|
||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
@@ -36,6 +40,8 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
volumes:
|
volumes:
|
||||||
- backups:/backups
|
- backups:/backups
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
[
|
[
|
||||||
@@ -57,12 +63,16 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
ports:
|
networks:
|
||||||
- "3000:3000"
|
- internal
|
||||||
|
- traefik-network
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.nexumi.rule=Host(`nexumi.de`)"
|
- "traefik.docker.network=traefik-network"
|
||||||
- "traefik.http.services.nexumi.loadbalancer.server.port=3000"
|
- "traefik.http.routers.nexumi-dashboard.rule=Host(`dashboard.nexumi.de`)"
|
||||||
|
- "traefik.http.routers.nexumi-dashboard.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.nexumi-dashboard.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.services.nexumi-dashboard.loadbalancer.server.port=3000"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
[
|
[
|
||||||
@@ -78,3 +88,9 @@ volumes:
|
|||||||
postgres_data:
|
postgres_data:
|
||||||
redis_data:
|
redis_data:
|
||||||
backups:
|
backups:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik-network:
|
||||||
|
external: true
|
||||||
|
internal:
|
||||||
|
driver: bridge
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
## Phase 6 – WebUI-Fundament (Status: abgeschlossen, manuell bestätigt)
|
## Phase 6 – WebUI-Fundament (Status: abgeschlossen, manuell bestätigt)
|
||||||
|
|
||||||
- OAuth/Sessions, Settings-Framework, Layout, Modul-Toggles, Access-Rules
|
- OAuth/Sessions, Settings-Framework, Layout, Modul-Toggles, Access-Rules
|
||||||
- Login manuell bestätigt (`WEBUI_URL=http://10.111.0.65:3000`)
|
- Login manuell bestätigt
|
||||||
|
|
||||||
## Phase 7 – WebUI Modul-Seiten + Owner-Panel (Status: abgeschlossen, manuell bestätigt)
|
## Phase 7 – WebUI Modul-Seiten + Owner-Panel (Status: abgeschlossen, manuell bestätigt)
|
||||||
|
|
||||||
@@ -20,6 +20,26 @@
|
|||||||
- Landing `/`, Status `/status`, Rechtsseiten, Public APIs, Core-Commands, `docs/verification.md`
|
- Landing `/`, Status `/status`, Rechtsseiten, Public APIs, Core-Commands, `docs/verification.md`
|
||||||
- Manuell bestätigt (User-Freigabe)
|
- Manuell bestätigt (User-Freigabe)
|
||||||
|
|
||||||
|
## Deploy-Branch (`deploy`) – Traefik + Dashboard-Domain
|
||||||
|
|
||||||
|
### Abgeschlossen (Code auf Branch `deploy`; `main` unverändert)
|
||||||
|
|
||||||
|
- Traefik-Vollstack: Compose-Netz `traefik-network` (external) + `internal`
|
||||||
|
- WebUI hinter Traefik: Host `dashboard.nexumi.de`, Entrypoint `websecure`, `certresolver=letsencrypt`
|
||||||
|
- Host-Port `3000` entfernt; nur Traefik exponiert die WebUI
|
||||||
|
- `WEBUI_URL=https://dashboard.nexumi.de` (OAuth-Callback: `/api/auth/callback`)
|
||||||
|
- Marketing-Landing archiviert unter `apps/webui/src/archived/landing-page.tsx`
|
||||||
|
- `/` zeigt Login; `/login` leitet auf `/` weiter (Query erhalten)
|
||||||
|
- **SPEC-Abweichung (bewusst):** öffentliche Landing später extern auf `nexumi.de`; Dashboard unter Subdomain
|
||||||
|
|
||||||
|
### Manuell testen (Deploy)
|
||||||
|
|
||||||
|
- [ ] DNS `dashboard.nexumi.de` → Traefik-Host
|
||||||
|
- [ ] Discord OAuth Redirect: `https://dashboard.nexumi.de/api/auth/callback`
|
||||||
|
- [ ] `docker compose up -d --build` (Netz `traefik-network` muss existieren)
|
||||||
|
- [ ] TLS-Zertifikat (Resolver-Name ggf. an Traefik anpassen)
|
||||||
|
- [ ] Login `/` → Dashboard; Session-Cookie `Secure`
|
||||||
|
|
||||||
## Post-Phase – Premium + DSGVO (Status: implementiert, manuelle Tests offen)
|
## Post-Phase – Premium + DSGVO (Status: implementiert, manuelle Tests offen)
|
||||||
|
|
||||||
### Abgeschlossen (Code)
|
### Abgeschlossen (Code)
|
||||||
@@ -43,7 +63,8 @@
|
|||||||
|
|
||||||
- Musik- und KI-Modul (nur auf Zuruf)
|
- Musik- und KI-Modul (nur auf Zuruf)
|
||||||
- Premium-Zahlungsanbindung
|
- Premium-Zahlungsanbindung
|
||||||
|
- Externe Marketing-Landing auf `nexumi.de`
|
||||||
|
|
||||||
## Nächster geplanter Schritt
|
## Nächster geplanter Schritt
|
||||||
|
|
||||||
- Nach manueller Freigabe: Musik/KI auf Zuruf.
|
- Deploy auf Traefik-Server manuell verifizieren; danach ggf. `deploy` → `main` mergen (nur auf Freigabe).
|
||||||
|
|||||||
Reference in New Issue
Block a user