From f1dbf0504bd7fe1e68fd834db623ee31d6baf5be Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 5 Apr 2026 12:32:44 +0200 Subject: [PATCH] docs(phase-17): complete phase execution --- .planning/ROADMAP.md | 2 +- .planning/STATE.md | 10 +- .../17-object-storage/17-VERIFICATION.md | 150 ++++++++++++++++++ 3 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 .planning/phases/17-object-storage/17-VERIFICATION.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 48ec8d1..c685b89 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -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 | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index dc2ddeb..16f19a4 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -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) diff --git a/.planning/phases/17-object-storage/17-VERIFICATION.md b/.planning/phases/17-object-storage/17-VERIFICATION.md new file mode 100644 index 0000000..9bebc46 --- /dev/null +++ b/.planning/phases/17-object-storage/17-VERIFICATION.md @@ -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 `` when truthy | +| `src/client/components/CandidateCard.tsx` | Candidate display using imageUrl from API | VERIFIED | Accepts `imageUrl` prop; renders `` 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 ? ` 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)_