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:
263
.planning/phases/03-images/03-03-PLAN.md
Normal file
263
.planning/phases/03-images/03-03-PLAN.md
Normal 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>
|
||||
Reference in New Issue
Block a user