harden media

This commit is contained in:
2025-12-31 15:39:28 +01:00
parent 92226800df
commit 8260bf7baf
413 changed files with 8361 additions and 1544 deletions

View File

@@ -22,7 +22,7 @@ import type { IPenaltyRepository } from '@core/racing/domain/repositories/IPenal
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
import type { IDriverStatsRepository } from '@core/racing/domain/repositories/IDriverStatsRepository';
import type { ITeamStatsRepository } from '@core/racing/domain/repositories/ITeamStatsRepository';
import type { ITeamStatsRepository, TeamStats } from '@core/racing/domain/repositories/ITeamStatsRepository';
import type { IMediaRepository } from '@core/racing/domain/repositories/IMediaRepository';
import { createRacingSeed } from './racing/RacingSeed';
import { seedId } from './racing/SeedIdHelper';
@@ -31,7 +31,6 @@ import { Result } from '@core/racing/domain/entities/result/Result';
import { Standing } from '@core/racing/domain/entities/Standing';
import { Team } from '@core/racing/domain/entities/Team';
import type { DriverStats } from '@core/racing/application/use-cases/IDriverStatsUseCase';
import type { TeamStats } from '@core/racing/domain/repositories/ITeamStatsRepository';
export type RacingSeedDependencies = {
driverRepository: IDriverRepository;
@@ -78,26 +77,33 @@ export class SeedRacingData {
return process.env.DATABASE_URL ? 'postgres' : 'inmemory';
}
private getMediaBaseUrl(): string {
return process.env.NODE_ENV === 'development' ? 'http://localhost:3001' : 'https://api.gridpilot.io';
}
async execute(): Promise<void> {
const existingDrivers = await this.seedDeps.driverRepository.findAll();
const existingTeams = await this.seedDeps.teamRepository.findAll().catch(() => []);
const persistence = this.getApiPersistence();
// Check for force reseed via environment variable
const forceReseedRaw = process.env.GRIDPILOT_API_FORCE_RESEED;
const forceReseed = forceReseedRaw !== undefined && forceReseedRaw !== '0' && forceReseedRaw.toLowerCase() !== 'false';
this.logger.info(
`[Bootstrap] Racing seed precheck: forceReseed=${forceReseed}, drivers=${existingDrivers.length}, teams=${existingTeams.length}, persistence=${persistence}`,
);
if (existingDrivers.length > 0 && !forceReseed) {
this.logger.info('[Bootstrap] Racing seed skipped (drivers already exist), ensuring scoring configs');
await this.ensureScoringConfigsForExistingData();
return;
}
if (forceReseed && existingDrivers.length > 0) {
this.logger.info('[Bootstrap] Force reseed enabled - clearing existing racing data');
// IMPORTANT:
// Force reseed must clear even when drivers are already empty.
// Otherwise stale teams can remain (e.g. with logoRef=system-default/logo),
// and the seed will "ignore duplicates" on create, leaving stale logoRefs in Postgres.
if (forceReseed) {
this.logger.info(
`[Bootstrap] Force reseed enabled - clearing existing racing data (drivers=${existingDrivers.length}, teams=${existingTeams.length})`,
);
await this.clearExistingRacingData();
}
@@ -109,9 +115,8 @@ export class SeedRacingData {
// Clear existing stats repositories
await this.seedDeps.driverStatsRepository.clear();
await this.seedDeps.teamStatsRepository.clear();
await this.seedDeps.mediaRepository.clear();
this.logger.info('[Bootstrap] Cleared existing stats and media repositories');
this.logger.info('[Bootstrap] Cleared existing stats repositories');
let sponsorshipRequestsSeededViaRepo = false;
const seedableSponsorshipRequests = this.seedDeps
@@ -316,9 +321,6 @@ export class SeedRacingData {
// Compute and store team stats from real data
await this.computeAndStoreTeamStats();
// Seed media assets (logos, images)
await this.seedMediaAssets(seed);
this.logger.info(
`[Bootstrap] Seeded racing data: drivers=${seed.drivers.length}, leagues=${seed.leagues.length}, races=${seed.races.length}`,
@@ -429,7 +431,7 @@ export class SeedRacingData {
this.logger.info(`[Bootstrap] Computed and stored stats for ${teams.length} teams`);
}
private calculateTeamStats(team: Team, results: Result[], drivers: Driver[]): TeamStats {
private calculateTeamStats(_team: Team, results: Result[], drivers: Driver[]): TeamStats {
const wins = results.filter(r => r.position.toNumber() === 1).length;
const totalRaces = results.length;
@@ -466,7 +468,6 @@ export class SeedRacingData {
})));
return {
logoUrl: `${this.getMediaBaseUrl()}/api/media/teams/${team.id}/logo`,
performanceLevel,
specialization,
region,
@@ -485,119 +486,171 @@ export class SeedRacingData {
return 'Beginner';
}
private async seedMediaAssets(seed: any): Promise<void> {
const baseUrl = this.getMediaBaseUrl();
// Seed driver avatars using static files
for (const driver of seed.drivers) {
const avatarUrl = this.getDriverAvatarUrl(driver.id);
const mediaRepo = this.seedDeps.mediaRepository as any;
if (mediaRepo.setDriverAvatar) {
mediaRepo.setDriverAvatar(driver.id, avatarUrl);
}
}
// Seed team logos using API routes
for (const team of seed.teams) {
const logoUrl = `${baseUrl}/api/media/teams/${team.id}/logo`;
const mediaRepo = this.seedDeps.mediaRepository as any;
if (mediaRepo.setTeamLogo) {
mediaRepo.setTeamLogo(team.id, logoUrl);
}
}
// Seed track images
for (const track of seed.tracks || []) {
const trackImageUrl = `${baseUrl}/api/media/tracks/${track.id}/image`;
const mediaRepo = this.seedDeps.mediaRepository as any;
if (mediaRepo.setTrackImage) {
mediaRepo.setTrackImage(track.id, trackImageUrl);
}
}
// Seed category icons (if categories exist)
const categories = ['beginner', 'intermediate', 'advanced', 'pro', 'endurance', 'sprint'];
for (const category of categories) {
const iconUrl = `${baseUrl}/api/media/categories/${category}/icon`;
const mediaRepo = this.seedDeps.mediaRepository as any;
if (mediaRepo.setCategoryIcon) {
mediaRepo.setCategoryIcon(category, iconUrl);
}
}
// Seed sponsor logos
for (const sponsor of seed.sponsors || []) {
const logoUrl = `${baseUrl}/api/media/sponsors/${sponsor.id}/logo`;
const mediaRepo = this.seedDeps.mediaRepository as any;
if (mediaRepo.setSponsorLogo) {
mediaRepo.setSponsorLogo(sponsor.id, logoUrl);
}
}
this.logger.info(`[Bootstrap] Seeded media assets for ${seed.drivers.length} drivers, ${seed.teams.length} teams`);
}
/**
* Get deterministic avatar URL for a driver based on their ID
* Uses static files from the website public directory
*/
private getDriverAvatarUrl(driverId: string): string {
// Deterministic selection based on driver ID
const numericSuffixMatch = driverId.match(/(\d+)$/);
let useFemale = false;
let useNeutral = false;
if (numericSuffixMatch && numericSuffixMatch[1]) {
const numericSuffix = parseInt(numericSuffixMatch[1], 10);
// 40% female, 40% male, 20% neutral
if (numericSuffix % 5 === 0) {
useNeutral = true;
} else if (numericSuffix % 2 === 0) {
useFemale = true;
}
} else {
// Fallback hash
let hash = 0;
for (let i = 0; i < driverId.length; i++) {
hash = (hash * 31 + driverId.charCodeAt(i)) | 0;
}
const hashValue = Math.abs(hash);
if (hashValue % 5 === 0) {
useNeutral = true;
} else if (hashValue % 2 === 0) {
useFemale = true;
}
}
// Return static file paths that Next.js can serve
if (useNeutral) {
return '/images/avatars/neutral-default-avatar.jpeg';
} else if (useFemale) {
return '/images/avatars/female-default-avatar.jpeg';
} else {
return '/images/avatars/male-default-avatar.jpg';
}
}
private async clearExistingRacingData(): Promise<void> {
// Get all existing drivers
const drivers = await this.seedDeps.driverRepository.findAll();
this.logger.info('[Bootstrap] Starting comprehensive clearing of all racing data');
// Delete drivers first (this should cascade to related data in most cases)
for (const driver of drivers) {
try {
await this.seedDeps.driverRepository.delete(driver.id);
} catch {
// Ignore errors
}
// Clear stats repositories first
try {
await this.seedDeps.driverStatsRepository.clear();
await this.seedDeps.teamStatsRepository.clear();
this.logger.info('[Bootstrap] Cleared stats repositories');
} catch (error) {
this.logger.warn('[Bootstrap] Could not clear stats repositories:', error);
}
// Try to clean up other data if repositories support it
// Clear race registrations - get all races first, then clear their registrations
try {
const races = await this.seedDeps.raceRepository.findAll();
for (const race of races) {
try {
await this.seedDeps.raceRegistrationRepository.clearRaceRegistrations(race.id.toString());
} catch {
// Ignore
}
}
this.logger.info('[Bootstrap] Cleared race registrations');
} catch (error) {
this.logger.warn('[Bootstrap] Could not clear race registrations:', error);
}
// Clear team join requests - get all teams first, then clear their join requests
try {
const teams = await this.seedDeps.teamRepository.findAll();
for (const team of teams) {
const joinRequests = await this.seedDeps.teamMembershipRepository.getJoinRequests(team.id.toString());
for (const request of joinRequests) {
try {
await this.seedDeps.teamMembershipRepository.removeJoinRequest(request.id);
} catch {
// Ignore
}
}
}
this.logger.info('[Bootstrap] Cleared team join requests');
} catch (error) {
this.logger.warn('[Bootstrap] Could not clear team join requests:', error);
}
// Clear team memberships
try {
const teams = await this.seedDeps.teamRepository.findAll();
for (const team of teams) {
const memberships = await this.seedDeps.teamMembershipRepository.getTeamMembers(team.id.toString());
for (const membership of memberships) {
try {
await this.seedDeps.teamMembershipRepository.removeMembership(team.id.toString(), membership.driverId.toString());
} catch {
// Ignore
}
}
}
this.logger.info('[Bootstrap] Cleared team memberships');
} catch (error) {
this.logger.warn('[Bootstrap] Could not clear team memberships:', error);
}
// Clear teams (this is critical - teams have stale logoRef)
try {
const teams = await this.seedDeps.teamRepository.findAll();
for (const team of teams) {
try {
await this.seedDeps.teamRepository.delete(team.id.toString());
} catch {
// Ignore
}
}
this.logger.info('[Bootstrap] Cleared teams');
} catch (error) {
this.logger.warn('[Bootstrap] Could not clear teams:', error);
}
// Clear results
try {
const results = await this.seedDeps.resultRepository.findAll();
for (const result of results) {
try {
await this.seedDeps.resultRepository.delete(result.id.toString());
} catch {
// Ignore
}
}
this.logger.info('[Bootstrap] Cleared results');
} catch (error) {
this.logger.warn('[Bootstrap] Could not clear results:', error);
}
// Clear standings
try {
const standings = await this.seedDeps.standingRepository.findAll();
for (const standing of standings) {
try {
await this.seedDeps.standingRepository.delete(standing.leagueId.toString(), standing.driverId.toString());
} catch {
// Ignore
}
}
this.logger.info('[Bootstrap] Cleared standings');
} catch (error) {
this.logger.warn('[Bootstrap] Could not clear standings:', error);
}
// Clear races
try {
const races = await this.seedDeps.raceRepository.findAll();
for (const race of races) {
try {
await this.seedDeps.raceRepository.delete(race.id.toString());
} catch {
// Ignore
}
}
this.logger.info('[Bootstrap] Cleared races');
} catch (error) {
this.logger.warn('[Bootstrap] Could not clear races:', error);
}
// Clear league join requests
try {
const leagues = await this.seedDeps.leagueRepository.findAll();
for (const league of leagues) {
const joinRequests = await this.seedDeps.leagueMembershipRepository.getJoinRequests(league.id.toString());
for (const request of joinRequests) {
try {
await this.seedDeps.leagueMembershipRepository.removeJoinRequest(request.id);
} catch {
// Ignore
}
}
}
this.logger.info('[Bootstrap] Cleared league join requests');
} catch (error) {
this.logger.warn('[Bootstrap] Could not clear league join requests:', error);
}
// Clear league memberships
try {
const leagues = await this.seedDeps.leagueRepository.findAll();
for (const league of leagues) {
const memberships = await this.seedDeps.leagueMembershipRepository.getLeagueMembers(league.id.toString());
for (const membership of memberships) {
try {
await this.seedDeps.leagueMembershipRepository.removeMembership(league.id.toString(), membership.driverId.toString());
} catch {
// Ignore
}
}
}
this.logger.info('[Bootstrap] Cleared league memberships');
} catch (error) {
this.logger.warn('[Bootstrap] Could not clear league memberships:', error);
}
// Note: Some repositories don't support direct deletion methods
// The key fix is clearing teams, team memberships, and join requests
// which resolves the logoRef issue
// Clear leagues
try {
const leagues = await this.seedDeps.leagueRepository.findAll();
for (const league of leagues) {
@@ -607,11 +660,48 @@ export class SeedRacingData {
// Ignore
}
}
} catch {
// Ignore
this.logger.info('[Bootstrap] Cleared leagues');
} catch (error) {
this.logger.warn('[Bootstrap] Could not clear leagues:', error);
}
this.logger.info('[Bootstrap] Cleared existing racing data');
// Clear drivers (do this last as other data depends on it)
try {
const drivers = await this.seedDeps.driverRepository.findAll();
for (const driver of drivers) {
try {
await this.seedDeps.driverRepository.delete(driver.id);
} catch {
// Ignore
}
}
this.logger.info('[Bootstrap] Cleared drivers');
} catch (error) {
this.logger.warn('[Bootstrap] Could not clear drivers:', error);
}
// Clear social data if repositories support it
try {
const seedableFeed = this.seedDeps.feedRepository as unknown as { clear?: () => void };
if (typeof seedableFeed.clear === 'function') {
seedableFeed.clear();
this.logger.info('[Bootstrap] Cleared feed repository');
}
} catch (error) {
this.logger.warn('[Bootstrap] Could not clear feed repository:', error);
}
try {
const seedableSocial = this.seedDeps.socialGraphRepository as unknown as { clear?: () => void };
if (typeof seedableSocial.clear === 'function') {
seedableSocial.clear();
this.logger.info('[Bootstrap] Cleared social graph repository');
}
} catch (error) {
this.logger.warn('[Bootstrap] Could not clear social graph repository:', error);
}
this.logger.info('[Bootstrap] Completed comprehensive clearing of all racing data');
}
private async ensureScoringConfigsForExistingData(): Promise<void> {
@@ -676,4 +766,4 @@ export class SeedRacingData {
return 'club-default';
}
}
}