Skip to content

feat(public): serve sponsor/track logos as cacheable URLs, downscale uploads - #15

Merged
balebbae merged 3 commits into
mainfrom
devin/1790714619-public-logo-urls
Sep 29, 2026
Merged

balebbae merged 3 commits into
mainfrom
devin/1790714619-public-logo-urls

Conversation

@devin-ai-integration

Copy link
Copy Markdown

Summary

Public consumers (zero-day site) currently receive every sponsor/track logo as inline base64 in the list JSON and end up inlining data: URIs into HTML — no browser/CDN caching, no resizing, multi-MB page HTML. This PR keeps HARP as the content source but moves logo bytes out of the JSON.

Backend — cmd/api/public.go, cmd/api/api.go

  • GET /v1/public/sponsors and GET /v1/public/tracks now return PublicSponsor / PublicTrack DTOs: logo_data + logo_content_type are replaced by
    logo_url: "{APP_URL}/v1/public/sponsors/{id}/logo?v={updated_at unix}"   // "" when no logo
    
    Admin endpoints (/v1/sponsors, /v1/tracks) are unchanged and still return logo_data.
  • New keyless GET /v1/public/sponsors/{sponsorID}/logo and GET /v1/public/tracks/{trackID}/logo (mounted outside APIKeyMiddleware so the URL works in <img> / next/image). They decode the stored base64 and respond with the stored Content-Type, ETag: "{updated_at unix}", X-Content-Type-Options: nosniff, and honor If-None-Match → 304.
  • Cache policy: Cache-Control: public, max-age=31536000, immutable only when ?v matches the current version; otherwise public, max-age=300. A new upload bumps updated_at, so the list emits a new URL and old ones can be cached forever without a stale-logo risk.
  • 404 for unknown id or no logo; 500 if stored base64 is corrupt.
  • Swagger regenerated (docs/docs.go).

Portal — client/portal/src/shared/lib/logo-image.ts

  • prepareLogoForUpload(file) decodes via createImageBitmap (fallback <img>), draws to a canvas capped at 512px on the longest side, encodes WebP q=0.85 (PNG fallback if the browser can't encode WebP), keeps the original bytes if re-encoding wouldn't shrink an already-small image. Transparency preserved; animated GIFs flatten to the first frame.
  • Sponsor/track stores' uploadLogo now go through it (covers both form dialog and table quick-upload). The file-picker limit was raised to a 10MB source cap (MAX_LOGO_SOURCE_BYTES); the backend decoded caps (1MB / 750KB) are still enforced post-compression in the store with a toast.
  • Sponsors' duplicated inline constants moved into pages/admin/sponsors/constants.ts to mirror tracks.

Consumer follow-up: zero-day-2026 needs to switch from logo_data to logo_url (companion PR).

Tests: cmd/api/public_logos_test.go covers list shape (no logo_data, correct logo_url), keyless image serving, immutable vs. non-immutable cache headers, ETag/304, 404s, 500. go test ./cmd/api/, go vet, gofmt, portal lint/format:check/build all pass.

Link to Devin session: https://app.devin.ai/sessions/919a1d99cd6c4f7d9452edb8a04fe394
Open in Devin Desktop: https://app.devin.ai/desktop/session/919a1d99cd6c4f7d9452edb8a04fe394?variant=devin
Requested by: @balebbae

…le uploads

- add keyless GET /v1/public/{sponsors,tracks}/{id}/logo returning raw
  image bytes with ETag and versioned immutable Cache-Control
- public list responses now return logo_url instead of inline
  logo_data/logo_content_type
- portal downscales logos to <=512px WebP on upload
@devin-ai-integration

Copy link
Copy Markdown
Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@balebbae
balebbae merged commit 58e7327 into main Sep 29, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant