diff --git a/apps/webui/next.config.ts b/apps/webui/next.config.ts index f5b3d39..a79ac4a 100644 --- a/apps/webui/next.config.ts +++ b/apps/webui/next.config.ts @@ -5,9 +5,7 @@ const nextConfig: NextConfig = { output: 'standalone', transpilePackages: ['@nexumi/shared'], outputFileTracingRoot: path.join(__dirname, '../..'), - // Keep Node-only packages out of the Next server webpack bundle so they - // cannot break React's RSC/client boundary during page-data collection. - serverExternalPackages: ['@sentry/node', '@prisma/client', 'ioredis', 'bullmq'] + serverExternalPackages: ['@prisma/client', 'ioredis', 'bullmq'] }; export default nextConfig; diff --git a/apps/webui/package.json b/apps/webui/package.json index 1d07fd6..910df60 100644 --- a/apps/webui/package.json +++ b/apps/webui/package.json @@ -23,7 +23,6 @@ "@radix-ui/react-separator": "^1.1.12", "@radix-ui/react-slot": "^1.3.0", "@radix-ui/react-switch": "^1.3.4", - "@sentry/node": "^10.67.0", "bullmq": "^5.34.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/apps/webui/src/app/dashboard/page.tsx b/apps/webui/src/app/dashboard/page.tsx index a90df44..685ad50 100644 --- a/apps/webui/src/app/dashboard/page.tsx +++ b/apps/webui/src/app/dashboard/page.tsx @@ -1,29 +1,9 @@ -import type { DashboardGuild } from '@nexumi/shared'; -import Link from 'next/link'; -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent } from '@/components/ui/card'; +import { DashboardGuildGrid } from '@/components/dashboard/dashboard-guild-grid'; import { requireAuthOrRedirect } from '@/lib/auth'; import { env } from '@/lib/env'; import { getManageableGuilds } from '@/lib/guilds'; import { getLocale, t } from '@/lib/i18n'; -function guildIconUrl(guild: Pick): string | undefined { - return guild.icon ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png?size=64` : undefined; -} - -function initials(name: string): string { - return ( - name - .split(' ') - .filter(Boolean) - .slice(0, 2) - .map((part) => part[0]?.toUpperCase()) - .join('') || '?' - ); -} - function buildInviteUrl(guildId: string): string { const url = new URL('https://discord.com/api/oauth2/authorize'); url.searchParams.set('client_id', env.BOT_CLIENT_ID); @@ -38,6 +18,11 @@ export default async function DashboardGuildListPage() { const guilds = await getManageableGuilds(session); const locale = await getLocale(); + const inviteUrls: Record = {}; + for (const guild of guilds) { + inviteUrls[guild.id] = buildInviteUrl(guild.id); + } + return (
@@ -46,47 +31,16 @@ export default async function DashboardGuildListPage() {

{t(locale, 'dashboard.guildList.subtitle')}

- {guilds.length === 0 ? ( - - - {t(locale, 'dashboard.guildList.empty')} - - - ) : ( -
- {guilds.map((guild) => ( - - -
- - - {initials(guild.name)} - -
-

{guild.name}

- {!guild.botPresent && ( - - {t(locale, 'dashboard.guildList.botMissing')} - - )} -
-
- {guild.botPresent ? ( - - ) : ( - - )} -
-
- ))} -
- )} +
); diff --git a/apps/webui/src/components/dashboard/dashboard-guild-grid.tsx b/apps/webui/src/components/dashboard/dashboard-guild-grid.tsx new file mode 100644 index 0000000..5fa682d --- /dev/null +++ b/apps/webui/src/components/dashboard/dashboard-guild-grid.tsx @@ -0,0 +1,86 @@ +'use client'; + +import type { DashboardGuild } from '@nexumi/shared'; +import Link from 'next/link'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; + +function guildIconUrl(guild: Pick): string | undefined { + return guild.icon + ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png?size=64` + : undefined; +} + +function initials(name: string): string { + return ( + name + .split(' ') + .filter(Boolean) + .slice(0, 2) + .map((part) => part[0]?.toUpperCase()) + .join('') || '?' + ); +} + +export function DashboardGuildGrid({ + guilds, + inviteUrls, + labels +}: { + guilds: DashboardGuild[]; + inviteUrls: Record; + labels: { + empty: string; + botMissing: string; + manage: string; + invite: string; + }; +}) { + if (guilds.length === 0) { + return ( + + + {labels.empty} + + + ); + } + + return ( +
+ {guilds.map((guild) => ( + + +
+ + + {initials(guild.name)} + +
+

{guild.name}

+ {!guild.botPresent && ( + + {labels.botMissing} + + )} +
+
+ {guild.botPresent ? ( + + ) : ( + + )} +
+
+ ))} +
+ ); +} diff --git a/apps/webui/src/components/ui/button.tsx b/apps/webui/src/components/ui/button.tsx index 30ba491..a810f1e 100644 --- a/apps/webui/src/components/ui/button.tsx +++ b/apps/webui/src/components/ui/button.tsx @@ -1,3 +1,5 @@ +'use client'; + import { Slot } from '@radix-ui/react-slot'; import { cva, type VariantProps } from 'class-variance-authority'; import * as React from 'react'; diff --git a/apps/webui/src/instrumentation.ts b/apps/webui/src/instrumentation.ts deleted file mode 100644 index bce497c..0000000 --- a/apps/webui/src/instrumentation.ts +++ /dev/null @@ -1,6 +0,0 @@ -export async function register(): Promise { - if (process.env.NEXT_RUNTIME === 'nodejs') { - const { initWebuiSentry } = await import('./lib/sentry'); - await initWebuiSentry(); - } -} diff --git a/apps/webui/src/lib/sentry.ts b/apps/webui/src/lib/sentry.ts index a0e11bd..fa1a59e 100644 --- a/apps/webui/src/lib/sentry.ts +++ b/apps/webui/src/lib/sentry.ts @@ -1,43 +1,20 @@ -let initialized = false; - +/** + * WebUI error reporting stub. + * + * `@sentry/node` is intentionally not used in the Next.js app: importing it + * (even lazily via `instrumentation.ts`) pulled OpenTelemetry into the build + * graph and broke page-data collection with + * `TypeError: createContext is not a function`. Bot-side Sentry remains in + * `apps/bot`. Capture here stays a structured console fallback so call sites + * can keep using one API. + */ export async function initWebuiSentry(): Promise { - if (initialized) { - return; - } - const dsn = process.env.SENTRY_DSN?.trim(); - if (!dsn) { - return; - } - - // Import only when a DSN is configured. Loading `@sentry/node` at module - // scope patches Node builtins and can break Next.js page-data collection - // (`createContext is not a function`) during `next build`. - const Sentry = await import('@sentry/node'); - Sentry.init({ - dsn, - environment: process.env.NODE_ENV ?? 'production', - release: 'nexumi-webui@0.1.0', - tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0, - initialScope: { - tags: { service: 'webui' } - } - }); - initialized = true; + // no-op in WebUI } export async function captureWebuiException( error: unknown, context?: Record ): Promise { - const dsn = process.env.SENTRY_DSN?.trim(); - if (!dsn) { - return; - } - const Sentry = await import('@sentry/node'); - Sentry.withScope((scope) => { - if (context) { - scope.setExtras(context); - } - Sentry.captureException(error); - }); + console.error('[webui]', error, context ?? {}); }