---
phase: 03-images
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/lib/server/db/schema.ts
  - src/lib/server/db/repository.ts
  - src/lib/server/images/storage.ts
  - src/lib/server/images/thumbnails.ts
  - src/routes/api/images/[id]/+server.ts
  - src/routes/api/images/[id]/thumbnail/+server.ts
autonomous: true

must_haves:
  truths:
    - "Images table exists in database with entryId foreign key"
    - "Image files can be written to filesystem"
    - "Thumbnails can be generated from uploaded images"
    - "Images can be served via API endpoints"
  artifacts:
    - path: "src/lib/server/db/schema.ts"
      provides: "images table definition"
      contains: "images = sqliteTable"
    - path: "src/lib/server/db/repository.ts"
      provides: "imageRepository with CRUD operations"
      contains: "imageRepository"
    - path: "src/lib/server/images/storage.ts"
      provides: "File write/read operations"
      exports: ["saveOriginal", "saveThumbnail", "deleteImage"]
    - path: "src/lib/server/images/thumbnails.ts"
      provides: "Sharp thumbnail generation"
      exports: ["generateThumbnail"]
    - path: "src/routes/api/images/[id]/+server.ts"
      provides: "GET endpoint for original images"
      exports: ["GET"]
    - path: "src/routes/api/images/[id]/thumbnail/+server.ts"
      provides: "GET endpoint for thumbnails"
      exports: ["GET"]
  key_links:
    - from: "src/lib/server/images/thumbnails.ts"
      to: "sharp"
      via: "import sharp"
      pattern: "import sharp from 'sharp'"
    - from: "src/routes/api/images/[id]/+server.ts"
      to: "src/lib/server/db/repository.ts"
      via: "imageRepository.getById"
      pattern: "imageRepository\\.getById"
---

<objective>
Create the foundation layer for image attachments: database schema, file storage utilities, thumbnail generation, and API endpoints for serving images.

Purpose: Establishes the data model and infrastructure that upload components will use. Without this layer, images cannot be stored, processed, or retrieved.

Output:
- Database table for images with entry relationship
- Repository layer for image CRUD
- Filesystem utilities for storing originals and thumbnails
- API endpoints that serve images to the browser
</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/ROADMAP.md
@.planning/phases/03-images/03-RESEARCH.md
@src/lib/server/db/schema.ts
@src/lib/server/db/repository.ts
@src/lib/server/db/index.ts
</context>

<tasks>

<task type="auto">
  <name>Task 1: Database schema and repository for images</name>
  <files>
    src/lib/server/db/schema.ts
    src/lib/server/db/repository.ts
  </files>
  <action>
1. Install sharp: `npm install sharp`

2. Add images table to schema.ts:
   - id: text primary key (nanoid)
   - entryId: text foreign key to entries.id (not null)
   - filename: text (original filename for display)
   - ext: text (file extension without dot: jpg, png, etc.)
   - createdAt: text ISO timestamp

   Export Image and NewImage types.

3. Add ImageRepository interface and implementation to repository.ts:
   - create(image: Omit<NewImage, 'id' | 'createdAt'>): Image
   - getById(id: string): Image | undefined
   - getByEntryId(entryId: string): Image[]
   - delete(id: string): boolean
   - deleteByEntryId(entryId: string): number (returns count deleted)

4. Export imageRepository singleton alongside entryRepository.

Use same patterns as existing entryRepository (nanoid for IDs, synchronous Drizzle operations).
  </action>
  <verify>
Run `npm run check` - TypeScript compiles without errors.
Verify exports: `grep -n "imageRepository" src/lib/server/db/repository.ts`
  </verify>
  <done>
images table defined with entryId relationship.
imageRepository exported with create, getById, getByEntryId, delete, deleteByEntryId methods.
  </done>
</task>

<task type="auto">
  <name>Task 2: File storage and thumbnail generation utilities</name>
  <files>
    src/lib/server/images/storage.ts
    src/lib/server/images/thumbnails.ts
  </files>
  <action>
1. Create src/lib/server/images/storage.ts:
   - UPLOAD_DIR constant: 'data/uploads'
   - ORIGINALS_DIR: 'data/uploads/originals'
   - THUMBNAILS_DIR: 'data/uploads/thumbnails'

   Functions:
   - ensureDirectories(): Creates directories if they don't exist (use mkdir recursive)
   - saveOriginal(id: string, ext: string, buffer: Buffer): Promise<void>
     Writes to `${ORIGINALS_DIR}/${id}.${ext}`
   - saveThumbnail(id: string, buffer: Buffer): Promise<void>
     Writes to `${THUMBNAILS_DIR}/${id}.jpg` (thumbnails always jpg)
   - getOriginalPath(id: string, ext: string): string
   - getThumbnailPath(id: string): string
   - deleteImage(id: string, ext: string): Promise<void>
     Removes both original and thumbnail files (use try/catch, don't throw if missing)

2. Create src/lib/server/images/thumbnails.ts:
   - import sharp from 'sharp'

   Function:
   - generateThumbnail(buffer: Buffer, size?: number): Promise<Buffer>
     Default size: 150
     CRITICAL: Call sharp.rotate() FIRST to handle EXIF orientation from mobile photos
     Then resize with fit: 'cover', position: 'center', withoutEnlargement: true
     Output as jpeg with quality 80
     Return the thumbnail buffer (caller saves it)
  </action>
  <verify>
Run `npm run check` - TypeScript compiles.
Run quick test: `node -e "import('sharp').then(s => console.log('sharp version:', s.default.versions?.sharp || 'loaded'))"` or just verify sharp installed in package.json.
  </verify>
  <done>
storage.ts exports directory management and file I/O functions.
thumbnails.ts exports generateThumbnail that handles EXIF rotation.
  </done>
</task>

<task type="auto">
  <name>Task 3: API endpoints for serving images</name>
  <files>
    src/routes/api/images/[id]/+server.ts
    src/routes/api/images/[id]/thumbnail/+server.ts
  </files>
  <action>
1. Create src/routes/api/images/[id]/+server.ts:
   - GET handler that:
     a. Gets id from params
     b. Looks up image in imageRepository.getById(id)
     c. Returns 404 error if not found
     d. Reads file from getOriginalPath(id, image.ext)
     e. Returns Response with:
        - Content-Type based on ext (jpg -> image/jpeg, png -> image/png, etc.)
        - Content-Length header
        - Cache-Control: public, max-age=31536000, immutable (images are immutable by ID)
     f. Catches file read errors, returns 404

2. Create src/routes/api/images/[id]/thumbnail/+server.ts:
   - Same pattern but reads from getThumbnailPath(id)
   - Content-Type always image/jpeg (thumbnails are always jpg)

Import imageRepository and storage functions. Use readFile from 'node:fs/promises'.
  </action>
  <verify>
Run `npm run check` - TypeScript compiles.
Verify endpoints exist: `ls -la src/routes/api/images/`
  </verify>
  <done>
GET /api/images/[id] serves original images with proper Content-Type.
GET /api/images/[id]/thumbnail serves thumbnails.
Both return 404 for missing images.
  </done>
</task>

</tasks>

<verification>
1. `npm run check` passes with no TypeScript errors
2. `npm run dev` starts without errors
3. Sharp is in dependencies: `grep sharp package.json`
4. All new files exist:
   - src/lib/server/db/schema.ts contains `images = sqliteTable`
   - src/lib/server/db/repository.ts exports `imageRepository`
   - src/lib/server/images/storage.ts exists
   - src/lib/server/images/thumbnails.ts exists
   - src/routes/api/images/[id]/+server.ts exists
   - src/routes/api/images/[id]/thumbnail/+server.ts exists
</verification>

<success_criteria>
- images table can store image metadata with entry relationship
- imageRepository provides typed CRUD for images
- Thumbnail generation uses Sharp with EXIF auto-rotation
- API endpoints can serve images from filesystem
- All code compiles without TypeScript errors
</success_criteria>

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