[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>
This commit is contained in:
2026-05-02 16:13:23 +00:00
parent 691f7570fe
commit 8bf6ef443a
74 changed files with 3005 additions and 88 deletions

View File

@@ -40,20 +40,20 @@ ui/src/app/<feature>/ Angular component dirs (+ .spec.ts colocated)
**Purpose**: Establish the monorepo layout, Docker Compose stack, and linting
baseline. No feature logic. All subsequent milestones build on this.
- [ ] T001 Create top-level monorepo layout: `api/`, `ui/`, `docker-compose.yml`, `.env.example`
- [ ] T002 Write `.env.example` with all variables from spec §5 (DATABASE_URL, S3_*, API_BASE_URL, MAX_UPLOAD_BYTES)
- [ ] T003 [P] Write `api/Dockerfile` (Python 3.12 slim, installs pyproject.toml deps, runs uvicorn)
- [ ] T004 [P] Scaffold Angular project with CLI into `ui/` (strict mode, standalone components, routing)
- [ ] T005 [P] Write `ui/Dockerfile` (Node LTS, `ng serve --host 0.0.0.0`)
- [ ] T006 Write `docker-compose.yml` defining: postgres, minio, api (depends_on postgres+minio), ui (depends_on api)
- [ ] T007 [P] Configure `api/pyproject.toml` with FastAPI, SQLAlchemy 2.x async, asyncpg, Alembic, aiobotocore, pydantic-settings, pytest, pytest-asyncio, ruff
- [ ] T008 [P] Configure `ui/package.json` / `angular.json` with eslint + prettier; add `ui/proxy.conf.json` routing `/api/*` to `http://localhost:8000`
- [ ] T009 Write API unit test: settings load from env vars without error in `api/tests/unit/test_config.py`
- [ ] T010 Write API integration test: `GET /api/v1/health` returns 200 `{"status":"ok"}` in `api/tests/integration/test_health.py`
- [ ] T011 Implement `api/app/config.py` (pydantic-settings reading all env vars)
- [ ] T012 Implement `api/app/main.py` (FastAPI factory, lifespan connecting to Postgres + MinIO, health route)
- [ ] T013 Configure Alembic in `api/alembic/` with async engine; apply `alembic upgrade head` on startup
- [ ] T014 [P] Add Angular default smoke test in `ui/src/app/app.component.spec.ts`
- [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.
@@ -68,17 +68,17 @@ story endpoint can be implemented. Establishes the `StorageBackend`,
**⚠️ CRITICAL**: No user story work can begin until this phase is complete.
- [ ] T015 Write unit test: SHA-256 hash of known bytes returns expected hex digest in `api/tests/unit/test_hashing.py`
- [ ] T016 Write unit test: MIME validator accepts jpeg/png/gif/webp and rejects pdf/mp4 in `api/tests/unit/test_validation.py`
- [ ] T017 Write unit test: file size validator rejects bytes exceeding MAX_UPLOAD_BYTES in `api/tests/unit/test_validation.py`
- [ ] T018 [P] Implement `StorageBackend` interface (put, get_presigned_url, delete) in `api/app/storage/backend.py`
- [ ] T019 [P] Implement `S3StorageBackend` using aiobotocore in `api/app/storage/s3_backend.py`
- [ ] T020 [P] Implement `AuthProvider` interface + `NoOpAuthProvider` in `api/app/auth/provider.py` and `api/app/auth/noop.py`
- [ ] T021 Implement MIME type + file size validation helpers in `api/app/routers/images.py` (or `api/app/validation.py`)
- [ ] T022 Write Alembic migration for `images` table in `api/alembic/versions/`
- [ ] T023 Implement `Image` SQLAlchemy model in `api/app/models.py`
- [ ] T024 Implement `ImageRepository` (create, get_by_id, get_by_hash) in `api/app/repositories/image_repo.py`
- [ ] T025 Wire `AuthProvider`, `StorageBackend`, and DB session into FastAPI dependency injection in `api/app/dependencies.py`
- [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.
@@ -97,20 +97,20 @@ and no duplicate in DB or MinIO.
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
- [ ] T026 [P] [US1] Integration test: valid JPEG upload → 201, record in DB, object in MinIO in `api/tests/integration/test_upload.py`
- [ ] T027 [P] [US1] Integration test: same image uploaded twice → 200, `duplicate: true`, no second MinIO object in `api/tests/integration/test_upload.py`
- [ ] T028 [P] [US1] Integration test: invalid MIME type → 422 with `{"detail":"...","code":"invalid_mime_type"}` in `api/tests/integration/test_upload.py`
- [ ] T029 [P] [US1] Integration test: file > MAX_UPLOAD_BYTES → 422 `file_too_large` in `api/tests/integration/test_upload.py`
- [ ] T030 [P] [US1] Angular unit test: tag chip input lowercases and splits on comma/space in `ui/src/app/upload/upload.component.spec.ts`
- [ ] T031 [P] [US1] Angular unit test: `duplicate: true` response → toast shown, navigate to detail in `ui/src/app/upload/upload.component.spec.ts`
- [ ] T032 [P] [US1] Angular unit test: `duplicate: false` response → success toast, navigate to detail in `ui/src/app/upload/upload.component.spec.ts`
- [ ] T033 [P] [US1] Angular unit test: error response → inline error shown, no navigation in `ui/src/app/upload/upload.component.spec.ts`
- [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
- [ ] 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`
- [ ] T035 [US1] Implement `ImageService` wrapping `GET /api/v1/images` and `GET /api/v1/images/{id}/file` in `ui/src/app/services/image.service.ts`
- [ ] 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`
- [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.
@@ -130,27 +130,27 @@ both tags must be present. Remove a filter, verify the grid expands.
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
- [ ] T037 [P] [US2] Unit test: tag normalisation — uppercase → lowercase, whitespace stripped in `api/tests/unit/test_tags.py`
- [ ] T038 [P] [US2] Unit test: tag validation — rejects names > 64 chars, invalid chars in `api/tests/unit/test_tags.py`
- [ ] T039 [P] [US2] Integration test: upload with tags → tags persisted, returned in response in `api/tests/integration/test_tags.py`
- [ ] T040 [P] [US2] Integration test: duplicate upload → existing record returned, tags unchanged in `api/tests/integration/test_tags.py`
- [ ] T041 [P] [US2] Integration test: `GET /api/v1/images?tags=cat,funny` → only images with both tags in `api/tests/integration/test_search.py`
- [ ] T042 [P] [US2] Integration test: same query excludes images with only one matching tag in `api/tests/integration/test_search.py`
- [ ] T043 [P] [US2] Angular unit test: `ImageService` constructs correct query params from filter state in `ui/src/app/services/image.service.spec.ts`
- [ ] T044 [P] [US2] Angular unit test: `LibraryComponent` renders image grid from mocked service in `ui/src/app/library/library.component.spec.ts`
- [ ] 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`
- [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
- [ ] T046 [US2] Write Alembic migration for `tags` and `image_tags` tables in `api/alembic/versions/`
- [ ] T047 [US2] Implement `Tag` and `ImageTag` SQLAlchemy models in `api/app/models.py`
- [ ] T048 [US2] Implement tag normalisation + validation helpers in `api/app/repositories/tag_repo.py`
- [ ] T049 [US2] Implement `TagRepository` (upsert_by_name, get_by_image_id) in `api/app/repositories/tag_repo.py`
- [ ] T050 [US2] Update `POST /api/v1/images` to process and persist the `tags` field in `api/app/routers/images.py`
- [ ] T051 [US2] Implement `GET /api/v1/images` with `tags` (AND-filter), `limit`, `offset` in `api/app/routers/images.py`
- [ ] T052 [US2] Implement `GET /api/v1/images/{id}` returning image + tags in `api/app/routers/images.py`
- [ ] T053 [US2] Update `ImageService` to support `tags` filter query param in `ui/src/app/services/image.service.ts`
- [ ] T054 [US2] Implement `LibraryComponent` (route `/`) with image grid, tag chips, debounced filter bar, "Load more" pagination in `ui/src/app/library/library.component.ts`
- [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.
@@ -169,19 +169,19 @@ filter by that tag, and confirm the image appears.
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
- [ ] 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`
- [ ] T056 [P] [US3] Integration test: `/file` for unknown ID → 404 `image_not_found` in `api/tests/integration/test_serving.py`
- [ ] 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`
- [ ] T058 [P] [US3] Integration test: PATCH with invalid tag → 422 `invalid_tag` in `api/tests/integration/test_tags.py`
- [ ] 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`
- [ ] T060 [P] [US3] Angular unit test: adding tag + Enter calls PATCH with new tag included in `ui/src/app/detail/detail.component.spec.ts`
- [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
- [ ] T061 [US3] Implement `GET /api/v1/images/{id}/file` (generate 1-hour pre-signed URL, return 302) in `api/app/routers/images.py`
- [ ] T062 [US3] Implement `TagRepository.replace_tags_on_image` in `api/app/repositories/tag_repo.py`
- [ ] T063 [US3] Implement `PATCH /api/v1/images/{id}/tags` in `api/app/routers/images.py`
- [ ] 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`
- [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.
@@ -200,17 +200,17 @@ library and that navigating to its former URL shows a not-found screen.
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
- [ ] T065 [P] [US4] Integration test: `DELETE /api/v1/images/{id}` → 204; subsequent `GET /{id}` returns 404 in `api/tests/integration/test_delete.py`
- [ ] T066 [P] [US4] Integration test: DELETE verifies MinIO object is removed in `api/tests/integration/test_delete.py`
- [ ] T067 [P] [US4] Integration test: DELETE of unknown ID → 404 `image_not_found` in `api/tests/integration/test_delete.py`
- [ ] T068 [P] [US4] Angular unit test: delete confirmation → DELETE called → navigation to Library in `ui/src/app/detail/detail.component.spec.ts`
- [ ] 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`
- [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
- [ ] T070 [US4] Implement `DELETE /api/v1/images/{id}` (delete image_tags rows, image record, S3 object) in `api/app/routers/images.py`
- [ ] T071 [US4] Add delete button with confirmation dialog + back-to-Library navigation to `DetailComponent` in `ui/src/app/detail/detail.component.ts`
- [ ] T072 [US4] Implement `NotFoundComponent` shown for all unrecognised routes in `ui/src/app/not-found/not-found.component.ts`
- [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.
@@ -230,15 +230,15 @@ matching tags remain.
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
- [ ] T073 [P] [US5] Integration test: `GET /api/v1/tags` returns all tags alphabetically with correct image_count in `api/tests/integration/test_tags.py`
- [ ] T074 [P] [US5] Integration test: `GET /api/v1/tags?q=ca` returns only tags prefixed "ca" in `api/tests/integration/test_tags.py`
- [ ] T075 [P] [US5] Angular unit test: `TagService` calls `GET /api/v1/tags` with `q` param in `ui/src/app/services/tag.service.spec.ts`
- [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
- [ ] T076 [US5] Implement `GET /api/v1/tags` with `q` prefix search, `limit`, `offset`, image_count in `api/app/routers/tags.py`
- [ ] T077 [US5] Implement `TagService` wrapping `GET /api/v1/tags` in `ui/src/app/services/tag.service.ts`
- [ ] T078 [US5] Wire `TagService` into `LibraryComponent` tag filter bar for tag autocomplete/selection in `ui/src/app/library/library.component.ts`
- [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.
@@ -248,16 +248,16 @@ matching tags remain.
**Purpose**: Improvements affecting multiple user stories and final validation.
- [ ] 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`
- [ ] T080 [P] Verify all API error responses include both `detail` and `code` fields (constitution §3.3) — check tests for T028, T029, T056, T058, T067
- [ ] T081 [P] Add empty-state UI for library with zero images in `ui/src/app/library/library.component.ts`
- [ ] T082 [P] Add empty-state UI for tag filter returning zero results in `ui/src/app/library/library.component.ts`
- [ ] T083 Configure Angular routing to show `NotFoundComponent` for all unrecognised routes in `ui/src/app/app.routes.ts`
- [ ] T084 [P] Run quickstart.md validation: `docker compose up`, upload an image, filter by tag, edit tag, delete image — full happy path
- [ ] T085 [P] Run `ruff check .` in `api/` — confirm zero lint errors
- [ ] T086 [P] Run `npm run lint` in `ui/` — confirm zero lint errors
- [ ] T087 Run all API tests: `docker compose run --rm api pytest` — confirm all pass
- [ ] T088 Run all UI tests: `docker compose run --rm ui ng test --watch=false` — confirm all pass
- [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
---