API responses now include file_url and thumbnail_url fields. When S3_PUBLIC_BASE_URL is configured, these point to the CDN domain; when unset, they fall back to the existing API proxy paths so local dev requires no additional setup. UI updated to use response URL fields directly instead of constructing proxy URLs client-side. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8.8 KiB
Tasks: CDN Image Serving
Input: Design documents from specs/014-r2-cdn-serving/
Prerequisites: plan.md ✅, spec.md ✅, research.md ✅, contracts/image-response.md ✅, quickstart.md ✅
Tests: Unit tests for URL construction logic; integration tests asserting file_url and thumbnail_url in all image responses. Tests accompany each implementation task per §5.1.
Organization: Phase 1 adds the config value (foundational — blocks everything). Phase 2 implements US1 (CDN URL serving in API + UI consumption). Phase 3 verifies US2 (local dev fallback). Polish runs the full suite and manual end-to-end check.
Format: [ID] [P?] [Story] Description
- [P]: Can run in parallel (different files, no dependencies)
- [Story]: Which user story this task belongs to
Phase 1: Foundational (Config)
Goal: Add s3_public_base_url to config and .env.example. All US1 and US2 tasks depend on this.
⚠️ CRITICAL: No user story work can begin until this phase is complete.
- T001 Add
s3_public_base_url: str | None = Noneto theSettingsclass inapi/app/config.py(afterapi_base_url); addS3_PUBLIC_BASE_URL=with comment "# CDN base URL for serving images (e.g. https://cdn.example.com). Leave empty in local dev to use API proxy fallback." to.env.exampleafter theAPI_BASE_URLline
Checkpoint: Config in place — user story work can begin.
Phase 2: User Story 1 — Images Load Directly from CDN (Priority: P1) 🎯 MVP
Goal: API returns file_url and thumbnail_url in all image responses; UI uses those fields to render images rather than constructing proxy URLs client-side.
Independent Test: With S3_PUBLIC_BASE_URL=https://cdn.reactbin.juggalol.com set, call GET /api/v1/images and confirm each item has file_url starting with https://cdn.reactbin.juggalol.com/ and thumbnail_url starting with https://cdn.reactbin.juggalol.com/ (or null). Open the library page in a browser and confirm image requests go to the CDN domain in the network panel.
-
T002 [US1] Write unit tests in
api/tests/unit/test_url_construction.pycovering four cases: (1) CDN base set, image has thumbnail —file_urlandthumbnail_urlare CDN URLs; (2) CDN base set, image has no thumbnail —thumbnail_urlis None; (3) CDN base not set, image has thumbnail —file_urlis/api/v1/images/{id}/fileandthumbnail_urlis/api/v1/images/{id}/thumbnail; (4) CDN base not set, no thumbnail —thumbnail_urlis None. Test the trailing-slash normalisation case (CDN base with trailing slash produces no double-slash). Import and call_image_to_dictdirectly with a mockImageobject. -
T003 [US1] Update
_image_to_dictinapi/app/routers/images.py: addcdn_base: str | None = Nonekeyword parameter; compute_base = cdn_base.rstrip("/") if cdn_base else None; setfile_url = f"{_base}/{image.storage_key}" if _base else f"/api/v1/images/{image.id}/file"; setthumbnail_url = (f"{_base}/{image.thumbnail_key}" if _base else f"/api/v1/images/{image.id}/thumbnail") if image.thumbnail_key else None; add"file_url": file_urland"thumbnail_url": thumbnail_urlto the returned dict. Runmake test-unitand confirm T002 tests pass. -
T004 [US1] Update every
_image_to_dict(...)call site inapi/app/routers/images.py: at the top of each endpoint function that calls_image_to_dict, add_cdn_base = get_settings().s3_public_base_url(importget_settingsis already present); passcdn_base=_cdn_baseto every_image_to_dictcall in that endpoint. Affected endpoints:upload_image,list_images,get_image,patch_image_tags. Confirmget_settings()is called once per endpoint, not once per image in a loop (forlist_images, call it before the list comprehension). -
T005 [US1] Update integration tests: in
api/tests/integration/test_upload.py, add assertions after existing response checks that"file_url"is present in the response body and starts with/api/v1/images/(since no CDN is configured in test env); add the same assertion for"thumbnail_url"intest_upload_returns_thumbnail_key; add assertion thatthumbnail_urlis None in the test that expectsthumbnail_keyto be None. Runmake test-integrationand confirm all pass. -
T006 [P] [US1] Update
ui/src/app/services/image.service.ts: addfile_url: stringandthumbnail_url: string | nullto theImageRecordinterface; remove thegetFileUrl(id: string): stringmethod; remove thegetThumbnailUrl(id: string): stringmethod. -
T007 [P] [US1] Update
ui/src/app/library/library.component.ts: replace[src]="imageService.getThumbnailUrl(img.id)"(line 77) with[src]="img.thumbnail_url ?? img.file_url"— fall back tofile_urlwhen thumbnail is absent (FR-009); updateui/src/app/library/library.component.spec.tsto addfile_urlandthumbnail_urlto any mockImageRecordobjects and remove any references togetThumbnailUrl(). -
T008 [P] [US1] Update
ui/src/app/detail/detail.component.ts: replace[src]="imageService.getFileUrl(image.id)"(line 52) with[src]="image.file_url"; updateui/src/app/detail/detail.component.spec.tsto addfile_urlandthumbnail_urlto any mockImageRecordobjects and remove any references togetFileUrl(). -
T009 [US1] Update
ui/src/app/services/image.service.spec.ts: addfile_urlandthumbnail_urlfields to any mockImageRecordobjects used in tests; remove any test cases that testgetFileUrl()orgetThumbnailUrl()(these methods no longer exist). Run UI tests and confirm they pass.
Checkpoint: US1 complete. API returns CDN URLs when configured; UI uses response fields to render images.
Phase 3: User Story 2 — Local Development Works Without CDN (Priority: P2)
Goal: Confirm that with no S3_PUBLIC_BASE_URL configured, file_url and thumbnail_url fall back to API proxy paths and images load correctly in local dev.
Independent Test: Run make test-unit && make test-integration with no S3_PUBLIC_BASE_URL set (the default). Confirm all tests pass and that file_url values in integration test responses begin with /api/v1/images/.
- T010 [US2] Verify US2: run
make test-unitand confirm the url-construction unit tests for the "no CDN base" case (T002 cases 3 and 4) pass; runmake test-integrationand confirm the updated upload tests (T005) pass — they already assert relative proxy paths since the test environment has noS3_PUBLIC_BASE_URL. Confirmdocker compose upstarts cleanly and images load in the browser via the proxy paths with no console errors.
Checkpoint: US2 verified. Local development requires no additional configuration.
Phase 4: Polish & Cross-Cutting Concerns
-
T011 [P] Run
ruff check api/app/routers/images.py api/app/config.pyand fix any lint issues; runruff format --checkand format if needed. -
T012 Run end-to-end verification per
specs/014-r2-cdn-serving/quickstart.md: in production withS3_PUBLIC_BASE_URLset, callGET /api/v1/imagesand confirmfile_urlandthumbnail_urlbegin withhttps://cdn.reactbin.juggalol.com/; open the library page in a browser and confirm image requests in the network panel go tocdn.reactbin.juggalol.com, not/api/.
Dependencies & Execution Order
- T001 must complete before any other task
- T002 before T003 (tests before implementation — unit test first)
- T003 before T004 (update helper before call sites)
- T004 before T005 (implementation before integration tests)
- T006, T007, T008 can run in parallel after T001 (different files)
- T009 after T006 (spec depends on updated interface)
- T010 after T003–T009 (verification requires full implementation)
- T011 after T003–T004 (lint the changed files)
- T012 last (manual end-to-end)
Execution Order Summary
Step 1: T001 (foundational: config)
Step 2: T002 (US1: unit tests first)
Step 3: T003 (US1: implement _image_to_dict)
Step 4: T004 ∥ T006 ∥ T007 ∥ T008 (US1: call sites + UI in parallel)
Step 5: T005 ∥ T009 (US1: integration tests + service spec)
Step 6: T010 (US2: verify local dev fallback)
Step 7: T011 (polish: lint)
Step 8: T012 (polish: manual end-to-end)
Implementation Strategy
MVP (US1 only — CDN URLs in API + UI)
- T001 — config
- T002–T005 — API implementation and tests
- T006–T009 — UI updates
- STOP and VALIDATE:
make test-unit && make test-integration, check browser network panel
Incremental Delivery
- T001–T005 (API only) → deploy → verify CDN URLs appear in API responses
- T006–T009 (UI) → deploy → verify browser fetches images from CDN
- T010 (local dev verification) → confirm fallback intact
- T011–T012 (polish + end-to-end) → ship