Feat: Add tag browser page at /tags with count-sorted tag list and library deep-link
- 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>
This commit is contained in:
96
specs/007-tag-browser/plan.md
Normal file
96
specs/007-tag-browser/plan.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user