docs(03): create phase plan

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>
This commit is contained in:
Thomas Richter
2026-01-29 15:15:30 +01:00
parent d99bd0d3a2
commit 04c2742e73
5 changed files with 1191 additions and 5 deletions

View File

@@ -0,0 +1,224 @@
---
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>