Files
gridpilot.gg/plans/media-streamlining-debug-fix-plan.md
2025-12-31 15:39:28 +01:00

16 KiB
Raw Blame History

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.

1) Current state (facts from code)

Backend (API)

Frontend (Website)

2) Suspected root causes (ranked)

A. URL shape mismatch in Website fallback builder

The Website builder getMediaUrl() 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() 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 via TeamProviders.

That adapter is explicitly described as “fake URLs” and has URL shapes that dont match the API controller, e.g. system-default returns ${base}/default/${ref.variant} in InMemoryMediaResolverAdapter.resolve().

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 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

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 and avoid runtime stubs.

3.4.1 Core (ports + use-cases)

We already have a core port MediaStoragePort 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 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

teamId = seedId(team-1)
team.logoRef = MediaReference.generated(team, teamId)
persist team.logoRef as JSON

4.2 API Use Case → Presenter → DTO

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

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

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.

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()
  • 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() 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:

curl -sS http://localhost:3001/teams/all | jq '.teams[0] | {id, name, logoUrl}'

Team logo bytes:

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):

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 in API runtime providers.

  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() and sub-resolvers to return /media/... paths:

Remove all “baseUrl” joining logic from resolvers.

6.3 Website must stop inventing wrong media URLs

  1. Replace or delete getMediaUrl().

    • Either remove it entirely, or redefine it to output canonical /media/... paths.
  2. Update all call sites found via:

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() 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:
  1. API presenter contract test:
  1. 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.
  1. 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)

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.