---
phase: 03-images
plan: 02
type: execute
wave: 2
depends_on: ["03-01"]
files_modified:
  - src/routes/+page.server.ts
  - src/lib/components/ImageUpload.svelte
  - src/lib/components/QuickCapture.svelte
autonomous: true

must_haves:
  truths:
    - "User can upload an image via file picker"
    - "User can upload an image via drag-and-drop"
    - "Uploaded image appears immediately (optimistic preview)"
    - "Image is stored on server with thumbnail generated"
  artifacts:
    - path: "src/routes/+page.server.ts"
      provides: "uploadImage form action"
      contains: "uploadImage:"
    - path: "src/lib/components/ImageUpload.svelte"
      provides: "Upload button + drag-drop zone"
      min_lines: 50
  key_links:
    - from: "src/lib/components/ImageUpload.svelte"
      to: "src/routes/+page.server.ts"
      via: "fetch to ?/uploadImage"
      pattern: "fetch.*\\?/uploadImage"
    - from: "src/routes/+page.server.ts"
      to: "src/lib/server/images/storage.ts"
      via: "saveOriginal import"
      pattern: "import.*storage"
---

<objective>
Implement file upload functionality with drag-and-drop support and optimistic preview.

Purpose: Enables users to attach images to entries via file selection or drag-and-drop on desktop. This is the primary upload path for desktop users.

Output:
- Form action that handles multipart file uploads
- ImageUpload component with button and drag-drop zone
- Integration with QuickCapture for upload during entry creation
</objective>

<execution_context>
@/home/tho/.claude/get-shit-done/workflows/execute-plan.md
@/home/tho/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/phases/03-images/03-RESEARCH.md
@.planning/phases/03-images/03-01-SUMMARY.md
@src/routes/+page.server.ts
@src/lib/components/QuickCapture.svelte
</context>

<tasks>

<task type="auto">
  <name>Task 1: Upload form action with thumbnail generation</name>
  <files>
    src/routes/+page.server.ts
  </files>
  <action>
Add uploadImage action to existing +page.server.ts actions:

1. Import at top:
   - import { imageRepository } from '$lib/server/db/repository'
   - import { saveOriginal, saveThumbnail, ensureDirectories } from '$lib/server/images/storage'
   - import { generateThumbnail } from '$lib/server/images/thumbnails'
   - import { nanoid } from 'nanoid'

2. Add uploadImage action:
   ```typescript
   uploadImage: async ({ request }) => {
     const formData = await request.formData();
     const file = formData.get('image') as File;
     const entryId = formData.get('entryId') as string;

     if (!file || file.size === 0) {
       return fail(400, { error: 'No file uploaded' });
     }

     if (!entryId) {
       return fail(400, { error: 'Entry ID required' });
     }

     // Verify entry exists
     const entry = entryRepository.getById(entryId);
     if (!entry) {
       return fail(404, { error: 'Entry not found' });
     }

     // Validate image type
     if (!file.type.startsWith('image/')) {
       return fail(400, { error: 'File must be an image' });
     }

     const imageId = nanoid();
     const ext = file.name.split('.').pop()?.toLowerCase() || 'jpg';
     const buffer = Buffer.from(await file.arrayBuffer());

     try {
       await ensureDirectories();

       // Save original
       await saveOriginal(imageId, ext, buffer);

       // Generate and save thumbnail
       const thumbnailBuffer = await generateThumbnail(buffer);
       await saveThumbnail(imageId, thumbnailBuffer);

       // Save to database
       imageRepository.create({
         entryId,
         filename: file.name,
         ext
       });

       return { success: true, imageId };
     } catch (err) {
       console.error('Upload error:', err);
       return fail(500, { error: 'Failed to save image' });
     }
   }
   ```

3. Update the load function to include images for each entry:
   - After getting entries, for each entry fetch its images via imageRepository.getByEntryId(entry.id)
   - Return entries with their images attached (map over entries)
  </action>
  <verify>
Run `npm run check` - TypeScript compiles.
Verify action exists: `grep -n "uploadImage:" src/routes/+page.server.ts`
  </verify>
  <done>
uploadImage action handles file upload, generates thumbnail, saves to database.
load function returns entries with their images attached.
  </done>
</task>

<task type="auto">
  <name>Task 2: ImageUpload component with drag-drop</name>
  <files>
    src/lib/components/ImageUpload.svelte
  </files>
  <action>
Create ImageUpload.svelte component:

Props:
- entryId: string (required)
- onUploadStart?: () => void (optional callback for optimistic UI)
- onUploadComplete?: (imageId: string) => void (optional callback)
- onUploadError?: (error: string) => void (optional callback)

State:
- isDragging: boolean ($state)
- isUploading: boolean ($state)
- previewUrl: string | null ($state) - for optimistic preview
- fileInput: HTMLInputElement reference

Functions:
- handleDragOver(e: DragEvent): preventDefault, set isDragging true
- handleDragLeave(e: DragEvent): preventDefault, set isDragging false
- handleDrop(e: DragEvent): preventDefault, isDragging false, get first file, call uploadFile
- handleFileSelect(e: Event): get file from input, call uploadFile
- uploadFile(file: File):
  1. Validate file.type starts with 'image/'
  2. Create optimistic preview: previewUrl = URL.createObjectURL(file)
  3. Call onUploadStart?.()
  4. Set isUploading true
  5. Create FormData with 'image' and 'entryId'
  6. fetch('?/uploadImage', { method: 'POST', body: formData })
  7. Parse response, handle errors
  8. Call onUploadComplete or onUploadError
  9. Revoke preview URL, clear state
  10. Call invalidateAll() to refresh list

Template:
- Drop zone div with:
  - Border dashed when not dragging, solid blue when dragging
  - ondragover, ondragleave, ondrop handlers
  - role="button", tabindex="0"
  - onclick opens file input
  - onkeydown Enter opens file input
- Hidden file input with accept="image/*"
- Show upload icon and "Drop image or click to upload" text
- Show spinner when isUploading
- Show optimistic preview image if previewUrl

Styling (Tailwind):
- Rounded border, dashed gray
- Blue border/bg when dragging
- Centered content with icon
- Touch-friendly size (min-h-[100px])
  </action>
  <verify>
Run `npm run check` - TypeScript compiles.
File exists with core functions: `grep -c "handleDrop\|uploadFile" src/lib/components/ImageUpload.svelte`
  </verify>
  <done>
ImageUpload component renders drag-drop zone with button fallback.
Shows optimistic preview during upload.
Calls form action and refreshes list on completion.
  </done>
</task>

<task type="auto">
  <name>Task 3: Integrate upload into QuickCapture</name>
  <files>
    src/lib/components/QuickCapture.svelte
  </files>
  <action>
Update QuickCapture.svelte to include image upload option:

1. Add state for pending image:
   - pendingImage: File | null ($state)
   - pendingPreviewUrl: string | null ($state)

2. Add hidden file input with accept="image/*" after the form

3. Add image upload button next to the submit button:
   - Icon-only button (camera/image icon)
   - Opens file picker on click
   - Disabled when already has pending image

4. When file selected:
   - Store in pendingImage
   - Create preview URL for display
   - Show small thumbnail preview next to input

5. Modify form submission flow:
   - If pendingImage exists:
     a. First submit the entry via existing create action
     b. Get the new entry ID from response
     c. Then upload the image with that entry ID
     d. Revoke preview URL
     e. Clear pendingImage

6. Add cancel button to remove pending image before submit

7. Show thumbnail preview (small, ~40px) inline when image is pending

Keep the quick capture bar clean - image attachment is optional, not prominent.
  </action>
  <verify>
Run `npm run check` - TypeScript compiles.
Run `npm run dev` and verify QuickCapture shows image button.
  </verify>
  <done>
QuickCapture has image attachment button.
User can add image before submitting new entry.
Image uploads after entry creation with the new entry ID.
  </done>
</task>

</tasks>

<verification>
1. `npm run check` passes
2. `npm run dev` - server starts
3. Test file upload:
   - Create a new entry
   - Expand entry and look for upload option (will be added in 03-04)
   - OR test via QuickCapture image button
4. Check upload directories created: `ls data/uploads/`
5. Check image in database: `sqlite3 data/taskplaner.db "SELECT * FROM images LIMIT 5;"`
</verification>

<success_criteria>
- Form action accepts multipart file upload
- Thumbnails generated with Sharp (EXIF rotation handled)
- ImageUpload component provides drag-drop and button interfaces
- Optimistic preview shows image immediately
- QuickCapture allows attaching image to new entry
</success_criteria>

<output>
After completion, create `.planning/phases/03-images/03-02-SUMMARY.md`
</output>
