Skip to main content

Labels & goal views

Task labels are free-form tags (string[], GIN-indexed, default []) that are not project-scoped. That makes them Project Tracker's lightest cross-project grouping key: a label such as q3-launch used across several projects behaves like a "goal that spans projects" without any new entity. (The heavier, structured cross-project constructs are cross-project epics (PT-446) and cross-cutting initiatives (PT-447).)

Three surfaces read labels, each with a deliberately different authorisation bar.

Cross-project label goal view (PT-448)

GET /api/tasks?labels=<label> lists every task carrying any of the requested labels across every project the caller can read (tenant-scoped). It requires full tasks.read (the list is not ownership-scoped, so a tasks.read.own-only caller is rejected — mirrors the per-project full task list) and projects.read (or .own) plus an active membership; a GUEST member or a projects.read.own-only caller is restricted to collaborator/creator projects. The labels param is required and bounded (at most 20 labels, each ≤ 100 characters).

Access is enforced two ways: tasks in projects the caller cannot read are omitted at the query level (the readable-project set narrows the query, on top of RLS), and any cross-project dependency reference returned on a row is redactedtaskKey and projectId are omitted and title/status are replaced with the redactCrossProjectDeps sentinels ((cross-project dependency) / UNKNOWN), with isCrossProject: true, so only the opaque id survives unchanged and the real key/title never leak (PT-283 precedent). The blocked/blocking indicator is exposed as booleans (isBlocked / isBlocking) derived server-side from same-project TaskLink BLOCKS links and incomplete dependencies, so it never leaks a linked task's identity.

Each row carries { project, taskKey, title, status, priority, isBlocked, isBlocking, dependsOn, dependedOnBy } and is sorted by priority (URGENT first), task key as the tiebreak. Results are capped at the 500 highest-priority rows (the cap is applied after a priority ranking pass, so urgent work is never dropped in favour of lower-priority rows); the response envelope carries truncated: true when more match. This is a fail-safe for high-cardinality labels — full cursor pagination is deferred to the PT-206 board. The goal view is surfaced in PT V2 at /goals?label=<label> — bookmarkable and shareable, since a label is effectively a goal URL.

Label suggestions (PT-482)

GET /api/tasks/labels returns the distinct labels in use for the tenant, most-used first — the autocomplete vocabulary behind the Jira-style label combobox (type-ahead + create-new) on the task-detail editor, the task popup, and the create-task modal. The aggregation is unnest(labels) + COUNT(DISTINCT task_id) (a task is counted once even if its labels array repeats a value) ordered by count desc then label asc, run under the tenant RLS context.

Query params (all optional): projectId (UUID — narrows to one project; omitted = tenant-wide), q (case-insensitive prefix filter, ≤ 100 chars), and limit (clamped to [1, 200], default 50). The response is { data: { labels: [{ label, count }] } }.

Authorisation is deliberately lighter than the goal view: an active member who can read or create tasks — any scope satisfies it, including the .own variants (tasks.read.own / tasks.create.own). The response is non-sensitive autocomplete strings (never task content), so it doesn't need the goal view's full-tasks.read gate.

Task-list label filter + unified filter axes (PT-483)

Both Tasks-page surfaces — the org-wide /tasks page and the project-scoped /projects/:id/tasks page — render the same filter set, fed by /api/my-tasks (the project page pins its projectId). Two changes keep them from drifting:

  • Labels filter. GET /api/my-tasks accepts ?labels=<a,b> (comma-separated, OR semantics, ≤ 20 labels of ≤ 100 chars each — the same parseLabelsParam bounds as the per-project and goal-view routes). Labels are a content field, not a visibility-leak vector, so the filter is honoured in both mode=all and mode=mine. It is task-only: the issue branch returns nothing when the filter is active, in both the Prisma and raw-SQL query paths.
  • Label options endpoint. GET /api/task-labels returns the distinct, sorted set of task labels for populating the filter chip (mirrors /api/issue-tags for the Issues Tag axis), scoped to exactly the tasks the caller can list via /api/my-tasks — distinct from the lighter, most-used-first GET /api/tasks/labels autocomplete vocabulary above (PT-482), which feeds the label editor tenant-wide. The filter-chip endpoint uses the collaborator-or-creator project window for GUEST / projects.read.own-only callers, and — for tasks.read.own-only callers — pins to their own assigned tasks, so the dropdown never exposes a label the caller's task list could not match.
  • Collaborators on both pages. The Collaborators filter (PT-471), previously only on the org-wide page, now also appears on the project page, which forwards collaboratorIds to /api/my-tasks (honoured in mode=all).

See also