Files
reactbin/specs/001-reaction-image-board/tasks.md
agatha 8bf6ef443a [Spec Kit] Implementation progress
Implements all 88 tasks for the Reaction Image Board (specs/001-reaction-image-board):

- docker-compose.yml: postgres, minio, minio-init, api, ui services with healthchecks
- api/: FastAPI app with SQLAlchemy 2.x async, Alembic migrations, S3/MinIO storage,
  full integration + unit test suite (pytest + pytest-asyncio)
- ui/: Angular 19 standalone app (Library, Upload, Detail, NotFound components)
- .env.example: all required environment variables
- .gitignore: Python, Node, Docker, IDE, .env patterns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 16:13:23 +00:00

353 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
description: "Task list for Reaction Image Board v1"
---
# Tasks: Reaction Image Board v1
**Input**: Design documents from `specs/001-reaction-image-board/`
**Prerequisites**: plan.md ✅, spec.md ✅, research.md ✅, data-model.md ✅, contracts/api.md ✅
**Tests**: Per §5.1 of the constitution, TDD is non-negotiable. Test tasks
MUST appear before every implementation task. Write the test, confirm it
fails, then implement until it passes.
**Organization**: Tasks follow the milestone order from plan.md (API-first,
serial). Each task is tagged with the user story it serves.
## Format: `[ID] [P?] [Story?] Description`
- **[P]**: Can run in parallel (different files, no dependencies on incomplete tasks)
- **[Story]**: Which user story this task serves (US1US5 per spec.md)
- Include exact file paths in descriptions
## Path Conventions
```
api/app/ API source
api/tests/unit/ API unit tests
api/tests/integration/ API integration tests
api/alembic/versions/ Database migrations
ui/src/app/ Angular source
ui/src/app/services/ Angular services (+ .spec.ts colocated)
ui/src/app/<feature>/ Angular component dirs (+ .spec.ts colocated)
```
---
## Phase 1: Setup (Project Skeleton — M0)
**Purpose**: Establish the monorepo layout, Docker Compose stack, and linting
baseline. No feature logic. All subsequent milestones build on this.
- [x] T001 Create top-level monorepo layout: `api/`, `ui/`, `docker-compose.yml`, `.env.example`
- [x] T002 Write `.env.example` with all variables from spec §5 (DATABASE_URL, S3_*, API_BASE_URL, MAX_UPLOAD_BYTES)
- [x] T003 [P] Write `api/Dockerfile` (Python 3.12 slim, installs pyproject.toml deps, runs uvicorn)
- [x] T004 [P] Scaffold Angular project with CLI into `ui/` (strict mode, standalone components, routing)
- [x] T005 [P] Write `ui/Dockerfile` (Node LTS, `ng serve --host 0.0.0.0`)
- [x] T006 Write `docker-compose.yml` defining: postgres, minio, api (depends_on postgres+minio), ui (depends_on api)
- [x] T007 [P] Configure `api/pyproject.toml` with FastAPI, SQLAlchemy 2.x async, asyncpg, Alembic, aiobotocore, pydantic-settings, pytest, pytest-asyncio, ruff
- [x] T008 [P] Configure `ui/package.json` / `angular.json` with eslint + prettier; add `ui/proxy.conf.json` routing `/api/*` to `http://localhost:8000`
- [x] T009 Write API unit test: settings load from env vars without error in `api/tests/unit/test_config.py`
- [x] T010 Write API integration test: `GET /api/v1/health` returns 200 `{"status":"ok"}` in `api/tests/integration/test_health.py`
- [x] T011 Implement `api/app/config.py` (pydantic-settings reading all env vars)
- [x] T012 Implement `api/app/main.py` (FastAPI factory, lifespan connecting to Postgres + MinIO, health route)
- [x] T013 Configure Alembic in `api/alembic/` with async engine; apply `alembic upgrade head` on startup
- [x] T014 [P] Add Angular default smoke test in `ui/src/app/app.component.spec.ts`
**Checkpoint**: `docker compose up` starts all four services. Health endpoint
returns 200. Both linters pass. All tests pass.
---
## Phase 2: Foundational (Upload API Infrastructure — M1 core)
**Purpose**: Core interfaces and repositories that MUST exist before any user
story endpoint can be implemented. Establishes the `StorageBackend`,
`AuthProvider`, and `ImageRepository` abstractions.
**⚠️ CRITICAL**: No user story work can begin until this phase is complete.
- [x] T015 Write unit test: SHA-256 hash of known bytes returns expected hex digest in `api/tests/unit/test_hashing.py`
- [x] T016 Write unit test: MIME validator accepts jpeg/png/gif/webp and rejects pdf/mp4 in `api/tests/unit/test_validation.py`
- [x] T017 Write unit test: file size validator rejects bytes exceeding MAX_UPLOAD_BYTES in `api/tests/unit/test_validation.py`
- [x] T018 [P] Implement `StorageBackend` interface (put, get_presigned_url, delete) in `api/app/storage/backend.py`
- [x] T019 [P] Implement `S3StorageBackend` using aiobotocore in `api/app/storage/s3_backend.py`
- [x] T020 [P] Implement `AuthProvider` interface + `NoOpAuthProvider` in `api/app/auth/provider.py` and `api/app/auth/noop.py`
- [x] T021 Implement MIME type + file size validation helpers in `api/app/routers/images.py` (or `api/app/validation.py`)
- [x] T022 Write Alembic migration for `images` table in `api/alembic/versions/`
- [x] T023 Implement `Image` SQLAlchemy model in `api/app/models.py`
- [x] T024 Implement `ImageRepository` (create, get_by_id, get_by_hash) in `api/app/repositories/image_repo.py`
- [x] T025 Wire `AuthProvider`, `StorageBackend`, and DB session into FastAPI dependency injection in `api/app/dependencies.py`
**Checkpoint**: All unit tests pass; foundation ready for user story endpoints.
---
## Phase 3: User Story 1 — Upload an Image (M1 endpoint + M5 UI) 🎯 MVP
**Goal**: A user can upload an image (with tags, though tag persistence is
deferred to Phase 4). Duplicate detection works. Errors shown inline.
**Independent Test**: Upload a JPEG via the UI, verify it appears in the
library grid. Re-upload the same file, verify "Already in your library" toast
and no duplicate in DB or MinIO.
### Tests for User Story 1 (REQUIRED per §5.1 — TDD) ⚠️
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
- [x] T026 [P] [US1] Integration test: valid JPEG upload → 201, record in DB, object in MinIO in `api/tests/integration/test_upload.py`
- [x] T027 [P] [US1] Integration test: same image uploaded twice → 200, `duplicate: true`, no second MinIO object in `api/tests/integration/test_upload.py`
- [x] T028 [P] [US1] Integration test: invalid MIME type → 422 with `{"detail":"...","code":"invalid_mime_type"}` in `api/tests/integration/test_upload.py`
- [x] T029 [P] [US1] Integration test: file > MAX_UPLOAD_BYTES → 422 `file_too_large` in `api/tests/integration/test_upload.py`
- [x] T030 [P] [US1] Angular unit test: tag chip input lowercases and splits on comma/space in `ui/src/app/upload/upload.component.spec.ts`
- [x] T031 [P] [US1] Angular unit test: `duplicate: true` response → toast shown, navigate to detail in `ui/src/app/upload/upload.component.spec.ts`
- [x] T032 [P] [US1] Angular unit test: `duplicate: false` response → success toast, navigate to detail in `ui/src/app/upload/upload.component.spec.ts`
- [x] T033 [P] [US1] Angular unit test: error response → inline error shown, no navigation in `ui/src/app/upload/upload.component.spec.ts`
### Implementation for User Story 1
- [x] T034 [US1] Implement `POST /api/v1/images` endpoint (MIME check, size check, SHA-256, duplicate query, storage write, record insert; tags field accepted but ignored) in `api/app/routers/images.py`
- [x] T035 [US1] Implement `ImageService` wrapping `GET /api/v1/images` and `GET /api/v1/images/{id}/file` in `ui/src/app/services/image.service.ts`
- [x] T036 [US1] Implement `UploadComponent` (route `/upload`) with drag-and-drop zone, click-to-browse, tag chip input, POST submit, duplicate/success/error handling in `ui/src/app/upload/upload.component.ts`
**Checkpoint**: Full upload flow works in browser. Duplicate detection gives
correct feedback. API tests and Angular unit tests all pass.
---
## Phase 4: User Story 2 — Browse and Filter the Library (M2 list/search + M4 UI)
**Goal**: User can view all images in a responsive grid and filter by one or
more tags (AND logic). Pagination works. Tags are persisted during upload.
**Independent Test**: Seed the library via the upload flow. Apply a single tag
filter and verify only matching images are shown. Add a second filter, verify
both tags must be present. Remove a filter, verify the grid expands.
### Tests for User Story 2 (REQUIRED per §5.1 — TDD) ⚠️
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
- [x] T037 [P] [US2] Unit test: tag normalisation — uppercase → lowercase, whitespace stripped in `api/tests/unit/test_tags.py`
- [x] T038 [P] [US2] Unit test: tag validation — rejects names > 64 chars, invalid chars in `api/tests/unit/test_tags.py`
- [x] T039 [P] [US2] Integration test: upload with tags → tags persisted, returned in response in `api/tests/integration/test_tags.py`
- [x] T040 [P] [US2] Integration test: duplicate upload → existing record returned, tags unchanged in `api/tests/integration/test_tags.py`
- [x] T041 [P] [US2] Integration test: `GET /api/v1/images?tags=cat,funny` → only images with both tags in `api/tests/integration/test_search.py`
- [x] T042 [P] [US2] Integration test: same query excludes images with only one matching tag in `api/tests/integration/test_search.py`
- [x] T043 [P] [US2] Angular unit test: `ImageService` constructs correct query params from filter state in `ui/src/app/services/image.service.spec.ts`
- [x] T044 [P] [US2] Angular unit test: `LibraryComponent` renders image grid from mocked service in `ui/src/app/library/library.component.spec.ts`
- [x] T045 [P] [US2] Angular unit test: filter change triggers new API call with updated `tags` param in `ui/src/app/library/library.component.spec.ts`
### Implementation for User Story 2
- [x] T046 [US2] Write Alembic migration for `tags` and `image_tags` tables in `api/alembic/versions/`
- [x] T047 [US2] Implement `Tag` and `ImageTag` SQLAlchemy models in `api/app/models.py`
- [x] T048 [US2] Implement tag normalisation + validation helpers in `api/app/repositories/tag_repo.py`
- [x] T049 [US2] Implement `TagRepository` (upsert_by_name, get_by_image_id) in `api/app/repositories/tag_repo.py`
- [x] T050 [US2] Update `POST /api/v1/images` to process and persist the `tags` field in `api/app/routers/images.py`
- [x] T051 [US2] Implement `GET /api/v1/images` with `tags` (AND-filter), `limit`, `offset` in `api/app/routers/images.py`
- [x] T052 [US2] Implement `GET /api/v1/images/{id}` returning image + tags in `api/app/routers/images.py`
- [x] T053 [US2] Update `ImageService` to support `tags` filter query param in `ui/src/app/services/image.service.ts`
- [x] T054 [US2] Implement `LibraryComponent` (route `/`) with image grid, tag chips, debounced filter bar, "Load more" pagination in `ui/src/app/library/library.component.ts`
**Checkpoint**: Library view shows real images with tags. Tag filtering (AND
logic) and pagination work end-to-end.
---
## Phase 5: User Story 3 — View Image Detail and Edit Tags (M3 serving + M6 detail UI)
**Goal**: User can view a full-size image and edit its tags inline. Changes
saved on blur/Enter.
**Independent Test**: Open an image detail page, add a new tag, navigate back,
filter by that tag, and confirm the image appears.
### Tests for User Story 3 (REQUIRED per §5.1 — TDD) ⚠️
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
- [x] T055 [P] [US3] Integration test: `GET /api/v1/images/{id}/file` → 302 with `Location` header pointing to MinIO URL in `api/tests/integration/test_serving.py`
- [x] T056 [P] [US3] Integration test: `/file` for unknown ID → 404 `image_not_found` in `api/tests/integration/test_serving.py`
- [x] T057 [P] [US3] Integration test: `PATCH /api/v1/images/{id}/tags` replaces tags, old tags unlinked, new tags upserted in `api/tests/integration/test_tags.py`
- [x] T058 [P] [US3] Integration test: PATCH with invalid tag → 422 `invalid_tag` in `api/tests/integration/test_tags.py`
- [x] T059 [P] [US3] Angular unit test: removing tag chip calls PATCH with updated list (removed tag absent) in `ui/src/app/detail/detail.component.spec.ts`
- [x] T060 [P] [US3] Angular unit test: adding tag + Enter calls PATCH with new tag included in `ui/src/app/detail/detail.component.spec.ts`
### Implementation for User Story 3
- [x] T061 [US3] Implement `GET /api/v1/images/{id}/file` (generate 1-hour pre-signed URL, return 302) in `api/app/routers/images.py`
- [x] T062 [US3] Implement `TagRepository.replace_tags_on_image` in `api/app/repositories/tag_repo.py`
- [x] T063 [US3] Implement `PATCH /api/v1/images/{id}/tags` in `api/app/routers/images.py`
- [x] T064 [US3] Implement `DetailComponent` (route `/images/:id`) with full-size image, editable tag chips (add/remove), save on blur/Enter via PATCH, back button in `ui/src/app/detail/detail.component.ts`
**Checkpoint**: Full-size image loads in browser via redirect. Tag editing
works from detail page. Changes persist across page navigation.
---
## Phase 6: User Story 4 — Delete an Image (M6 detail UI + M2 delete endpoint)
**Goal**: User can permanently delete an image with confirmation. Returns to
library afterwards.
**Independent Test**: Delete a known image, confirm it no longer appears in the
library and that navigating to its former URL shows a not-found screen.
### Tests for User Story 4 (REQUIRED per §5.1 — TDD) ⚠️
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
- [x] T065 [P] [US4] Integration test: `DELETE /api/v1/images/{id}` → 204; subsequent `GET /{id}` returns 404 in `api/tests/integration/test_delete.py`
- [x] T066 [P] [US4] Integration test: DELETE verifies MinIO object is removed in `api/tests/integration/test_delete.py`
- [x] T067 [P] [US4] Integration test: DELETE of unknown ID → 404 `image_not_found` in `api/tests/integration/test_delete.py`
- [x] T068 [P] [US4] Angular unit test: delete confirmation → DELETE called → navigation to Library in `ui/src/app/detail/detail.component.spec.ts`
- [x] T069 [P] [US4] Angular unit test: cancel confirmation dialog → no DELETE call, stays on detail page in `ui/src/app/detail/detail.component.spec.ts`
### Implementation for User Story 4
- [x] T070 [US4] Implement `DELETE /api/v1/images/{id}` (delete image_tags rows, image record, S3 object) in `api/app/routers/images.py`
- [x] T071 [US4] Add delete button with confirmation dialog + back-to-Library navigation to `DetailComponent` in `ui/src/app/detail/detail.component.ts`
- [x] T072 [US4] Implement `NotFoundComponent` shown for all unrecognised routes in `ui/src/app/not-found/not-found.component.ts`
**Checkpoint**: Full CRUD loop works: upload → view → re-tag → delete.
Deleted images gone from library and storage.
---
## Phase 7: User Story 5 — Browse and Search Tags (M2 tags endpoint + UI)
**Goal**: User can list all tags alphabetically with image counts and narrow
by prefix.
**Independent Test**: Open the tag browser, verify every tag present in the
library appears with a correct image count. Type a prefix and verify only
matching tags remain.
### Tests for User Story 5 (REQUIRED per §5.1 — TDD) ⚠️
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
- [x] T073 [P] [US5] Integration test: `GET /api/v1/tags` returns all tags alphabetically with correct image_count in `api/tests/integration/test_tags.py`
- [x] T074 [P] [US5] Integration test: `GET /api/v1/tags?q=ca` returns only tags prefixed "ca" in `api/tests/integration/test_tags.py`
- [x] T075 [P] [US5] Angular unit test: `TagService` calls `GET /api/v1/tags` with `q` param in `ui/src/app/services/tag.service.spec.ts`
### Implementation for User Story 5
- [x] T076 [US5] Implement `GET /api/v1/tags` with `q` prefix search, `limit`, `offset`, image_count in `api/app/routers/tags.py`
- [x] T077 [US5] Implement `TagService` wrapping `GET /api/v1/tags` in `ui/src/app/services/tag.service.ts`
- [x] T078 [US5] Wire `TagService` into `LibraryComponent` tag filter bar for tag autocomplete/selection in `ui/src/app/library/library.component.ts`
**Checkpoint**: All user stories independently functional and tested.
---
## Phase 8: Polish & Cross-Cutting Concerns
**Purpose**: Improvements affecting multiple user stories and final validation.
- [x] T079 [P] Add `GET /api/v1/images/{id}` 404 test to verify error envelope shape `{"detail":"...","code":"image_not_found"}` in `api/tests/integration/test_upload.py`
- [x] T080 [P] Verify all API error responses include both `detail` and `code` fields (constitution §3.3) — check tests for T028, T029, T056, T058, T067
- [x] T081 [P] Add empty-state UI for library with zero images in `ui/src/app/library/library.component.ts`
- [x] T082 [P] Add empty-state UI for tag filter returning zero results in `ui/src/app/library/library.component.ts`
- [x] T083 Configure Angular routing to show `NotFoundComponent` for all unrecognised routes in `ui/src/app/app.routes.ts`
- [x] T084 [P] Run quickstart.md validation: `docker compose up`, upload an image, filter by tag, edit tag, delete image — full happy path
- [x] T085 [P] Run `ruff check .` in `api/` — confirm zero lint errors
- [x] T086 [P] Run `npm run lint` in `ui/` — confirm zero lint errors
- [x] T087 Run all API tests: `docker compose run --rm api pytest` — confirm all pass
- [x] T088 Run all UI tests: `docker compose run --rm ui ng test --watch=false` — confirm all pass
---
## Dependencies & Execution Order
### Phase Dependencies
- **Phase 1 (Setup)**: No dependencies — start immediately
- **Phase 2 (Foundational)**: Depends on Phase 1 — BLOCKS all user stories
- **Phase 3 (US1 Upload)**: Depends on Phase 2 — API endpoint + Angular upload UI
- **Phase 4 (US2 Browse)**: Depends on Phase 3 (tags API needs upload first)
- **Phase 5 (US3 Detail/Edit)**: Depends on Phase 4 (detail view needs list view navigation)
- **Phase 6 (US4 Delete)**: Depends on Phase 5 (delete is in the detail component)
- **Phase 7 (US5 Tags)**: Can start after Phase 4 (tag endpoint is independent)
- **Phase 8 (Polish)**: Depends on all prior phases
### User Story Dependencies
- **US1 (Upload)**: Foundational complete → API endpoint first, then Angular component
- **US2 (Browse)**: US1 must exist to seed data; tag schema (Phase 4) needed
- **US3 (View/Edit)**: US2 complete (library navigation leads to detail)
- **US4 (Delete)**: US3 complete (delete is on the detail view)
- **US5 (Tags)**: Independent after Phase 4 tag endpoint; can start in parallel with US3/US4
### Within Each Phase
- Test tasks MUST be written and confirmed FAILING before implementation
- Models before repositories before endpoints
- API endpoint before Angular service
- Angular service before Angular component
- Story complete before moving to next phase
### Parallel Opportunities
- All Phase 1 tasks marked [P] can run in parallel after T001/T002
- T018T020 (interfaces) can run in parallel
- All integration tests within a phase marked [P] can be written in parallel
- Angular unit tests within a phase marked [P] can be written in parallel
- T085/T086/T087/T088 (lint + test runs) can run in parallel
---
## Parallel Example: Phase 3 (User Story 1)
```bash
# Write all tests for US1 in parallel (all different files):
T026: api/tests/integration/test_upload.py (new upload)
T027: api/tests/integration/test_upload.py (duplicate)
T028: api/tests/integration/test_upload.py (invalid MIME)
T029: api/tests/integration/test_upload.py (oversized)
T030: ui/src/app/upload/upload.component.spec.ts (tag chips)
T031: ui/src/app/upload/upload.component.spec.ts (duplicate response)
# Then implement sequentially:
T034: POST /api/v1/images endpoint
T035: Angular ImageService
T036: Angular UploadComponent
```
---
## Implementation Strategy
### MVP First (User Stories 1 + 2 only)
1. Complete Phase 1: Setup
2. Complete Phase 2: Foundational — BLOCKS all stories
3. Complete Phase 3: US1 Upload
4. Complete Phase 4: US2 Browse/Filter
5. **STOP and VALIDATE**: Upload via UI, filter by tag, verify end-to-end
6. Can demo at this point: full read+write loop without detail view
### Incremental Delivery
1. Phase 1 + 2 → Foundation ready
2. Phase 3 → US1 Upload works (MVP start)
3. Phase 4 → US2 Browse + tag filtering works (MVP complete)
4. Phase 5 → US3 Detail + tag editing
5. Phase 6 → US4 Delete (completes full CRUD)
6. Phase 7 → US5 Tag browser (supplementary)
7. Phase 8 → Polish + final validation
---
## Notes
- [P] = different files, no incomplete dependencies — safe to parallelise
- [USN] label maps task to user story for traceability
- TDD is non-negotiable (§5.1): test → fail → implement → pass
- API tests run against real Postgres + MinIO (no mocks per §5.2)
- Milestone done-criterion: all tests pass + linter passes
- `docker compose up` must keep working after every milestone