11 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 17-object-storage | 03 | execute | 3 |
|
|
true |
|
|
Purpose: Complete the client-side transition from /uploads/ paths to presigned URLs, and provide a one-time migration script for existing images. After this plan, the full stack uses MinIO for image storage. Output: All client image references use imageUrl from API. Migration script ready to run.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/17-object-storage/17-CONTEXT.md @.planning/phases/17-object-storage/17-RESEARCH.md @.planning/phases/17-object-storage/17-01-SUMMARY.md@src/client/components/ImageUpload.tsx @src/client/components/ItemCard.tsx @src/client/components/CandidateCard.tsx @src/client/components/CandidateListItem.tsx @src/client/components/ComparisonTable.tsx @src/client/routes/setups/$setupId.tsx
From src/server/services/storage.service.ts: ```typescript export async function uploadImage(buffer: Buffer | ArrayBuffer, filename: string, contentType: string): Promise; ``` Task 1: Update client components to use imageUrl from API responses src/client/components/ImageUpload.tsx, src/client/components/ItemCard.tsx, src/client/components/CandidateCard.tsx, src/client/components/CandidateListItem.tsx, src/client/components/ComparisonTable.tsx, src/client/routes/setups/$setupId.tsx - src/client/components/ImageUpload.tsx (current /uploads/ usage) - src/client/components/ItemCard.tsx (current /uploads/ usage) - src/client/components/CandidateCard.tsx (current /uploads/ usage) - src/client/components/CandidateListItem.tsx (current /uploads/ usage) - src/client/components/ComparisonTable.tsx (current /uploads/ usage) - src/client/routes/setups/$setupId.tsx (current imageFilename usage) Per D-10: Client components use the presigned URL directly from API responses.The API (refactored in Plan 02) now returns imageUrl: string | null alongside imageFilename: string | null on every item/candidate record. Components should use imageUrl for display.
-
src/client/components/ItemCard.tsx:- Update props interface: add
imageUrl: string | nullalongside existingimageFilename - Replace
src={/uploads/${imageFilename}}withsrc={imageUrl}(which is already a full URL) - Guard: render image only when
imageUrlis truthy (not whenimageFilenameis truthy)
- Update props interface: add
-
src/client/components/CandidateCard.tsx:- Update props interface: add
imageUrl: string | null - Replace
src={/uploads/${imageFilename}}withsrc={imageUrl} - Guard on
imageUrlinstead ofimageFilename
- Update props interface: add
-
src/client/components/CandidateListItem.tsx:- Props already receive full candidate object. The candidate object now has
imageUrl. - Replace
src={/uploads/${candidate.imageFilename}}withsrc={candidate.imageUrl} - Guard on
candidate.imageUrlinstead ofcandidate.imageFilename
- Props already receive full candidate object. The candidate object now has
-
src/client/components/ComparisonTable.tsx:- Update candidate type in props: add
imageUrl: string | null - Replace
src={/uploads/${c.imageFilename}}withsrc={c.imageUrl} - Guard on
c.imageUrl
- Update candidate type in props: add
-
src/client/components/ImageUpload.tsx:- This shows a preview of the uploaded image. Currently uses
/uploads/${value}wherevalueis the imageFilename. - After upload, the API returns
{ filename }. The preview needs a URL. - Two approaches: (a) accept an
imageUrlprop for existing images, or (b) construct a temporary preview from the File object. - RECOMMENDED: Accept both
value(imageFilename) andimageUrl(presigned URL) as props. For NEW uploads, useURL.createObjectURL(file)for instant preview. For EXISTING images (editing), use theimageUrlprop passed from the parent. - Update: add
imageUrl?: string | nullprop. For preview display: if imageUrl is provided use it, else if a local file was just selected use createObjectURL. - Parents (ItemForm, CandidateForm) will pass
imageUrlfrom the record data.
- This shows a preview of the uploaded image. Currently uses
-
src/client/routes/setups/$setupId.tsx:- This renders setup items with images. The setup API response now includes
imageUrlon each item. - Replace any
imageFilename={item.imageFilename}prop passing withimageUrl={item.imageUrl}(and keep imageFilename for form data if needed).
- This renders setup items with images. The setup API response now includes
-
Update parent form components that pass imageFilename to ImageUpload:
src/client/components/ItemForm.tsx: PassimageUrlfrom item record to ImageUpload componentsrc/client/components/CandidateForm.tsx: PassimageUrlfrom candidate record to ImageUpload component
IMPORTANT: Do NOT break the upload flow. When a new image is uploaded via POST /api/images, the response still returns { filename }. The imageUrl will be available on subsequent GET requests. For immediate preview after upload, use a local object URL or re-fetch.
grep -rn "/uploads/" src/client/ | grep -v node_modules && echo "FAIL: still has /uploads/ references" || echo "PASS: no /uploads/ references in client"
<acceptance_criteria>
- grep -rn "/uploads/" src/client/ returns NO matches
- grep -q "imageUrl" src/client/components/ItemCard.tsx
- grep -q "imageUrl" src/client/components/CandidateCard.tsx
- grep -q "imageUrl" src/client/components/CandidateListItem.tsx
- grep -q "imageUrl" src/client/components/ComparisonTable.tsx
- grep -q "imageUrl" src/client/components/ImageUpload.tsx
</acceptance_criteria>
All 6 client components and the setup route use imageUrl from API responses. Zero /uploads/ path references remain in client code.
-
Create the script:
- Import
uploadImagefrom../src/server/services/storage.service - Import
readdirandreadFilefromnode:fs/promises - Import
join,extnamefromnode:path - Define UPLOADS_DIR = "uploads"
- Content type mapping:
.jpg/.jpeg->image/jpeg,.png->image/png,.webp->image/webp
- Import
-
Main function:
- Check if uploads/ directory exists. If not, log "No uploads directory found. Nothing to migrate." and exit 0.
- Read all files from uploads/ directory (readdir)
- Filter to only image files (.jpg, .jpeg, .png, .webp)
- Log: "Found {N} images to migrate"
- For each file:
a. Read file contents with
Bun.file(path).arrayBuffer()b. Determine content type from extension c. Callawait uploadImage(Buffer.from(buffer), filename, contentType)— filename is just the basename, per D-12 (no path changes) d. Log success: "Migrated: {filename}" e. On error: log error and continue (don't abort entire migration for one failure) - Track success/failure counts
- Log summary: "Migration complete: {success}/{total} files migrated, {failed} failures"
- If any failures, log: "Re-run this script to retry failed uploads"
- Do NOT delete original files per discretion recommendation. Log: "Original files preserved in uploads/. Delete manually after verifying: rm -rf uploads/"
-
Run with:
bun run scripts/migrate-images-to-minio.ts- Script should be self-contained, executable directly with bun
- Requires S3 env vars to be set (S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY) test -f scripts/migrate-images-to-minio.ts && grep -q "uploadImage" scripts/migrate-images-to-minio.ts && grep -q "readdir" scripts/migrate-images-to-minio.ts && echo "PASS" <acceptance_criteria>
- test -f scripts/migrate-images-to-minio.ts
- grep -q "uploadImage" scripts/migrate-images-to-minio.ts
- grep -q "readdir" scripts/migrate-images-to-minio.ts
- grep -q "image/jpeg" scripts/migrate-images-to-minio.ts
- grep -q "image/png" scripts/migrate-images-to-minio.ts
- grep -q "uploads" scripts/migrate-images-to-minio.ts
- Script does NOT contain "unlink" or "rm" calls (per discretion: do not auto-delete) </acceptance_criteria> Migration script reads all images from uploads/, uploads each to MinIO preserving filenames, logs progress, does not delete originals.
<success_criteria>
- All client components use imageUrl from API responses (zero /uploads/ references)
- Migration script exists and handles: directory check, file reading, S3 upload, error handling, progress logging
- Migration script preserves original filenames as object keys (per D-12)
- Migration script does not auto-delete originals </success_criteria>