docs(phase-17): complete phase execution

This commit is contained in:
2026-04-05 12:32:44 +02:00
parent 4109f9fd78
commit f1dbf0504b
3 changed files with 156 additions and 6 deletions

View File

@@ -200,5 +200,5 @@ Plans:
| 14. PostgreSQL Migration | v2.0 | 0/? | Not started | - |
| 15. External Authentication | v2.0 | 0/? | Not started | - |
| 16. Multi-User Data Model | v2.0 | 2/4 | Complete | 2026-04-05 |
| 17. Object Storage | v2.0 | 3/3 | Complete | 2026-04-05 |
| 17. Object Storage | v2.0 | 3/3 | Complete | 2026-04-05 |
| 18. Global Items & Public Profiles | v2.0 | 0/? | Not started | - |

View File

@@ -4,8 +4,8 @@ milestone: v1.3
milestone_name: Research & Decision Tools
status: planning
stopped_at: Completed 17-03-PLAN.md
last_updated: "2026-04-05T10:29:15.199Z"
last_activity: 2026-04-03 — v2.0 roadmap created (Phases 14-18)
last_updated: "2026-04-05T10:32:40.016Z"
last_activity: 2026-04-05
progress:
total_phases: 11
completed_phases: 10
@@ -25,10 +25,10 @@ See: .planning/PROJECT.md (updated 2026-04-03)
## Current Position
Phase: 14 of 18 (PostgreSQL Migration)
Plan: 0 of ? in current phase
Phase: 17 of 18 (PostgreSQL Migration)
Plan: Not started
Status: Ready to plan
Last activity: 2026-04-03 — v2.0 roadmap created (Phases 14-18)
Last activity: 2026-04-05
Progress: [----------] 0% (v2.0 milestone)

View File

@@ -0,0 +1,150 @@
---
phase: 17-object-storage
verified: 2026-04-04T00:00:00Z
status: passed
score: 10/10 must-haves verified
re_verification: 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 || null` — imageUrl from parent form props | Yes — ItemForm passes `items?.find(...)?.imageUrl` from query cache | FLOWING |
---
### 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)_