Feature #1544
closedFeature #1511: Replace ticket view with Kanban board for CTO and assigned developers
API: per-user board column preferences
0%
docs/superpowers/specs/2026-08-03-kanban-board-design.md
Description
**Surface:** backend (`api/`)
**Depends on:** nothing
**Blocks:** "Frontend: Kanban board replaces the ticketing list view"
The board's column show/hide filter must persist per user (epic AC #4). No per-user preference store exists in this repo today — the only server-persisted per-user setting is `clients.language` via `PATCH /auth/me/language`. This ticket creates the first real one, deliberately scoped to board columns rather than built as a general-purpose settings service.
Add an Alembic migration for a `(user, project)`-keyed table holding the hidden column status ids, plus read and write endpoints under the authenticated user's namespace. Validate incoming ids against the canonical `STAGE_ORDER` (`domain/tasks/value_objects.py:92`) — do not re-derive stage order.
**Default when no row exists:** Shipped (13) hidden, every other status column shown.
> **Open PO question, resolve before building:** the epic permits *"browser cache **or** backend storage"*. This ticket specifies the server-side store, which additionally gives cross-device persistence. A localStorage-only implementation would satisfy the same user-visible AC for a fraction of the work (precedent: `src/features/claude/fallbackPreference.ts`), at the cost of per-device preferences and this whole child. Confirm before starting.
## Acceptance criteria
- [ ] Alembic migration creates the table with a unique constraint on `(user_id, project_id)`; `alembic upgrade head` then `downgrade` both run clean against an ephemeral `pgvector/pgvector:pg16`.
- [ ] A GET endpoint returns the caller's hidden column ids for a project; with **no stored row** it returns the documented default (exactly the Shipped status id) with 200 — not a 404.
- [ ] A write endpoint persists the set and is idempotent: a repeated identical write leaves exactly one row.
- [ ] A status id absent from `STAGE_ORDER` is rejected with 422 and nothing is persisted.
- [ ] A stored preference referencing a status id later removed from `STAGE_ORDER` is ignored gracefully — 200 with that entry dropped, never a 500.
- [ ] **Isolation:** user A's write leaves user B's read unchanged for the same project, and a write on project 1 does not affect project 2.
- [ ] A caller cannot read or write preferences for another user, nor for a project they are not a member of — same not-found shape used elsewhere, no existence leak.
- [ ] **Preferences cannot widen visibility:** hiding or showing columns leaves the set of ticket ids returned by the board endpoint identical (test). Column preferences are presentation-only and must never interact with the D2 matrix.
- [ ] Hiding every column is either rejected or documented as legal and rendered as an explicit empty-board state; whichever is chosen is asserted by a test.
- [ ] Gates green: `python -m pytest -q` (**run serially**), `ruff check .`, `mypy src`.