Files
GearBox/.planning/phases/17-object-storage/17-03-PLAN.md

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
17-01
17-02
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
scripts/migrate-images-to-minio.ts
true
IMG-02
IMG-03
truths artifacts key_links
Client components display images using presigned URLs from API responses, not /uploads/ paths
Migration script uploads all files from uploads/ directory to MinIO bucket
Migration script preserves original filenames as MinIO object keys
No /uploads/ path references remain in client code
path provides contains
scripts/migrate-images-to-minio.ts One-time migration of local images to MinIO uploadImage
path provides
src/client/components/ItemCard.tsx Item display using imageUrl from API
path provides
src/client/components/CandidateCard.tsx Candidate display using imageUrl from API
from to via pattern
src/client/components/ItemCard.tsx API response imageUrl prop instead of /uploads/ path imageUrl
from to via pattern
scripts/migrate-images-to-minio.ts src/server/services/storage.service.ts uploadImage() for each file uploadImage
Update all client components to use presigned URLs and create the image migration script.

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.

  1. src/client/components/ItemCard.tsx:

    • Update props interface: add imageUrl: string | null alongside existing imageFilename
    • Replace src={/uploads/${imageFilename}} with src={imageUrl} (which is already a full URL)
    • Guard: render image only when imageUrl is truthy (not when imageFilename is truthy)
  2. src/client/components/CandidateCard.tsx:

    • Update props interface: add imageUrl: string | null
    • Replace src={/uploads/${imageFilename}} with src={imageUrl}
    • Guard on imageUrl instead of imageFilename
  3. src/client/components/CandidateListItem.tsx:

    • Props already receive full candidate object. The candidate object now has imageUrl.
    • Replace src={/uploads/${candidate.imageFilename}} with src={candidate.imageUrl}
    • Guard on candidate.imageUrl instead of candidate.imageFilename
  4. src/client/components/ComparisonTable.tsx:

    • Update candidate type in props: add imageUrl: string | null
    • Replace src={/uploads/${c.imageFilename}} with src={c.imageUrl}
    • Guard on c.imageUrl
  5. src/client/components/ImageUpload.tsx:

    • This shows a preview of the uploaded image. Currently uses /uploads/${value} where value is the imageFilename.
    • After upload, the API returns { filename }. The preview needs a URL.
    • Two approaches: (a) accept an imageUrl prop for existing images, or (b) construct a temporary preview from the File object.
    • RECOMMENDED: Accept both value (imageFilename) and imageUrl (presigned URL) as props. For NEW uploads, use URL.createObjectURL(file) for instant preview. For EXISTING images (editing), use the imageUrl prop passed from the parent.
    • Update: add imageUrl?: string | null prop. For preview display: if imageUrl is provided use it, else if a local file was just selected use createObjectURL.
    • Parents (ItemForm, CandidateForm) will pass imageUrl from the record data.
  6. src/client/routes/setups/$setupId.tsx:

    • This renders setup items with images. The setup API response now includes imageUrl on each item.
    • Replace any imageFilename={item.imageFilename} prop passing with imageUrl={item.imageUrl} (and keep imageFilename for form data if needed).
  7. Update parent form components that pass imageFilename to ImageUpload:

    • src/client/components/ItemForm.tsx: Pass imageUrl from item record to ImageUpload component
    • src/client/components/CandidateForm.tsx: Pass imageUrl from 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.

Task 2: Create image migration script scripts/migrate-images-to-minio.ts - src/server/services/storage.service.ts (uploadImage API from Plan 01) - .planning/phases/17-object-storage/17-RESEARCH.md (migration section, D-11, D-12) Per D-11, D-12: Create `scripts/migrate-images-to-minio.ts` — a one-time migration script.
  1. Create the script:

    • Import uploadImage from ../src/server/services/storage.service
    • Import readdir and readFile from node:fs/promises
    • Import join, extname from node:path
    • Define UPLOADS_DIR = "uploads"
    • Content type mapping: .jpg/.jpeg -> image/jpeg, .png -> image/png, .webp -> image/webp
  2. 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. Call await 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/"
  3. 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.
- `grep -rn "/uploads/" src/client/` returns NO matches - `grep -rn "/uploads/" src/server/` returns NO matches (verified in Plan 02) - `scripts/migrate-images-to-minio.ts` exists and contains uploadImage, readdir - `bun run lint` passes on all modified files

<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>
After completion, create `.planning/phases/17-object-storage/17-03-SUMMARY.md`