harden media
This commit is contained in:
462
plans/MEDIA_ARCHITECTURE_COMPLETE_ANALYSIS.md
Normal file
462
plans/MEDIA_ARCHITECTURE_COMPLETE_ANALYSIS.md
Normal file
@@ -0,0 +1,462 @@
|
||||
# Media Architecture: Complete Analysis & Corrected Solution
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Your media architecture plans contain **fundamental flaws** based on misunderstandings of the current codebase. This document provides a complete analysis and the correct, streamlined solution.
|
||||
|
||||
**Key Finding:** Your plans solve non-existent problems while ignoring real ones, and over-engineer simple solutions.
|
||||
|
||||
---
|
||||
|
||||
## Part 1: What's Wrong with Your Plans
|
||||
|
||||
### 1.1 Critical Flaws
|
||||
|
||||
#### **Flaw #1: Solving Non-Existent Problems**
|
||||
|
||||
**Your Claim:** "Database stores logoUrl in teams table"
|
||||
```typescript
|
||||
// Your plan claims this exists:
|
||||
teams table: { id: '123', logoUrl: '/images/logos/team-123.jpg' }
|
||||
```
|
||||
|
||||
**Reality:**
|
||||
```typescript
|
||||
// adapters/racing/persistence/typeorm/entities/TeamOrmEntity.ts
|
||||
@Entity({ name: 'racing_teams' })
|
||||
export class TeamOrmEntity {
|
||||
@PrimaryColumn({ type: 'uuid' })
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
name!: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
tag!: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
description!: string;
|
||||
|
||||
@Column({ type: 'uuid' })
|
||||
ownerId!: string;
|
||||
|
||||
@Column({ type: 'uuid', array: true })
|
||||
leagues!: string[];
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
category!: string | null;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
isRecruiting!: boolean;
|
||||
|
||||
@Column({ type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
}
|
||||
```
|
||||
|
||||
**❌ NO logoUrl column exists!** Your plan is solving a problem that doesn't exist.
|
||||
|
||||
#### **Flaw #2: Duplicating Existing Work**
|
||||
|
||||
**Your Claim:** "Need to implement SVG generation"
|
||||
**Reality:** Already exists in `MediaController`
|
||||
|
||||
```typescript
|
||||
// apps/api/src/domain/media/MediaController.ts
|
||||
@Get('avatar/:driverId')
|
||||
async getDriverAvatar(
|
||||
@Param('driverId') driverId: string,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const svg = this.generateDriverAvatarSVG(driverId); // ✅ Already implemented
|
||||
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||
res.send(svg);
|
||||
}
|
||||
|
||||
private generateDriverAvatarSVG(driverId: string): string {
|
||||
faker.seed(this.hashCode(driverId)); // ✅ Already using Faker
|
||||
// ... 50+ lines of SVG generation
|
||||
}
|
||||
```
|
||||
|
||||
**Your Claim:** "Need Next.js rewrites"
|
||||
**Reality:** Already configured
|
||||
|
||||
```javascript
|
||||
// apps/website/next.config.mjs
|
||||
async rewrites() {
|
||||
const baseUrl = 'http://api:3000';
|
||||
return [
|
||||
{
|
||||
source: '/api/:path*',
|
||||
destination: `${baseUrl}/:path*`,
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
#### **Flaw #3: Ignoring Real Problems**
|
||||
|
||||
**Real Problem 1: Controller Business Logic**
|
||||
```typescript
|
||||
// apps/api/src/domain/media/MediaController.ts
|
||||
private generateDriverAvatarSVG(driverId: string): string {
|
||||
faker.seed(this.hashCode(driverId));
|
||||
const firstName = faker.person.firstName();
|
||||
const lastName = faker.person.lastName();
|
||||
const initials = ((firstName?.[0] || 'D') + (lastName?.[0] || 'R')).toUpperCase();
|
||||
const primaryColor = faker.color.rgb({ format: 'hex' });
|
||||
const secondaryColor = faker.color.rgb({ format: 'hex' });
|
||||
const patterns = ['gradient', 'stripes', 'circles', 'diamond'];
|
||||
const pattern = faker.helpers.arrayElement(patterns);
|
||||
// ... 40 more lines
|
||||
}
|
||||
```
|
||||
**Your Plans:** Don't address this
|
||||
|
||||
**Real Problem 2: Inconsistent Seeds**
|
||||
```typescript
|
||||
// adapters/bootstrap/SeedRacingData.ts
|
||||
for (const driver of seed.drivers) {
|
||||
const avatarUrl = this.getDriverAvatarUrl(driver.id); // ❌ Static files
|
||||
mediaRepo.setDriverAvatar(driver.id, avatarUrl);
|
||||
}
|
||||
|
||||
for (const team of seed.teams) {
|
||||
const logoUrl = `/api/media/teams/${team.id}/logo`; // ✅ API endpoints
|
||||
mediaRepo.setTeamLogo(team.id, logoUrl);
|
||||
}
|
||||
```
|
||||
**Your Plans:** Claim seeds use API (partially true, but inconsistent)
|
||||
|
||||
**Real Problem 3: Mixed Repository**
|
||||
```typescript
|
||||
// adapters/racing/persistence/media/InMemoryMediaRepository.ts
|
||||
// Stores static file paths AND API endpoints
|
||||
// Purpose unclear
|
||||
```
|
||||
**Your Plans:** Don't address this
|
||||
|
||||
#### **Flaw #4: Over-Engineering**
|
||||
|
||||
**Simple Problem:** Generate SVG for avatar
|
||||
**Your Solution:** 4+ layers
|
||||
```
|
||||
Controller → Service → Use Case → Generator → Repository → Presenter
|
||||
```
|
||||
|
||||
**Correct Solution:** 2 layers
|
||||
```
|
||||
Controller → Domain Service
|
||||
```
|
||||
|
||||
#### **Flaw #5: Violating Your Own Rules**
|
||||
|
||||
**Your Plans Claim:** "Domain should not store URLs"
|
||||
**Your Proposed Domain:**
|
||||
```typescript
|
||||
// core/media/domain/entities/MediaAsset.ts
|
||||
export class MediaAsset {
|
||||
constructor(
|
||||
public readonly id: MediaId,
|
||||
public readonly type: MediaType,
|
||||
public readonly url: MediaUrl, // ❌ Still storing URLs!
|
||||
public readonly generationParams: MediaGenerationParams
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 2: The Real Problems
|
||||
|
||||
### Problem 1: Controller Business Logic
|
||||
**Location:** `apps/api/src/domain/media/MediaController.ts` (lines 214-330)
|
||||
**Issue:** 100+ lines of SVG generation in controller
|
||||
**Impact:** Violates clean architecture, hard to test
|
||||
|
||||
### Problem 2: Inconsistent Seed Approach
|
||||
**Location:** `adapters/bootstrap/SeedRacingData.ts`
|
||||
**Issue:** Driver avatars use static files, team logos use API
|
||||
**Impact:** Inconsistent behavior, static files still needed
|
||||
|
||||
### Problem 3: Mixed Repository Responsibilities
|
||||
**Location:** `adapters/racing/persistence/media/InMemoryMediaRepository.ts`
|
||||
**Issue:** Stores both static URLs and API endpoints
|
||||
**Impact:** Unclear purpose, violates single responsibility
|
||||
|
||||
### Problem 4: No Clean Architecture Separation
|
||||
**Issue:** No proper domain layer for media
|
||||
**Impact:** Infrastructure mixed with application logic
|
||||
|
||||
---
|
||||
|
||||
## Part 3: Correct Solution
|
||||
|
||||
### 3.1 Architecture Design
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Presentation (apps/website) │
|
||||
│ - MediaService returns API endpoints │
|
||||
│ - Components use <img src="/api/media/..."> │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ HTTP Layer (apps/api) │
|
||||
│ - MediaController (HTTP only) │
|
||||
│ - Routes: /api/media/avatar/:id, /api/media/teams/:id/logo│
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Domain Layer (core/media/domain) │
|
||||
│ - MediaGenerationService (business logic) │
|
||||
│ - MediaGenerator (port) │
|
||||
│ - MediaRepository (port) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Infrastructure (adapters/media) │
|
||||
│ - FakerMediaGenerator (seeds) │
|
||||
│ - InMemoryMediaRepository (seeds) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 Implementation Steps
|
||||
|
||||
#### **Step 1: Create Domain Service**
|
||||
```typescript
|
||||
// core/media/domain/services/MediaGenerationService.ts
|
||||
export class MediaGenerationService {
|
||||
generateDriverAvatar(driverId: string): string {
|
||||
faker.seed(this.hashCode(driverId));
|
||||
// ... SVG generation logic
|
||||
}
|
||||
|
||||
generateTeamLogo(teamId: string): string {
|
||||
faker.seed(this.hashCode(teamId));
|
||||
// ... SVG generation logic
|
||||
}
|
||||
|
||||
private hashCode(str: string): number {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
return Math.abs(hash);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **Step 2: Clean Controller**
|
||||
```typescript
|
||||
// apps/api/src/domain/media/MediaController.ts
|
||||
@Get('avatar/:driverId')
|
||||
async getDriverAvatar(
|
||||
@Param('driverId') driverId: string,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const svg = this.mediaGenerationService.generateDriverAvatar(driverId);
|
||||
|
||||
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||
res.status(HttpStatus.OK).send(svg);
|
||||
}
|
||||
|
||||
// ❌ REMOVE duplicate endpoints
|
||||
// ❌ REMOVE generateDriverAvatarSVG() method
|
||||
// ❌ REMOVE generateTeamLogoSVG() method
|
||||
// ❌ REMOVE hashCode() method
|
||||
```
|
||||
|
||||
#### **Step 3: Fix Seeds**
|
||||
```typescript
|
||||
// adapters/bootstrap/SeedRacingData.ts
|
||||
private async seedMediaAssets(seed: any): Promise<void> {
|
||||
// ✅ ALL media uses API endpoints
|
||||
for (const driver of seed.drivers) {
|
||||
const avatarUrl = `/api/media/avatar/${driver.id}`;
|
||||
const mediaRepo = this.seedDeps.mediaRepository as any;
|
||||
if (mediaRepo.setDriverAvatar) {
|
||||
mediaRepo.setDriverAvatar(driver.id, avatarUrl);
|
||||
}
|
||||
}
|
||||
|
||||
for (const team of seed.teams) {
|
||||
const logoUrl = `/api/media/teams/${team.id}/logo`;
|
||||
const mediaRepo = this.seedDeps.mediaRepository as any;
|
||||
if (mediaRepo.setTeamLogo) {
|
||||
mediaRepo.setTeamLogo(team.id, logoUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Remove static file logic
|
||||
// ✅ Remove getDriverAvatarUrl() method
|
||||
}
|
||||
```
|
||||
|
||||
#### **Step 4: Clean Repository**
|
||||
```typescript
|
||||
// adapters/racing/persistence/media/InMemoryMediaRepository.ts
|
||||
export class InMemoryMediaRepository implements IMediaRepository {
|
||||
private driverAvatars = new Map<string, string>();
|
||||
private teamLogos = new Map<string, string>();
|
||||
|
||||
setDriverAvatar(driverId: string, apiUrl: string): void {
|
||||
this.driverAvatars.set(driverId, apiUrl);
|
||||
}
|
||||
|
||||
setTeamLogo(teamId: string, apiUrl: string): void {
|
||||
this.teamLogos.set(teamId, apiUrl);
|
||||
}
|
||||
|
||||
// ✅ Remove unused methods
|
||||
// ❌ remove getTrackImage, getCategoryIcon, getSponsorLogo
|
||||
}
|
||||
```
|
||||
|
||||
#### **Step 5: Remove Static Files**
|
||||
```bash
|
||||
rm -f apps/website/public/images/avatars/male-default-avatar.jpg
|
||||
rm -f apps/website/public/images/avatars/female-default-avatar.jpeg
|
||||
rm -f apps/website/public/images/avatars/neutral-default-avatar.jpeg
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 4: File Changes Summary
|
||||
|
||||
### Files to Modify
|
||||
|
||||
1. **apps/api/src/domain/media/MediaController.ts**
|
||||
- Remove SVG generation logic (lines 214-330)
|
||||
- Remove duplicate endpoints
|
||||
- Call domain service
|
||||
|
||||
2. **adapters/bootstrap/SeedRacingData.ts**
|
||||
- Use API endpoints for ALL media
|
||||
- Remove static file logic
|
||||
- Remove getDriverAvatarUrl()
|
||||
|
||||
3. **adapters/racing/persistence/media/InMemoryMediaRepository.ts**
|
||||
- Simplify to store only API endpoints
|
||||
- Remove unused methods
|
||||
|
||||
4. **core/media/domain/services/MediaGenerationService.ts** (NEW)
|
||||
- Contains all SVG generation logic
|
||||
- Uses Faker for seeds
|
||||
|
||||
### Files to Delete
|
||||
|
||||
1. **apps/website/public/images/avatars/** (all static files)
|
||||
|
||||
### Files to Keep (Already Correct)
|
||||
|
||||
1. **apps/website/lib/services/media/MediaService.ts** ✅
|
||||
2. **apps/website/next.config.mjs** ✅
|
||||
3. **apps/api/src/domain/media/MediaController.ts** (cleaned version)
|
||||
|
||||
---
|
||||
|
||||
## Part 5: Implementation Timeline
|
||||
|
||||
### Day 1: Controller Cleanup
|
||||
- Create MediaGenerationService
|
||||
- Move SVG logic from controller
|
||||
- Remove duplicate endpoints
|
||||
|
||||
### Day 2: Seed Fixes
|
||||
- Update SeedRacingData to use API endpoints
|
||||
- Remove static file logic
|
||||
- Clean up InMemoryMediaRepository
|
||||
|
||||
### Day 3: Testing & Cleanup
|
||||
- Remove static files
|
||||
- TypeScript compilation
|
||||
- Integration tests
|
||||
|
||||
**Total: 3 days** (vs 10+ days in your plans)
|
||||
|
||||
---
|
||||
|
||||
## Part 6: Success Criteria
|
||||
|
||||
After implementation:
|
||||
|
||||
1. ✅ **No static files** in `apps/website/public/images/avatars/`
|
||||
2. ✅ **No SVG generation** in `MediaController`
|
||||
3. ✅ **Consistent seed approach** - all API endpoints
|
||||
4. ✅ **Clean repository** - single responsibility
|
||||
5. ✅ **All TypeScript errors resolved**
|
||||
6. ✅ **Website displays all media correctly**
|
||||
7. ✅ **Same ID always produces same SVG** (via Faker seeding)
|
||||
|
||||
---
|
||||
|
||||
## Part 7: Comparison Table
|
||||
|
||||
| Aspect | Your Plans | Correct Solution |
|
||||
|--------|------------|------------------|
|
||||
| **Database Changes** | Remove logoUrl (❌ don't exist) | No changes needed |
|
||||
| **Next.js Config** | Add rewrites (❌ already exists) | Keep existing |
|
||||
| **API Endpoints** | Add 8 endpoints (❌ duplicates) | Keep 4 existing |
|
||||
| **SVG Generation** | Use cases + generators (❌ over-engineered) | Domain service |
|
||||
| **Seeds** | Hybrid approach (❌ confusing) | All API endpoints |
|
||||
| **Architecture** | Complex layers (❌ over-engineered) | Clean & simple |
|
||||
| **Static Files** | Keep some (❌ inconsistent) | Remove all |
|
||||
| **Implementation Time** | 10+ days | 3 days |
|
||||
|
||||
---
|
||||
|
||||
## Part 8: Why Your Plans Fail
|
||||
|
||||
1. **Lack of Analysis:** Written without understanding current state
|
||||
2. **Over-Engineering:** Adding layers where simple solutions suffice
|
||||
3. **Inconsistent:** Claims to solve problems that don't exist
|
||||
4. **Violates Own Rules:** Criticizes URL storage, then proposes it
|
||||
5. **Duplicates Work:** Implements what already exists
|
||||
|
||||
---
|
||||
|
||||
## Part 9: The Bottom Line
|
||||
|
||||
### Your Plans Are:
|
||||
- ❌ Based on incorrect assumptions
|
||||
- ❌ Solving non-existent problems
|
||||
- ❌ Ignoring real problems
|
||||
- ❌ Over-engineering simple solutions
|
||||
- ❌ Duplicating existing work
|
||||
|
||||
### Your Plans Should Be:
|
||||
- ✅ Based on actual current state
|
||||
- ✅ Solving real problems only
|
||||
- ✅ Simple and direct
|
||||
- ✅ Clean architecture without complexity
|
||||
- ✅ Implementable in 3 days
|
||||
|
||||
---
|
||||
|
||||
## Part 10: Recommendation
|
||||
|
||||
**DO NOT implement your current plans.**
|
||||
|
||||
Instead, implement this streamlined solution that:
|
||||
1. Fixes actual problems (controller logic, inconsistent seeds, mixed repository)
|
||||
2. Ignores imaginary problems (database schema, rewrites, SVG implementation)
|
||||
3. Uses simple, direct architecture
|
||||
4. Can be completed in 3 days
|
||||
|
||||
**Your plans have good intentions but are fundamentally flawed.** This document provides the correct path forward.
|
||||
|
||||
---
|
||||
|
||||
## Files Created
|
||||
|
||||
- `plans/MEDIA_ARCHITECTURE_COMPLETE_ANALYSIS.md` (this file)
|
||||
- `plans/MEDIA_ARCHITECTURE_ANALYSIS.md` (detailed analysis)
|
||||
- `plans/MEDIA_STREAMLINED_SOLUTION.md` (corrected approach)
|
||||
- `plans/CHALLENGE_TO_YOUR_PLANS.md` (point-by-point challenge)
|
||||
|
||||
**Recommendation:** Keep only this file and delete the others.
|
||||
221
plans/media-avatars-team-league-logos-streamlining-plan.md
Normal file
221
plans/media-avatars-team-league-logos-streamlining-plan.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# Media Streamlining Plan: Driver Avatars, Team Logos, League Logos
|
||||
|
||||
## Goal
|
||||
Create one clean, conflict-free way to represent and deliver:
|
||||
|
||||
- Driver avatars (defaults now; user uploads later)
|
||||
- Team logos (seeded)
|
||||
- League logos (seeded)
|
||||
|
||||
So that:
|
||||
|
||||
- Seeding never produces conflicting behavior across environments.
|
||||
- The UI never has to guess whether a value is a file path, an API route, a generated asset, or an uploaded asset.
|
||||
- There is exactly one place that decides the final image URL for each entity.
|
||||
|
||||
## What exists today (inventory, by responsibility)
|
||||
|
||||
### Driver avatars
|
||||
|
||||
Where they surface:
|
||||
|
||||
- Driver lists, driver leaderboards, race entry lists, dashboard summaries, and social/friend UI elements.
|
||||
- API payloads sometimes include an avatar URL; other times the client constructs a URL from the driver id.
|
||||
- There are multiple fallback strategies: empty string, null, or client-side default image.
|
||||
|
||||
Where they come from:
|
||||
|
||||
- A “default avatar set” of three files exists in the website public assets.
|
||||
- There is also a server route that can generate an avatar image for a driver id.
|
||||
- Some parts of the system treat driver avatar as a user-uploadable media setting.
|
||||
|
||||
Observed problems:
|
||||
|
||||
- Mixed meaning of the avatar field: sometimes it is an absolute URL, sometimes a relative path, sometimes a server route string.
|
||||
- Multiple fallbacks implemented in multiple places leads to inconsistent UI and hard-to-debug “missing image” bugs.
|
||||
- Multiple “demo/fake” avatar generators exist, creating divergent behavior between environments.
|
||||
|
||||
### Team logos
|
||||
|
||||
Where they surface:
|
||||
|
||||
- Team cards, team leaderboards, recruiting/featured team sections.
|
||||
- Sometimes the UI uses `logoUrl` from the API payload; other times it falls back to a server route based on id.
|
||||
|
||||
Where they come from:
|
||||
|
||||
- A server route can generate a team logo image for a team id.
|
||||
- Seed logic also “pre-seeds” team logos by writing route strings into an in-memory store.
|
||||
|
||||
Observed problems:
|
||||
|
||||
- Team “logoUrl” may be an actual URL, or it may be a placeholder, or it may be a server route string stored as data.
|
||||
- Storing route strings as if they were media values creates conflicts when routes change.
|
||||
- In some persistence modes the “seeded logo store” is not truly persisted, so bootstrapping may re-trigger reseeding or create inconsistent results.
|
||||
|
||||
### League logos
|
||||
|
||||
Where they surface:
|
||||
|
||||
- League cards, league headers, league pages.
|
||||
- UI tends to call a client-side helper that builds a league-logo URL from id.
|
||||
|
||||
Where they come from:
|
||||
|
||||
- A server route can generate a league logo image for a league id.
|
||||
- Seed logic also “pre-seeds” league logos by writing route strings into an in-memory store.
|
||||
|
||||
Observed problems:
|
||||
|
||||
- Same class of conflicts as team logos.
|
||||
- There is no single authoritative rule for when a league has a “real” logo versus a generated one.
|
||||
|
||||
## Proposed streamlined model (single canonical representation)
|
||||
|
||||
### Canonical concept: Media Reference (not a URL)
|
||||
Instead of treating stored values as “final URLs”, define a single canonical *media reference* for each entity image.
|
||||
|
||||
Media reference types:
|
||||
|
||||
- **System default**: a fixed asset shipped with the website (driver defaults: male/female/neutral).
|
||||
- **Generated**: deterministically generated from an entity id and a seeded pseudo-random source (team/league logos).
|
||||
- **Uploaded**: a user-uploaded object managed by the media subsystem.
|
||||
- **None**: intentionally unset.
|
||||
|
||||
Key rule: **only one layer resolves media references into URLs**.
|
||||
|
||||
### URL resolution responsibilities
|
||||
|
||||
- **Backend** resolves *references* into *final URLs* for API payloads.
|
||||
- **Backend** also serves the image bytes for generated assets and uploaded assets.
|
||||
- **Frontend** treats received URLs as ready-to-render and does not invent additional fallbacks beyond a single last-resort placeholder.
|
||||
|
||||
## Seeding strategy (cleanest route)
|
||||
|
||||
### Teams and leagues: seeded via faker, but without storing URLs
|
||||
|
||||
Requirement: “seed team and league logos via faker”.
|
||||
|
||||
Clean approach:
|
||||
|
||||
- During seed, assign each team/league a **Generated** media reference.
|
||||
- The generator uses faker with a seed derived from the entity id to produce a deterministic “logo identity” (colors, initials, shapes, etc.).
|
||||
- The stored value is **only the reference** (type + seed key), not a route string and not a URL.
|
||||
- When the UI needs to show the logo, it either receives a resolved URL in the API payload or uses a single, standardized media URL builder.
|
||||
|
||||
Benefits:
|
||||
|
||||
- Deterministic results: same team id always yields the same logo.
|
||||
- No conflicts when URLs/routes change.
|
||||
- No need to persist binary files for seeded logos.
|
||||
|
||||
### Drivers: seeded from the 3 default avatar images
|
||||
|
||||
Requirement: “seed driver logos from these defaults” and later “normally these would be user uploads”.
|
||||
|
||||
Clean approach:
|
||||
|
||||
- During seed, assign each driver a **System default** media reference selecting one of:
|
||||
- male-default-avatar
|
||||
- female-default-avatar
|
||||
- neutral-default-avatar
|
||||
- Selection is deterministic (based on driver id) so reseeding does not change faces randomly.
|
||||
- Later, if a user uploads an avatar, the reference switches to **Uploaded** and overrides the default.
|
||||
|
||||
Benefits:
|
||||
|
||||
- No dependency on generated avatars for baseline.
|
||||
- No ambiguous meaning of the avatar field.
|
||||
|
||||
## Contract rules (what the UI can rely on)
|
||||
|
||||
### Field semantics
|
||||
|
||||
- Every API payload that includes a driver/team/league image should provide a **single resolved URL field** for that image.
|
||||
- Resolved URL is either:
|
||||
- a valid URL string the UI can render immediately, or
|
||||
- null (meaning: show a generic placeholder).
|
||||
- Never send empty strings.
|
||||
- Never send “sometimes relative file path, sometimes server route” style mixed values.
|
||||
|
||||
### Fallback rules
|
||||
|
||||
- The backend resolver must guarantee a valid URL whenever it can (system default or generated).
|
||||
- The frontend uses exactly one last-resort placeholder if it receives null.
|
||||
- No per-component bespoke fallbacks.
|
||||
|
||||
## Streamlining work items (what changes where)
|
||||
|
||||
### 1) Centralize media reference resolution
|
||||
|
||||
Create one “media resolver” concept used by:
|
||||
|
||||
- API payload assembly for all places that include avatars/logos.
|
||||
- Image-serving routes for generated assets and uploaded assets.
|
||||
|
||||
This resolver is the only place that knows:
|
||||
|
||||
- how to map media references to a concrete image URL
|
||||
- what the fallback is when no uploaded media exists
|
||||
|
||||
### 2) Stop storing server route strings as data
|
||||
|
||||
Remove the pattern where seed logic writes values like “/api/media/.../logo” into an in-memory media store.
|
||||
|
||||
Replace it with:
|
||||
|
||||
- stored media references (generated/system-default/uploaded)
|
||||
- consistent URL resolution at response time
|
||||
|
||||
### 3) Normalize route prefixes and caching behavior
|
||||
|
||||
- Choose one public URL shape for these images and apply it universally.
|
||||
- Add consistent cache headers for generated assets (deterministic) so the browser and CDN can cache safely.
|
||||
|
||||
### 4) Align frontend consumption
|
||||
|
||||
- Ensure the UI always prefers the resolved URL from the API payload.
|
||||
- Where the UI only has an id (e.g. very lightweight list items), use a single shared “URL builder” instead of ad-hoc string concatenation.
|
||||
- Remove duplicate “if missing then fallback to …” logic sprinkled across components.
|
||||
|
||||
### 5) Align tests and demo fakes
|
||||
|
||||
- Eliminate competing fake avatar/logo generators.
|
||||
- Ensure all test fixtures use the same deterministic rules as seed and runtime generation.
|
||||
- Ensure snapshot/contract tests treat empty string as invalid and expect null instead.
|
||||
|
||||
### 6) Make bootstrapping/reseeding conflict-proof
|
||||
|
||||
- Reseed decision should be based on durable data correctness (presence of required entities) rather than transient “in-memory media store” state.
|
||||
- Ensure “missing avatar/logo” checks are aligned with the new media reference model.
|
||||
|
||||
### 7) Migration and cleanup
|
||||
|
||||
- Define how existing seeded databases are handled:
|
||||
- either a one-time cleanup that rewrites old stored values into the new reference model, or
|
||||
- a documented wipe-and-reseed path for local/dev environments.
|
||||
- Ensure the migration path eliminates stored route strings.
|
||||
|
||||
## Mermaid: Target flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
UI[Website UI] --> API[API payloads include resolved image URLs]
|
||||
API --> RES[Media resolver]
|
||||
RES --> UP[Uploaded media storage]
|
||||
RES --> GEN[Deterministic generator]
|
||||
RES --> DEF[System default assets]
|
||||
GEN --> IMG[Image response with cache headers]
|
||||
UP --> IMG
|
||||
DEF --> UI
|
||||
IMG --> UI
|
||||
```
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Driver avatars always render with one of the three defaults unless an upload exists.
|
||||
- Team and league logos always render deterministically in dev/test seed, without persisting URLs.
|
||||
- No API payload returns empty string for avatar/logo.
|
||||
- No UI component constructs its own bespoke fallback logic.
|
||||
- No bootstrapping loop caused by “missing media” when media is generated or defaults are available.
|
||||
|
||||
364
plans/media-streamlining-debug-fix-plan.md
Normal file
364
plans/media-streamlining-debug-fix-plan.md
Normal file
@@ -0,0 +1,364 @@
|
||||
# Media streamlining debug fix plan
|
||||
|
||||
Goal: make media rendering (avatars, team logos, league logos) deterministic, debuggable, and boring. Remove misleading stubs from runtime, converge on one URL shape (`/media/...`) end-to-end, and add observability so broken images can be diagnosed in minutes.
|
||||
|
||||
Non-goals:
|
||||
- No CDN rollout (we still design for it).
|
||||
- No “AI generation” pipeline. Keep existing deterministic SVG generation in [`MediaGenerationService`](core/media/domain/services/MediaGenerationService.ts:9).
|
||||
|
||||
## 1) Current state (facts from code)
|
||||
|
||||
### Backend (API)
|
||||
|
||||
- The canonical HTTP routes exist in [`MediaController`](apps/api/src/domain/media/MediaController.ts:25):
|
||||
- Team logo: `GET /media/teams/:teamId/logo` (SVG) [`getTeamLogo()`](apps/api/src/domain/media/MediaController.ts:72)
|
||||
- League logo: `GET /media/leagues/:leagueId/logo` (SVG) [`getLeagueLogo()`](apps/api/src/domain/media/MediaController.ts:83)
|
||||
- Driver avatar: `GET /media/avatar/:driverId` (SVG) [`getDriverAvatar()`](apps/api/src/domain/media/MediaController.ts:111)
|
||||
- Default: `GET /media/default/:variant` (PNG placeholder) [`getDefaultMedia()`](apps/api/src/domain/media/MediaController.ts:125)
|
||||
|
||||
- Seeding sets `logoRef` for teams/leagues to “generated” references:
|
||||
- Team: [`RacingTeamFactory.createTeams()`](adapters/bootstrap/racing/RacingTeamFactory.ts:26) sets [`MediaReference.generated()`](core/domain/media/MediaReference.ts:114) via line [`logoRef: MediaReference.generated('team', teamId)`](adapters/bootstrap/racing/RacingTeamFactory.ts:51)
|
||||
- League: [`RacingLeagueFactory.create()`](adapters/bootstrap/racing/RacingLeagueFactory.ts:14) sets [`logoRef: MediaReference.generated('league', leagueData.id)`](adapters/bootstrap/racing/RacingLeagueFactory.ts:403)
|
||||
|
||||
- Presenters resolve `MediaReference` → URL string via a `MediaResolverPort`:
|
||||
- Teams list: [`AllTeamsPresenter.present()`](apps/api/src/domain/team/presenters/AllTeamsPresenter.ts:25) resolves via [`this.mediaResolver.resolve()`](apps/api/src/domain/team/presenters/AllTeamsPresenter.ts:45)
|
||||
|
||||
### Frontend (Website)
|
||||
|
||||
- The landing page cards render with Next `Image`:
|
||||
- Team card: [`TeamCard`](apps/website/components/teams/TeamCard.tsx:67) uses [`<Image src={logoUrl}>`](apps/website/components/teams/TeamCard.tsx:101)
|
||||
|
||||
- Some UI code uses an internal URL builder that does not match the API’s route shapes:
|
||||
- [`getMediaUrl()`](apps/website/lib/utilities/media.ts:11) builds `/media/generated/team-logo/:id` etc.
|
||||
- Example usage: [`TeamLadderRow`](apps/website/components/teams/TeamLadderRow.tsx:18) uses [`getMediaUrl('team-logo', teamId)`](apps/website/components/teams/TeamLadderRow.tsx:29)
|
||||
|
||||
- Next.js image config currently allows localhost and allows SVG:
|
||||
- [`next.config.mjs`](apps/website/next.config.mjs:1) includes `remotePatterns` for `localhost:3001` and `dangerouslyAllowSVG: true`.
|
||||
|
||||
## 2) Suspected root causes (ranked)
|
||||
|
||||
### A. URL shape mismatch in Website fallback builder
|
||||
|
||||
The Website builder [`getMediaUrl()`](apps/website/lib/utilities/media.ts:11) generates paths like:
|
||||
|
||||
- `/media/generated/team-logo/:id`
|
||||
|
||||
But the API serves:
|
||||
|
||||
- `/media/teams/:id/logo` or `/media/generated/team/:id` (generic endpoint)
|
||||
|
||||
Result: 404s for any page that uses [`getMediaUrl()`](apps/website/lib/utilities/media.ts:11) instead of `logoUrl` returned by the API.
|
||||
|
||||
### B. Runtime accidentally uses the in-memory resolver (misleading)
|
||||
|
||||
In API Team DI, the runtime media resolver is currently the stub [`InMemoryMediaResolverAdapter`](adapters/media/MediaResolverInMemoryAdapter.ts:60) via [`TeamProviders`](apps/api/src/domain/team/TeamProviders.ts:28).
|
||||
|
||||
That adapter is explicitly described as “fake URLs” and has URL shapes that don’t match the API controller, e.g. system-default returns `${base}/default/${ref.variant}` in [`InMemoryMediaResolverAdapter.resolve()`](adapters/media/MediaResolverInMemoryAdapter.ts:80).
|
||||
|
||||
Even if team logos are “generated” and map to `/media/teams/:id/logo`, this is an architectural footgun:
|
||||
- It makes it easy for other entity presenters (drivers/leagues/etc.) to emit non-existent URLs.
|
||||
- It undermines confidence when debugging.
|
||||
|
||||
### C. Next.js `Image` error symptoms
|
||||
|
||||
You reported: Next.js `Image` errors about remote host not configured and or SVG blocked.
|
||||
|
||||
Given [`next.config.mjs`](apps/website/next.config.mjs:12) appears to allow `localhost:3001` and enables SVG, this suggests at least one of:
|
||||
- The actual `src` host differs (e.g. `127.0.0.1`, `api:3000`, or another hostname).
|
||||
- The `src` is not a valid URL string at runtime (empty string, malformed).
|
||||
- A stale container is running with older config.
|
||||
|
||||
The plan below makes `src` always same-origin to the Website (relative `/media/...`), eliminating this entire class of errors.
|
||||
|
||||
## 3) Target architecture (strict, minimal, easy-to-reason)
|
||||
|
||||
### 3.1 Invariants (rules)
|
||||
|
||||
1) Canonical media URLs are always *paths* starting with `/media/`.
|
||||
2) API DTO fields like `team.logoUrl` are either:
|
||||
- `null`, or
|
||||
- a path `/media/...` (never absolute URLs, never empty string).
|
||||
3) The Website renders media using *only*:
|
||||
- DTO-provided `/media/...` URLs, or
|
||||
- a single shared Website builder that produces `/media/...` URLs matching the API routes.
|
||||
4) The Website never needs to know `http://localhost:3001`.
|
||||
5) All runtime resolution uses exactly one resolver implementation (no stubs).
|
||||
|
||||
### 3.2 One canonical path schema
|
||||
|
||||
Canonical HTTP paths (served by API, fetched by browser via Website proxy rewrite):
|
||||
|
||||
- Team logo SVG: `/media/teams/{teamId}/logo`
|
||||
- League logo SVG: `/media/leagues/{leagueId}/logo`
|
||||
- Driver avatar SVG: `/media/avatar/{driverId}`
|
||||
- Defaults (PNG): `/media/default/{variant}`
|
||||
- Uploaded: `/media/uploaded/{mediaId}`
|
||||
|
||||
`/media/generated/:type/:id` can remain, but should become an internal alias only (not returned by resolvers/presenters).
|
||||
|
||||
### 3.3 Single resolver for the whole API
|
||||
|
||||
- Runtime resolver: [`MediaResolverAdapter`](adapters/media/MediaResolverAdapter.ts:53) using the concrete sub-resolvers:
|
||||
- [`DefaultMediaResolverAdapter`](adapters/media/resolvers/DefaultMediaResolverAdapter.ts:34)
|
||||
- [`GeneratedMediaResolverAdapter`](adapters/media/resolvers/GeneratedMediaResolverAdapter.ts:35)
|
||||
- [`UploadedMediaResolverAdapter`](adapters/media/resolvers/UploadedMediaResolverAdapter.ts:37)
|
||||
|
||||
Resolver output must be *path-only*:
|
||||
- For any `MediaReference`, `resolve()` returns `/media/...` or `null`.
|
||||
- No `baseUrl` parameter is needed for DTOs.
|
||||
|
||||
Rationale: once URLs are path-only, the Website can proxy them and Next `Image` becomes deterministic.
|
||||
|
||||
### 3.4 Proper storage abstraction (core port) + adapter implementation
|
||||
|
||||
This is required to align with Clean Architecture rules in [`DATA_FLOW.md`](docs/architecture/DATA_FLOW.md:1) and avoid runtime stubs.
|
||||
|
||||
#### 3.4.1 Core (ports + use-cases)
|
||||
|
||||
We already have a core port [`MediaStoragePort`](apps/api/src/domain/media/MediaProviders.ts:9) used by the media use-cases (upload/delete). The plan is to make it real and remove mock usage in runtime.
|
||||
|
||||
Target responsibilities:
|
||||
|
||||
- Core Application port (interface): `MediaStoragePort`
|
||||
- `uploadMedia(file, metadata) -> { success, url?, filename?, storageKey?, contentType? }`
|
||||
- `deleteMedia(storageKey) -> void`
|
||||
- (optional but recommended) `getReadStream(storageKey) -> stream` or `getBytes(storageKey) -> Buffer`
|
||||
|
||||
- Core Domain entity (or value object): `Media` should reference a storage identifier (e.g. `storageKey`) and `contentType`.
|
||||
- The domain does not store absolute URLs.
|
||||
- The resolver + controller decide how a `storageKey` becomes `/media/uploaded/{id}`.
|
||||
|
||||
#### 3.4.2 Adapters (file storage)
|
||||
|
||||
Add a concrete adapter: `FileSystemMediaStorageAdapter` under `adapters/`.
|
||||
|
||||
Implementation rules:
|
||||
|
||||
- Store files under a single base directory (configured via env):
|
||||
- `GRIDPILOT_MEDIA_STORAGE_DIR=/data/media` (container path)
|
||||
- Use deterministic, collision-resistant keys:
|
||||
- `uploaded/{mediaId}/{originalFilename}` or `uploaded/{mediaId}` (single-file per mediaId)
|
||||
- Enforce content-type allowlist for images (at minimum `image/png`, `image/jpeg`, `image/svg+xml`).
|
||||
- Never return public absolute URLs from the adapter. Return `storageKey` only.
|
||||
|
||||
Docker alignment:
|
||||
|
||||
- Add a named volume mounted into `api` container for persisted dev media.
|
||||
|
||||
#### 3.4.3 API serving route for uploaded media
|
||||
|
||||
The API endpoint [`GET /media/uploaded/:mediaId`](apps/api/src/domain/media/MediaController.ts:169) is currently a stub.
|
||||
|
||||
Target:
|
||||
|
||||
- Look up `Media` by `mediaId` in `IMediaRepository`.
|
||||
- Read bytes/stream from `MediaStoragePort` using `storageKey`.
|
||||
- Set headers:
|
||||
- `Content-Type: <stored contentType>`
|
||||
- `Cache-Control: public, max-age=31536000, immutable` (if content-addressed) OR `max-age=3600` (if mutable)
|
||||
- Return 404 if missing.
|
||||
|
||||
This makes “uploaded” a first-class, debuggable path in the same `/media/...` scheme.
|
||||
|
||||
## 4) End-to-end trace (pseudocode)
|
||||
|
||||
This is the required mental model for debugging.
|
||||
|
||||
### 4.1 Seed → DB
|
||||
|
||||
```text
|
||||
teamId = seedId(team-1)
|
||||
team.logoRef = MediaReference.generated(team, teamId)
|
||||
persist team.logoRef as JSON
|
||||
```
|
||||
|
||||
### 4.2 API Use Case → Presenter → DTO
|
||||
|
||||
```text
|
||||
usecase GetAllTeamsUseCase
|
||||
loads Team entities
|
||||
returns { teams: [{ id, name, logoRef, logoUrl: null, ... }] }
|
||||
|
||||
presenter AllTeamsPresenter
|
||||
for each team:
|
||||
ref = MediaReference.fromJSON(team.logoRef)
|
||||
dto.logoUrl = MediaResolver.resolve(ref)
|
||||
=> /media/teams/{teamId}/logo
|
||||
response JSON contains logoUrl string or null
|
||||
```
|
||||
|
||||
### 4.3 Website → React component → img src
|
||||
|
||||
```text
|
||||
LandingService.getHomeDiscovery
|
||||
calls GET {apiBaseUrl}/teams/all
|
||||
creates TeamCardViewModel with dto.logoUrl
|
||||
|
||||
TeamCard
|
||||
Image src = team.logoUrl
|
||||
(src is relative /media/...)
|
||||
```
|
||||
|
||||
### 4.4 Browser fetch → Website rewrite → API bytes
|
||||
|
||||
```text
|
||||
browser GET http://localhost:3000/media/teams/{id}/logo
|
||||
Next rewrite proxies to http://api:3000/media/teams/{id}/logo
|
||||
API returns image/svg+xml bytes
|
||||
browser renders
|
||||
```
|
||||
|
||||
## 5) Debuggability improvements (must-have)
|
||||
|
||||
### 5.1 Add a debug resolve endpoint in API
|
||||
|
||||
Add `GET /media/debug/resolve` in [`MediaController`](apps/api/src/domain/media/MediaController.ts:25).
|
||||
|
||||
Input options:
|
||||
- Query param `ref` as base64url JSON of `MediaReferenceProps`.
|
||||
- Or explicit query params: `type`, `variant`, `avatarVariant`, `generationRequestId`, `mediaId`.
|
||||
|
||||
Output JSON:
|
||||
- `ref`: the parsed ref (as JSON)
|
||||
- `refHash`: same as [`MediaReference.hash()`](core/domain/media/MediaReference.ts:271)
|
||||
- `resolvedPath`: `/media/...` or null
|
||||
- `resolver`: which branch handled it (default or generated or uploaded or none)
|
||||
- `notes`: validation warnings (e.g. generationRequestId format)
|
||||
|
||||
This endpoint exists to debug resolvers without hitting entity APIs.
|
||||
|
||||
### 5.2 Structured logs
|
||||
|
||||
Add structured logs on each media request:
|
||||
|
||||
- In [`MediaController.getTeamLogo()`](apps/api/src/domain/media/MediaController.ts:72) and similar endpoints:
|
||||
- log: route, entityId, cache-control chosen
|
||||
- log: svg length, deterministic seed used
|
||||
|
||||
- In resolver:
|
||||
- log: `refHash`, resolved path, branch
|
||||
|
||||
### 5.3 Curl recipes (copy/paste)
|
||||
|
||||
Teams API returning logoUrl:
|
||||
|
||||
```bash
|
||||
curl -sS http://localhost:3001/teams/all | jq '.teams[0] | {id, name, logoUrl}'
|
||||
```
|
||||
|
||||
Team logo bytes:
|
||||
|
||||
```bash
|
||||
TEAM_ID=$(curl -sS http://localhost:3001/teams/all | jq -r '.teams[0].id')
|
||||
curl -i http://localhost:3001/media/teams/$TEAM_ID/logo | sed -n '1,20p'
|
||||
```
|
||||
|
||||
Expected:
|
||||
- `HTTP/1.1 200 OK`
|
||||
- `content-type: image/svg+xml`
|
||||
|
||||
Website proxy path (after rewrite is added):
|
||||
|
||||
```bash
|
||||
curl -i http://localhost:3000/media/teams/$TEAM_ID/logo | sed -n '1,20p'
|
||||
```
|
||||
|
||||
## 6) Concrete fixes (file-by-file)
|
||||
|
||||
### 6.1 Remove misleading runtime stubs
|
||||
|
||||
1) Stop using [`InMemoryMediaResolverAdapter`](adapters/media/MediaResolverInMemoryAdapter.ts:60) in API runtime providers.
|
||||
- Replace in [`TeamProviders`](apps/api/src/domain/team/TeamProviders.ts:28) (and similar providers in drivers/leagues if present) with the real [`MediaResolverAdapter`](adapters/media/MediaResolverAdapter.ts:53).
|
||||
|
||||
2) Ensure any “in-memory” resolver remains test-only:
|
||||
- Keep it referenced only in unit tests, not in app modules/providers.
|
||||
|
||||
### 6.2 Make resolver output path-only
|
||||
|
||||
Update [`MediaResolverAdapter.resolve()`](adapters/media/MediaResolverAdapter.ts:81) and sub-resolvers to return `/media/...` paths:
|
||||
|
||||
- [`DefaultMediaResolverAdapter.resolve()`](adapters/media/resolvers/DefaultMediaResolverAdapter.ts:44): `/media/default/...`
|
||||
- [`GeneratedMediaResolverAdapter.resolve()`](adapters/media/resolvers/GeneratedMediaResolverAdapter.ts:45):
|
||||
- team → `/media/teams/{id}/logo`
|
||||
- league → `/media/leagues/{id}/logo`
|
||||
- driver → `/media/avatar/{id}`
|
||||
- [`UploadedMediaResolverAdapter.resolve()`](adapters/media/resolvers/UploadedMediaResolverAdapter.ts:47): `/media/uploaded/{mediaId}`
|
||||
|
||||
Remove all “baseUrl” joining logic from resolvers.
|
||||
|
||||
### 6.3 Website must stop inventing wrong media URLs
|
||||
|
||||
1) Replace or delete [`getMediaUrl()`](apps/website/lib/utilities/media.ts:11).
|
||||
- Either remove it entirely, or redefine it to output canonical `/media/...` paths.
|
||||
|
||||
2) Update all call sites found via:
|
||||
- [`TeamLadderRow`](apps/website/components/teams/TeamLadderRow.tsx:18)
|
||||
- [`LeagueHeader`](apps/website/components/leagues/LeagueHeader.tsx:1)
|
||||
- [`FriendPill`](apps/website/components/social/FriendPill.tsx:1)
|
||||
- [`apps/website/app/teams/[id]/page.tsx`](apps/website/app/teams/[id]/page.tsx:195)
|
||||
- [`apps/website/app/profile/page.tsx`](apps/website/app/profile/page.tsx:409)
|
||||
|
||||
to use either:
|
||||
- DTO-provided URLs, or
|
||||
- a single canonical builder aligned with API routes.
|
||||
|
||||
### 6.4 Add Website rewrite for `/media/*`
|
||||
|
||||
Extend [`next.config.mjs rewrites()`](apps/website/next.config.mjs:47) to also proxy `/media/:path*` to `http://api:3000/media/:path*` in dev.
|
||||
|
||||
This yields same-origin image URLs for the browser:
|
||||
- `src=/media/...` always.
|
||||
|
||||
### 6.5 Tests
|
||||
|
||||
1) Unit tests for resolver mapping:
|
||||
- Add tests around [`GeneratedMediaResolverAdapter.resolve()`](adapters/media/resolvers/GeneratedMediaResolverAdapter.ts:45) to ensure `team-<id>` → `/media/teams/<id>/logo`.
|
||||
|
||||
2) API presenter contract test:
|
||||
- Verify `logoUrl` is `null` or starts with `/media/` in [`AllTeamsPresenter`](apps/api/src/domain/team/presenters/AllTeamsPresenter.ts:8).
|
||||
|
||||
3) E2E Playwright image smoke:
|
||||
- Add a test that loads the landing page, finds at least one team logo `<img>`, and asserts the image request returns 200.
|
||||
- Use existing Playwright config files like [`playwright.website.config.ts`](playwright.website.config.ts:1).
|
||||
|
||||
4) Media upload + serve integration test:
|
||||
|
||||
- Upload an image via `POST /media/upload`.
|
||||
- Verify response includes a `mediaId` and DTO uses `/media/uploaded/{mediaId}` (path-only rule).
|
||||
- Fetch `/media/uploaded/{mediaId}` and assert status 200 + correct `Content-Type`.
|
||||
|
||||
## 7) Mermaid flow (new architecture)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Bootstrap seed sets MediaReference] --> B[DB stores logoRef JSON]
|
||||
B --> C[API use case returns logoRef]
|
||||
C --> D[Presenter resolves ref to media path]
|
||||
D --> E[DTO logoUrl is slash media path]
|
||||
E --> F[Website renders Image src slash media path]
|
||||
F --> G[Next rewrite proxies to API media route]
|
||||
G --> H[MediaController returns SVG or PNG bytes]
|
||||
```
|
||||
|
||||
## 8) TDD execution order (implementation guidance)
|
||||
|
||||
1) Add unit tests for canonical resolver mapping (generated/system-default/uploaded).
|
||||
2) Change resolver implementations to return path-only and make tests pass.
|
||||
3) Update API providers to use real resolver everywhere (remove runtime usage of in-memory resolver).
|
||||
4) Add `/media/:path*` rewrite in Website.
|
||||
5) Replace Website `getMediaUrl` and all call sites.
|
||||
6) Add API debug endpoint and structured logs.
|
||||
7) Replace mock `MediaStoragePort` with real filesystem adapter, wire env + volume.
|
||||
8) Implement uploaded media serving endpoint (remove stub), add integration test.
|
||||
9) Add Playwright test verifying image loads.
|
||||
|
||||
## 9) Acceptance criteria
|
||||
|
||||
1) `GET http://localhost:3001/teams/all` returns `logoUrl` values that are either `null` or begin with `/media/`.
|
||||
2) `GET http://localhost:3000/media/teams/{id}/logo` returns 200 with `image/svg+xml`.
|
||||
3) No Next `Image` remote-host/SVG errors in dev for logos.
|
||||
4) Playwright test passes: at least one image request returns 200 on a real page.
|
||||
|
||||
5) Upload flow works end-to-end:
|
||||
- `POST /media/upload` stores a file via filesystem adapter.
|
||||
- `GET /media/uploaded/{mediaId}` returns the stored bytes with correct headers.
|
||||
52
plans/team-logos-force-reseed-fix-plan.md
Normal file
52
plans/team-logos-force-reseed-fix-plan.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Team logos wrong after force reseed: TDD plan
|
||||
|
||||
## Observed runtime failure
|
||||
- Browser console: Internal server error.
|
||||
- Team images still not shown.
|
||||
|
||||
## Hypothesis
|
||||
The current force-reseed cleanup in [`SeedRacingData.clearExistingRacingData()`](adapters/bootstrap/SeedRacingData.ts:479) still leaves inconsistent DB state (or errors during deletion), so seeding or the teams endpoint fails.
|
||||
|
||||
## Approach (TDD)
|
||||
|
||||
### 1) Reproduce via HTTP
|
||||
- Use curl against `/teams/all` and capture:
|
||||
- HTTP status
|
||||
- response body
|
||||
- server logs correlating to request
|
||||
|
||||
### 2) Capture docker logs around bootstrap
|
||||
- Start/ensure dev stack is up via [`docker-compose.dev.yml`](docker-compose.dev.yml:1).
|
||||
- Collect:
|
||||
- API logs from container startup through seeding
|
||||
- DB logs if errors/constraint violations occur
|
||||
|
||||
### 3) Add regression test (make it fail first)
|
||||
- Add an API e2e/integration test that:
|
||||
1. Runs with postgres persistence and force reseed on.
|
||||
2. Calls `/teams/all`.
|
||||
3. Asserts every team returns a generated logo URL:
|
||||
- `logoUrl` matches `/media/teams/{id}/logo` (or resolver output for generated ref)
|
||||
- must not be `/media/default/logo.png`
|
||||
|
||||
Candidate location: existing media module tests under [`apps/api/src/domain/media`](apps/api/src/domain/media/MediaModule.test.ts:1) or a new teams controller test.
|
||||
|
||||
### 4) Diagnose failing test
|
||||
- If 500:
|
||||
- Identify stack trace and failing query.
|
||||
- Confirm whether failures occur during reseed or request handling.
|
||||
- If 200 but wrong URLs:
|
||||
- Query DB for `racing_teams.logoRef` and verify it is generated.
|
||||
|
||||
### 5) Minimal fix
|
||||
Prefer fixing cleanup by:
|
||||
- Deleting in correct order to satisfy FKs.
|
||||
- Ensuring `racing_teams` + dependent tables are cleared.
|
||||
- Avoiding partial deletes that can leave orphaned rows.
|
||||
|
||||
### 6) Verification
|
||||
- Run eslint, tsc, tests.
|
||||
- Manual verification:
|
||||
- `curl http://localhost:3001/teams/all` returns `logoUrl: /media/teams/{id}/logo`.
|
||||
- Requesting one returned URL is `200 OK`.
|
||||
|
||||
Reference in New Issue
Block a user