Enhance environment configuration and expand Prisma schema for bot management

- Added OWNER_USER_IDS to .env.example for specifying global bot owners.
- Introduced new models in the Prisma schema: CommandOverride, OwnerTeamMember, GlobalUserBlacklist, GuildBlacklist, FeatureFlag, BotPresenceConfig, ChangelogEntry, and OwnerAuditLog to support advanced bot management features.
- Updated env.ts to preprocess OWNER_USER_IDS for better handling of global bot owner IDs.
- Modified ModulePlaceholderPage to ensure proper routing for remaining dashboard modules.
This commit is contained in:
smueller
2026-07-22 14:51:56 +02:00
parent 946283dfba
commit 1de28fa494
54 changed files with 3564 additions and 3 deletions

View File

@@ -0,0 +1,46 @@
import { AutomodConfigDashboardPatchSchema } from '@nexumi/shared';
import { type NextRequest, NextResponse } from 'next/server';
import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth';
import { writeDashboardAudit } from '@/lib/audit';
import { getAutomodDashboard, updateAutomodDashboard } from '@/lib/module-configs/automod';
import { prisma } from '@/lib/prisma';
interface RouteParams {
params: Promise<{ guildId: string }>;
}
export async function GET(_request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
await requireGuildAccess(guildId);
const config = await getAutomodDashboard(guildId);
return NextResponse.json(config);
} catch (error) {
return toApiErrorResponse(error);
}
}
export async function PATCH(request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
const session = await requireGuildAccess(guildId);
const body = await request.json();
const patch = AutomodConfigDashboardPatchSchema.parse(body);
const before = await getAutomodDashboard(guildId);
const after = await updateAutomodDashboard(guildId, patch);
await writeDashboardAudit(prisma, {
guildId,
actorUserId: session.user.id,
action: 'automod.update',
path: `/dashboard/${guildId}/automod`,
before,
after
});
return NextResponse.json(after);
} catch (error) {
return toApiErrorResponse(error);
}
}

View File

@@ -0,0 +1,25 @@
import { CaseListQuerySchema } from '@nexumi/shared';
import { type NextRequest, NextResponse } from 'next/server';
import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth';
import { listCases } from '@/lib/module-configs/cases';
interface RouteParams {
params: Promise<{ guildId: string }>;
}
export async function GET(request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
await requireGuildAccess(guildId);
const searchParams = request.nextUrl.searchParams;
const query = CaseListQuerySchema.parse({
search: searchParams.get('search') ?? undefined,
action: searchParams.get('action') ?? undefined,
limit: searchParams.get('limit') ?? undefined
});
const cases = await listCases(guildId, query);
return NextResponse.json({ cases });
} catch (error) {
return toApiErrorResponse(error);
}
}

View File

@@ -0,0 +1,46 @@
import { EconomyConfigDashboardPatchSchema } from '@nexumi/shared';
import { type NextRequest, NextResponse } from 'next/server';
import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth';
import { writeDashboardAudit } from '@/lib/audit';
import { getEconomyDashboard, updateEconomyDashboard } from '@/lib/module-configs/economy';
import { prisma } from '@/lib/prisma';
interface RouteParams {
params: Promise<{ guildId: string }>;
}
export async function GET(_request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
await requireGuildAccess(guildId);
const config = await getEconomyDashboard(guildId);
return NextResponse.json(config);
} catch (error) {
return toApiErrorResponse(error);
}
}
export async function PATCH(request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
const session = await requireGuildAccess(guildId);
const body = await request.json();
const patch = EconomyConfigDashboardPatchSchema.parse(body);
const before = await getEconomyDashboard(guildId);
const after = await updateEconomyDashboard(guildId, patch);
await writeDashboardAudit(prisma, {
guildId,
actorUserId: session.user.id,
action: 'economy.update',
path: `/dashboard/${guildId}/economy`,
before,
after
});
return NextResponse.json(after);
} catch (error) {
return toApiErrorResponse(error);
}
}

View File

@@ -0,0 +1,46 @@
import { FunConfigDashboardPatchSchema } from '@nexumi/shared';
import { type NextRequest, NextResponse } from 'next/server';
import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth';
import { writeDashboardAudit } from '@/lib/audit';
import { getFunDashboard, updateFunDashboard } from '@/lib/module-configs/fun';
import { prisma } from '@/lib/prisma';
interface RouteParams {
params: Promise<{ guildId: string }>;
}
export async function GET(_request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
await requireGuildAccess(guildId);
const config = await getFunDashboard(guildId);
return NextResponse.json(config);
} catch (error) {
return toApiErrorResponse(error);
}
}
export async function PATCH(request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
const session = await requireGuildAccess(guildId);
const body = await request.json();
const patch = FunConfigDashboardPatchSchema.parse(body);
const before = await getFunDashboard(guildId);
const after = await updateFunDashboard(guildId, patch);
await writeDashboardAudit(prisma, {
guildId,
actorUserId: session.user.id,
action: 'fun.update',
path: `/dashboard/${guildId}/fun`,
before,
after
});
return NextResponse.json(after);
} catch (error) {
return toApiErrorResponse(error);
}
}

View File

@@ -0,0 +1,46 @@
import { LevelingConfigDashboardPatchSchema } from '@nexumi/shared';
import { type NextRequest, NextResponse } from 'next/server';
import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth';
import { writeDashboardAudit } from '@/lib/audit';
import { getLevelingDashboard, updateLevelingDashboard } from '@/lib/module-configs/leveling';
import { prisma } from '@/lib/prisma';
interface RouteParams {
params: Promise<{ guildId: string }>;
}
export async function GET(_request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
await requireGuildAccess(guildId);
const config = await getLevelingDashboard(guildId);
return NextResponse.json(config);
} catch (error) {
return toApiErrorResponse(error);
}
}
export async function PATCH(request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
const session = await requireGuildAccess(guildId);
const body = await request.json();
const patch = LevelingConfigDashboardPatchSchema.parse(body);
const before = await getLevelingDashboard(guildId);
const after = await updateLevelingDashboard(guildId, patch);
await writeDashboardAudit(prisma, {
guildId,
actorUserId: session.user.id,
action: 'leveling.update',
path: `/dashboard/${guildId}/leveling`,
before,
after
});
return NextResponse.json(after);
} catch (error) {
return toApiErrorResponse(error);
}
}

View File

@@ -0,0 +1,46 @@
import { LoggingConfigDashboardPatchSchema } from '@nexumi/shared';
import { type NextRequest, NextResponse } from 'next/server';
import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth';
import { writeDashboardAudit } from '@/lib/audit';
import { getLoggingDashboard, updateLoggingDashboard } from '@/lib/module-configs/logging';
import { prisma } from '@/lib/prisma';
interface RouteParams {
params: Promise<{ guildId: string }>;
}
export async function GET(_request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
await requireGuildAccess(guildId);
const config = await getLoggingDashboard(guildId);
return NextResponse.json(config);
} catch (error) {
return toApiErrorResponse(error);
}
}
export async function PATCH(request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
const session = await requireGuildAccess(guildId);
const body = await request.json();
const patch = LoggingConfigDashboardPatchSchema.parse(body);
const before = await getLoggingDashboard(guildId);
const after = await updateLoggingDashboard(guildId, patch);
await writeDashboardAudit(prisma, {
guildId,
actorUserId: session.user.id,
action: 'logging.update',
path: `/dashboard/${guildId}/logging`,
before,
after
});
return NextResponse.json(after);
} catch (error) {
return toApiErrorResponse(error);
}
}

View File

@@ -0,0 +1,46 @@
import { ModerationDashboardPatchSchema } from '@nexumi/shared';
import { type NextRequest, NextResponse } from 'next/server';
import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth';
import { writeDashboardAudit } from '@/lib/audit';
import { getModerationDashboard, updateModerationDashboard } from '@/lib/module-configs/moderation';
import { prisma } from '@/lib/prisma';
interface RouteParams {
params: Promise<{ guildId: string }>;
}
export async function GET(_request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
await requireGuildAccess(guildId);
const config = await getModerationDashboard(guildId);
return NextResponse.json(config);
} catch (error) {
return toApiErrorResponse(error);
}
}
export async function PATCH(request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
const session = await requireGuildAccess(guildId);
const body = await request.json();
const patch = ModerationDashboardPatchSchema.parse(body);
const before = await getModerationDashboard(guildId);
const after = await updateModerationDashboard(guildId, patch);
await writeDashboardAudit(prisma, {
guildId,
actorUserId: session.user.id,
action: 'moderation.update',
path: `/dashboard/${guildId}/moderation`,
before,
after
});
return NextResponse.json(after);
} catch (error) {
return toApiErrorResponse(error);
}
}

View File

@@ -0,0 +1,46 @@
import { VerificationConfigDashboardPatchSchema } from '@nexumi/shared';
import { type NextRequest, NextResponse } from 'next/server';
import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth';
import { writeDashboardAudit } from '@/lib/audit';
import { getVerificationDashboard, updateVerificationDashboard } from '@/lib/module-configs/verification';
import { prisma } from '@/lib/prisma';
interface RouteParams {
params: Promise<{ guildId: string }>;
}
export async function GET(_request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
await requireGuildAccess(guildId);
const config = await getVerificationDashboard(guildId);
return NextResponse.json(config);
} catch (error) {
return toApiErrorResponse(error);
}
}
export async function PATCH(request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
const session = await requireGuildAccess(guildId);
const body = await request.json();
const patch = VerificationConfigDashboardPatchSchema.parse(body);
const before = await getVerificationDashboard(guildId);
const after = await updateVerificationDashboard(guildId, patch);
await writeDashboardAudit(prisma, {
guildId,
actorUserId: session.user.id,
action: 'verification.update',
path: `/dashboard/${guildId}/verification`,
before,
after
});
return NextResponse.json(after);
} catch (error) {
return toApiErrorResponse(error);
}
}

View File

@@ -0,0 +1,23 @@
import { WarningListQuerySchema } from '@nexumi/shared';
import { type NextRequest, NextResponse } from 'next/server';
import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth';
import { listWarnings } from '@/lib/module-configs/warnings';
interface RouteParams {
params: Promise<{ guildId: string }>;
}
export async function GET(request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
await requireGuildAccess(guildId);
const searchParams = request.nextUrl.searchParams;
const query = WarningListQuerySchema.parse({
userId: searchParams.get('userId') ?? undefined
});
const warnings = await listWarnings(guildId, query);
return NextResponse.json({ warnings });
} catch (error) {
return toApiErrorResponse(error);
}
}

View File

@@ -0,0 +1,46 @@
import { WelcomeConfigDashboardPatchSchema } from '@nexumi/shared';
import { type NextRequest, NextResponse } from 'next/server';
import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth';
import { writeDashboardAudit } from '@/lib/audit';
import { getWelcomeDashboard, updateWelcomeDashboard } from '@/lib/module-configs/welcome';
import { prisma } from '@/lib/prisma';
interface RouteParams {
params: Promise<{ guildId: string }>;
}
export async function GET(_request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
await requireGuildAccess(guildId);
const config = await getWelcomeDashboard(guildId);
return NextResponse.json(config);
} catch (error) {
return toApiErrorResponse(error);
}
}
export async function PATCH(request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
const session = await requireGuildAccess(guildId);
const body = await request.json();
const patch = WelcomeConfigDashboardPatchSchema.parse(body);
const before = await getWelcomeDashboard(guildId);
const after = await updateWelcomeDashboard(guildId, patch);
await writeDashboardAudit(prisma, {
guildId,
actorUserId: session.user.id,
action: 'welcome.update',
path: `/dashboard/${guildId}/welcome`,
before,
after
});
return NextResponse.json(after);
} catch (error) {
return toApiErrorResponse(error);
}
}

View File

@@ -1,10 +1,31 @@
import { DASHBOARD_MODULES } from '@nexumi/shared';
import { DASHBOARD_MODULES, type DashboardModuleId } from '@nexumi/shared';
import { Construction } from 'lucide-react';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { Card, CardContent } from '@/components/ui/card';
import { getLocale, t } from '@/lib/i18n';
/**
* Batch A modules (Phase 7) got dedicated route folders (e.g.
* `dashboard/[guildId]/moderation/page.tsx`), which Next.js prefers over this
* catch-all - so they never reach this component. This placeholder now only
* serves the remaining Batch B/C modules until they get dedicated pages too.
*/
const REMAINING_MODULE_IDS: ReadonlySet<DashboardModuleId> = new Set([
'giveaways',
'tickets',
'selfroles',
'tags',
'starboard',
'suggestions',
'birthdays',
'tempvoice',
'stats',
'feeds',
'scheduler',
'guildbackup'
]);
interface ModulePlaceholderPageProps {
params: Promise<{ guildId: string; module: string }>;
}
@@ -13,7 +34,7 @@ export default async function ModulePlaceholderPage({ params }: ModulePlaceholde
const { guildId, module: moduleHref } = await params;
const moduleEntry = DASHBOARD_MODULES.find((entry) => entry.href === moduleHref);
if (!moduleEntry) {
if (!moduleEntry || !REMAINING_MODULE_IDS.has(moduleEntry.id)) {
notFound();
}

View File

@@ -0,0 +1,15 @@
import { Skeleton } from '@/components/ui/skeleton';
export default function AutomodLoading() {
return (
<div className="space-y-6">
<div className="space-y-2">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-4 w-80" />
</div>
{Array.from({ length: 3 }).map((_, index) => (
<Skeleton key={index} className="h-40 rounded-lg" />
))}
</div>
);
}

View File

@@ -0,0 +1,22 @@
import { AutomodForm } from '@/components/modules/automod-form';
import { getLocale, t } from '@/lib/i18n';
import { getAutomodDashboard } from '@/lib/module-configs/automod';
interface AutomodPageProps {
params: Promise<{ guildId: string }>;
}
export default async function AutomodPage({ params }: AutomodPageProps) {
const { guildId } = await params;
const [locale, config] = await Promise.all([getLocale(), getAutomodDashboard(guildId)]);
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">{t(locale, 'modules.automod.label')}</h1>
<p className="text-sm text-muted-foreground">{t(locale, 'modules.automod.description')}</p>
</div>
<AutomodForm guildId={guildId} initialValue={config} />
</div>
);
}

View File

@@ -0,0 +1,15 @@
import { Skeleton } from '@/components/ui/skeleton';
export default function EconomyLoading() {
return (
<div className="space-y-6">
<div className="space-y-2">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-4 w-80" />
</div>
{Array.from({ length: 2 }).map((_, index) => (
<Skeleton key={index} className="h-48 rounded-lg" />
))}
</div>
);
}

View File

@@ -0,0 +1,22 @@
import { EconomyForm } from '@/components/modules/economy-form';
import { getLocale, t } from '@/lib/i18n';
import { getEconomyDashboard } from '@/lib/module-configs/economy';
interface EconomyPageProps {
params: Promise<{ guildId: string }>;
}
export default async function EconomyPage({ params }: EconomyPageProps) {
const { guildId } = await params;
const [locale, config] = await Promise.all([getLocale(), getEconomyDashboard(guildId)]);
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">{t(locale, 'modules.economy.label')}</h1>
<p className="text-sm text-muted-foreground">{t(locale, 'modules.economy.description')}</p>
</div>
<EconomyForm guildId={guildId} initialValue={config} />
</div>
);
}

View File

@@ -0,0 +1,13 @@
import { Skeleton } from '@/components/ui/skeleton';
export default function FunLoading() {
return (
<div className="space-y-6">
<div className="space-y-2">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-4 w-80" />
</div>
<Skeleton className="h-40 rounded-lg" />
</div>
);
}

View File

@@ -0,0 +1,22 @@
import { FunForm } from '@/components/modules/fun-form';
import { getLocale, t } from '@/lib/i18n';
import { getFunDashboard } from '@/lib/module-configs/fun';
interface FunPageProps {
params: Promise<{ guildId: string }>;
}
export default async function FunPage({ params }: FunPageProps) {
const { guildId } = await params;
const [locale, config] = await Promise.all([getLocale(), getFunDashboard(guildId)]);
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">{t(locale, 'modules.fun.label')}</h1>
<p className="text-sm text-muted-foreground">{t(locale, 'modules.fun.description')}</p>
</div>
<FunForm guildId={guildId} initialValue={config} />
</div>
);
}

View File

@@ -0,0 +1,15 @@
import { Skeleton } from '@/components/ui/skeleton';
export default function LevelingLoading() {
return (
<div className="space-y-6">
<div className="space-y-2">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-4 w-80" />
</div>
{Array.from({ length: 3 }).map((_, index) => (
<Skeleton key={index} className="h-48 rounded-lg" />
))}
</div>
);
}

View File

@@ -0,0 +1,22 @@
import { LevelingForm } from '@/components/modules/leveling-form';
import { getLocale, t } from '@/lib/i18n';
import { getLevelingDashboard } from '@/lib/module-configs/leveling';
interface LevelingPageProps {
params: Promise<{ guildId: string }>;
}
export default async function LevelingPage({ params }: LevelingPageProps) {
const { guildId } = await params;
const [locale, config] = await Promise.all([getLocale(), getLevelingDashboard(guildId)]);
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">{t(locale, 'modules.leveling.label')}</h1>
<p className="text-sm text-muted-foreground">{t(locale, 'modules.leveling.description')}</p>
</div>
<LevelingForm guildId={guildId} initialValue={config} />
</div>
);
}

View File

@@ -0,0 +1,15 @@
import { Skeleton } from '@/components/ui/skeleton';
export default function LoggingLoading() {
return (
<div className="space-y-6">
<div className="space-y-2">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-4 w-80" />
</div>
{Array.from({ length: 2 }).map((_, index) => (
<Skeleton key={index} className="h-48 rounded-lg" />
))}
</div>
);
}

View File

@@ -0,0 +1,22 @@
import { LoggingForm } from '@/components/modules/logging-form';
import { getLocale, t } from '@/lib/i18n';
import { getLoggingDashboard } from '@/lib/module-configs/logging';
interface LoggingPageProps {
params: Promise<{ guildId: string }>;
}
export default async function LoggingPage({ params }: LoggingPageProps) {
const { guildId } = await params;
const [locale, config] = await Promise.all([getLocale(), getLoggingDashboard(guildId)]);
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">{t(locale, 'modules.logging.label')}</h1>
<p className="text-sm text-muted-foreground">{t(locale, 'modules.logging.description')}</p>
</div>
<LoggingForm guildId={guildId} initialValue={config} />
</div>
);
}

View File

@@ -0,0 +1,15 @@
import { Skeleton } from '@/components/ui/skeleton';
export default function ModerationLoading() {
return (
<div className="space-y-6">
<div className="space-y-2">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-4 w-80" />
</div>
{Array.from({ length: 3 }).map((_, index) => (
<Skeleton key={index} className="h-40 rounded-lg" />
))}
</div>
);
}

View File

@@ -0,0 +1,30 @@
import { CaseListQuerySchema } from '@nexumi/shared';
import { CasesTable } from '@/components/modules/cases-table';
import { ModerationForm } from '@/components/modules/moderation-form';
import { getLocale, t } from '@/lib/i18n';
import { listCases } from '@/lib/module-configs/cases';
import { getModerationDashboard } from '@/lib/module-configs/moderation';
interface ModerationPageProps {
params: Promise<{ guildId: string }>;
}
export default async function ModerationPage({ params }: ModerationPageProps) {
const { guildId } = await params;
const [locale, moderation, cases] = await Promise.all([
getLocale(),
getModerationDashboard(guildId),
listCases(guildId, CaseListQuerySchema.parse({}))
]);
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">{t(locale, 'modules.moderation.label')}</h1>
<p className="text-sm text-muted-foreground">{t(locale, 'modules.moderation.description')}</p>
</div>
<ModerationForm guildId={guildId} initialValue={moderation} />
<CasesTable guildId={guildId} initialCases={cases} />
</div>
);
}

View File

@@ -0,0 +1,13 @@
import { Skeleton } from '@/components/ui/skeleton';
export default function VerificationLoading() {
return (
<div className="space-y-6">
<div className="space-y-2">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-4 w-80" />
</div>
<Skeleton className="h-64 rounded-lg" />
</div>
);
}

View File

@@ -0,0 +1,22 @@
import { VerificationForm } from '@/components/modules/verification-form';
import { getLocale, t } from '@/lib/i18n';
import { getVerificationDashboard } from '@/lib/module-configs/verification';
interface VerificationPageProps {
params: Promise<{ guildId: string }>;
}
export default async function VerificationPage({ params }: VerificationPageProps) {
const { guildId } = await params;
const [locale, config] = await Promise.all([getLocale(), getVerificationDashboard(guildId)]);
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">{t(locale, 'modules.verification.label')}</h1>
<p className="text-sm text-muted-foreground">{t(locale, 'modules.verification.description')}</p>
</div>
<VerificationForm guildId={guildId} initialValue={config} />
</div>
);
}

View File

@@ -0,0 +1,15 @@
import { Skeleton } from '@/components/ui/skeleton';
export default function WelcomeLoading() {
return (
<div className="space-y-6">
<div className="space-y-2">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-4 w-80" />
</div>
{Array.from({ length: 2 }).map((_, index) => (
<Skeleton key={index} className="h-64 rounded-lg" />
))}
</div>
);
}

View File

@@ -0,0 +1,22 @@
import { WelcomeForm } from '@/components/modules/welcome-form';
import { getLocale, t } from '@/lib/i18n';
import { getWelcomeDashboard } from '@/lib/module-configs/welcome';
interface WelcomePageProps {
params: Promise<{ guildId: string }>;
}
export default async function WelcomePage({ params }: WelcomePageProps) {
const { guildId } = await params;
const [locale, config] = await Promise.all([getLocale(), getWelcomeDashboard(guildId)]);
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">{t(locale, 'modules.welcome.label')}</h1>
<p className="text-sm text-muted-foreground">{t(locale, 'modules.welcome.description')}</p>
</div>
<WelcomeForm guildId={guildId} initialValue={config} />
</div>
);
}