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,263 @@
---
phase: 03-images
plan: 03
type: execute
wave: 2
depends_on: ["03-01"]
files_modified:
- src/lib/components/CameraCapture.svelte
autonomous: true
must_haves:
truths:
- "User can open camera modal"
- "User can see live camera preview"
- "User can capture a photo"
- "User can preview captured photo before confirming"
- "User can retake if not satisfied"
- "Camera stream stops when modal closes"
artifacts:
- path: "src/lib/components/CameraCapture.svelte"
provides: "Camera capture modal with preview and confirm"
min_lines: 100
key_links:
- from: "src/lib/components/CameraCapture.svelte"
to: "navigator.mediaDevices.getUserMedia"
via: "browser API call"
pattern: "getUserMedia"
- from: "src/lib/components/CameraCapture.svelte"
to: "canvas.toBlob"
via: "photo capture"
pattern: "toBlob"
---
<objective>
Implement camera capture component for mobile photo taking with preview and confirm flow.
Purpose: Enables users to take photos directly in the app, critical for the core use case of photographing paper notes. Direct camera access feels more native than file picker on mobile.
Output:
- CameraCapture modal component with live preview
- Capture, preview, retake, confirm flow
- Proper stream cleanup to prevent battery drain
</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
</context>
<tasks>
<task type="auto">
<name>Task 1: CameraCapture component with getUserMedia</name>
<files>
src/lib/components/CameraCapture.svelte
</files>
<action>
Create CameraCapture.svelte - a modal for camera capture:
Props:
- entryId: string (required)
- onClose: () => void (required - parent controls open state)
- onCapture: (imageId: string) => void (optional callback after upload)
State:
- stream: MediaStream | null ($state)
- capturedBlob: Blob | null ($state)
- capturedPreviewUrl: string | null ($state)
- error: string | null ($state)
- isUploading: boolean ($state)
- facingMode: 'environment' | 'user' ($state) - default 'environment' (back camera)
Element refs:
- videoElement: HTMLVideoElement
- canvasElement: HTMLCanvasElement
Functions:
1. startCamera():
- Check navigator.mediaDevices?.getUserMedia exists, set error if not
- Stop existing stream if any
- Try getUserMedia with: video: { facingMode, width: { ideal: 1920 }, height: { ideal: 1080 } }, audio: false
- Catch errors:
- NotAllowedError: "Camera permission denied"
- NotFoundError: "No camera found"
- Other: "Could not access camera"
- Set stream, assign to videoElement.srcObject, call play()
2. stopCamera():
- stream?.getTracks().forEach(track => track.stop())
- stream = null
- videoElement.srcObject = null
3. capturePhoto():
- Get 2D context from canvas
- Set canvas dimensions to video dimensions
- drawImage(videoElement, 0, 0)
- canvas.toBlob() with 'image/jpeg', 0.9 quality
- Store blob, create preview URL
- Stop camera (save battery while reviewing)
4. retake():
- Revoke preview URL
- Clear capturedBlob and capturedPreviewUrl
- startCamera() again
5. confirmAndUpload():
- isUploading = true
- Create FormData with blob as 'image' (filename: 'camera-capture.jpg')
- Add entryId
- fetch('?/uploadImage', { method: 'POST', body: formData })
- Handle response
- Call onCapture, onClose
- Revoke preview URL, clean up
6. switchCamera():
- Toggle facingMode
- Restart camera with new facing mode
7. handleClose():
- stopCamera()
- Revoke any preview URL
- onClose()
Lifecycle:
- $effect: startCamera() on mount
- $effect: cleanup on unmount (stopCamera)
Template structure:
```
<div class="fixed inset-0 z-50 bg-black flex flex-col">
<!-- Header with close button and switch camera button -->
<div class="flex justify-between p-4">
<button close>X</button>
<button switchCamera>Flip</button>
</div>
<!-- Main content area -->
<div class="flex-1 flex items-center justify-center">
{#if error}
<p class="text-white">{error}</p>
<button onclick={onClose}>Close</button>
{:else if capturedPreviewUrl}
<!-- Preview captured photo -->
<img src={capturedPreviewUrl} alt="Captured" class="max-h-full max-w-full object-contain" />
{:else}
<!-- Live camera view -->
<video bind:this={videoElement} autoplay playsinline muted class="max-h-full max-w-full object-contain" />
{/if}
</div>
<!-- Hidden canvas for capture -->
<canvas bind:this={canvasElement} class="hidden" />
<!-- Action buttons -->
<div class="p-4 flex justify-center gap-4">
{#if capturedPreviewUrl}
<button retake disabled={isUploading}>Retake</button>
<button confirm disabled={isUploading}>
{isUploading ? 'Uploading...' : 'Use Photo'}
</button>
{:else if !error}
<button capture class="w-16 h-16 rounded-full bg-white">
<!-- Capture button - large circle -->
</button>
{/if}
</div>
</div>
```
CRITICAL: Always stop camera stream when component closes or captures. Camera indicator should not stay on.
Styling:
- Full screen black background (typical camera UI)
- Large touch targets for capture button (min 64px)
- Video fills available space while maintaining aspect ratio
</action>
<verify>
Run `npm run check` - TypeScript compiles.
File exists and has key functions: `grep -c "getUserMedia\|capturePhoto\|stopCamera" src/lib/components/CameraCapture.svelte`
</verify>
<done>
CameraCapture modal with live preview renders.
Capture creates blob from canvas.
Preview shows captured image with retake/confirm options.
Camera stream properly stopped on close and after capture.
</done>
</task>
<task type="auto">
<name>Task 2: Feature detection and graceful fallback</name>
<files>
src/lib/components/CameraCapture.svelte
</files>
<action>
Enhance CameraCapture with proper feature detection and error handling:
1. Add hasCameraSupport check:
```typescript
const hasCameraSupport = typeof navigator !== 'undefined' &&
!!navigator.mediaDevices?.getUserMedia;
```
2. If no camera support, show friendly message immediately (don't try to call getUserMedia):
"Camera not supported in this browser. Use the file upload option instead."
3. Add isSecureContext check:
- Camera API requires HTTPS (except localhost)
- Show message if not secure: "Camera requires HTTPS connection"
4. Handle permission denied gracefully:
- Clear error message explaining how to enable camera
- "Camera access denied. Check browser settings to allow camera."
5. Handle no camera found:
- "No camera found on this device. Use file upload instead."
6. Add timeout for camera start:
- If camera doesn't start within 10 seconds, show error
- Prevents infinite loading state
7. Test on desktop without webcam - should show "No camera found" not crash
The error states should have a close button that calls onClose().
</action>
<verify>
Run `npm run check` - TypeScript compiles.
Grep for error handling: `grep -c "NotAllowedError\|NotFoundError\|hasCameraSupport" src/lib/components/CameraCapture.svelte`
</verify>
<done>
CameraCapture gracefully handles missing camera, permission denied, insecure context.
Error messages are user-friendly with action guidance.
</done>
</task>
</tasks>
<verification>
1. `npm run check` passes
2. `npm run dev` - server starts
3. Component file exists: `ls src/lib/components/CameraCapture.svelte`
4. Has required functions: grep for getUserMedia, stopCamera, capturePhoto, toBlob
5. Has error handling: grep for NotAllowedError, NotFoundError
</verification>
<success_criteria>
- CameraCapture opens with live camera preview
- Capture button takes photo and shows preview
- Retake returns to live camera
- Confirm uploads the photo
- Camera stream stops when modal closes
- Graceful error handling for permission denied, no camera, unsupported browser
</success_criteria>
<output>
After completion, create `.planning/phases/03-images/03-03-SUMMARY.md`
</output>