fix issues
This commit is contained in:
@@ -5,8 +5,21 @@
|
|||||||
"declaration": true,
|
"declaration": true,
|
||||||
"declarationMap": true,
|
"declarationMap": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"types": ["vitest/globals"]
|
"types": ["vitest/globals"],
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts", "**/*.d.ts", "../core/**/*.ts", "../core/**/*.d.ts"],
|
"include": ["**/*.ts", "**/*.d.ts", "../core/**/*.ts", "../core/**/*.d.ts"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": [
|
||||||
}
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.integration.test.ts",
|
||||||
|
"**/__tests__/**",
|
||||||
|
"../core/**/*.test.ts",
|
||||||
|
"../core/**/*.spec.ts",
|
||||||
|
"../core/**/*.integration.test.ts",
|
||||||
|
"../core/**/__tests__/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Provider } from '@nestjs/common';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { InMemoryAdminPersistenceModule } from '../../persistence/inmemory/InMemoryAdminPersistenceModule';
|
import { InMemoryAdminPersistenceModule } from '../../persistence/inmemory/InMemoryAdminPersistenceModule';
|
||||||
import { AdminService } from './AdminService';
|
import { AdminService } from './AdminService';
|
||||||
@@ -7,6 +8,7 @@ import { DashboardStatsPresenter } from './presenters/DashboardStatsPresenter';
|
|||||||
import { AuthModule } from '../auth/AuthModule';
|
import { AuthModule } from '../auth/AuthModule';
|
||||||
import { ListUsersUseCase } from '@core/admin/application/use-cases/ListUsersUseCase';
|
import { ListUsersUseCase } from '@core/admin/application/use-cases/ListUsersUseCase';
|
||||||
import { GetDashboardStatsUseCase } from './use-cases/GetDashboardStatsUseCase';
|
import { GetDashboardStatsUseCase } from './use-cases/GetDashboardStatsUseCase';
|
||||||
|
import { InitializationLogger } from '../../shared/logging/InitializationLogger';
|
||||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||||
import type { ListUsersResult } from '@core/admin/application/use-cases/ListUsersUseCase';
|
import type { ListUsersResult } from '@core/admin/application/use-cases/ListUsersUseCase';
|
||||||
import type { DashboardStatsResult } from './use-cases/GetDashboardStatsUseCase';
|
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 LIST_USERS_OUTPUT_PORT_TOKEN = 'ListUsersOutputPort';
|
||||||
export const DASHBOARD_STATS_OUTPUT_PORT_TOKEN = 'DashboardStatsOutputPort';
|
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<ListUsersResult>,
|
||||||
|
) => new ListUsersUseCase(repository, output),
|
||||||
|
inject: [ADMIN_USER_REPOSITORY_TOKEN, LIST_USERS_OUTPUT_PORT_TOKEN],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: GetDashboardStatsUseCase,
|
||||||
|
useFactory: (
|
||||||
|
repository: IAdminUserRepository,
|
||||||
|
output: UseCaseOutputPort<DashboardStatsResult>,
|
||||||
|
) => 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({
|
@Module({
|
||||||
imports: [InMemoryAdminPersistenceModule, AuthModule],
|
imports: [InMemoryAdminPersistenceModule, AuthModule],
|
||||||
controllers: [AdminController],
|
controllers: [AdminController],
|
||||||
providers: [
|
providers: [...adminProviders],
|
||||||
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<ListUsersResult>,
|
|
||||||
) => new ListUsersUseCase(repository, output),
|
|
||||||
inject: [ADMIN_USER_REPOSITORY_TOKEN, LIST_USERS_OUTPUT_PORT_TOKEN],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: GetDashboardStatsUseCase,
|
|
||||||
useFactory: (
|
|
||||||
repository: IAdminUserRepository,
|
|
||||||
output: UseCaseOutputPort<DashboardStatsResult>,
|
|
||||||
) => new GetDashboardStatsUseCase(repository, output),
|
|
||||||
inject: [ADMIN_USER_REPOSITORY_TOKEN, DASHBOARD_STATS_OUTPUT_PORT_TOKEN],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
exports: [AdminService],
|
exports: [AdminService],
|
||||||
})
|
})
|
||||||
export class AdminModule {}
|
export class AdminModule {}
|
||||||
|
|||||||
@@ -25,15 +25,25 @@ export class AuthSessionDTO {
|
|||||||
|
|
||||||
export class SignupParamsDTO {
|
export class SignupParamsDTO {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
@IsEmail()
|
||||||
email!: string;
|
email!: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@MinLength(8)
|
||||||
password!: string;
|
password!: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@MinLength(2)
|
||||||
displayName!: string;
|
displayName!: string;
|
||||||
|
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
iracingCustomerId?: string;
|
iracingCustomerId?: string;
|
||||||
|
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
primaryDriverId?: string;
|
primaryDriverId?: string;
|
||||||
|
|
||||||
@ApiProperty({ required: false, nullable: true })
|
@ApiProperty({ required: false, nullable: true })
|
||||||
avatarUrl?: string | null;
|
avatarUrl?: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ import { DashboardProviders } from './DashboardProviders';
|
|||||||
providers: [DashboardService, ...DashboardProviders],
|
providers: [DashboardService, ...DashboardProviders],
|
||||||
exports: [DashboardService],
|
exports: [DashboardService],
|
||||||
})
|
})
|
||||||
export class DashboardModule {}
|
export class DashboardModule {}
|
||||||
|
|||||||
@@ -20,20 +20,34 @@ import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/Das
|
|||||||
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
||||||
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
|
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
|
||||||
import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresenter';
|
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
|
// Re-export tokens for convenience (legacy imports)
|
||||||
export const LOGGER_TOKEN = 'Logger';
|
export {
|
||||||
export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
|
DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN,
|
||||||
export const RACE_REPOSITORY_TOKEN = 'IRaceRepository';
|
DASHBOARD_OVERVIEW_USE_CASE_TOKEN,
|
||||||
export const RESULT_REPOSITORY_TOKEN = 'IResultRepository';
|
DRIVER_REPOSITORY_TOKEN,
|
||||||
export const LEAGUE_REPOSITORY_TOKEN = 'ILeagueRepository';
|
IMAGE_SERVICE_TOKEN,
|
||||||
export const STANDING_REPOSITORY_TOKEN = 'IStandingRepository';
|
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||||
export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
|
LEAGUE_REPOSITORY_TOKEN,
|
||||||
export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository';
|
LOGGER_TOKEN,
|
||||||
export const IMAGE_SERVICE_TOKEN = 'IImageServicePort';
|
RACE_REGISTRATION_REPOSITORY_TOKEN,
|
||||||
export const DASHBOARD_OVERVIEW_USE_CASE_TOKEN = 'DashboardOverviewUseCase';
|
RACE_REPOSITORY_TOKEN,
|
||||||
export const DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN = 'DashboardOverviewOutputPort';
|
RESULT_REPOSITORY_TOKEN,
|
||||||
|
STANDING_REPOSITORY_TOKEN,
|
||||||
|
} from './DashboardTokens';
|
||||||
|
|
||||||
export const DashboardProviders: Provider[] = [
|
export const DashboardProviders: Provider[] = [
|
||||||
DashboardOverviewPresenter,
|
DashboardOverviewPresenter,
|
||||||
@@ -93,19 +107,4 @@ export const DashboardProviders: Provider[] = [
|
|||||||
DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN,
|
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,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|||||||
@@ -7,8 +7,13 @@ import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresen
|
|||||||
import type { Logger } from '@core/shared/application/Logger';
|
import type { Logger } from '@core/shared/application/Logger';
|
||||||
import type { ImageServicePort } from '@core/media/application/ports/ImageServicePort';
|
import type { ImageServicePort } from '@core/media/application/ports/ImageServicePort';
|
||||||
|
|
||||||
// Tokens
|
// Tokens (standalone to avoid circular imports)
|
||||||
import { DASHBOARD_OVERVIEW_USE_CASE_TOKEN, LOGGER_TOKEN, DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN, IMAGE_SERVICE_TOKEN } from './DashboardProviders';
|
import {
|
||||||
|
DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN,
|
||||||
|
DASHBOARD_OVERVIEW_USE_CASE_TOKEN,
|
||||||
|
IMAGE_SERVICE_TOKEN,
|
||||||
|
LOGGER_TOKEN,
|
||||||
|
} from './DashboardTokens';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DashboardService {
|
export class DashboardService {
|
||||||
@@ -228,4 +233,4 @@ export class DashboardService {
|
|||||||
friends: [],
|
friends: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
apps/api/src/domain/dashboard/DashboardTokens.ts
Normal file
16
apps/api/src/domain/dashboard/DashboardTokens.ts
Normal file
@@ -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';
|
||||||
|
|
||||||
@@ -87,13 +87,17 @@ async function bootstrap() {
|
|||||||
|
|
||||||
// Handle uncaught errors
|
// Handle uncaught errors
|
||||||
process.on('uncaughtException', (error) => {
|
process.on('uncaughtException', (error) => {
|
||||||
console.error('🚨 Uncaught Exception:', error.message);
|
console.error('🚨 Uncaught Exception:', error.stack ?? error.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason: unknown) => {
|
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);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
@@ -217,8 +217,13 @@ export default function SignupPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Refresh session in context so header updates immediately
|
// Refresh session in context so header updates immediately
|
||||||
await refreshSession();
|
try {
|
||||||
router.push(returnTo);
|
await refreshSession();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to refresh session after signup:', error);
|
||||||
|
}
|
||||||
|
// Always redirect to dashboard after signup
|
||||||
|
router.push('/dashboard');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setErrors({
|
setErrors({
|
||||||
submit: error instanceof Error ? error.message : 'Signup failed. Please try again.',
|
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 authService.demoLogin({ role: 'driver' });
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
router.push(returnTo === '/onboarding' ? '/dashboard' : returnTo);
|
// Always redirect to dashboard after demo login
|
||||||
|
router.push('/dashboard');
|
||||||
} catch {
|
} catch {
|
||||||
setErrors({
|
setErrors({
|
||||||
submit: 'Demo login failed. Please try again.',
|
submit: 'Demo login failed. Please try again.',
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import AlphaFooter from '@/components/alpha/AlphaFooter';
|
import AlphaFooter from '@/components/alpha/AlphaFooter';
|
||||||
import { AlphaNav } from '@/components/alpha/AlphaNav';
|
import { AlphaNav } from '@/components/alpha/AlphaNav';
|
||||||
import DevToolbar from '@/components/dev/DevToolbar';
|
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 { 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 { AuthProvider } from '@/lib/auth/AuthContext';
|
||||||
import { getAppMode } from '@/lib/mode';
|
import { getAppMode } from '@/lib/mode';
|
||||||
import { ServiceProvider } from '@/lib/services/ServiceProvider';
|
import { ServiceProvider } from '@/lib/services/ServiceProvider';
|
||||||
@@ -75,10 +74,6 @@ export default async function RootLayout({
|
|||||||
</main>
|
</main>
|
||||||
<AlphaFooter />
|
<AlphaFooter />
|
||||||
<DevToolbar />
|
<DevToolbar />
|
||||||
{/* API Status Toolbar for development - only shows in dev mode */}
|
|
||||||
{process.env.NODE_ENV === 'development' && (
|
|
||||||
<ApiStatusToolbar position="bottom-right" autoHide={true} />
|
|
||||||
)}
|
|
||||||
</ApiErrorBoundary>
|
</ApiErrorBoundary>
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
@@ -121,10 +116,6 @@ export default async function RootLayout({
|
|||||||
<div className="pt-16">
|
<div className="pt-16">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
{/* API Status Toolbar for development */}
|
|
||||||
{process.env.NODE_ENV === 'development' && (
|
|
||||||
<ApiStatusToolbar position="bottom-right" autoHide={true} />
|
|
||||||
)}
|
|
||||||
</ApiErrorBoundary>
|
</ApiErrorBoundary>
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -78,6 +78,14 @@ export function AdminDashboardPage() {
|
|||||||
return null;
|
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 (
|
return (
|
||||||
<div className="container mx-auto p-6 space-y-6">
|
<div className="container mx-auto p-6 space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -112,7 +120,7 @@ export function AdminDashboardPage() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-gray-400 mb-1">Admins</div>
|
<div className="text-sm text-gray-400 mb-1">Admins</div>
|
||||||
<div className="text-3xl font-bold text-white">{stats.adminCount}</div>
|
<div className="text-3xl font-bold text-white">{adminCount}</div>
|
||||||
</div>
|
</div>
|
||||||
<Shield className="w-8 h-8 text-purple-400" />
|
<Shield className="w-8 h-8 text-purple-400" />
|
||||||
</div>
|
</div>
|
||||||
@@ -145,8 +153,8 @@ export function AdminDashboardPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<h3 className="text-lg font-semibold text-white mb-4">Recent Activity</h3>
|
<h3 className="text-lg font-semibold text-white mb-4">Recent Activity</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{stats.recentActivity.length > 0 ? (
|
{recentActivity.length > 0 ? (
|
||||||
stats.recentActivity.map((activity, index) => (
|
recentActivity.map((activity, index: number) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="flex items-center justify-between p-3 bg-iron-gray/30 rounded-lg border border-charcoal-outline/50"
|
className="flex items-center justify-between p-3 bg-iron-gray/30 rounded-lg border border-charcoal-outline/50"
|
||||||
@@ -178,20 +186,20 @@ export function AdminDashboardPage() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm text-gray-400">System Health</span>
|
<span className="text-sm text-gray-400">System Health</span>
|
||||||
<span className="px-2 py-1 text-xs rounded-full bg-performance-green/20 text-performance-green">
|
<span className="px-2 py-1 text-xs rounded-full bg-performance-green/20 text-performance-green">
|
||||||
{stats.systemHealth}
|
{systemHealth}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm text-gray-400">Total Sessions</span>
|
<span className="text-sm text-gray-400">Total Sessions</span>
|
||||||
<span className="text-white font-medium">{stats.totalSessions}</span>
|
<span className="text-white font-medium">{totalSessions}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm text-gray-400">Active Sessions</span>
|
<span className="text-sm text-gray-400">Active Sessions</span>
|
||||||
<span className="text-white font-medium">{stats.activeSessions}</span>
|
<span className="text-white font-medium">{activeSessions}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm text-gray-400">Avg Session Duration</span>
|
<span className="text-sm text-gray-400">Avg Session Duration</span>
|
||||||
<span className="text-white font-medium">{stats.avgSessionDuration}</span>
|
<span className="text-white font-medium">{avgSessionDuration}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -214,4 +222,4 @@ export function AdminDashboardPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) => {
|
const handleDeleteUser = async (userId: string) => {
|
||||||
if (!confirm('Are you sure you want to delete this user? This action cannot be undone.')) {
|
if (!confirm('Are you sure you want to delete this user? This action cannot be undone.')) {
|
||||||
return;
|
return;
|
||||||
@@ -255,7 +270,10 @@ export function AdminUsersPage() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 px-4">
|
<td className="py-3 px-4">
|
||||||
<StatusBadge status={user.statusBadge.label.toLowerCase()} />
|
{(() => {
|
||||||
|
const badge = toStatusBadgeProps(user.status);
|
||||||
|
return <StatusBadge status={badge.status} label={badge.label} />;
|
||||||
|
})()}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 px-4">
|
<td className="py-3 px-4">
|
||||||
<div className="text-sm text-gray-400">
|
<div className="text-sm text-gray-400">
|
||||||
@@ -338,4 +356,4 @@ export function AdminUsersPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ReactNode, useState } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
|
|
||||||
interface AccordionProps {
|
interface AccordionProps {
|
||||||
title: string;
|
title: string;
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
defaultOpen?: boolean;
|
isOpen: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Accordion({ title, icon, children, defaultOpen = false }: AccordionProps) {
|
export function Accordion({ title, icon, children, isOpen, onToggle }: AccordionProps) {
|
||||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-charcoal-outline rounded-lg overflow-hidden bg-iron-gray/30">
|
<div className="border border-charcoal-outline rounded-lg overflow-hidden bg-iron-gray/30">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={onToggle}
|
||||||
className="w-full flex items-center justify-between px-3 py-2 hover:bg-iron-gray/50 transition-colors"
|
className="w-full flex items-center justify-between px-3 py-2 hover:bg-iron-gray/50 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ export default function DevToolbar() {
|
|||||||
const [circuitBreakers, setCircuitBreakers] = useState(() => CircuitBreakerRegistry.getInstance().getStatus());
|
const [circuitBreakers, setCircuitBreakers] = useState(() => CircuitBreakerRegistry.getInstance().getStatus());
|
||||||
const [checkingHealth, setCheckingHealth] = useState(false);
|
const [checkingHealth, setCheckingHealth] = useState(false);
|
||||||
|
|
||||||
|
// Accordion state - only one open at a time
|
||||||
|
const [openAccordion, setOpenAccordion] = useState<string | null>('notifications');
|
||||||
|
|
||||||
const currentDriverId = useEffectiveDriverId();
|
const currentDriverId = useEffectiveDriverId();
|
||||||
|
|
||||||
// Sync login mode with actual session state on mount
|
// Sync login mode with actual session state on mount
|
||||||
@@ -350,17 +353,18 @@ export default function DevToolbar() {
|
|||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="p-4 space-y-3">
|
<div className="p-4 space-y-3">
|
||||||
{/* Notification Section - Accordion */}
|
{/* Notification Section - Accordion */}
|
||||||
<Accordion
|
<Accordion
|
||||||
title="Notifications"
|
title="Notifications"
|
||||||
icon={<MessageSquare className="w-4 h-4 text-gray-400" />}
|
icon={<MessageSquare className="w-4 h-4 text-gray-400" />}
|
||||||
defaultOpen={true}
|
isOpen={openAccordion === 'notifications'}
|
||||||
|
onToggle={() => setOpenAccordion(openAccordion === 'notifications' ? null : 'notifications')}
|
||||||
>
|
>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<NotificationTypeSection
|
<NotificationTypeSection
|
||||||
selectedType={selectedType}
|
selectedType={selectedType}
|
||||||
onSelectType={setSelectedType}
|
onSelectType={setSelectedType}
|
||||||
/>
|
/>
|
||||||
<UrgencySection
|
<UrgencySection
|
||||||
selectedUrgency={selectedUrgency}
|
selectedUrgency={selectedUrgency}
|
||||||
onSelectUrgency={setSelectedUrgency}
|
onSelectUrgency={setSelectedUrgency}
|
||||||
/>
|
/>
|
||||||
@@ -375,10 +379,11 @@ export default function DevToolbar() {
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
{/* API Status Section - Accordion */}
|
{/* API Status Section - Accordion */}
|
||||||
<Accordion
|
<Accordion
|
||||||
title="API Status"
|
title="API Status"
|
||||||
icon={<Activity className="w-4 h-4 text-gray-400" />}
|
icon={<Activity className="w-4 h-4 text-gray-400" />}
|
||||||
defaultOpen={false}
|
isOpen={openAccordion === 'apiStatus'}
|
||||||
|
onToggle={() => setOpenAccordion(openAccordion === 'apiStatus' ? null : 'apiStatus')}
|
||||||
>
|
>
|
||||||
<APIStatusSection
|
<APIStatusSection
|
||||||
apiStatus={apiStatus}
|
apiStatus={apiStatus}
|
||||||
@@ -392,10 +397,11 @@ export default function DevToolbar() {
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
{/* Login Section - Accordion */}
|
{/* Login Section - Accordion */}
|
||||||
<Accordion
|
<Accordion
|
||||||
title="Demo Login"
|
title="Demo Login"
|
||||||
icon={<LogIn className="w-4 h-4 text-gray-400" />}
|
icon={<LogIn className="w-4 h-4 text-gray-400" />}
|
||||||
defaultOpen={false}
|
isOpen={openAccordion === 'login'}
|
||||||
|
onToggle={() => setOpenAccordion(openAccordion === 'login' ? null : 'login')}
|
||||||
>
|
>
|
||||||
<LoginSection
|
<LoginSection
|
||||||
loginMode={loginMode}
|
loginMode={loginMode}
|
||||||
|
|||||||
@@ -1,329 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { ApiConnectionMonitor, ConnectionStatus } from '@/lib/api/base/ApiConnectionMonitor';
|
|
||||||
import { CircuitBreakerRegistry } from '@/lib/api/base/RetryHandler';
|
|
||||||
import {
|
|
||||||
Activity,
|
|
||||||
Wifi,
|
|
||||||
WifiOff,
|
|
||||||
AlertTriangle,
|
|
||||||
CheckCircle2,
|
|
||||||
RefreshCw,
|
|
||||||
Terminal,
|
|
||||||
Shield,
|
|
||||||
Clock,
|
|
||||||
TrendingUp
|
|
||||||
} from 'lucide-react';
|
|
||||||
|
|
||||||
interface ApiStatusToolbarProps {
|
|
||||||
position?: 'top-right' | 'bottom-right' | 'top-left' | 'bottom-left';
|
|
||||||
autoHide?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Development toolbar showing real-time API connection status
|
|
||||||
* Integrates with existing DevToolbar or works standalone
|
|
||||||
*/
|
|
||||||
export function ApiStatusToolbar({ position = 'bottom-right', autoHide = false }: ApiStatusToolbarProps) {
|
|
||||||
const [status, setStatus] = useState<ConnectionStatus>('disconnected');
|
|
||||||
const [health, setHealth] = useState(ApiConnectionMonitor.getInstance().getHealth());
|
|
||||||
const [expanded, setExpanded] = useState(false);
|
|
||||||
const [show, setShow] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const monitor = ApiConnectionMonitor.getInstance();
|
|
||||||
const registry = CircuitBreakerRegistry.getInstance();
|
|
||||||
|
|
||||||
const updateState = () => {
|
|
||||||
setStatus(monitor.getStatus());
|
|
||||||
setHealth(monitor.getHealth());
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initial state
|
|
||||||
updateState();
|
|
||||||
|
|
||||||
// Listen for events
|
|
||||||
monitor.on('connected', updateState);
|
|
||||||
monitor.on('disconnected', updateState);
|
|
||||||
monitor.on('degraded', updateState);
|
|
||||||
monitor.on('success', updateState);
|
|
||||||
monitor.on('failure', updateState);
|
|
||||||
|
|
||||||
// Auto-hide logic
|
|
||||||
if (autoHide) {
|
|
||||||
const hideTimer = setTimeout(() => setShow(false), 5000);
|
|
||||||
const showOnInteraction = () => setShow(true);
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', showOnInteraction);
|
|
||||||
document.addEventListener('click', showOnInteraction);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearTimeout(hideTimer);
|
|
||||||
document.removeEventListener('mousemove', showOnInteraction);
|
|
||||||
document.removeEventListener('click', showOnInteraction);
|
|
||||||
monitor.off('connected', updateState);
|
|
||||||
monitor.off('disconnected', updateState);
|
|
||||||
monitor.off('degraded', updateState);
|
|
||||||
monitor.off('success', updateState);
|
|
||||||
monitor.off('failure', updateState);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
monitor.off('connected', updateState);
|
|
||||||
monitor.off('disconnected', updateState);
|
|
||||||
monitor.off('degraded', updateState);
|
|
||||||
monitor.off('success', updateState);
|
|
||||||
monitor.off('failure', updateState);
|
|
||||||
};
|
|
||||||
}, [autoHide]);
|
|
||||||
|
|
||||||
const handleHealthCheck = async () => {
|
|
||||||
const monitor = ApiConnectionMonitor.getInstance();
|
|
||||||
await monitor.performHealthCheck();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
ApiConnectionMonitor.getInstance().reset();
|
|
||||||
CircuitBreakerRegistry.getInstance().resetAll();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getReliabilityColor = (reliability: number) => {
|
|
||||||
if (reliability >= 95) return 'text-green-400';
|
|
||||||
if (reliability >= 80) return 'text-yellow-400';
|
|
||||||
return 'text-red-400';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusIcon = () => {
|
|
||||||
switch (status) {
|
|
||||||
case 'connected':
|
|
||||||
return <CheckCircle2 className="w-4 h-4 text-green-400" />;
|
|
||||||
case 'degraded':
|
|
||||||
return <AlertTriangle className="w-4 h-4 text-yellow-400" />;
|
|
||||||
case 'disconnected':
|
|
||||||
return <WifiOff className="w-4 h-4 text-red-400" />;
|
|
||||||
case 'checking':
|
|
||||||
return <RefreshCw className="w-4 h-4 animate-spin text-blue-400" />;
|
|
||||||
default:
|
|
||||||
return <Wifi className="w-4 h-4 text-gray-400" />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusColor = () => {
|
|
||||||
switch (status) {
|
|
||||||
case 'connected': return 'bg-green-500/20 border-green-500/40';
|
|
||||||
case 'degraded': return 'bg-yellow-500/20 border-yellow-500/40';
|
|
||||||
case 'disconnected': return 'bg-red-500/20 border-red-500/40';
|
|
||||||
default: return 'bg-gray-500/20 border-gray-500/40';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const reliability = ((health.successfulRequests / Math.max(health.totalRequests, 1)) * 100).toFixed(1);
|
|
||||||
|
|
||||||
if (!show) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={() => setShow(true)}
|
|
||||||
className={`fixed p-2 bg-iron-gray border border-charcoal-outline rounded-lg shadow-lg hover:bg-charcoal-outline transition-all ${
|
|
||||||
position === 'bottom-right' ? 'bottom-4 right-4' :
|
|
||||||
position === 'top-right' ? 'top-4 right-4' :
|
|
||||||
position === 'bottom-left' ? 'bottom-4 left-4' :
|
|
||||||
'top-4 left-4'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Activity className="w-5 h-5 text-primary-blue" />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`fixed z-50 transition-all ${
|
|
||||||
position === 'bottom-right' ? 'bottom-4 right-4' :
|
|
||||||
position === 'top-right' ? 'top-4 right-4' :
|
|
||||||
position === 'bottom-left' ? 'bottom-4 left-4' :
|
|
||||||
'top-4 left-4'
|
|
||||||
}`}>
|
|
||||||
{/* Compact Status Indicator */}
|
|
||||||
{!expanded ? (
|
|
||||||
<button
|
|
||||||
onClick={() => setExpanded(true)}
|
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg border shadow-lg backdrop-blur-md transition-all hover:scale-105 ${getStatusColor()}`}
|
|
||||||
>
|
|
||||||
{getStatusIcon()}
|
|
||||||
<span className="text-sm font-semibold text-white">{status.toUpperCase()}</span>
|
|
||||||
<span className={`text-xs ${getReliabilityColor(parseFloat(reliability))}`}>
|
|
||||||
{reliability}%
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
/* Expanded Panel */
|
|
||||||
<div className={`w-80 rounded-lg border shadow-2xl backdrop-blur-md overflow-hidden ${getStatusColor()}`}>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="bg-iron-gray/80 border-b border-charcoal-outline px-3 py-2 flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Terminal className="w-4 h-4 text-primary-blue" />
|
|
||||||
<span className="text-xs font-bold text-white">API STATUS</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<button
|
|
||||||
onClick={handleHealthCheck}
|
|
||||||
className="p-1 hover:bg-charcoal-outline rounded transition-colors"
|
|
||||||
title="Run Health Check"
|
|
||||||
>
|
|
||||||
<RefreshCw className="w-3 h-3 text-gray-400 hover:text-white" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleReset}
|
|
||||||
className="p-1 hover:bg-charcoal-outline rounded transition-colors"
|
|
||||||
title="Reset Stats"
|
|
||||||
>
|
|
||||||
<span className="text-xs text-gray-400 hover:text-white">R</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setExpanded(false)}
|
|
||||||
className="p-1 hover:bg-charcoal-outline rounded transition-colors"
|
|
||||||
>
|
|
||||||
<span className="text-xs text-gray-400 hover:text-white">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Body */}
|
|
||||||
<div className="px-3 py-2 space-y-2 bg-deep-graphite/90">
|
|
||||||
{/* Status Row */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-xs text-gray-400">Status</span>
|
|
||||||
<span className={`text-xs font-bold uppercase ${status === 'connected' ? 'text-green-400' : status === 'degraded' ? 'text-yellow-400' : 'text-red-400'}`}>
|
|
||||||
{status}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Reliability */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-xs text-gray-400">Reliability</span>
|
|
||||||
<span className={`text-xs font-bold ${getReliabilityColor(parseFloat(reliability))}`}>
|
|
||||||
{reliability}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Request Stats */}
|
|
||||||
<div className="grid grid-cols-3 gap-2 text-center">
|
|
||||||
<div className="bg-iron-gray/50 rounded p-1">
|
|
||||||
<div className="text-[10px] text-gray-400">Total</div>
|
|
||||||
<div className="text-sm font-bold text-white">{health.totalRequests}</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-iron-gray/50 rounded p-1">
|
|
||||||
<div className="text-[10px] text-gray-400">Success</div>
|
|
||||||
<div className="text-sm font-bold text-green-400">{health.successfulRequests}</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-iron-gray/50 rounded p-1">
|
|
||||||
<div className="text-[10px] text-gray-400">Failed</div>
|
|
||||||
<div className="text-sm font-bold text-red-400">{health.failedRequests}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Performance */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-xs text-gray-400">Avg Response</span>
|
|
||||||
<span className="text-xs font-mono text-blue-400">
|
|
||||||
{health.averageResponseTime.toFixed(0)}ms
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Consecutive Failures */}
|
|
||||||
{health.consecutiveFailures > 0 && (
|
|
||||||
<div className="flex items-center justify-between bg-red-500/10 rounded px-2 py-1">
|
|
||||||
<span className="text-xs text-red-400">Consecutive Failures</span>
|
|
||||||
<span className="text-xs font-bold text-red-400">{health.consecutiveFailures}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Circuit Breakers */}
|
|
||||||
<div className="border-t border-charcoal-outline pt-2">
|
|
||||||
<div className="flex items-center gap-1 mb-1">
|
|
||||||
<Shield className="w-3 h-3 text-gray-400" />
|
|
||||||
<span className="text-[10px] text-gray-400 font-bold">CIRCUIT BREAKERS</span>
|
|
||||||
</div>
|
|
||||||
<CircuitBreakerStatus />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Last Check */}
|
|
||||||
<div className="border-t border-charcoal-outline pt-2 flex items-center justify-between">
|
|
||||||
<span className="text-[10px] text-gray-500">Last Check</span>
|
|
||||||
<span className="text-[10px] text-gray-400 font-mono">
|
|
||||||
{health.lastCheck ? new Date(health.lastCheck).toLocaleTimeString() : 'Never'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="grid grid-cols-2 gap-2 pt-1">
|
|
||||||
<button
|
|
||||||
onClick={handleHealthCheck}
|
|
||||||
className="px-2 py-1 bg-primary-blue hover:bg-primary-blue/80 text-white text-xs rounded transition-colors"
|
|
||||||
>
|
|
||||||
Check Health
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
const monitor = ApiConnectionMonitor.getInstance();
|
|
||||||
const report = monitor.getDebugReport();
|
|
||||||
alert(report);
|
|
||||||
}}
|
|
||||||
className="px-2 py-1 bg-iron-gray hover:bg-charcoal-outline text-gray-300 text-xs rounded transition-colors border border-charcoal-outline"
|
|
||||||
>
|
|
||||||
Debug Report
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 (
|
|
||||||
<div className="text-[10px] text-gray-500 italic">No active circuit breakers</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-1 max-h-20 overflow-auto">
|
|
||||||
{entries.map(([endpoint, breaker]) => (
|
|
||||||
<div key={endpoint} className="flex items-center justify-between text-[10px]">
|
|
||||||
<span className="text-gray-400 truncate flex-1">{endpoint.split('/').pop() || endpoint}</span>
|
|
||||||
<span className={`px-1 rounded ${
|
|
||||||
breaker.state === 'CLOSED' ? 'bg-green-500/20 text-green-400' :
|
|
||||||
breaker.state === 'OPEN' ? 'bg-red-500/20 text-red-400' :
|
|
||||||
'bg-yellow-500/20 text-yellow-400'
|
|
||||||
}`}>
|
|
||||||
{breaker.state}
|
|
||||||
</span>
|
|
||||||
{breaker.failures > 0 && (
|
|
||||||
<span className="text-red-400 ml-1">({breaker.failures})</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -301,7 +301,7 @@ let globalReporter: EnhancedErrorReporter | null = null;
|
|||||||
export function getGlobalErrorReporter(): EnhancedErrorReporter {
|
export function getGlobalErrorReporter(): EnhancedErrorReporter {
|
||||||
if (!globalReporter) {
|
if (!globalReporter) {
|
||||||
// Import the console logger
|
// Import the console logger
|
||||||
const { ConsoleLogger } = require('./ConsoleLogger');
|
const { ConsoleLogger } = require('./logging/ConsoleLogger');
|
||||||
globalReporter = new EnhancedErrorReporter(new ConsoleLogger(), {
|
globalReporter = new EnhancedErrorReporter(new ConsoleLogger(), {
|
||||||
showUserNotifications: true,
|
showUserNotifications: true,
|
||||||
logToConsole: true,
|
logToConsole: true,
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
"declarationMap": true,
|
"declarationMap": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"types": ["vitest/globals"],
|
"types": ["vitest/globals"],
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"],
|
"@/*": ["./*"],
|
||||||
"@core/*": ["./*"],
|
"@core/*": ["./*"],
|
||||||
@@ -17,5 +19,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts", "bootstrap/**/*.ts"],
|
"include": ["**/*.ts", "bootstrap/**/*.ts"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": [
|
||||||
}
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.integration.test.ts",
|
||||||
|
"**/__tests__/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user