13 KiB
phase, verified, status, score, re_verification
| phase | verified | status | score | re_verification |
|---|---|---|---|---|
| 17-object-storage | 2026-04-04T00:00:00Z | passed | 10/10 must-haves verified | false |
Phase 17: Object Storage Verification Report
Phase Goal: Images are stored in and served from MinIO instead of the local filesystem Verified: 2026-04-04 Status: PASSED Re-verification: No — initial verification
Goal Achievement
Observable Truths
| # | Truth | Status | Evidence |
|---|---|---|---|
| 1 | Storage service can upload a buffer to S3-compatible storage | VERIFIED | uploadImage uses PutObjectCommand via s3.send in storage.service.ts |
| 2 | Storage service can delete an object from S3-compatible storage | VERIFIED | deleteImage uses DeleteObjectCommand via s3.send |
| 3 | Storage service can generate a presigned URL for an object | VERIFIED | getImageUrl uses getSignedUrl from @aws-sdk/s3-request-presigner |
| 4 | Docker Compose starts MinIO with automatic bucket creation | VERIFIED | Both compose files have minio + minio-init with mc mb gearbox-images |
| 5 | Image upload via POST /api/images stores file in MinIO, not local filesystem | VERIFIED | imageRoutes calls uploadImage from storage.service; no Bun.write |
| 6 | Image upload via POST /api/images/from-url stores fetched image in MinIO | VERIFIED | fetchImageFromUrl calls uploadImage from storage.service; no Bun.write |
| 7 | Deleting an item or candidate with an image deletes the image from MinIO | VERIFIED | items.ts and threads.ts both call deleteImage(deleted.imageFilename) |
| 8 | API responses include imageUrl field with presigned URLs for items and candidates | VERIFIED | items.ts, threads.ts, setups.ts all call withImageUrl/withImageUrls |
| 9 | Static file serving for /uploads/* is removed from the server | VERIFIED | No /uploads/ route in index.ts; only SPA serveStatic remains |
| 10 | Client components display images using presigned URLs from API responses | VERIFIED | ItemCard, CandidateCard, CandidateListItem, ComparisonTable, ImageUpload all use imageUrl prop; zero /uploads/ path construction in client |
Score: 10/10 truths verified
Required Artifacts
| Artifact | Expected | Status | Details |
|---|---|---|---|
src/server/services/storage.service.ts |
S3 storage abstraction | VERIFIED | Exports uploadImage, deleteImage, getImageUrl, withImageUrl, withImageUrls; forcePathStyle: true |
tests/services/storage.service.test.ts |
Storage service unit tests with mocked S3Client | VERIFIED | 8 tests, all pass; mocks S3Client.send and getSignedUrl |
docker-compose.dev.yml |
MinIO service for local development | VERIFIED | Uses quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z, minio-init creates gearbox-images bucket |
docker-compose.yml |
MinIO service for production | VERIFIED | Same image, env var creds, S3 vars injected into app service, no uploads volume |
src/server/services/image.service.ts |
URL-based image fetch using storage service | VERIFIED | Imports and calls uploadImage; no Bun.write or mkdir |
src/server/routes/images.ts |
Image upload routes using storage service | VERIFIED | POST / calls uploadImage; no local filesystem writes |
src/server/index.ts |
Server entry without /uploads/* static serving | VERIFIED | Only SPA serveStatic present; no /uploads/ route |
src/client/components/ItemCard.tsx |
Item display using imageUrl from API | VERIFIED | Accepts imageUrl prop; renders <img src={imageUrl}> when truthy |
src/client/components/CandidateCard.tsx |
Candidate display using imageUrl from API | VERIFIED | Accepts imageUrl prop; renders <img src={imageUrl}> when truthy |
src/client/components/ImageUpload.tsx |
Upload component with presigned URL support | VERIFIED | Accepts imageUrl prop; priority: localPreview > imageUrl > null |
scripts/migrate-images-to-minio.ts |
One-time migration of local images to MinIO | VERIFIED | Reads uploads/, calls uploadImage per file, preserves filenames, no auto-delete |
tests/services/image.service.test.ts |
Image service tests with mocked storage | VERIFIED | 5 tests, all pass; mocks storage.service |
tests/routes/images.test.ts |
Image routes tests with mocked storage | VERIFIED | 5 tests, all pass; verifies mockUploadImage is called on valid upload |
Key Link Verification
| From | To | Via | Status | Details |
|---|---|---|---|---|
src/server/services/storage.service.ts |
@aws-sdk/client-s3 |
S3Client with forcePathStyle: true |
WIRED | Line 20: forcePathStyle: true |
docker-compose.dev.yml |
minio-init |
mc mb --ignore-existing myminio/gearbox-images |
WIRED | Lines 61-62 confirmed |
src/server/routes/images.ts |
src/server/services/storage.service.ts |
uploadImage() call |
WIRED | Line 6: import { uploadImage } from "../services/storage.service" |
src/server/routes/items.ts |
src/server/services/storage.service.ts |
deleteImage, withImageUrl, withImageUrls |
WIRED | Lines 15-18: all three imported and used |
src/server/routes/threads.ts |
src/server/services/storage.service.ts |
deleteImage, withImageUrls |
WIRED | Lines 13-15: imported and used in GET /:id and DELETE handlers |
src/client/components/ItemCard.tsx |
API response | imageUrl prop instead of /uploads/ path |
WIRED | Line 154: {imageUrl ? <img src={imageUrl}> renders presigned URL |
scripts/migrate-images-to-minio.ts |
src/server/services/storage.service.ts |
uploadImage() for each file |
WIRED | Line 18: import { uploadImage } from storage.service; Line 64: called per file |
Data-Flow Trace (Level 4)
| Artifact | Data Variable | Source | Produces Real Data | Status |
|---|---|---|---|---|
src/server/routes/items.ts |
imageUrl |
withImageUrls(items) → getImageUrl() → getSignedUrl() |
Yes — calls AWS SDK getSignedUrl with actual S3 command | FLOWING |
src/client/components/ItemCard.tsx |
imageUrl |
Passed as prop from CollectionView.tsx (line 231) which receives from React Query useItems |
Yes — flows from API GET /api/items which calls withImageUrls | FLOWING |
src/client/components/ImageUpload.tsx |
displayUrl |
`localPreview | imageUrl |
Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|---|---|---|---|
| Storage service unit tests pass | bun test tests/services/storage.service.test.ts --timeout 30000 |
8 pass, 0 fail | PASS |
| Image service unit tests pass | bun test tests/services/image.service.test.ts --timeout 30000 |
5 pass, 0 fail | PASS |
| Image routes tests pass | bun test tests/routes/images.test.ts --timeout 30000 |
5 pass, 0 fail (exit 99 is PGlite teardown, not a test failure) | PASS |
| No /uploads/ references in server source | grep -rn "/uploads/" src/server/ (excluding tests) |
0 matches | PASS |
| No /uploads/ references in client source | grep -rn "/uploads/" src/client/ |
0 matches | PASS |
| No Bun.write/unlink uploads in server | grep -rn "Bun\.write|unlink.*uploads" src/server/ |
0 matches | PASS |
| Migration script imports uploadImage and reads files | grep -q "uploadImage|readdir" scripts/migrate-images-to-minio.ts |
Both present | PASS |
| Migration script does not auto-delete originals | grep -q "unlink|rm -rf" scripts/migrate-images-to-minio.ts |
Not present | PASS |
Requirements Coverage
| Requirement | Source Plan(s) | Description | Status | Evidence |
|---|---|---|---|---|
| IMG-01 | 17-01, 17-02 | Images are stored in MinIO (S3-compatible) instead of local filesystem | SATISFIED | uploadImage via @aws-sdk/client-s3 in storage.service.ts; no local writes remain |
| IMG-02 | 17-03 | Existing uploaded images are migrated to MinIO | SATISFIED | scripts/migrate-images-to-minio.ts reads uploads/, calls uploadImage per file |
| IMG-03 | 17-02, 17-03 | Image upload and retrieval work through the new storage layer | SATISFIED | All upload routes call uploadImage; all GET responses call withImageUrl(s) |
| IMG-04 | 17-01 | Docker Compose provides MinIO for local development | SATISFIED | docker-compose.dev.yml has minio service + minio-init bucket creation; docker-compose.yml has same for production |
All 4 requirements SATISFIED. No orphaned requirements.
Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|---|---|---|---|---|
| None | — | — | — | — |
No TODO, FIXME, placeholder, stub returns, or empty handlers found in any phase 17 files.
Human Verification Required
1. MinIO Container Connectivity
Test: Run docker compose -f docker-compose.dev.yml up minio minio-init and verify the gearbox-images bucket is created automatically.
Expected: minio-init container exits 0, MinIO console at http://localhost:9001 shows gearbox-images bucket.
Why human: Requires Docker daemon and network connectivity — cannot verify programmatically without running services.
2. End-to-End Image Upload and Display
Test: Start the dev stack with MinIO running. Log in, create an item, upload a photo via the UI. Verify the image displays correctly on the item card.
Expected: Image renders in ItemCard via a presigned S3 URL (URL starts with http://localhost:9000/gearbox-images/...?X-Amz-Signature=...). No /uploads/ paths in network requests.
Why human: Requires running server + MinIO + browser — cannot assert presigned URL format or image rendering programmatically.
3. Presigned URL Expiry
Test: Verify a presigned URL returned by GET /api/items includes X-Amz-Expires=3600 (or the value set in S3_PRESIGN_EXPIRY).
Expected: URL contains X-Amz-Expires=3600 by default, and respects the env var override.
Why human: Requires a live MinIO instance to generate real presigned URLs.
Gaps Summary
No gaps. All 10 observable truths are VERIFIED. All 13 artifacts exist, are substantive, and are properly wired. All 4 required image requirements (IMG-01 through IMG-04) are satisfied. All test suites pass. Zero /uploads/ or local filesystem write references remain in client or server source code.
Verified: 2026-04-04 Verifier: Claude (gsd-verifier)