diff --git a/adapters/tsconfig.json b/adapters/tsconfig.json index d629e292f..9de039259 100644 --- a/adapters/tsconfig.json +++ b/adapters/tsconfig.json @@ -5,8 +5,21 @@ "declaration": true, "declarationMap": true, "sourceMap": true, - "types": ["vitest/globals"] + "types": ["vitest/globals"], + "noUnusedLocals": false, + "noUnusedParameters": false }, "include": ["**/*.ts", "**/*.d.ts", "../core/**/*.ts", "../core/**/*.d.ts"], - "exclude": ["node_modules", "dist"] -} \ No newline at end of file + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.spec.ts", + "**/*.integration.test.ts", + "**/__tests__/**", + "../core/**/*.test.ts", + "../core/**/*.spec.ts", + "../core/**/*.integration.test.ts", + "../core/**/__tests__/**" + ] +} diff --git a/apps/api/src/domain/admin/AdminModule.ts b/apps/api/src/domain/admin/AdminModule.ts index ee1e0d958..e3ebb3ed8 100644 --- a/apps/api/src/domain/admin/AdminModule.ts +++ b/apps/api/src/domain/admin/AdminModule.ts @@ -1,3 +1,4 @@ +import type { Provider } from '@nestjs/common'; import { Module } from '@nestjs/common'; import { InMemoryAdminPersistenceModule } from '../../persistence/inmemory/InMemoryAdminPersistenceModule'; import { AdminService } from './AdminService'; @@ -7,6 +8,7 @@ import { DashboardStatsPresenter } from './presenters/DashboardStatsPresenter'; import { AuthModule } from '../auth/AuthModule'; import { ListUsersUseCase } from '@core/admin/application/use-cases/ListUsersUseCase'; import { GetDashboardStatsUseCase } from './use-cases/GetDashboardStatsUseCase'; +import { InitializationLogger } from '../../shared/logging/InitializationLogger'; import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; import type { ListUsersResult } from '@core/admin/application/use-cases/ListUsersUseCase'; import type { DashboardStatsResult } from './use-cases/GetDashboardStatsUseCase'; @@ -16,38 +18,80 @@ export const ADMIN_USER_REPOSITORY_TOKEN = 'IAdminUserRepository'; export const LIST_USERS_OUTPUT_PORT_TOKEN = 'ListUsersOutputPort'; export const DASHBOARD_STATS_OUTPUT_PORT_TOKEN = 'DashboardStatsOutputPort'; +const initLogger = InitializationLogger.getInstance(); + +const adminProviders: Provider[] = [ + AdminService, + ListUsersPresenter, + DashboardStatsPresenter, + { + provide: LIST_USERS_OUTPUT_PORT_TOKEN, + useExisting: ListUsersPresenter, + }, + { + provide: DASHBOARD_STATS_OUTPUT_PORT_TOKEN, + useExisting: DashboardStatsPresenter, + }, + { + provide: ListUsersUseCase, + useFactory: ( + repository: IAdminUserRepository, + output: UseCaseOutputPort, + ) => new ListUsersUseCase(repository, output), + inject: [ADMIN_USER_REPOSITORY_TOKEN, LIST_USERS_OUTPUT_PORT_TOKEN], + }, + { + provide: GetDashboardStatsUseCase, + useFactory: ( + repository: IAdminUserRepository, + output: UseCaseOutputPort, + ) => new GetDashboardStatsUseCase(repository, output), + inject: [ADMIN_USER_REPOSITORY_TOKEN, DASHBOARD_STATS_OUTPUT_PORT_TOKEN], + }, +]; + +// Diagnostics: Nest will crash with "metatype is not a constructor" if any provider resolves +// to a non-constructable value (e.g. undefined import, object, etc.). +for (const provider of adminProviders) { + // Class providers are functions at runtime. + if (typeof provider === 'function') { + continue; + } + + // Custom providers should be objects with a `provide` token. + if (!provider || typeof provider !== 'object' || !('provide' in provider)) { + initLogger.error( + `[AdminModule] Invalid provider entry (expected class or provider object): ${String(provider)}`, + ); + continue; + } + + const token = (provider as { provide: unknown }).provide; + const tokenLabel = typeof token === 'function' ? token.name : String(token); + + if ('useClass' in provider) { + const useClass = (provider as { useClass?: unknown }).useClass; + if (typeof useClass !== 'function') { + initLogger.error( + `[AdminModule] Provider "${tokenLabel}" has non-constructable useClass: ${String(useClass)}`, + ); + } + } + + if ('useExisting' in provider) { + const useExisting = (provider as { useExisting?: unknown }).useExisting; + if (typeof useExisting !== 'function' && typeof useExisting !== 'string' && typeof useExisting !== 'symbol') { + initLogger.warn( + `[AdminModule] Provider "${tokenLabel}" has suspicious useExisting: ${String(useExisting)}`, + ); + } + } +} + @Module({ imports: [InMemoryAdminPersistenceModule, AuthModule], controllers: [AdminController], - providers: [ - AdminService, - ListUsersPresenter, - DashboardStatsPresenter, - { - provide: LIST_USERS_OUTPUT_PORT_TOKEN, - useExisting: ListUsersPresenter, - }, - { - provide: DASHBOARD_STATS_OUTPUT_PORT_TOKEN, - useExisting: DashboardStatsPresenter, - }, - { - provide: ListUsersUseCase, - useFactory: ( - repository: IAdminUserRepository, - output: UseCaseOutputPort, - ) => new ListUsersUseCase(repository, output), - inject: [ADMIN_USER_REPOSITORY_TOKEN, LIST_USERS_OUTPUT_PORT_TOKEN], - }, - { - provide: GetDashboardStatsUseCase, - useFactory: ( - repository: IAdminUserRepository, - output: UseCaseOutputPort, - ) => new GetDashboardStatsUseCase(repository, output), - inject: [ADMIN_USER_REPOSITORY_TOKEN, DASHBOARD_STATS_OUTPUT_PORT_TOKEN], - }, - ], + providers: [...adminProviders], exports: [AdminService], }) -export class AdminModule {} \ No newline at end of file +export class AdminModule {} diff --git a/apps/api/src/domain/auth/dtos/AuthDto.ts b/apps/api/src/domain/auth/dtos/AuthDto.ts index c837adb6a..50b4b7200 100644 --- a/apps/api/src/domain/auth/dtos/AuthDto.ts +++ b/apps/api/src/domain/auth/dtos/AuthDto.ts @@ -25,15 +25,25 @@ export class AuthSessionDTO { export class SignupParamsDTO { @ApiProperty() + @IsEmail() email!: string; + @ApiProperty() + @IsString() + @MinLength(8) password!: string; + @ApiProperty() + @IsString() + @MinLength(2) displayName!: string; + @ApiProperty({ required: false }) iracingCustomerId?: string; + @ApiProperty({ required: false }) primaryDriverId?: string; + @ApiProperty({ required: false, nullable: true }) avatarUrl?: string | null; } diff --git a/apps/api/src/domain/dashboard/DashboardModule.ts b/apps/api/src/domain/dashboard/DashboardModule.ts index f2f085b64..608eb897b 100644 --- a/apps/api/src/domain/dashboard/DashboardModule.ts +++ b/apps/api/src/domain/dashboard/DashboardModule.ts @@ -11,4 +11,4 @@ import { DashboardProviders } from './DashboardProviders'; providers: [DashboardService, ...DashboardProviders], exports: [DashboardService], }) -export class DashboardModule {} \ No newline at end of file +export class DashboardModule {} diff --git a/apps/api/src/domain/dashboard/DashboardProviders.ts b/apps/api/src/domain/dashboard/DashboardProviders.ts index af92612b8..db28c6d6b 100644 --- a/apps/api/src/domain/dashboard/DashboardProviders.ts +++ b/apps/api/src/domain/dashboard/DashboardProviders.ts @@ -20,20 +20,34 @@ import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/Das import { ConsoleLogger } from '@adapters/logging/ConsoleLogger'; import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter'; import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresenter'; -import { DashboardService } from './DashboardService'; +import { + DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN, + DASHBOARD_OVERVIEW_USE_CASE_TOKEN, + DRIVER_REPOSITORY_TOKEN, + IMAGE_SERVICE_TOKEN, + LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, + LEAGUE_REPOSITORY_TOKEN, + LOGGER_TOKEN, + RACE_REGISTRATION_REPOSITORY_TOKEN, + RACE_REPOSITORY_TOKEN, + RESULT_REPOSITORY_TOKEN, + STANDING_REPOSITORY_TOKEN, +} from './DashboardTokens'; -// Define injection tokens -export const LOGGER_TOKEN = 'Logger'; -export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository'; -export const RACE_REPOSITORY_TOKEN = 'IRaceRepository'; -export const RESULT_REPOSITORY_TOKEN = 'IResultRepository'; -export const LEAGUE_REPOSITORY_TOKEN = 'ILeagueRepository'; -export const STANDING_REPOSITORY_TOKEN = 'IStandingRepository'; -export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository'; -export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository'; -export const IMAGE_SERVICE_TOKEN = 'IImageServicePort'; -export const DASHBOARD_OVERVIEW_USE_CASE_TOKEN = 'DashboardOverviewUseCase'; -export const DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN = 'DashboardOverviewOutputPort'; +// Re-export tokens for convenience (legacy imports) +export { + DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN, + DASHBOARD_OVERVIEW_USE_CASE_TOKEN, + DRIVER_REPOSITORY_TOKEN, + IMAGE_SERVICE_TOKEN, + LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, + LEAGUE_REPOSITORY_TOKEN, + LOGGER_TOKEN, + RACE_REGISTRATION_REPOSITORY_TOKEN, + RACE_REPOSITORY_TOKEN, + RESULT_REPOSITORY_TOKEN, + STANDING_REPOSITORY_TOKEN, +} from './DashboardTokens'; export const DashboardProviders: Provider[] = [ DashboardOverviewPresenter, @@ -93,19 +107,4 @@ export const DashboardProviders: Provider[] = [ DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN, ], }, - { - provide: DashboardService, - useFactory: ( - logger: Logger, - dashboardOverviewUseCase: DashboardOverviewUseCase, - presenter: DashboardOverviewPresenter, - imageService: ImageServicePort, - ) => new DashboardService(logger, dashboardOverviewUseCase, presenter, imageService), - inject: [ - LOGGER_TOKEN, - DASHBOARD_OVERVIEW_USE_CASE_TOKEN, - DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN, - IMAGE_SERVICE_TOKEN, - ], - }, -]; \ No newline at end of file +]; diff --git a/apps/api/src/domain/dashboard/DashboardService.ts b/apps/api/src/domain/dashboard/DashboardService.ts index 01b6736da..5853417c3 100644 --- a/apps/api/src/domain/dashboard/DashboardService.ts +++ b/apps/api/src/domain/dashboard/DashboardService.ts @@ -7,8 +7,13 @@ import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresen import type { Logger } from '@core/shared/application/Logger'; import type { ImageServicePort } from '@core/media/application/ports/ImageServicePort'; -// Tokens -import { DASHBOARD_OVERVIEW_USE_CASE_TOKEN, LOGGER_TOKEN, DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN, IMAGE_SERVICE_TOKEN } from './DashboardProviders'; +// Tokens (standalone to avoid circular imports) +import { + DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN, + DASHBOARD_OVERVIEW_USE_CASE_TOKEN, + IMAGE_SERVICE_TOKEN, + LOGGER_TOKEN, +} from './DashboardTokens'; @Injectable() export class DashboardService { @@ -228,4 +233,4 @@ export class DashboardService { friends: [], }; } -} \ No newline at end of file +} diff --git a/apps/api/src/domain/dashboard/DashboardTokens.ts b/apps/api/src/domain/dashboard/DashboardTokens.ts new file mode 100644 index 000000000..e30f0d6b6 --- /dev/null +++ b/apps/api/src/domain/dashboard/DashboardTokens.ts @@ -0,0 +1,16 @@ +// Dashboard injection tokens +// NOTE: kept in a standalone file to avoid circular imports between +// providers and services. + +export const LOGGER_TOKEN = 'Logger'; +export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository'; +export const RACE_REPOSITORY_TOKEN = 'IRaceRepository'; +export const RESULT_REPOSITORY_TOKEN = 'IResultRepository'; +export const LEAGUE_REPOSITORY_TOKEN = 'ILeagueRepository'; +export const STANDING_REPOSITORY_TOKEN = 'IStandingRepository'; +export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository'; +export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository'; +export const IMAGE_SERVICE_TOKEN = 'IImageServicePort'; +export const DASHBOARD_OVERVIEW_USE_CASE_TOKEN = 'DashboardOverviewUseCase'; +export const DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN = 'DashboardOverviewOutputPort'; + diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 498373889..0bdf1651d 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -87,13 +87,17 @@ async function bootstrap() { // Handle uncaught errors process.on('uncaughtException', (error) => { - console.error('🚨 Uncaught Exception:', error.message); + console.error('🚨 Uncaught Exception:', error.stack ?? error.message); process.exit(1); }); process.on('unhandledRejection', (reason: unknown) => { - console.error('🚨 Unhandled Rejection:', reason instanceof Error ? reason.message : reason); + if (reason instanceof Error) { + console.error('🚨 Unhandled Rejection:', reason.stack ?? reason.message); + } else { + console.error('🚨 Unhandled Rejection:', reason); + } process.exit(1); }); -bootstrap(); \ No newline at end of file +bootstrap(); diff --git a/apps/website/app/auth/signup/page.tsx b/apps/website/app/auth/signup/page.tsx index b82decee8..2896457e8 100644 --- a/apps/website/app/auth/signup/page.tsx +++ b/apps/website/app/auth/signup/page.tsx @@ -217,8 +217,13 @@ export default function SignupPage() { }); // Refresh session in context so header updates immediately - await refreshSession(); - router.push(returnTo); + try { + await refreshSession(); + } catch (error) { + console.error('Failed to refresh session after signup:', error); + } + // Always redirect to dashboard after signup + router.push('/dashboard'); } catch (error) { setErrors({ submit: error instanceof Error ? error.message : 'Signup failed. Please try again.', @@ -237,7 +242,8 @@ export default function SignupPage() { await authService.demoLogin({ role: 'driver' }); await new Promise(resolve => setTimeout(resolve, 500)); - router.push(returnTo === '/onboarding' ? '/dashboard' : returnTo); + // Always redirect to dashboard after demo login + router.push('/dashboard'); } catch { setErrors({ submit: 'Demo login failed. Please try again.', diff --git a/apps/website/app/layout.tsx b/apps/website/app/layout.tsx index 7e96ddab1..f9f1a66d2 100644 --- a/apps/website/app/layout.tsx +++ b/apps/website/app/layout.tsx @@ -1,10 +1,9 @@ import AlphaFooter from '@/components/alpha/AlphaFooter'; import { AlphaNav } from '@/components/alpha/AlphaNav'; import DevToolbar from '@/components/dev/DevToolbar'; -import NotificationProvider from '@/components/notifications/NotificationProvider'; -import { NotificationIntegration } from '@/components/errors/NotificationIntegration'; import { ApiErrorBoundary } from '@/components/errors/ApiErrorBoundary'; -import { ApiStatusToolbar } from '@/components/errors/ApiStatusToolbar'; +import { NotificationIntegration } from '@/components/errors/NotificationIntegration'; +import NotificationProvider from '@/components/notifications/NotificationProvider'; import { AuthProvider } from '@/lib/auth/AuthContext'; import { getAppMode } from '@/lib/mode'; import { ServiceProvider } from '@/lib/services/ServiceProvider'; @@ -75,10 +74,6 @@ export default async function RootLayout({ - {/* API Status Toolbar for development - only shows in dev mode */} - {process.env.NODE_ENV === 'development' && ( - - )} @@ -121,10 +116,6 @@ export default async function RootLayout({
{children}
- {/* API Status Toolbar for development */} - {process.env.NODE_ENV === 'development' && ( - - )} diff --git a/apps/website/components/admin/AdminDashboardPage.tsx b/apps/website/components/admin/AdminDashboardPage.tsx index abd02bbc7..938af9a68 100644 --- a/apps/website/components/admin/AdminDashboardPage.tsx +++ b/apps/website/components/admin/AdminDashboardPage.tsx @@ -78,6 +78,14 @@ export function AdminDashboardPage() { return null; } + // Temporary UI fields (not yet provided by API/ViewModel) + const adminCount = stats.systemAdmins; + const recentActivity: Array<{ description: string; timestamp: string; type: string }> = []; + const systemHealth = 'Healthy'; + const totalSessions = 0; + const activeSessions = 0; + const avgSessionDuration = '—'; + return (
{/* Header */} @@ -112,7 +120,7 @@ export function AdminDashboardPage() {
Admins
-
{stats.adminCount}
+
{adminCount}
@@ -145,8 +153,8 @@ export function AdminDashboardPage() {

Recent Activity

- {stats.recentActivity.length > 0 ? ( - stats.recentActivity.map((activity, index) => ( + {recentActivity.length > 0 ? ( + recentActivity.map((activity, index: number) => (
System Health - {stats.systemHealth} + {systemHealth}
Total Sessions - {stats.totalSessions} + {totalSessions}
Active Sessions - {stats.activeSessions} + {activeSessions}
Avg Session Duration - {stats.avgSessionDuration} + {avgSessionDuration}
@@ -214,4 +222,4 @@ export function AdminDashboardPage() {
); -} \ No newline at end of file +} diff --git a/apps/website/components/admin/AdminUsersPage.tsx b/apps/website/components/admin/AdminUsersPage.tsx index fa4c9e3f7..45fc6a318 100644 --- a/apps/website/components/admin/AdminUsersPage.tsx +++ b/apps/website/components/admin/AdminUsersPage.tsx @@ -70,6 +70,21 @@ export function AdminUsersPage() { } }; + const toStatusBadgeProps = ( + status: string, + ): { status: 'success' | 'warning' | 'error' | 'neutral'; label: string } => { + switch (status) { + case 'active': + return { status: 'success', label: 'Active' }; + case 'suspended': + return { status: 'warning', label: 'Suspended' }; + case 'deleted': + return { status: 'error', label: 'Deleted' }; + default: + return { status: 'neutral', label: status }; + } + }; + const handleDeleteUser = async (userId: string) => { if (!confirm('Are you sure you want to delete this user? This action cannot be undone.')) { return; @@ -255,7 +270,10 @@ export function AdminUsersPage() { - + {(() => { + const badge = toStatusBadgeProps(user.status); + return ; + })()}
@@ -338,4 +356,4 @@ export function AdminUsersPage() { )}
); -} \ No newline at end of file +} diff --git a/apps/website/components/dev/Accordion.tsx b/apps/website/components/dev/Accordion.tsx index a842759ec..a091ce214 100644 --- a/apps/website/components/dev/Accordion.tsx +++ b/apps/website/components/dev/Accordion.tsx @@ -1,22 +1,21 @@ 'use client'; -import { ReactNode, useState } from 'react'; +import { ReactNode } from 'react'; import { ChevronDown, ChevronUp } from 'lucide-react'; interface AccordionProps { title: string; icon: ReactNode; children: ReactNode; - defaultOpen?: boolean; + isOpen: boolean; + onToggle: () => void; } -export function Accordion({ title, icon, children, defaultOpen = false }: AccordionProps) { - const [isOpen, setIsOpen] = useState(defaultOpen); - +export function Accordion({ title, icon, children, isOpen, onToggle }: AccordionProps) { return (
- ); - } - - return ( -
- {/* Compact Status Indicator */} - {!expanded ? ( - - ) : ( - /* Expanded Panel */ -
- {/* Header */} -
-
- - API STATUS -
-
- - - -
-
- - {/* Body */} -
- {/* Status Row */} -
- Status - - {status} - -
- - {/* Reliability */} -
- Reliability - - {reliability}% - -
- - {/* Request Stats */} -
-
-
Total
-
{health.totalRequests}
-
-
-
Success
-
{health.successfulRequests}
-
-
-
Failed
-
{health.failedRequests}
-
-
- - {/* Performance */} -
- Avg Response - - {health.averageResponseTime.toFixed(0)}ms - -
- - {/* Consecutive Failures */} - {health.consecutiveFailures > 0 && ( -
- Consecutive Failures - {health.consecutiveFailures} -
- )} - - {/* Circuit Breakers */} -
-
- - CIRCUIT BREAKERS -
- -
- - {/* Last Check */} -
- Last Check - - {health.lastCheck ? new Date(health.lastCheck).toLocaleTimeString() : 'Never'} - -
- - {/* Actions */} -
- - -
-
-
- )} -
- ); -} - -/** - * Circuit Breaker Status Component - */ -function CircuitBreakerStatus() { - const [status, setStatus] = useState(CircuitBreakerRegistry.getInstance().getStatus()); - - useEffect(() => { - const registry = CircuitBreakerRegistry.getInstance(); - - // Poll for updates every 2 seconds - const interval = setInterval(() => { - setStatus(registry.getStatus()); - }, 2000); - - return () => clearInterval(interval); - }, []); - - const entries = Object.entries(status); - - if (entries.length === 0) { - return ( -
No active circuit breakers
- ); - } - - return ( -
- {entries.map(([endpoint, breaker]) => ( -
- {endpoint.split('/').pop() || endpoint} - - {breaker.state} - - {breaker.failures > 0 && ( - ({breaker.failures}) - )} -
- ))} -
- ); -} \ No newline at end of file diff --git a/apps/website/lib/infrastructure/EnhancedErrorReporter.ts b/apps/website/lib/infrastructure/EnhancedErrorReporter.ts index e275de190..ff6cc580f 100644 --- a/apps/website/lib/infrastructure/EnhancedErrorReporter.ts +++ b/apps/website/lib/infrastructure/EnhancedErrorReporter.ts @@ -301,7 +301,7 @@ let globalReporter: EnhancedErrorReporter | null = null; export function getGlobalErrorReporter(): EnhancedErrorReporter { if (!globalReporter) { // Import the console logger - const { ConsoleLogger } = require('./ConsoleLogger'); + const { ConsoleLogger } = require('./logging/ConsoleLogger'); globalReporter = new EnhancedErrorReporter(new ConsoleLogger(), { showUserNotifications: true, logToConsole: true, diff --git a/core/tsconfig.json b/core/tsconfig.json index 0265e8cac..c9ba594a7 100644 --- a/core/tsconfig.json +++ b/core/tsconfig.json @@ -9,6 +9,8 @@ "declarationMap": true, "sourceMap": true, "types": ["vitest/globals"], + "noUnusedLocals": false, + "noUnusedParameters": false, "paths": { "@/*": ["./*"], "@core/*": ["./*"], @@ -17,5 +19,12 @@ } }, "include": ["**/*.ts", "bootstrap/**/*.ts"], - "exclude": ["node_modules", "dist"] -} \ No newline at end of file + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.spec.ts", + "**/*.integration.test.ts", + "**/__tests__/**" + ] +}