- 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>
5.3 KiB
Implementation Plan: Tag Browser
Branch: 007-tag-browser | Date: 2026-05-06 | Spec: spec.md
Input: Feature specification from specs/007-tag-browser/spec.md
Summary
Add a /tags page that lists every tag with its image count, sorted by popularity, each linking to the filtered library view. Requires: (1) two new query parameters on the existing /api/v1/tags endpoint to support sort-by-count and zero-count exclusion, (2) query-parameter-driven filtering on the library route so tag browser links deep-link correctly, (3) a new TagBrowserComponent, and (4) a navigation entry point from the library.
Technical Context
Language/Version: Python 3.12 (API), TypeScript strict / Angular 19 (UI) Primary Dependencies: FastAPI, SQLAlchemy 2.x async, Angular standalone components Storage: PostgreSQL (read-only for this feature — no schema changes) Testing: pytest + httpx (API integration), Jasmine/Karma (Angular unit) Target Platform: Web (same stack as all prior features) Project Type: Web service + SPA Performance Goals: Tag list page load perceived as instant (same bar as library) Constraints: No schema changes; no new dependencies; counts must be accurate at page-load time Scale/Scope: Personal library — tag count is bounded; no pagination UI needed for tag browser, but the API call uses existing paginated endpoint
Constitution Check
| Principle | Status | Notes |
|---|---|---|
| §2.1 Strict separation of concerns | ✅ | UI calls API; API owns all DB logic |
| §2.5 Repository layer | ✅ | All query changes go in TagRepository.list_tags() |
| §2.6 No speculative abstraction | ✅ | No new interfaces; extends existing repo method |
§3.1 API versioning /api/v1/ |
✅ | Modifying existing versioned endpoint |
| §3.2 OpenAPI as contract | ✅ | New query params documented via FastAPI |
| §3.3 Error shape | ✅ | No new error paths |
| §3.4 Pagination | ✅ | Existing endpoint already paginates; tag browser fetches with limit=500 (safe upper bound for a personal library) |
| §4.1 Tags lowercase normalised | ✅ | No change to tag creation/normalisation |
| §5.1 TDD non-negotiable | ✅ | Tests written before implementation in tasks |
| §5.3 Tests colocated | ✅ | API tests in api/tests/, Angular spec next to component |
| §6 Tech stack | ✅ | No new dependencies |
| §7.3 Linting/formatting enforced | ✅ | ng lint + ruff gates in tasks |
Gate: All principles pass. Phase 0 research not required — no unknowns.
Project Structure
Documentation (this feature)
specs/007-tag-browser/
├── plan.md ← this file
├── research.md ← not required (no unknowns)
├── data-model.md ← see below (derived data, no schema changes)
├── contracts/
│ └── tags-endpoint.md ← enhanced GET /api/v1/tags contract
└── tasks.md ← generated by /speckit-tasks
Source Code Changes
api/
├── app/
│ ├── repositories/
│ │ └── tag_repo.py ← extend list_tags() with sort + min_count params
│ └── routers/
│ └── tags.py ← expose sort + min_count as query params
└── tests/
├── integration/
│ └── test_tags.py ← new tests: sort=count_desc, min_count=1
└── unit/
└── test_tags.py ← unit tests for repo sort/filter logic (if applicable)
ui/src/app/
├── tags/
│ ├── tags.component.ts ← new TagBrowserComponent
│ └── tags.component.spec.ts ← component tests
├── services/
│ └── tag.service.ts ← add sort param to list() method
├── library/
│ └── library.component.ts ← read ?tags= query param on init; add /tags nav link
└── app.routes.ts ← add /tags route (lazy-loaded)
Design Decisions
API: extend existing endpoint rather than add new one
The /api/v1/tags endpoint already returns tags with image_count. Two new optional query parameters make it serve the tag browser without breaking existing callers (the library autocomplete uses the endpoint unchanged):
sort:name(default, current behaviour) |count_desc(tag browser use case)min_count: integer, default0(all tags, current behaviour) |1(excludes zero-count tags)
Library: query param deep-linking
The library component currently manages activeFilters in memory only. Adding ?tags=cat,funny query parameter support (read on ngOnInit via ActivatedRoute) allows the tag browser to link directly to a pre-filtered library view. The library already uses addFilter() / applyFilter() internally — reading from query params simply pre-populates activeFilters before the initial load() call. Navigation from within the library that changes filters should update the URL to keep it shareable, but that is a polish concern — minimum requirement is that arriving at /?tags=cat shows the cat-filtered library.
Tag browser UI layout
A responsive chip/card grid sorted by count descending. Each item shows the tag name and count. Each item is a routerLink to /?tags=<name>. Follows the existing design token system (--surface, --accent, --chip styles). Empty state if no tags exist.