From b7074e04b886cccf3c4aac2e874d6e43c6031677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 30 Jul 2026 19:55:29 +0200 Subject: [PATCH] docs(openapi): add GET/PUT /api/auth/me/report-selection Documents the saved doctor-report scope endpoint, reusing the real savedReportProfileSchema so the spec's leaf cap and version literal stay tied to the source of truth. --- docs/api/openapi.yaml | 131 ++++++++++++++++++++++++++++++ src/lib/openapi/routes/profile.ts | 74 +++++++++++++++++ 2 files changed, 205 insertions(+) diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 9e7b33d60..f9a3ce047 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -5699,6 +5699,48 @@ paths: $ref: "#/components/schemas/ErrorEnvelope" "422": *a3 "429": *a4 + /api/auth/me/report-selection: + get: + tags: + - Auth + summary: Read the owner's saved doctor-report selection + description: "Returns the saved report-scope profile, or `profile: null` when the account has never saved one (or the + stored blob no longer parses against the current leaf catalogue). Replaces the retired `GET + /api/auth/me/doctor-report-prefs`. Auth via cookie or Bearer." + responses: + "200": + description: The saved profile, or null. + content: + application/json: + schema: + $ref: "#/components/schemas/GetReportSelectionResponseEnvelope" + "401": *a1 + "422": *a3 + "429": *a4 + put: + tags: + - Auth + summary: Replace the owner's saved doctor-report selection + description: Replace, not merge — the body states the whole scope or it is not a scope; a leaf omitted from the body is + a leaf removed from the saved profile. An unknown leaf id is refused with a 422 that names it. The saved + selection is also what the FHIR REST face and the MCP doctor-visit surfaces replay for a caller that cannot ask + a human, so every write is audit-logged. Auth via cookie or Bearer. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/SavedReportProfile" + responses: + "200": + description: The saved profile, echoed back. + content: + application/json: + schema: + $ref: "#/components/schemas/PutReportSelectionResponseEnvelope" + "401": *a1 + "422": *a3 + "429": *a4 /api/insights/chat: get: tags: @@ -13691,6 +13733,40 @@ components: `null` switches the alert off. `baseUpdatedAt` is the optional optimistic-concurrency token: echo it to have the server refuse (409) a write that would silently revert a category another writer set from a fresher read (e.g. iOS `medication.clientManaged`). Omit it for the legacy unconditional write." + SavedReportProfile: + type: object + properties: + v: + type: number + const: 2 + leaves: + maxItems: 91 + type: array + items: + type: string + minLength: 1 + maxLength: 64 + format: + type: string + enum: + - pdf + - fhir + - package + rangeDays: + type: integer + minimum: 1 + maximum: 365 + includeCharts: + type: boolean + required: + - v + - leaves + - format + - rangeDays + - includeCharts + additionalProperties: false + description: "A saved doctor-report scope: which leaves are included plus the export format, trailing window and chart + toggle. Replace-only — there is no partial-merge write." FencedCoachChatRequest: type: object properties: @@ -24686,6 +24762,61 @@ components: - data - error additionalProperties: false + GetReportSelectionResponseEnvelope: + type: object + properties: + data: + $ref: "#/components/schemas/GetReportSelectionResponse" + error: + type: "null" + meta: + type: object + properties: + requestId: + type: string + additionalProperties: false + required: + - data + - error + additionalProperties: false + GetReportSelectionResponse: + type: object + properties: + profile: + anyOf: + - $ref: "#/components/schemas/SavedReportProfile" + - type: "null" + description: The saved profile, or null when the account has never saved one, or when the stored blob no longer parses + against the current leaf catalogue. Null is never resolved to a default scope server-side — the client + applies its own named template and shows that as a visible choice. + required: + - profile + additionalProperties: false + PutReportSelectionResponseEnvelope: + type: object + properties: + data: + $ref: "#/components/schemas/PutReportSelectionResponse" + error: + type: "null" + meta: + type: object + properties: + requestId: + type: string + additionalProperties: false + required: + - data + - error + additionalProperties: false + PutReportSelectionResponse: + type: object + properties: + profile: + $ref: "#/components/schemas/SavedReportProfile" + required: + - profile + additionalProperties: false CoachConversationsPageResponse: type: object properties: diff --git a/src/lib/openapi/routes/profile.ts b/src/lib/openapi/routes/profile.ts index 9271ef953..dd724fe96 100644 --- a/src/lib/openapi/routes/profile.ts +++ b/src/lib/openapi/routes/profile.ts @@ -12,6 +12,7 @@ import { notificationPrefsSchema } from "@/lib/validations/notification-prefs"; import { sourcePrioritySchema } from "@/lib/validations/source-priority"; import { modulePrefsPatchSchema } from "@/lib/validations/modules"; import { MODULE_KEYS } from "@/lib/modules/registry"; +import { savedReportProfileSchema } from "@/lib/report-selection/profile-shape"; import { dataEnvelope, errorEnvelope, @@ -199,6 +200,33 @@ const coachPrefsPutRequest = coachPrefsSchema "Coach prompt-tuning preferences to persist, plus the optional optimistic-concurrency `baseUpdatedAt` token. The guarded write covers both `coachPrefsJson` and the mirrored `insightsExcludeMetrics` column atomically. Omit the token for the legacy unconditional write.", }); +// v1.28-era — the owner's saved doctor-report selection (leaf list plus the +// render choices the export panel restores). `v` is the report-selection +// schema version (currently locked at 2); `leaves` is a closed catalogue of +// section/field ids, canonicalised to a fixed order server-side so two +// clients that chose the same scope persist the same bytes. This is the ONLY +// request body for the PUT — it replaces the stored profile whole, it never +// merges. +const savedReportProfile = savedReportProfileSchema.meta({ + id: "SavedReportProfile", + description: + "A saved doctor-report scope: which leaves are included plus the export format, trailing window and chart toggle. Replace-only — there is no partial-merge write.", +}); + +const reportSelectionGetResponse = z + .object({ + profile: savedReportProfile + .nullable() + .describe( + "The saved profile, or null when the account has never saved one, or when the stored blob no longer parses against the current leaf catalogue. Null is never resolved to a default scope server-side — the client applies its own named template and shows that as a visible choice.", + ), + }) + .meta({ id: "GetReportSelectionResponse" }); + +const reportSelectionPutResponse = z + .object({ profile: savedReportProfile }) + .meta({ id: "PutReportSelectionResponse" }); + // v1.8.6 — profile read/write surface for the native client. The // runtime validation lives in `src/lib/validations/auth.ts` // (`profileSchema`, whose transforms normalise empty → null and run the @@ -850,4 +878,50 @@ export const profilePaths: NonNullable = { }, }, }, + "/api/auth/me/report-selection": { + get: { + tags: ["Auth"], + summary: "Read the owner's saved doctor-report selection", + description: + "Returns the saved report-scope profile, or `profile: null` when the account has never saved one (or the stored blob no longer parses against the current leaf catalogue). Replaces the retired `GET /api/auth/me/doctor-report-prefs`. Auth via cookie or Bearer.", + responses: { + "200": { + description: "The saved profile, or null.", + content: { + "application/json": { + schema: dataEnvelope( + reportSelectionGetResponse, + "GetReportSelectionResponseEnvelope", + ), + }, + }, + }, + ...stdResponses, + }, + }, + put: { + tags: ["Auth"], + summary: "Replace the owner's saved doctor-report selection", + description: + "Replace, not merge — the body states the whole scope or it is not a scope; a leaf omitted from the body is a leaf removed from the saved profile. An unknown leaf id is refused with a 422 that names it. The saved selection is also what the FHIR REST face and the MCP doctor-visit surfaces replay for a caller that cannot ask a human, so every write is audit-logged. Auth via cookie or Bearer.", + requestBody: { + required: true, + content: { "application/json": { schema: savedReportProfile } }, + }, + responses: { + "200": { + description: "The saved profile, echoed back.", + content: { + "application/json": { + schema: dataEnvelope( + reportSelectionPutResponse, + "PutReportSelectionResponseEnvelope", + ), + }, + }, + }, + ...stdResponses, + }, + }, + }, };