# Implementation Plan: Tag Browser **Branch**: `007-tag-browser` | **Date**: 2026-05-06 | **Spec**: [spec.md](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) ```text 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 ```text 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, default `0` (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=`. Follows the existing design token system (`--surface`, `--accent`, `--chip` styles). Empty state if no tags exist.