mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
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.
58 lines
2.3 KiB
TypeScript
58 lines
2.3 KiB
TypeScript
/**
|
|
* v1.21.3 (B1) — request schemas for the Coach goal / if-then plan surface
|
|
* (`/api/coach/plans` + `/api/coach/plans/[id]`).
|
|
*
|
|
* Lives outside the route files so the OpenAPI registry can import it without
|
|
* touching the route modules (route files may only export handlers + config)
|
|
* and without dragging the Prisma-backed helper module into the generator
|
|
* script.
|
|
*
|
|
* No `userId` field anywhere — the owner is always narrowed from the session.
|
|
* The extractor is the only writer of plan TEXT; this surface only lets the
|
|
* user CONFIRM (proposed → active), mark a plan met / abandoned, set a review
|
|
* date, or soft-delete. It never accepts the encrypted free-text fields, so a
|
|
* client can never inject or overwrite a plan's prose.
|
|
*/
|
|
import { z } from "zod/v4";
|
|
|
|
import { COACH_PLAN_STATUSES } from "@/lib/ai/coach/plans";
|
|
|
|
/**
|
|
* 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
|
|
* `proposed` plan moves to `active` (confirm), or a plan moves to `met` /
|
|
* `abandoned`. `reviewDate` optionally pins / clears a check-in checkpoint
|
|
* (null clears). Strict: unknown keys 422, so a client cannot smuggle a
|
|
* userId, a metric, or the encrypted text.
|
|
*/
|
|
export const coachPlanPatchSchema = z
|
|
.object({
|
|
status: z.enum(COACH_PLAN_STATUSES).optional(),
|
|
reviewDate: z.iso.datetime({ offset: true }).nullable().optional(),
|
|
})
|
|
.strict()
|
|
.refine(
|
|
(v) => v.status !== undefined || v.reviewDate !== undefined,
|
|
"At least one of status or reviewDate must be provided",
|
|
);
|