--- 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" --- 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 @/home/tho/.claude/get-shit-done/workflows/execute-plan.md @/home/tho/.claude/get-shit-done/templates/summary.md @.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 Task 1: Database schema and repository for images src/lib/server/db/schema.ts src/lib/server/db/repository.ts 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): 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). Run `npm run check` - TypeScript compiles without errors. Verify exports: `grep -n "imageRepository" src/lib/server/db/repository.ts` images table defined with entryId relationship. imageRepository exported with create, getById, getByEntryId, delete, deleteByEntryId methods. Task 2: File storage and thumbnail generation utilities src/lib/server/images/storage.ts src/lib/server/images/thumbnails.ts 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 Writes to `${ORIGINALS_DIR}/${id}.${ext}` - saveThumbnail(id: string, buffer: Buffer): Promise 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 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 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) 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. storage.ts exports directory management and file I/O functions. thumbnails.ts exports generateThumbnail that handles EXIF rotation. Task 3: API endpoints for serving images src/routes/api/images/[id]/+server.ts src/routes/api/images/[id]/thumbnail/+server.ts 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'. Run `npm run check` - TypeScript compiles. Verify endpoints exist: `ls -la src/routes/api/images/` GET /api/images/[id] serves original images with proper Content-Type. GET /api/images/[id]/thumbnail serves thumbnails. Both return 404 for missing images. 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 - 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 After completion, create `.planning/phases/03-images/03-01-SUMMARY.md`