feat(coach): scope filter + conversation provenance on the plans list

Groundwork for the plan confirm/manage UI. The CoachPlan pipeline has been
complete on the server since v1.21.3/v1.22 — extractor writes proposals
(status "proposed", per-user cap, de-dup, encrypted prose), the PATCH
confirms proposed -> active, buildCoachPlansBlock injects the top-6 active
plans into the snapshot, and the review sweep drives the reviewDate
lifecycle — but no surface ever listed or confirmed a proposal, so the
extraction spend had no user-visible outcome.

Two additive contract changes the UI needs:
- GET /api/coach/plans accepts ?scope=open|past|all (open = proposed +
  active + review_due, past = met + abandoned + reviewed, all = no status
  clause) so the management page reads its ledger in one round-trip.
  Mutually exclusive with ?status=; both omitted keeps today's
  non-terminal default unchanged.
- Plan items now carry sourceConversationId so the chat thread can show
  only the proposals born in the open conversation.

OpenAPI regenerated; route tests cover the scope mappings, the
status+scope 422, and the provenance passthrough.
This commit is contained in:
Marc-André Bombeck committed 2026-07-05 19:45:20 +02:00
1 parent 4d0f16d5a9
commit acd2f29da2
6 files changed
+152 -16

No files matched your search

+20 -4
View File
@@ -17,10 +17,26 @@ import { z } from "zod/v4";
import { COACH_PLAN_STATUSES } from "@/lib/ai/coach/plans";
/** Optional `?status=` filter on the list endpoint. */
export const coachPlansListQuerySchema = z.object({
status: z.enum(COACH_PLAN_STATUSES).optional(),
});
/**
* Optional `?scope=` filter on the list endpoint — a named status group so
* the plans management surface can pull its whole ledger in one read:
* - `open`: proposed + active + review_due (everything still standing)
* - `past`: met + abandoned + reviewed (the settled history)
* - `all`: every non-deleted plan
* Additive next to `?status=`; the two are mutually exclusive per request.
*/
export const COACH_PLAN_SCOPES = ["open", "past", "all"] as const;
/** Optional `?status=` / `?scope=` filters on the list endpoint. */
export const coachPlansListQuerySchema = z
.object({
status: z.enum(COACH_PLAN_STATUSES).optional(),
scope: z.enum(COACH_PLAN_SCOPES).optional(),
})
.refine(
(v) => !(v.status !== undefined && v.scope !== undefined),
"Provide either status or scope, not both",
);
/**
* The user-facing PATCH body. `status` is the only mutable lifecycle field; a