website refactor

This commit is contained in:
2026-01-18 13:26:35 +01:00
parent 350c78504d
commit 0b301feb61
225 changed files with 1678 additions and 26666 deletions

View File

@@ -31,7 +31,7 @@ export class ApiConnectionMonitor extends EventEmitter {
private isChecking = false;
private checkInterval: NodeJS.Timeout | null = null;
private healthCheckEndpoint: string;
private readonly CHECK_INTERVAL = 30000; // 30 seconds
private readonly CHECK_INTERVAL = 300000; // 5 minutes
private readonly DEGRADATION_THRESHOLD = 0.7; // 70% failure rate
private constructor(healthCheckEndpoint: string = '/health') {

View File

@@ -47,7 +47,7 @@ export class BaseApiClient {
};
// Start monitoring connection health
this.connectionMonitor.startMonitoring();
// this.connectionMonitor.startMonitoring();
}
/**

View File

@@ -13,7 +13,7 @@ export interface RetryConfig {
}
export const DEFAULT_RETRY_CONFIG: RetryConfig = {
maxRetries: 3,
maxRetries: 1,
baseDelay: 1000,
maxDelay: 10000,
backoffMultiplier: 2,

View File

@@ -28,7 +28,10 @@ export class DriversApiClient extends BaseApiClient {
/** Get current driver (based on session) */
getCurrent(): Promise<GetDriverOutputDTO | null> {
return this.get<GetDriverOutputDTO | null>('/drivers/current', { allowUnauthenticated: true });
return this.get<GetDriverOutputDTO | null>('/drivers/current', {
allowUnauthenticated: true,
retry: false
});
}
/** Get driver registration status for a specific race */

View File

@@ -46,12 +46,12 @@ function parseRaceDTOArray(value: unknown): RaceDTO[] {
export class LeaguesApiClient extends BaseApiClient {
/** Get all leagues with capacity information */
getAllWithCapacity(): Promise<AllLeaguesWithCapacityDTO> {
return this.get<AllLeaguesWithCapacityDTO>('/leagues/all-with-capacity');
return this.get<AllLeaguesWithCapacityDTO>('/leagues/all-with-capacity', { retry: false });
}
/** Get all leagues with capacity + scoring summary (for leagues page filters) */
getAllWithCapacityAndScoring(): Promise<AllLeaguesWithCapacityAndScoringDTO> {
return this.get<AllLeaguesWithCapacityAndScoringDTO>('/leagues/all-with-capacity-and-scoring');
return this.get<AllLeaguesWithCapacityAndScoringDTO>('/leagues/all-with-capacity-and-scoring', { retry: false });
}
/** Get total number of leagues */

View File

@@ -22,10 +22,10 @@ export function QueryClientProvider({ children }: QueryClientProviderProps) {
defaultOptions: {
queries: {
// Disable automatic refetching in production for better performance
refetchOnWindowFocus: process.env.NODE_ENV === 'development',
refetchOnWindowFocus: false,
refetchOnReconnect: true,
retry: 1,
staleTime: 5 * 60 * 1000, // 5 minutes
staleTime: 10 * 60 * 1000, // 10 minutes
},
mutations: {
retry: 0,

View File

@@ -263,13 +263,13 @@ export const routeMatchers = {
* Check if path is public
*/
isPublic(path: string): boolean {
logger.info('[RouteConfig] isPublic check', { path });
// logger.info('[RouteConfig] isPublic check', { path });
const publicPatterns = this.getPublicPatterns();
// Check exact matches
if (publicPatterns.includes(path)) {
logger.info('[RouteConfig] Path is public (exact match)', { path });
// logger.info('[RouteConfig] Path is public (exact match)', { path });
return true;
}
@@ -279,19 +279,19 @@ export const routeMatchers = {
if (segments.length === 2) {
const [group, slug] = segments;
if (group === 'leagues' && slug !== 'create') {
logger.info('[RouteConfig] Path is public (league detail)', { path });
// logger.info('[RouteConfig] Path is public (league detail)', { path });
return true;
}
if (group === 'races') {
logger.info('[RouteConfig] Path is public (race detail)', { path });
// logger.info('[RouteConfig] Path is public (race detail)', { path });
return true;
}
if (group === 'drivers') {
logger.info('[RouteConfig] Path is public (driver detail)', { path });
// logger.info('[RouteConfig] Path is public (driver detail)', { path });
return true;
}
if (group === 'teams' && slug !== 'create') {
logger.info('[RouteConfig] Path is public (team detail)', { path });
// logger.info('[RouteConfig] Path is public (team detail)', { path });
return true;
}
}
@@ -306,11 +306,11 @@ export const routeMatchers = {
return false;
});
if (isPublicParam) {
logger.info('[RouteConfig] Path is public (parameterized match)', { path });
} else {
logger.info('[RouteConfig] Path is NOT public', { path });
}
// if (isPublicParam) {
// logger.info('[RouteConfig] Path is public (parameterized match)', { path });
// } else {
// logger.info('[RouteConfig] Path is NOT public', { path });
// }
return isPublicParam;
},
@@ -326,11 +326,11 @@ export const routeMatchers = {
* Check if path requires specific role
*/
requiresRole(path: string): string[] | null {
logger.info('[RouteConfig] requiresRole check', { path });
// logger.info('[RouteConfig] requiresRole check', { path });
// Public routes never require a role
if (this.isPublic(path)) {
logger.info('[RouteConfig] Path is public, no role required', { path });
// logger.info('[RouteConfig] Path is public, no role required', { path });
return null;
}
@@ -338,14 +338,14 @@ export const routeMatchers = {
// Website session roles come from the API and are more specific than just "admin".
// Keep "admin"/"owner" for backwards compatibility.
const roles = ['admin', 'owner', 'league-admin', 'league-steward', 'league-owner', 'system-owner', 'super-admin'];
logger.info('[RouteConfig] Path requires admin roles', { path, roles });
// logger.info('[RouteConfig] Path requires admin roles', { path, roles });
return roles;
}
if (this.isInGroup(path, 'sponsor')) {
logger.info('[RouteConfig] Path requires sponsor role', { path });
// logger.info('[RouteConfig] Path requires sponsor role', { path });
return ['sponsor'];
}
logger.info('[RouteConfig] Path requires no specific role', { path });
// logger.info('[RouteConfig] Path requires no specific role', { path });
return null;
},
};

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { LeagueDetailViewModel, LeagueViewModel, DriverViewModel, RaceViewModel } from './LeagueDetailViewModel';
import { LeagueDetailViewModel, LeagueViewModel, LeagueDetailDriverViewModel, LeagueDetailRaceViewModel } from './LeagueDetailViewModel';
describe('LeagueDetailViewModel', () => {
const baseLeague = {
@@ -40,9 +40,9 @@ describe('LeagueDetailViewModel', () => {
expect(vm.league).toBeInstanceOf(LeagueViewModel);
expect(vm.drivers).toHaveLength(2);
expect(vm.drivers[0]).toBeInstanceOf(DriverViewModel);
expect(vm.drivers[0]).toBeInstanceOf(LeagueDetailDriverViewModel);
expect(vm.races).toHaveLength(2);
expect(vm.races[0]).toBeInstanceOf(RaceViewModel);
expect(vm.races[0]).toBeInstanceOf(LeagueDetailRaceViewModel);
});
it('keeps core league metrics available on LeagueViewModel', () => {

View File

@@ -8,13 +8,39 @@ import { RaceViewModel as SharedRaceViewModel } from "./RaceViewModel";
*/
export class LeagueDetailViewModel {
league: LeagueViewModel;
drivers: SharedDriverViewModel[];
races: SharedRaceViewModel[];
drivers: LeagueDetailDriverViewModel[];
races: LeagueDetailRaceViewModel[];
constructor(data: { league: unknown; drivers: unknown[]; races: unknown[] }) {
this.league = new LeagueViewModel(data.league);
this.drivers = data.drivers.map(driver => new SharedDriverViewModel(driver as any));
this.races = data.races.map(race => new SharedRaceViewModel(race as any));
this.drivers = data.drivers.map(driver => new LeagueDetailDriverViewModel(driver as any));
this.races = data.races.map(race => new LeagueDetailRaceViewModel(race as any));
}
}
export class LeagueDetailDriverViewModel extends SharedDriverViewModel {
impressions: number;
constructor(dto: any) {
super(dto);
this.impressions = dto.impressions || 0;
}
get formattedImpressions(): string {
return this.impressions.toLocaleString();
}
}
export class LeagueDetailRaceViewModel extends SharedRaceViewModel {
views: number;
constructor(dto: any) {
super(dto);
this.views = dto.views || 0;
}
get formattedViews(): string {
return this.views.toLocaleString();
}
}

View File

@@ -1,7 +1,7 @@
export interface LeagueSummaryViewModel {
id: string;
name: string;
description: string;
description: string | null;
logoUrl: string | null;
ownerId: string;
createdAt: string;
@@ -12,7 +12,7 @@ export interface LeagueSummaryViewModel {
structureSummary: string;
scoringPatternSummary?: string;
timingSummary: string;
category?: string;
category?: string | null;
scoring?: {
gameId: string;
gameName: string;