---
phase: 17-object-storage
plan: 02
type: execute
wave: 2
depends_on: ["17-01"]
files_modified:
  - src/server/services/image.service.ts
  - src/server/routes/images.ts
  - src/server/routes/items.ts
  - src/server/routes/threads.ts
  - src/server/routes/setups.ts
  - src/server/index.ts
  - src/server/mcp/tools/items.ts
  - src/server/mcp/tools/threads.ts
  - src/server/mcp/tools/images.ts
  - tests/services/image.service.test.ts
  - tests/routes/images.test.ts
autonomous: true
requirements: [IMG-01, IMG-03]

must_haves:
  truths:
    - "Image upload via POST /api/images stores file in MinIO, not local filesystem"
    - "Image upload via POST /api/images/from-url stores fetched image in MinIO"
    - "Deleting an item or candidate with an image deletes the image from MinIO"
    - "API responses include imageUrl field with presigned URLs for items and candidates"
    - "Static file serving for /uploads/* is removed from the server"
    - "MCP tools use storage service for image operations"
  artifacts:
    - path: "src/server/services/image.service.ts"
      provides: "URL-based image fetch using storage service"
    - path: "src/server/routes/images.ts"
      provides: "Image upload routes using storage service"
    - path: "src/server/index.ts"
      provides: "Server entry without /uploads/* static serving"
  key_links:
    - from: "src/server/routes/images.ts"
      to: "src/server/services/storage.service.ts"
      via: "uploadImage() call"
      pattern: "import.*uploadImage.*storage"
    - from: "src/server/routes/items.ts"
      to: "src/server/services/storage.service.ts"
      via: "deleteImage() for cleanup, withImageUrl/withImageUrls for responses"
      pattern: "import.*deleteImage.*storage"
    - from: "src/server/routes/threads.ts"
      to: "src/server/services/storage.service.ts"
      via: "deleteImage() and withImageUrl for candidate images"
      pattern: "import.*storage"
---

<objective>
Refactor all server-side image handling to use the S3 storage service instead of local filesystem.

Purpose: Replace every Bun.write, unlink, and /uploads/ reference on the server with storage service calls. Enrich API responses with presigned URLs so clients can fetch images directly from MinIO.
Output: All server image operations go through storage.service.ts. API responses include imageUrl field. Static /uploads/* serving removed.
</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/server/services/image.service.ts
@src/server/routes/images.ts
@src/server/routes/items.ts
@src/server/routes/threads.ts
@src/server/index.ts
@src/server/mcp/tools/images.ts
@src/server/mcp/tools/items.ts
@src/server/mcp/tools/threads.ts

<interfaces>
<!-- From Plan 01 - storage.service.ts exports -->
From src/server/services/storage.service.ts:
```typescript
export async function uploadImage(buffer: Buffer | ArrayBuffer, filename: string, contentType: string): Promise<void>;
export async function deleteImage(filename: string): Promise<void>;
export async function getImageUrl(filename: string): Promise<string>;
export async function withImageUrl<T extends { imageFilename: string | null }>(record: T): Promise<T & { imageUrl: string | null }>;
export async function withImageUrls<T extends { imageFilename: string | null }>(records: T[]): Promise<(T & { imageUrl: string | null })[]>;
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Refactor image service and image routes to use storage service</name>
  <files>src/server/services/image.service.ts, src/server/routes/images.ts, tests/services/image.service.test.ts, tests/routes/images.test.ts</files>
  <read_first>
    - src/server/services/storage.service.ts (created in Plan 01 — the storage API)
    - src/server/services/image.service.ts (current local fs logic to replace)
    - src/server/routes/images.ts (current upload routes)
    - tests/services/image.service.test.ts (existing tests to update)
    - tests/routes/images.test.ts (existing tests to update)
  </read_first>
  <action>
1. Refactor `src/server/services/image.service.ts` per D-07:
   - Remove `mkdir` and `Bun.write` imports
   - Remove `uploadsDir` parameter from `fetchImageFromUrl`
   - Import `uploadImage` from `./storage.service`
   - After fetching and validating the image buffer, call `await uploadImage(Buffer.from(buffer), filename, contentType)` instead of `Bun.write`
   - Keep ALL validation logic unchanged (URL parsing, protocol check, content type, size limits, timeout)
   - Keep UUID filename generation unchanged per D-12

2. Refactor `src/server/routes/images.ts` per D-06:
   - Remove `mkdir`, `join`, `Bun.write` usage
   - Import `uploadImage` from `../services/storage.service`
   - In POST `/` handler: after validation, call `await uploadImage(Buffer.from(buffer), filename, file.type)` instead of mkdir + Bun.write
   - In POST `/from-url` handler: no changes needed (delegates to image.service.ts which is already refactored)
   - Keep content type validation and size validation unchanged
   - Keep filename generation pattern unchanged

3. Update `tests/services/image.service.test.ts`:
   - Mock the storage.service module using `mock.module` so `uploadImage` is a mock function
   - Update assertions: verify uploadImage was called with correct buffer, filename, contentType instead of checking Bun.write
   - Remove any assertions about local filesystem writes

4. Update `tests/routes/images.test.ts`:
   - Mock storage.service module
   - Update assertions to verify uploadImage calls instead of filesystem writes
   - Test that POST /api/images returns { filename } with 201 status
   - Test that POST /api/images/from-url returns { filename, sourceUrl } with 201 status
  </action>
  <verify>
    <automated>bun test tests/services/image.service.test.ts tests/routes/images.test.ts</automated>
  </verify>
  <acceptance_criteria>
    - grep -q "import.*uploadImage.*storage" src/server/services/image.service.ts
    - grep -qv "Bun.write" src/server/services/image.service.ts
    - grep -qv "mkdir" src/server/services/image.service.ts
    - grep -q "import.*uploadImage.*storage" src/server/routes/images.ts
    - grep -qv "Bun.write" src/server/routes/images.ts
    - grep -qv "mkdir" src/server/routes/images.ts
    - bun test tests/services/image.service.test.ts passes
    - bun test tests/routes/images.test.ts passes
  </acceptance_criteria>
  <done>Image service and routes use storage service for uploads. No local filesystem writes remain. Tests pass with mocked storage.</done>
</task>

<task type="auto">
  <name>Task 2: Refactor item/thread/setup routes, remove static serving, update MCP tools</name>
  <files>src/server/routes/items.ts, src/server/routes/threads.ts, src/server/routes/setups.ts, src/server/index.ts, src/server/mcp/tools/items.ts, src/server/mcp/tools/threads.ts, src/server/mcp/tools/images.ts</files>
  <read_first>
    - src/server/services/storage.service.ts (withImageUrl, withImageUrls, deleteImage APIs)
    - src/server/routes/items.ts (unlink usage on delete, response patterns)
    - src/server/routes/threads.ts (unlink usage on delete, response patterns)
    - src/server/routes/setups.ts (response patterns for setup items with images)
    - src/server/index.ts (serveStatic for /uploads/*)
    - src/server/mcp/tools/items.ts (MCP tool response patterns)
    - src/server/mcp/tools/threads.ts (MCP tool response patterns)
    - src/server/mcp/tools/images.ts (fetchImageFromUrl usage)
  </read_first>
  <action>
1. Refactor `src/server/routes/items.ts` per D-08, D-09:
   - Remove `unlink` and `join` imports related to uploads
   - Import `deleteImage`, `withImageUrl`, `withImageUrls` from `../services/storage.service`
   - On item delete: replace `unlink(join("uploads", deleted.imageFilename))` with `await deleteImage(deleted.imageFilename)`
   - On GET single item: wrap response with `withImageUrl()` before returning
   - On GET list items: wrap response array with `withImageUrls()` before returning
   - Keep try/catch around deleteImage (missing object is not an error, same as current pattern)

2. Refactor `src/server/routes/threads.ts` per D-08, D-09:
   - Remove `unlink` and `join` imports related to uploads
   - Import `deleteImage`, `withImageUrl`, `withImageUrls` from `../services/storage.service`
   - On thread delete (where candidate images are cleaned up): replace `unlink(join("uploads", filename))` with `await deleteImage(filename)` in the loop
   - On candidate delete: replace `unlink(join("uploads", deleted.imageFilename))` with `await deleteImage(deleted.imageFilename)`
   - On GET thread with candidates: enrich candidate records with `withImageUrls()` before returning
   - On GET thread list: if threads include image data, enrich accordingly

3. Refactor `src/server/routes/setups.ts` per D-09:
   - Import `withImageUrls` from `../services/storage.service`
   - On GET setup detail (which includes items with imageFilename): enrich the items array with `withImageUrls()` before returning
   - On GET setup list: if list includes items with images, enrich accordingly

4. Update `src/server/index.ts` per D-08:
   - Remove the line `app.use("/uploads/*", serveStatic({ root: "./" }))` entirely
   - Remove `serveStatic` import if no longer used elsewhere (check — it IS still used for production SPA serving)
   - Actually: `serveStatic` is still used for SPA serving in production. Only remove the `/uploads/*` line.

5. Update MCP tools per D-09:
   - `src/server/mcp/tools/items.ts`: After getting items from service, enrich with `withImageUrl`/`withImageUrls` before returning in tool response
   - `src/server/mcp/tools/threads.ts`: After getting thread with candidates, enrich candidate images with `withImageUrls`
   - `src/server/mcp/tools/images.ts`: No changes needed if it calls fetchImageFromUrl (already refactored in Task 1). Verify it does not reference local filesystem directly.
  </action>
  <verify>
    <automated>grep -rn "unlink.*uploads\|Bun\.write.*uploads\|/uploads/" src/server/ | grep -v node_modules | grep -v "\.test\." && echo "FAIL: still has uploads references" || echo "PASS: no uploads references in server"</automated>
  </verify>
  <acceptance_criteria>
    - grep -qv "unlink.*uploads" src/server/routes/items.ts
    - grep -q "deleteImage" src/server/routes/items.ts
    - grep -q "withImageUrl" src/server/routes/items.ts
    - grep -qv "unlink.*uploads" src/server/routes/threads.ts
    - grep -q "deleteImage" src/server/routes/threads.ts
    - grep -qv 'uploads/\*.*serveStatic' src/server/index.ts
    - grep -q "withImageUrl" src/server/mcp/tools/items.ts
    - No remaining references to "unlink.*uploads" or "/uploads/" in src/server/ (excluding test files)
  </acceptance_criteria>
  <done>All server routes use storage service for image deletion and URL generation. Static /uploads/* serving removed. MCP tools return presigned URLs. Zero local filesystem image references remain in server code.</done>
</task>

</tasks>

<verification>
- `grep -rn "/uploads/" src/server/ | grep -v node_modules` returns NO matches (all server references removed)
- `grep -rn "Bun.write" src/server/ | grep -v node_modules` returns NO image-related matches
- `grep -rn "unlink.*uploads" src/server/` returns NO matches
- `bun test tests/services/image.service.test.ts tests/routes/images.test.ts` passes
- `bun run lint` passes (no unused imports from removed code)
</verification>

<success_criteria>
- All image uploads go through storage.service.ts (no Bun.write to uploads/)
- All image deletions go through storage.service.ts (no unlink of uploads/)
- All API responses with images include imageUrl presigned URL field
- Static /uploads/* serving removed from server
- MCP tools return presigned URLs
- All existing image-related tests pass with mocked storage
</success_criteria>

<output>
After completion, create `.planning/phases/17-object-storage/17-02-SUMMARY.md`
</output>
