Feature #1541
closedFeature #1511: Replace ticket view with Kanban board for CTO and assigned developers
Aggregator: emit assignee and priority on ticket_list
0%
docs/superpowers/specs/2026-08-03-kanban-board-design.md
Description
**Surface:** aggregator (`../aggregator` — separate sibling repo)
**Depends on:** nothing
**Blocks (soft):** "API: assignee, priority and parent on the task list payload"
Extend the aggregator's `GET /api/projects/{project_key}` so each `ticket_list` entry carries the two fields the board's cards need and that the read-plane does not expose today: the **assignee** (stable id + display name) and the **priority** (stable id/key + display name + ordering position). Source them from the Redmine enrichment path that already populates `ticket_list`.
Both fields must be **nullable and purely additive**: Redmine issues can be unassigned, and priority may be absent. The dashboard is deployed on its own pipeline and will at some point run against either side of this change, in both directions.
The dashboard must keep getting all board data from here — no direct Redmine call is added downstream (dashboard gotcha #3: the aggregator is the only source of truth).
## Acceptance criteria
- [ ] Every `ticket_list` entry on `GET /api/projects/{project_key}` carries `assignee` (nullable object: id, display_name) and `priority` (nullable object: id/key, name, position).
- [ ] Both fields are declared optional/nullable in the response schema; a fixture with no assignee and no priority serialises to explicit `null` and returns 200.
- [ ] An issue whose Redmine assignee is a group or a deleted user serialises to `null` rather than raising.
- [ ] The change is purely additive: a consumer written against the previous schema still validates the new response (regression test on the pre-change response shape). No existing field is renamed, removed, or made non-nullable.
- [ ] Call-count assertion: rendering a project's ticket list issues the same number of upstream Redmine requests as before the change — no per-ticket detail call is introduced.
- [ ] A contract test pins the exact field names and types agreed with the dashboard's mapper, so a later rename fails here rather than in production.
- [ ] The enrichment doc under `../aggregator/docs/` documents both fields, their nullability, and their Redmine source.
- [ ] Gates green: `python -m pytest -q && ruff check .` in `../aggregator`.
RA Updated by Redmine Admin 1 day ago
- Status changed from Spec to In development
Ticket claimed — starting backend work on the aggregator read-plane (`ticket_list` assignee/priority enrichment).
RA Updated by Redmine Admin 1 day ago
- Status changed from In development to QA
- branch set to feat/1541-ticket-list-assignee-priority
- pr_url set to https://github.com/omdev-tech/aggregator/pull/24
PR ready for QA: https://github.com/omdev-tech/aggregator/pull/24 (aggregator repo → `dev`)
Branch: `feat/1541-ticket-list-assignee-priority`, branched off a fresh `origin/master`.
**What shipped.** Every `GET /api/projects/{key}` → `ticket_list` entry now carries two nullable objects, sourced from the Redmine enrichment path that already populates the list:
- `assignee_ref` → `{id, display_name}`
- `priority_ref` → `{id, key, name, position}`
**One judgement call the reviewer must check.** The ticket asks for fields *named* `assignee` and `priority`, but those two keys **already exist today as flat display-name strings**. Retyping them string → object would break every consumer written against the previous schema, contradicting this ticket's own strongest AC ("purely additive… no existing field is renamed, removed, or made non-nullable"). So the structured data landed under new keys and the flat strings are untouched. The dashboard's outward `TaskSummaryOut` names in child #1542 are independent of the wire names here, so the epic's contract is unaffected — #1542 simply maps `assignee_ref`/`priority_ref` → `assignee`/`priority`.
**`position` source & call count.** The issue payload carries the priority id/name but not its rank, so `position` comes from Redmine's `/enumerations/issue_priorities.json` (ordered by its `position` column). It is fetched **once per poll**, hoisted out of the project loop — not per project, not per ticket — so no per-ticket detail call is added and the request count stays flat as tickets grow. If it is unavailable, only `position` degrades to `null`.
**Known limitation (documented in `docs/ENRICHMENT.md`).** A group assignee is nulled when Redmine tags the principal with `type: "Group"`. On a Redmine version that omits `type` from `assigned_to`, a group is indistinguishable from a user in the issue-list payload and is emitted as a ref. Resolving that would require an extra admin-only `/groups.json` call per poll, which the call-count AC rules out. The flat `assignee` string has always had the same ambiguity.
**Acceptance criteria** — all met: nullable + explicit-null serialisation; group/deleted-user → `null` rather than raising; purely additive with a regression fence over all 24 pre-change fields; call-count assertion at 3 and 150 tickets; a contract test pinning the exact field names and types; `docs/ENRICHMENT.md` documents both fields, their nullability and their Redmine source.
**Gate green:** `python -m pytest -q` → 158 passed (148 before), run twice; `ruff check .` → all checks passed.