- Extends GET /api/v1/tags with sort=count_desc and min_count query params - New TagsComponent at /tags (public, no auth guard) shows all tags sorted by image count - Clicking a tag navigates to /?tags=<name> for a pre-filtered library view - LibraryComponent reads ?tags= query param on init to support deep-linking from tag browser - Library header gains a "Browse tags" link to /tags for discoverability - All 15 TDD tasks complete; ruff, ng lint, and ng build clean Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8.5 KiB
Tasks: Tag Browser
Input: Design documents from specs/007-tag-browser/
Prerequisites: plan.md ✅, spec.md ✅, data-model.md ✅, contracts/ ✅, quickstart.md ✅
Tests: TDD is non-negotiable (§5.1). Every implementation task is preceded by a failing-test task. Test tasks MUST be written and confirmed failing before the corresponding implementation task begins.
Organization: Foundational API + service changes first (block all stories), then one phase per user story.
Format: [ID] [P?] [Story] Description
- [P]: Can run in parallel with other [P] tasks in the same phase
- [Story]: Which user story this task belongs to
- Exact file paths included in every task description
Phase 1: Setup
No new project structure required. The existing layout accommodates all changes.
Phase 2: Foundational — API Enhancement & Service Update
Purpose: Extend GET /api/v1/tags with sort and min_count query parameters; update the Angular TagService to pass them. All three user stories depend on the API returning tags sorted by count with zero-count tags excluded.
⚠️ CRITICAL: No user story work can begin until this phase is complete.
- T001 [P] Write failing API integration tests for
sort=count_descandmin_count=1params inapi/tests/integration/test_tags.py— assert response is ordered highest-count-first and excludes zero-count tags - T002 [P] Write failing spec for updated
TagService.list()acceptingsortandminCountparams inui/src/app/services/tag.service.spec.ts— final signature:list(prefix = '', limit = 100, offset = 0, sort?: string, minCount?: number) - T003 Extend
TagRepository.list_tags()inapi/app/repositories/tag_repo.py— addsort: str = "name"andmin_count: int = 0params; applyORDER BY image_count DESC, name ASCwhensort="count_desc"; applyHAVING image_count >= min_countfilter — run AFTER T001 (TDD) - T004 Expose
sortandmin_countas optional query params inapi/app/routers/tags.py— pass through totag_repo.list_tags()— run AFTER T003 - T005 Update
TagService.list()inui/src/app/services/tag.service.ts— final signature:list(prefix = '', limit = 100, offset = 0, sort?: string, minCount?: number); includesortandmin_countinHttpParamswhen provided — run AFTER T002 (TDD)
Execution order: T001 ∥ T002 → T003 (after T001), T005 (after T002) → T004 (after T003)
Checkpoint: GET /api/v1/tags?sort=count_desc&min_count=1 returns tags sorted by image count descending with zero-count tags excluded. TagService.list() passes the new params.
Phase 3: User Story 1 — Browse All Tags (Priority: P1) 🎯 MVP
Goal: A /tags page that lists every tag (with count ≥ 1) sorted from most-used to least-used, with loading skeleton, empty state, and error state matching the existing design system.
Independent Test: Navigate to /tags while logged out. Confirm every tag with at least one image is shown with its count, ordered by count descending. Confirm the empty state appears when no tags exist.
Tests for User Story 1
- T006 [US1] Write failing spec for
TagBrowserComponentinui/src/app/tags/tags.component.spec.tscovering: (a) skeleton shown while loading, (b) tag list rendered with name and count after load, (c) tags ordered by count descending, (d) empty state shown when tag list is empty, (e) error state shown on fetch failure with retry button, (f) each rendered tag element has anhrefof/?tags=<tagname>(FR-005 coverage), (g) component renders whenAuthServiceis not present / user is unauthenticated (FR-006 coverage)
Implementation for User Story 1
- T007 [US1] Create
TagBrowserComponentinui/src/app/tags/tags.component.ts— standalone component; on init calltagService.list('', 500, 0, 'count_desc', 1)(positional order matches T005 signature); display tag chips with name + count; each chip is arouterLink="/"with[queryParams]="{tags: tag.name}"so the href renders as/?tags=<name>; include skeleton loading state (reuse.skeletonclass from global styles), empty state, and error state with retry; apply design tokens throughout - T008 [P] [US1] Add
/tagslazy route toui/src/app/app.routes.ts— loadTagBrowserComponent; no auth guard (public route)
Checkpoint: /tags renders a sorted, filterable tag list visible without authentication.
Phase 4: User Story 2 — Navigate from Tag to Library (Priority: P1)
Goal: Clicking a tag on the tag browser navigates to the library pre-filtered to that tag. Requires the library to read ?tags=<name> from the URL on init and apply it as an active filter before the first image load.
Independent Test: Navigate directly to /?tags=cat in the browser. Confirm the library loads showing only images tagged cat and the cat chip appears in the active filter bar.
Tests for User Story 2
- T009 [US2] Write failing spec for
LibraryComponentreading?tags=query param inui/src/app/library/library.component.spec.ts— assert that when the component initialises with?tags=catin the URL,activeFilterscontains['cat']andimageService.listis called with['cat']
Implementation for User Story 2
- T010 [US2] Update
LibraryComponentinui/src/app/library/library.component.ts— injectActivatedRoute; inngOnInit, readsnapshot.queryParamMap.get('tags'); if present, split by comma, setactiveFiltersbefore callingload()so the first fetch is already filtered
Checkpoint: Navigating to /?tags=cat from the tag browser shows the correctly filtered library.
Phase 5: User Story 3 — Tag Browser Discoverable from Library (Priority: P2)
Goal: A visible "Browse tags" link in the library page header navigates to /tags. Makes the tag browser discoverable without requiring the user to type the URL.
Independent Test: Load the library page. Confirm a link to /tags is visible in the header and navigates correctly when clicked.
Tests for User Story 3
- T011 [US3] Write failing spec for library nav link to
/tagsinui/src/app/library/library.component.spec.ts— assert a link element withhref="/tags"is present in the rendered header
Implementation for User Story 3
- T012 [US3] Add "Browse tags"
routerLink="/tags"link toLibraryComponentheader inui/src/app/library/library.component.ts— place alongside the existing Upload button; style consistently with the existing header button pattern
Checkpoint: All three user stories are independently functional.
Phase 6: Polish & Cross-Cutting Concerns
- T013 [P] Run
ruff check api/app/ api/tests/and fix any violations - T014 [P] Run
ng lintinui/— zero violations required - T015 Run
ng buildinui/— zero errors required
Dependencies & Execution Order
Phase Dependencies
- Phase 2 (Foundational): Blocks all user story phases — must complete first
- Phase 3 (US1): Depends on Phase 2 — TagBrowserComponent needs the sorted tag endpoint
- Phase 4 (US2): Depends on Phase 2 — library deep-link needs no API change, but should follow US1 for coherent testing
- Phase 5 (US3): Depends on Phase 3 (needs the
/tagsroute to exist for the link to be meaningful) - Phase 6 (Polish): Depends on all prior phases
Within Phase 2
- T001 ∥ T002 (different repos, both write failing tests)
- T003 after T001 (TDD: failing test must exist first)
- T005 after T002 (TDD: failing test must exist first)
- T003 ∥ T005 (different repos, after their respective tests)
- T004 after T003 (router wraps repo)
Execution Order (Phase 2)
Step 1 (parallel): T001, T002
Step 2 (parallel): T003 (after T001), T005 (after T002)
Step 3: T004 (after T003)
Parallel Opportunities (Phases 3–5)
- T007 and T008 are parallel within Phase 3
Implementation Strategy
MVP (US1 + US2 — both P1)
- Complete Phase 2 (Foundational)
- Complete Phase 3 (US1 — TagBrowserComponent)
- Complete Phase 4 (US2 — library deep-link)
- Validate: Navigate from tag browser → library → confirm pre-filtered results
- Phases 5–6 add discoverability and polish
Incremental Delivery
- After Phase 3:
/tagspage is live and usable (visitors can browse tags) - After Phase 4: clicking a tag works end-to-end (browse → filtered library)
- After Phase 5: tag browser is discoverable from the library without typing the URL
- After Phase 6: lint and build clean, ready for merge