# Saved views

**Status:** in production
**Lives at:** the view picker in each filtered page header (`/entries`,
`/projects`, `/cc-expenses`) + the "Pinned" nav group. Code:
`app/models/saved_view.py`, `app/services/saved_views.py`,
`app/routers/saved_views.py`, `frontend/src/lib/savedViews.ts`,
`frontend/src/components/SavedViewPicker.tsx`, the AppShell `PinnedViews`.
**Spec:** 09-build-plan.md §M8 + §UX program ("saved views — the
'easier to navigate' centerpiece; the hardcoded quick-chips retire").

## Summary
Per-user named views over any filtered table: name the current filter/
sort state, re-apply it in one click, pin the recurring ones into the
nav. This retires the old hardcoded "quick chips".

## Why
The old app's navigation grew by accretion (a 10-item nav) and its
"quick filters" were hardcoded per page — rian couldn't save his own
recurring views ("last month's PlusROI", the pending backlog, blocked
triage). The rebuild's IA is redesigned; saved views are the promised
R1 "easier to navigate" centerpiece.

## Behavior
- Every filtered page (`/entries`, `/projects`, CC batches) has a
  **Views ▾** picker in its header. It lists the user's views for that
  page (pinned first), applies one in a click (replacing the URL filter
  state), and shows which view — if any — matches the current filters.
- **Name this view** captures the current URL query string. Optionally
  pin it. Names are unique per (user, page), case-insensitive.
- **Manage** inline per view: rename, delete, pin/unpin.
- **Pinned views surface in the nav** under a "Pinned" group — one-click
  links to their page + filter state from anywhere in the app.
- **Seeded recurring views** (idempotent code, not migration data) give
  rian his known views on first boot: entries → *Last month's PlusROI*,
  *Pending backlog*, *Blocked triage*; projects → *Hourly blocks*
  (the load-bearing support-block drawdown view — `date=all` is
  required), *Missing info*, *Last month admin*. These replace the
  retired quick-chips 1:1 so nothing is lost.

## Constraints & edge cases
- **Per-user, keyed on the REAL user** (`actor.real_user_id`) — a
  personal nav preference, never impersonated. While a super admin
  views-as someone, they still see and manage THEIR OWN views (like a
  browser's own bookmarks). Deliberately different from the
  effective-user identity that gates tenant data (ADR #009).
- **Not tenant data / not audited.** A view holds only a saved filter
  string the owner chose; its mutations are not written to the tenant
  audit trail (the security route-audit meta-test allowlists these
  routes).
- Bounds: name ≤ 80 chars, search string ≤ 2 KB. Unknown `page` (not
  `entries`/`projects`/`cc`) → 422. Duplicate name → 409 (DB unique
  index `uq_saved_views_user_page_lname` + a pre-check + a race guard).
- A view id you don't own is a 404 (never reveals another user's view).
- CC batches carries the picker for parity/pin-to-nav even though it has
  no filter controls yet (future filters get views for free).

## Permissions
Any signed-in user manages their own views (requires a resolved `users`
row → `real_user_id`; else 401). No capability gate — views are personal
prefs, not tenant actions. The CC page itself remains owner-only (the
picker only renders past that gate).

## Open considerations
- View sharing / team views (a tenant-scoped view) — R2+, deliberately
  out of R1 (views are personal now).
- Drag-reorder of pinned views (sort_order exists; only seeded order is
  set today).

## Tests
- Service (SQLite): `tests/saved_views/test_saved_views_service.py` —
  create/list ordering, page filter, `?`-strip, case-insensitive
  uniqueness (per page, per user), page/name validation, 401 without a
  real user, rename/pin/reorder payload-absent guard, per-user isolation
  (404 on another user's id), idempotent default-view seeding.
- API/auth (Postgres harness): `tests/saved_views/test_saved_views_api.py`
  — anon 401, create→list→patch→delete round-trip, 409 dup, 422 bad
  page, per-user isolation over HTTP.
- Frontend (vitest): `frontend/src/lib/savedViews.test.ts` — stable
  `searchToString` serialization, round-trip, active-view compare.

## Changelog
- 2026-07-08 — Shipped (M8, 0.9.0). New `saved_views` table (alembic
  0005), `/api/saved-views` CRUD, the header picker + nav pinning, and
  the seeded recurring views. Retired the hardcoded quick-chips on
  `/entries` and `/projects`.
