harden media
This commit is contained in:
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user