Phase 03: Images - 4 plan(s) in 3 wave(s) - Wave 1: 03-01 (foundation) - Wave 2: 03-02, 03-03 (parallel - upload + camera) - Wave 3: 03-04 (integration + verification) - Ready for execution Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
225 lines
7.9 KiB
Markdown
225 lines
7.9 KiB
Markdown
---
|
|
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>
|