222 lines
11 KiB
Markdown
222 lines
11 KiB
Markdown
---
|
|
phase: 17-object-storage
|
|
plan: 03
|
|
type: execute
|
|
wave: 3
|
|
depends_on: ["17-01", "17-02"]
|
|
files_modified:
|
|
- 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
|
|
autonomous: true
|
|
requirements: [IMG-02, IMG-03]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "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"
|
|
artifacts:
|
|
- path: "scripts/migrate-images-to-minio.ts"
|
|
provides: "One-time migration of local images to MinIO"
|
|
contains: "uploadImage"
|
|
- path: "src/client/components/ItemCard.tsx"
|
|
provides: "Item display using imageUrl from API"
|
|
- path: "src/client/components/CandidateCard.tsx"
|
|
provides: "Candidate display using imageUrl from API"
|
|
key_links:
|
|
- from: "src/client/components/ItemCard.tsx"
|
|
to: "API response"
|
|
via: "imageUrl prop instead of /uploads/ path"
|
|
pattern: "imageUrl"
|
|
- from: "scripts/migrate-images-to-minio.ts"
|
|
to: "src/server/services/storage.service.ts"
|
|
via: "uploadImage() for each file"
|
|
pattern: "uploadImage"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<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
|
|
|
|
<interfaces>
|
|
<!-- From Plan 01 - storage.service.ts exports used by migration script -->
|
|
From src/server/services/storage.service.ts:
|
|
```typescript
|
|
export async function uploadImage(buffer: Buffer | ArrayBuffer, filename: string, contentType: string): Promise<void>;
|
|
```
|
|
|
|
<!-- API responses will now include imageUrl alongside imageFilename (from Plan 02) -->
|
|
<!-- Components receive props like: { imageFilename: string | null, imageUrl: string | null } -->
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Update client components to use imageUrl from API responses</name>
|
|
<files>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</files>
|
|
<read_first>
|
|
- 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)
|
|
</read_first>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>grep -rn "/uploads/" src/client/ | grep -v node_modules && echo "FAIL: still has /uploads/ references" || echo "PASS: no /uploads/ references in client"</automated>
|
|
</verify>
|
|
<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>
|
|
<done>All 6 client components and the setup route use imageUrl from API responses. Zero /uploads/ path references remain in client code.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Create image migration script</name>
|
|
<files>scripts/migrate-images-to-minio.ts</files>
|
|
<read_first>
|
|
- 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)
|
|
</read_first>
|
|
<action>
|
|
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)
|
|
</action>
|
|
<verify>
|
|
<automated>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"</automated>
|
|
</verify>
|
|
<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>
|
|
<done>Migration script reads all images from uploads/, uploads each to MinIO preserving filenames, logs progress, does not delete originals.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `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
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/17-object-storage/17-03-SUMMARY.md`
|
|
</output>
|