# 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=<name>`. Follows the existing design token system (`--surface`, `--accent`, `--chip` styles). Empty state if no tags exist.
