{"openapi":"3.0.0","paths":{"/api/config":{"get":{"operationId":"AppController_getConfig","parameters":[{"name":"country","required":false,"in":"query","description":"ISO 3166-1 alpha-2 code of the verified App Store / Play storefront (e.g. \"US\"). Region-gated values are withheld when omitted or unsupported — safe fallback to IAP-only. Clients must send the real storefront and omit it when unverified; never a default.","schema":{"type":"string"}},{"name":"platform","required":false,"in":"query","description":"Client platform (\"ios\" | \"android\" | \"web\"). Selects the region allowlist, since Apple and Google permit external purchase links in different countries.","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"summary":"Get public frontend configuration","tags":["App"]}},"/api/health":{"get":{"operationId":"AppController_health","parameters":[],"responses":{"200":{"description":""}},"summary":"Health check","tags":["App"]}},"/api/debug-sentry":{"get":{"operationId":"AppController_getError","parameters":[],"responses":{"200":{"description":""}},"summary":"Sentry smoke test — always throws (disabled in production)","tags":["App"]}},"/api/auth/login":{"post":{"operationId":"AuthController_login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...","id":5,"email":"user@example.com","role":"client","subscriptionTier":"standard","coachConsentRequired":false,"needsHealthDataConsent":false,"emailVerified":true,"phoneVerified":false,"programStatus":null,"profile":{"firstName":"Jane","lastName":"Doe","profileImageUrl":null,"questionnaireCompleted":true,"phone":"+15551234567","city":null,"state":null},"subscription":{"type":"standard","status":"active","trialEndsAt":null,"endsAt":null,"isFounder":false}}}}}},"401":{"description":"Invalid credentials"}},"summary":"Login with email and password","tags":["Auth"]}},"/api/auth/register":{"post":{"operationId":"AuthController_register","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...","user":{"id":6,"email":"new@example.com","role":"user","name":"John Doe"}}}}}},"400":{"description":"Invalid access code"},"409":{"description":"Email already registered"}},"summary":"Register with access code","tags":["Auth"]}},"/api/auth/register-open":{"post":{"description":"Pass ?paywallFlow=v2 from app builds that ship the new store-paywall trial flow. Older app builds that omit it keep getting the automatic no-card open trial unchanged.","operationId":"AuthController_registerOpen","parameters":[{"name":"paywallFlow","required":true,"in":"query","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterOpenDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...","user":{"id":8,"email":"open@example.com","role":"lead"}}}}}}},"summary":"Open registration without access code","tags":["Auth"]}},"/api/auth/logout":{"post":{"operationId":"AuthController_logout","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Logged out successfully"}}}}}},"security":[{"bearer":[]}],"summary":"Logout current user","tags":["Auth"]}},"/api/auth/me":{"get":{"operationId":"AuthController_getMe","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"id":5,"email":"user@example.com","role":"user","name":"Jane Doe","phone":"+1234567890","water_goal_oz":64,"created_at":"2026-01-01T00:00:00.000Z"}}}}}},"security":[{"bearer":[]}],"summary":"Get current user profile","tags":["Auth"]}},"/api/auth/status":{"get":{"operationId":"AuthController_getStatus","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"authenticated":true,"userId":5,"role":"user","trialActive":true,"trialEndsAt":"2026-03-01T00:00:00.000Z","daysRemaining":9}}}}}},"security":[{"bearer":[]}],"summary":"Check auth status and trial info","tags":["Auth"]}},"/api/auth/change-password":{"post":{"operationId":"AuthController_changePassword","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Password changed successfully"}}}}}},"security":[{"bearer":[]}],"summary":"Change password for authenticated user","tags":["Auth"]}},"/api/auth/forgot-password":{"post":{"operationId":"AuthController_forgotPassword","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgotPasswordDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"OTP sent to your email"}}}}}},"summary":"Request password reset OTP","tags":["Auth"]}},"/api/auth/verify-otp":{"post":{"operationId":"AuthController_verifyOtp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyOtpDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"OTP verified","resetToken":"abc123..."}}}}}},"summary":"Verify password reset OTP","tags":["Auth"]}},"/api/auth/reset-password":{"post":{"operationId":"AuthController_resetPassword","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Password reset successfully"}}}}}},"summary":"Reset password with valid OTP","tags":["Auth"]}},"/api/auth/check-phone":{"get":{"operationId":"AuthController_checkPhone","parameters":[{"name":"phone","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"exists":true}}}}}},"summary":"Check if a phone number exists in the system","tags":["Auth"]}},"/api/auth/verify-email":{"get":{"operationId":"AuthController_verifyEmail","parameters":[{"name":"token","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"HTML page confirming verification result"}},"summary":"Verify email address via link from verification email","tags":["Auth"]}},"/api/auth/resend-email-verification":{"post":{"operationId":"AuthController_resendEmailVerification","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"message":"We sent a verification link to your email."}}}}}},"summary":"Resend email verification link","tags":["Auth"]}},"/api/auth/send-phone-verification":{"post":{"operationId":"AuthController_sendPhoneVerification","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"message":"We sent a code to your phone."}}}}}},"summary":"Send phone verification OTP","tags":["Auth"]}},"/api/auth/verify-phone-otp":{"post":{"operationId":"AuthController_verifyPhoneOtp","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"message":"Your phone has been verified.","verified":true}}}}},"400":{"description":"Invalid OTP"}},"summary":"Verify phone OTP","tags":["Auth"]}},"/api/auth/session/end":{"post":{"description":"Called by the mobile app when it goes to background or closes. Logs the session duration so the admin dashboard can compute average time-in-app.","operationId":"AuthController_recordSessionEnd","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true}}}}}},"summary":"Record app session duration","tags":["Auth"]}},"/api/auth/verification-status":{"post":{"operationId":"AuthController_getVerificationStatus","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"exists":true,"emailVerified":true,"phoneVerified":false,"emailMasked":"u***r@example.com","phoneMasked":"***-***-9876"}}}}}},"summary":"Get verification status for account recovery","tags":["Auth"]}},"/api/auth-check":{"get":{"operationId":"AuthCheckController_authCheck","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"authenticated":true,"userId":5,"role":"user"}}}}}},"security":[{"bearer":[]}],"summary":"Validate JWT token and return user identity","tags":["Auth"]}},"/api/subscription/prices":{"get":{"operationId":"SubscriptionController_getPrices","parameters":[],"responses":{"200":{"description":""}},"summary":"Get active Stripe prices for the marketing site","tags":["Subscription"]}},"/api/subscription/founder-status":{"get":{"operationId":"SubscriptionController_founderStatus","parameters":[],"responses":{"200":{"description":""}},"summary":"Check if founder offer is currently active (for marketing site)","tags":["Subscription"]}},"/api/subscription/send-checkout-otp":{"post":{"operationId":"SubscriptionController_sendCheckoutOtp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendCheckoutOtpDto"}}}},"responses":{"201":{"description":""}},"summary":"Send OTP to email for checkout verification","tags":["Subscription"]}},"/api/subscription/verify-checkout-otp":{"post":{"operationId":"SubscriptionController_verifyCheckoutOtp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyCheckoutOtpDto"}}}},"responses":{"201":{"description":""}},"summary":"Verify OTP before initiating Stripe checkout","tags":["Subscription"]}},"/api/subscription/create-checkout":{"post":{"operationId":"SubscriptionController_createCheckout","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCheckoutDto"}}}},"responses":{"201":{"description":""}},"summary":"Create Stripe checkout session for web purchase","tags":["Subscription"]}},"/api/subscription/verify-session":{"get":{"operationId":"SubscriptionController_verifySession","parameters":[{"name":"session_id","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"summary":"Verify a Stripe checkout session (for marketing site success page)","tags":["Subscription"]}},"/api/subscription/webhook/revenuecat":{"post":{"operationId":"SubscriptionController_revenuecatWebhook","parameters":[{"name":"authorization","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"summary":"RevenueCat webhook endpoint","tags":["Subscription"]}},"/api/subscription/webhook/stripe":{"post":{"operationId":"SubscriptionController_stripeWebhook","parameters":[{"name":"stripe-signature","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"summary":"Stripe webhook endpoint (checkout completed only)","tags":["Subscription"]}},"/api/subscription/billing-portal":{"get":{"operationId":"SubscriptionController_billingPortal","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get Stripe billing portal URL for current user","tags":["Subscription"]}},"/api/subscription/status":{"get":{"operationId":"SubscriptionController_getStatus","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get current user active subscription status","tags":["Subscription"]}},"/api/subscription/decline-trial":{"post":{"operationId":"SubscriptionController_declineTrial","parameters":[],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Records that the user picked \"just track free\" over the 7-day trial. Grants no access — marks the user so a later upgrade shows the regular price instead of the trial discount.","tags":["Subscription"]}},"/api/subscription/cancel":{"post":{"operationId":"SubscriptionController_cancelOwn","parameters":[],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"User-initiated period-end cancellation. Stripe only — Apple/Google must be canceled in-store.","tags":["Subscription"]}},"/api/subscription/resume":{"post":{"operationId":"SubscriptionController_resumeOwn","parameters":[],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Undo a previously-scheduled period-end cancellation (Stripe only).","tags":["Subscription"]}},"/api/subscription/extend-for-coaching":{"post":{"operationId":"SubscriptionController_extendForCoaching","parameters":[],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Extend current user subscription for coaching enrollment","tags":["Subscription"]}},"/api/marketing/device":{"post":{"description":"Additive, idempotent — upserts on (user_id, appsflyer_id) and flushes any marketing events that were waiting on this identity (AppsFlyer S2S plan §3.3).","operationId":"MarketingEventsController_registerDevice","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterDeviceDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Register this device's AppsFlyer id against the current user","tags":["Marketing Attribution"]}},"/api/notifications/history":{"get":{"operationId":"PushNotificationController_getHistory","parameters":[{"name":"page","required":false,"in":"query","description":"Page number (default: 1)","schema":{"type":"number"}},{"name":"limit","required":false,"in":"query","description":"Results per page (default: 20)","schema":{"type":"number"}},{"name":"status","required":false,"in":"query","description":"Filter by status","schema":{"enum":["sent","failed","skipped_preference","skipped_muted"],"type":"string"}},{"name":"pushOnly","required":false,"in":"query","description":"Exclude chat notifications (default: true)","schema":{"type":"boolean"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get notification history for the current user","tags":["notifications"]}},"/api/notifications/preferences":{"get":{"operationId":"PushNotificationController_getPreferences","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get notification preferences","tags":["notifications"]},"post":{"operationId":"PushNotificationController_updatePreferences","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePreferencesDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Update notification preferences","tags":["notifications"]}},"/api/notifications/sync-tags":{"post":{"operationId":"PushNotificationController_syncTags","parameters":[],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Sync user tags to OneSignal for campaign segmentation","tags":["notifications"]}},"/api/admin/push-campaigns/preview":{"post":{"description":"Returns an upper-bound recipient count from our DB (filters on role, subscription_tier, is_founder, is_enrolled). Platform / app version filters are evaluated by OneSignal at send time and are not reflected in the count.","operationId":"PushCampaignAdminController_preview","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreviewPushCampaignDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"estimate":412,"isExact":false,"disclaimer":"Platform, app version..."}}}}}},"security":[{"bearer":[]}],"summary":"Estimate audience size for a campaign targeting spec","tags":["Admin Push Campaigns"]}},"/api/admin/push-campaigns/send":{"post":{"description":"Requires at least one targeting dimension (refuses to broadcast to all users). Persists a row in push_campaigns with the audit trail and returns the OneSignal-reported recipient count.","operationId":"PushCampaignAdminController_send","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendPushCampaignDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"campaignId":17,"recipientCount":312,"onesignalNotificationId":"b98cf9..."}}}}}},"security":[{"bearer":[]}],"summary":"Send a push notification campaign to a OneSignal-defined audience","tags":["Admin Push Campaigns"]}},"/api/admin/push-campaigns":{"get":{"operationId":"PushCampaignAdminController_list","parameters":[{"name":"limit","required":false,"in":"query","schema":{"type":"number"}},{"name":"offset","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List sent push campaigns (most recent first)","tags":["Admin Push Campaigns"]}},"/api/chat/sync":{"get":{"operationId":"ChatController_sync","parameters":[{"name":"cursor","required":false,"in":"query","description":"Last seq the client holds. Omit for a cold start.","schema":{"example":"91422","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"minimum":1,"maximum":1000,"default":500,"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Everything that changed since a cursor, across all rooms","tags":["chat"]}},"/api/chat/rooms":{"get":{"operationId":"ChatController_getRooms","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List chat rooms for current user","tags":["chat"]}},"/api/chat/rooms/{roomId}":{"get":{"operationId":"ChatController_getRoom","parameters":[{"name":"roomId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"One chat room, in the same shape the list returns","tags":["chat"]}},"/api/chat/history":{"get":{"operationId":"ChatController_getHistory","parameters":[{"name":"room_id","required":true,"in":"query","description":"Room to page back through","schema":{"type":"number"}},{"name":"before","required":false,"in":"query","description":"Return messages older than this id","schema":{"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"minimum":1,"maximum":100,"default":30,"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Messages before a cursor, for a sync client","tags":["chat"]}},"/api/chat/rooms/{roomId}/messages":{"get":{"operationId":"ChatController_getMessages","parameters":[{"name":"roomId","required":true,"in":"path","schema":{"type":"number"}},{"name":"before","required":false,"in":"query","description":"Load messages before this message ID (cursor pagination)","schema":{"example":100,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"minimum":1,"maximum":100,"default":30,"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get paginated message history","tags":["chat"]},"post":{"operationId":"ChatController_sendMessage","parameters":[{"name":"roomId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Send a message (REST fallback, supports image and file uploads)","tags":["chat"]}},"/api/chat/rooms/{roomId}/messages/{messageId}":{"delete":{"operationId":"ChatController_deleteMessage","parameters":[{"name":"roomId","required":true,"in":"path","schema":{"type":"number"}},{"name":"messageId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Delete a message (soft delete)","tags":["chat"]}},"/api/chat/rooms/{roomId}/done":{"post":{"operationId":"ChatController_markWaitDone","parameters":[{"name":"roomId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Mark a client's 1:1 done so it no longer waits on the coach","tags":["chat"]}},"/api/chat/rooms/{roomId}/read":{"post":{"operationId":"ChatController_markRead","parameters":[{"name":"roomId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Mark messages as read up to a cursor","tags":["chat"]}},"/api/chat/read":{"post":{"operationId":"ChatController_markRoomsRead","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkRoomsReadDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Mark several rooms read up to their newest message","tags":["chat"]}},"/api/chat/rooms/{roomId}/messages/{messageId}/reactions":{"post":{"operationId":"ChatController_setReaction","parameters":[{"name":"roomId","required":true,"in":"path","schema":{"type":"number"}},{"name":"messageId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Set or clear the current user's reaction on a message","tags":["chat"]}},"/api/chat/rooms/{roomId}/media-urls":{"post":{"operationId":"ChatController_signRoomMedia","parameters":[{"name":"roomId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaUrlsDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Re-sign storage keys for media in a room","tags":["chat"]}},"/api/chat/unread-count":{"get":{"deprecated":true,"description":"Deprecated (7 Oct 2026). Apps from that date count unread conversations from the room list instead. Kept for older builds until the minimum app version passes that release.","operationId":"ChatController_getUnreadCount","parameters":[{"name":"roomType","required":false,"in":"query","description":"Count only rooms of this type. Omitted, every room the user belongs to counts.","schema":{"example":"coach_dm","type":"string","enum":["coach_dm","group","client_dm","client_group","pc_lead","staff_dm","staff_team"]}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get total unread message count","tags":["chat"]}},"/api/chat/rooms/{roomId}/photo":{"patch":{"operationId":"ChatController_updateRoomPhoto","parameters":[{"name":"roomId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Upload/replace a group chat room photo","tags":["chat"]},"delete":{"operationId":"ChatController_removeRoomPhoto","parameters":[{"name":"roomId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Remove a group chat room photo","tags":["chat"]}},"/api/chat/rooms/{roomId}/mute":{"put":{"operationId":"ChatController_setRoomMute","parameters":[{"name":"roomId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomMuteDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Mute or unmute a group chat's push notifications for the caller","tags":["chat"]}},"/api/chat/peers/config":{"get":{"operationId":"PeerChatController_config","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Whether client DMs are switched on, and the report reasons","tags":["chat"]}},"/api/chat/peers/settings":{"get":{"operationId":"PeerChatController_getSettings","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"The caller's message-request setting","tags":["chat"]},"patch":{"operationId":"PeerChatController_updateSettings","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PeerSettingsDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Allow or stop message requests from other clients","tags":["chat"]}},"/api/chat/peers/users/{userId}":{"get":{"operationId":"PeerChatController_relationWith","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Where the caller stands with another client","tags":["chat"]}},"/api/chat/peers/requests":{"get":{"operationId":"PeerChatController_listRequests","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Message requests waiting on the caller, the ones they sent, and the ones they declined","tags":["chat"]},"post":{"operationId":"PeerChatController_sendRequest","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PeerUserDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Ask another client from a shared coaching group to message","tags":["chat"]}},"/api/chat/peers/requests/{requestId}/accept":{"post":{"operationId":"PeerChatController_accept","parameters":[{"name":"requestId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Accept a message request, pending or one the caller declined; returns the thread","tags":["chat"]}},"/api/chat/peers/requests/{requestId}/decline":{"post":{"operationId":"PeerChatController_decline","parameters":[{"name":"requestId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Decline a message request. The sender is not told.","tags":["chat"]}},"/api/chat/peers/blocks":{"get":{"operationId":"PeerChatController_listBlocks","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"The clients the caller has blocked","tags":["chat"]},"post":{"operationId":"PeerChatController_block","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PeerUserDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Block another client. They are not told.","tags":["chat"]}},"/api/chat/peers/blocks/{userId}":{"delete":{"operationId":"PeerChatController_unblock","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Unblock a client","tags":["chat"]}},"/api/chat/peers/reports":{"post":{"operationId":"PeerChatController_report","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PeerReportDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Report the other person in a thread, with the messages it is about","tags":["chat"]}},"/api/admin/peer-reports":{"get":{"operationId":"PeerReportsAdminController_list","parameters":[{"name":"status","required":false,"in":"query","schema":{"type":"string","enum":["open","dismissed","warned","suspended","banned"]}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Client-to-client reports, newest first","tags":["Admin"]}},"/api/admin/peer-reports/{reportId}":{"get":{"operationId":"PeerReportsAdminController_get","parameters":[{"name":"reportId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"One report with the messages attached to it","tags":["Admin"]}},"/api/admin/peer-reports/{reportId}/resolve":{"post":{"operationId":"PeerReportsAdminController_resolve","parameters":[{"name":"reportId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolvePeerReportDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Dismiss, warn, suspend or ban","tags":["Admin"]}},"/api/admin/peer-reports/{reportId}/triage":{"post":{"operationId":"PeerReportsAdminController_triage","parameters":[{"name":"reportId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Mark a report as looked at","tags":["Admin"]}},"/api/admin/peer-reports/{reportId}/freeze":{"post":{"operationId":"PeerReportsAdminController_freeze","parameters":[{"name":"reportId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Pause the thread of an open safety report while it is reviewed","tags":["Admin"]}},"/api/admin/peer-reports/{reportId}/unfreeze":{"post":{"operationId":"PeerReportsAdminController_unfreeze","parameters":[{"name":"reportId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Lift the pause on a report's thread","tags":["Admin"]}},"/api/admin/peer-reports/{reportId}/messages/{messageId}/remove":{"post":{"operationId":"PeerReportsAdminController_removeMessage","parameters":[{"name":"reportId","required":true,"in":"path","schema":{"type":"number"}},{"name":"messageId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Remove a reported message from the thread. The report keeps its copy.","tags":["Admin"]}},"/api/admin/peer-reports/{reportId}/legal-hold":{"post":{"operationId":"PeerReportsAdminController_legalHold","parameters":[{"name":"reportId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PeerLegalHoldDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Keep a report past its retention period, or release it","tags":["Admin"]}},"/api/admin/peer-reports/users/{userId}/lift":{"post":{"operationId":"PeerReportsAdminController_lift","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"End a client's messaging suspension or ban","tags":["Admin"]}},"/api/chat/staff/directory":{"get":{"operationId":"StaffChatController_directory","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"The other coaches, with the thread to each if one exists","tags":["chat"]}},"/api/chat/staff/dm":{"post":{"operationId":"StaffChatController_openDm","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenStaffDmDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Open the 1:1 thread with another coach","tags":["chat"]}},"/api/chat/staff/team":{"get":{"operationId":"StaffChatController_team","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"The coaching team channel","tags":["chat"]}},"/api/admin/chat/settings":{"get":{"operationId":"ChatSettingsAdminController_getSettings","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Chat-wide settings, including the client DM release switch","tags":["Admin"]},"patch":{"operationId":"ChatSettingsAdminController_updateSettings","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatSettingsDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Turn client-to-client DMs on or off for every client","tags":["Admin"]}},"/api/chat/pins":{"patch":{"operationId":"ChatPinsController_update","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatPinsUpdateDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Pin or unpin chats for the caller","tags":["chat"]}},"/api/admin/work-items":{"get":{"operationId":"AdminWorkItemsController_list","parameters":[{"name":"workType","required":false,"in":"query","schema":{"type":"string"}},{"name":"status","required":false,"in":"query","schema":{"type":"string","enum":["open","assigned","resolved","dismissed"]}},{"name":"assigneeId","required":false,"in":"query","schema":{"type":"number"}},{"name":"page","required":false,"in":"query","schema":{"type":"number"}},{"name":"pageSize","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Ops queue: every work item (scheduling sweeps, alert assignments, …), filterable and paged","tags":["Admin"]}},"/api/admin/work-items/{id}":{"patch":{"operationId":"AdminWorkItemsController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWorkItemDto"}}}},"responses":{"400":{"description":"Nothing to update, or the assignee is not an active coach"},"409":{"description":"The item is owned by another flow (see readOnlyReason), or already closed"}},"security":[{"bearer":[]}],"summary":"Resolve, dismiss, or reassign an open work item. Items owned by a client alert or an assignment step are read-only here; assignees must be active coaches.","tags":["Admin"]}},"/api/referral/validate":{"get":{"operationId":"ReferralController_validate","parameters":[{"name":"token","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"summary":"Validate a referral token from a QR code link","tags":["referral"]}},"/api/admin/provisioning":{"get":{"operationId":"GhlProvisioningAdminController_list","parameters":[{"name":"status","required":true,"in":"query","schema":{"type":"string"}},{"name":"q","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Provisioning rows: filter by status, search by email or GHL contact id","tags":["Admin"]}},"/api/admin/provisioning/program-tags":{"get":{"operationId":"GhlProvisioningAdminController_listTags","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Which GHL tag means which program","tags":["Admin"]},"post":{"operationId":"GhlProvisioningAdminController_addTag","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddProgramTagDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Map another GHL tag to a program (stored lowercased)","tags":["Admin"]}},"/api/admin/provisioning/program-tags/{tagId}":{"delete":{"operationId":"GhlProvisioningAdminController_removeTag","parameters":[{"name":"tagId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Remove a tag mapping; contacts left with no mapped tag are revoked when they next change","tags":["Admin"]}},"/api/admin/provisioning/by-user/{userId}":{"get":{"operationId":"GhlProvisioningAdminController_forUser","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"The provisioning row behind a client (Sold-By), or null","tags":["Admin"]}},"/api/admin/provisioning/{id}":{"get":{"operationId":"GhlProvisioningAdminController_detail","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"One row with its full change log","tags":["Admin"]},"patch":{"operationId":"GhlProvisioningAdminController_edit","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EditProvisioningDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Change program or end date; the row stops following GHL","tags":["Admin"]}},"/api/admin/provisioning/{id}/retry":{"post":{"operationId":"GhlProvisioningAdminController_retry","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Re-run matching and the grant from the row's stored GHL values","tags":["Admin"]}},"/api/admin/provisioning/{id}/dismiss":{"post":{"operationId":"GhlProvisioningAdminController_dismiss","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DismissProvisioningDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Close a failed row, with a note","tags":["Admin"]}},"/api/admin/provisioning/{id}/resend-code":{"post":{"operationId":"GhlProvisioningAdminController_resendCode","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Send a new access code to the client's email (pending rows)","tags":["Admin"]}},"/api/admin/provisioning/{id}/resume-sync":{"post":{"operationId":"GhlProvisioningAdminController_resumeSync","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Hand the row back to the poller and apply what GHL says","tags":["Admin"]}},"/api/access-codes/validate":{"post":{"operationId":"AccessCodesController_validate","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateCodeDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"valid":true,"codeType":"standard","code":"ABC123"}}}}},"400":{"description":"Invalid or expired code"}},"summary":"Validate an access code before registration","tags":["Access Codes"]}},"/api/admin/validate-access":{"get":{"operationId":"AdminController_validateAccess","parameters":[{"name":"token","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"valid":true,"role":"admin"}}}}}},"security":[{"bearer":[]}],"summary":"Validate admin panel access token","tags":["Admin"]}},"/api/admin/switch-to-app":{"post":{"operationId":"AdminController_switchToApp","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}}}}}},"security":[{"bearer":[]}],"summary":"Generate token to switch from admin panel to app","tags":["Admin"]}},"/api/admin/dashboard-stats":{"get":{"operationId":"AdminController_getDashboardStats","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"totalUsers":120,"activeToday":45,"newThisWeek":8}}}}}},"security":[{"bearer":[]}],"summary":"Get admin dashboard statistics","tags":["Admin"]}},"/api/admin/dashboard-summary":{"get":{"operationId":"AdminController_getDashboardSummary","parameters":[{"name":"days","required":false,"in":"query","description":"Trend window in days (1–365, default 30). Ignored if from/to provided.","schema":{"type":"string"}},{"name":"from","required":false,"in":"query","description":"ISO date YYYY-MM-DD. Inclusive start of range.","schema":{"type":"string"}},{"name":"to","required":false,"in":"query","description":"ISO date YYYY-MM-DD. Inclusive end of range. Defaults to today if from is set.","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Comprehensive admin dashboard with optional date range and prior-period comparison","tags":["Admin"]}},"/api/admin/dashboard-summary-beta":{"get":{"operationId":"AdminController_getDashboardSummaryBeta","parameters":[{"name":"days","required":false,"in":"query","description":"Trend window in days (1–365, default 30). Ignored if from/to provided.","schema":{"type":"string"}},{"name":"from","required":false,"in":"query","description":"ISO date YYYY-MM-DD. Inclusive start of range.","schema":{"type":"string"}},{"name":"to","required":false,"in":"query","description":"ISO date YYYY-MM-DD. Inclusive end of range. Defaults to today if from is set.","schema":{"type":"string"}},{"name":"cohortFrom","required":false,"in":"query","description":"ISO date YYYY-MM-DD. Anchor for the trial-vs-free rollout cohort. Independent of from/to. Defaults to TRIAL_SYSTEM_LAUNCH_DATE, else the rollout floor 2026-07-27 (the later of Android 07-21 / iOS 07-27). Dates before the floor pull in pre-rollout signups who never had a plan choice to make, inflating noPlanChosen and legacyFree.","schema":{"type":"string"}},{"name":"timezone","required":false,"in":"query","description":"IANA zone of the viewer (e.g. 'Asia/Kolkata'). All day boundaries are computed in it, so 'today' means today where the caller is. Unrecognised values fall back to the app default; the zone actually used is echoed back as `timezone`.","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Fixed dashboard-summary (admin_data_fixes) — see docs/dashboard-validation/runbook.md","tags":["Admin"]}},"/api/admin/users":{"get":{"operationId":"AdminController_getUsers","parameters":[{"name":"page","required":false,"in":"query","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"string"}},{"name":"role","required":false,"in":"query","schema":{"type":"string"}},{"name":"active","required":false,"in":"query","schema":{"type":"string"}},{"name":"search","required":false,"in":"query","schema":{"type":"string"}},{"name":"program_status","required":false,"in":"query","schema":{"type":"string"}},{"name":"is_admin","required":false,"in":"query","schema":{"type":"string"}},{"name":"include_deleted","required":false,"in":"query","description":"Include soft-deleted users in the list. Pass \"true\" to include. Soft-deleted users are always included when `search` is set.","schema":{"type":"string"}},{"name":"dateFrom","required":false,"in":"query","description":"ISO date YYYY-MM-DD; filters on created_at","schema":{"type":"string"}},{"name":"dateTo","required":false,"in":"query","description":"ISO date YYYY-MM-DD; filters on created_at (inclusive)","schema":{"type":"string"}},{"name":"is_internal","required":false,"in":"query","description":"Staff/tester accounts. \"true\" = only internal, \"false\" = exclude internal, omit = all.","schema":{"type":"string"}},{"name":"sold_by","required":false,"in":"query","description":"Sales rep (V2-07), case-insensitive, from the user's latest GHL provisioning row. Each user in the response carries `sold_by` (null when not provisioned from GHL).","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List users with pagination and filtering","tags":["Admin"]},"post":{"operationId":"AdminController_createUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminCreateUserDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"userId":42}}}}}},"security":[{"bearer":[]}],"summary":"Admin creates a new user account with a temporary password sent by email","tags":["Admin"]}},"/api/admin/users/{id}":{"get":{"operationId":"AdminController_getUserDetail","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get full user detail including profile, subscription and macros","tags":["Admin"]},"patch":{"operationId":"AdminController_updateUser","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUpdateUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"User updated successfully"}}}}}},"security":[{"bearer":[]}],"summary":"Update user and profile fields (admin only)","tags":["Admin"]}},"/api/admin/programs":{"get":{"operationId":"AdminController_listPrograms","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List available program types (admin and coach)","tags":["Admin"]}},"/api/admin/users/{id}/enroll":{"post":{"operationId":"AdminController_enrollClient","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnrollClientDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Enroll a client into a program (admin and coach; only an admin can reinstate a past client)","tags":["Admin"]}},"/api/admin/users/{id}/enrollments":{"get":{"operationId":"AdminController_getClientEnrollments","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Enrollment history for a client (admin and coach)","tags":["Admin"]}},"/api/admin/enrollments/{id}/status":{"patch":{"operationId":"AdminController_updateEnrollmentStatus","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEnrollmentStatusDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Complete or drop a single enrollment — the same write-through ProgramService.setStatus applies for the automatic client-enrollment-expiry sweep, so a manual fix here can never disagree with what the sweep would have done.","tags":["Admin"]}},"/api/admin/legal/policy-versions":{"post":{"operationId":"AdminController_publishPolicyVersion","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishPolicyVersionDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Publish a new policy version — the guard and GET /legal/current pick it up immediately (cache invalidated on write)","tags":["Admin"]}},"/api/admin/legal/notice-campaigns":{"get":{"operationId":"AdminController_listNoticeCampaigns","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"All notice campaigns with their progress, newest first","tags":["Admin"]},"post":{"description":"Rejected unless effectiveAt is at least 30 days away and both bodies link to /privacy… and /terms…. Nothing is sent until start.","operationId":"AdminController_createNoticeCampaign","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePolicyNoticeCampaignDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Create a policy-change notice email (draft)","tags":["Admin"]}},"/api/admin/legal/notice-campaigns/preview":{"post":{"operationId":"AdminController_previewNoticeCampaign","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreviewPolicyNoticeDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Render unsaved notice copy as a recipient would get it (sends nothing)","tags":["Admin"]}},"/api/admin/legal/notice-campaigns/{id}":{"patch":{"operationId":"AdminController_updateNoticeCampaign","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePolicyNoticeCampaignDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Edit a notice campaign that has not started sending","tags":["Admin"]},"get":{"description":"Audience, sent / retrying / failed / pending, first and last send, and the earliest effective date that still gives 30 days from the last email.","operationId":"AdminController_getNoticeCampaign","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Notice campaign progress","tags":["Admin"]}},"/api/admin/legal/notice-campaigns/{id}/test":{"post":{"operationId":"AdminController_testNoticeCampaign","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PolicyNoticeTestDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Send the saved notice to 1–10 sample addresses (not recorded; no user is emailed)","tags":["Admin"]}},"/api/admin/legal/notice-campaigns/{id}/recipients":{"get":{"description":"Each row has suspectedTest (a reason, or null) for accounts that look like test or internal ones. Flags only; nothing is excluded until an admin excludes it.","operationId":"AdminController_noticeRecipients","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"offset","required":false,"in":"query","schema":{"example":0,"type":"number"}},{"name":"limit","required":false,"in":"query","description":"1–500 (default 100)","schema":{"example":100,"type":"number"}},{"name":"search","required":false,"in":"query","description":"Matches email or name, across every pending recipient","schema":{"type":"string"}},{"name":"suspectedOnly","required":false,"in":"query","description":"Only accounts flagged as possible test or internal ones","schema":{"type":"boolean"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Review list: the users the next send would email, in send order","tags":["Admin"]}},"/api/admin/legal/notice-campaigns/{id}/excluded":{"get":{"operationId":"AdminController_noticeExcluded","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Users excluded from a notice, with who excluded them and why","tags":["Admin"]}},"/api/admin/legal/notice-campaigns/{id}/exclude":{"post":{"operationId":"AdminController_excludeNoticeRecipients","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PolicyNoticeExcludeDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Take users out of a notice (test or internal accounts); users already emailed are unchanged","tags":["Admin"]}},"/api/admin/legal/notice-campaigns/{id}/include":{"post":{"operationId":"AdminController_includeNoticeRecipients","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PolicyNoticeIncludeDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Put excluded users back in a notice","tags":["Admin"]}},"/api/admin/legal/notice-campaigns/{id}/send-batch":{"post":{"description":"Every id must still be pending (400 lists any that are not). Returns at once; the batch sends in the background. 409 while another send is running. Send `{ \"userIds\": [...], \"confirm\": true }`.","operationId":"AdminController_sendNoticeBatch","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PolicyNoticeSendBatchDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Email a reviewed batch of 1–500 users","tags":["Admin"]}},"/api/admin/legal/notice-campaigns/{id}/sends":{"get":{"description":"Email, name, status, attempts, send time and the mail server message id. \"sent\" means the mail server accepted the email; a later bounce is not tracked here.","operationId":"AdminController_noticeSends","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"status","required":false,"in":"query","description":"Omit for all","schema":{"type":"string","enum":["sent","retrying","failed","excluded"]}},{"name":"search","required":false,"in":"query","description":"Matches email or name","schema":{"type":"string"}},{"name":"offset","required":false,"in":"query","schema":{"example":0,"type":"number"}},{"name":"limit","required":false,"in":"query","description":"1–5000 (default 100; use 5000 for a CSV export)","schema":{"example":100,"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Delivery log: users sent, retrying, failed or excluded, newest first","tags":["Admin"]}},"/api/admin/legal/notice-campaigns/{id}/failed":{"get":{"operationId":"AdminController_noticeFailed","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Sends that failed three times and were given up on, with the last error","tags":["Admin"]}},"/api/admin/legal/notice-campaigns/{id}/retry-failed":{"post":{"description":"All failed sends, or `userIds`. They reappear in the review list and go with the next send.","operationId":"AdminController_retryFailedNotices","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PolicyNoticeRetryFailedDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Put failed sends back in the queue","tags":["Admin"]}},"/api/admin/legal/notice-campaigns/{id}/reports":{"post":{"description":"Counts, sends per day (PT), the copy sent, and one row per user (id, status, time, message id, address SHA-256). Stored with a SHA-256 seal; never changed afterwards.","operationId":"AdminController_createNoticeReport","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PolicyNoticeReportDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Freeze a delivery report (the record of notice) into the database","tags":["Admin"]},"get":{"operationId":"AdminController_listNoticeReports","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Stored delivery reports for a notice, newest first","tags":["Admin"]}},"/api/admin/legal/notice-reports/{reportId}":{"get":{"operationId":"AdminController_getNoticeReport","parameters":[{"name":"reportId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"One stored delivery report, exactly as generated (`payload` hashes to `sha256`)","tags":["Admin"]}},"/api/admin/legal/notice-campaigns/{id}/start":{"post":{"description":"Returns at once; the send runs in the background. Send `{ \"confirm\": true }`.","operationId":"AdminController_startNoticeCampaign","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartPolicyNoticeDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Start (or resume) emailing every live account","tags":["Admin"]}},"/api/admin/legal/notice-campaigns/{id}/pause":{"post":{"operationId":"AdminController_pauseNoticeCampaign","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Pause a notice campaign; emails already sent stay recorded","tags":["Admin"]}},"/api/admin/compliance/access-log":{"get":{"description":"REW-955 admin query: `?rewardGrantId=` lists every coach read made under a prize grant, on every surface (portal, in-app coach view, Lucille bridge), allowed and denied. Also filters by `userId` (live account) or `subjectRef` (deleted account). One filter is required.","operationId":"AdminController_getComplianceAccessLog","parameters":[{"name":"rewardGrantId","required":true,"in":"query","schema":{"type":"string"}},{"name":"userId","required":true,"in":"query","schema":{"type":"string"}},{"name":"subjectRef","required":true,"in":"query","schema":{"type":"string"}},{"name":"page","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Coach access log, filtered by reward grant, live user or archived subject","tags":["Admin"]}},"/api/admin/users/{id}/compliance-record":{"get":{"description":"For a deleted account, look the subject up by email (POST compliance/lookup) and read GET compliance/subjects/:subjectRef instead — user ids are swapped for a subject_ref at deletion.","operationId":"AdminController_getUserComplianceRecord","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"page","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"A live user's consent events and coach access log (REW-977 evidence)","tags":["Admin"]}},"/api/admin/compliance/subjects/{subjectRef}":{"get":{"operationId":"AdminController_getArchivedComplianceRecord","parameters":[{"name":"subjectRef","required":true,"in":"path","schema":{"type":"string"}},{"name":"page","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"A deleted account's retained consent events and coach access log","tags":["Admin"]}},"/api/admin/compliance/lookup":{"post":{"description":"Decrypts the archived references (COMPLIANCE_RECORD_KEY) and returns the matching subject_ref(s) and former user id. Recorded in admin_activity_log (without the email).","operationId":"AdminController_lookupComplianceSubject","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceLookupDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Re-link a deleted account's compliance record by email (disputes, REW-977)","tags":["Admin"]}},"/api/admin/compliance/legal-hold":{"patch":{"description":"A held record is skipped by the three-year retention purge (PP v2.4 §9: \"unless … reasonably necessary for a pending claim, dispute, investigation, or legal hold\"). Pass exactly one of `userId` (live account) or `subjectRef` (deleted account). Recorded in admin_activity_log.","operationId":"AdminController_setComplianceLegalHold","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComplianceLegalHoldDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Place or lift a legal hold on a compliance record (REW-1088)","tags":["Admin"]}},"/api/admin/programs/backfill-expired-clients":{"post":{"operationId":"AdminController_backfillExpiredClients","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackfillExpiredClientsDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"One-time backfill: flip an explicit list of clients whose program already ended to member access. Not a sweep of all history -- the caller supplies exactly who to fix.","tags":["Admin"]}},"/api/admin/users/{id}/reset-password":{"post":{"operationId":"AdminController_adminResetPassword","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminResetPasswordDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true}}}}}},"security":[{"bearer":[]}],"summary":"Admin resets a user password and notifies via email","tags":["Admin"]}},"/api/admin/users/{id}/reactivate":{"post":{"description":"Clears deleted_at and re-enables login. Subscriptions are NOT auto-restored — Stripe subs were cancelled at the provider, and admin-grant subs were ended locally. Use grant-access separately if comp access should be restored.","operationId":"AdminController_reactivateUser","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true}}}}}},"security":[{"bearer":[]}],"summary":"Reactivate a soft-deleted user within the 30-day grace window","tags":["Admin"]}},"/api/admin/users-with-groups":{"get":{"operationId":"AdminController_getUsersWithGroups","parameters":[{"name":"role","required":false,"in":"query","schema":{"type":"string"}},{"name":"active","required":false,"in":"query","schema":{"type":"string"}},{"name":"group","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"users":[{"id":5,"name":"Jane Doe","email":"jane@example.com","role":"client","groups":[{"id":1,"name":"January 2026 Cohort"}]}]}}}}}},"security":[{"bearer":[]}],"summary":"List users with their group assignments","tags":["Admin"]}},"/api/admin/user-macro-targets/{userId}":{"get":{"operationId":"AdminController_getUserMacroTargets","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"targets":{"calories":1500,"protein":120,"carbs":150,"fat":50,"fiber":25}}}}}}},"security":[{"bearer":[]}],"summary":"Get macro targets for a specific user","tags":["Admin"]}},"/api/admin/lead-users":{"get":{"operationId":"AdminController_getLeadUsers","parameters":[{"name":"page","required":false,"in":"query","description":"Page number (1-indexed). Default 1.","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","description":"Page size (1-100). Default 50.","schema":{"type":"string"}},{"name":"search","required":false,"in":"query","description":"Match against email or first/last name.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"all | new | active | expiring_soon | expired","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"leads":[{"id":7,"email":"lead@example.com","firstName":"Lead","lastName":"User","trialExpiresAt":"2026-03-01T00:00:00.000Z","trialStatus":"active","daysRemaining":9}],"total":240,"page":1,"limit":50,"stats":{"active":180,"expiringSoon":12,"expired":40,"new":8}}}}}}},"security":[{"bearer":[]}],"summary":"Get lead/trial users with trial status, paginated","tags":["Admin"]}},"/api/admin/lead-users/{id}/extend-trial":{"patch":{"operationId":"AdminController_extendLeadTrial","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Extend trial period for a lead user by N days","tags":["Admin"]}},"/api/admin/groups/{id}":{"delete":{"operationId":"AdminController_deleteGroup","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Soft-delete a group (sets is_active = false)","tags":["Admin"]},"patch":{"operationId":"AdminController_updateGroup","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update group name, description, startDate, endDate, or programId","tags":["Admin"]}},"/api/admin/pending-subscriptions":{"get":{"operationId":"AdminController_getPendingSubscriptions","parameters":[{"name":"q","required":false,"in":"query","description":"Substring match against pending_email or pending_name (case-insensitive)","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List subscriptions awaiting user signup (pending_email set)","tags":["Admin"]}},"/api/admin/pending-subscriptions/{id}/attach":{"post":{"description":"Use when the customer paid with one email (e.g. iCloud / different Stripe customer email) but signed up with another. Sets user_id, clears pending_email, copies Stripe customer id to the user if missing, and busts the AI rate-limit tier cache.","operationId":"AdminController_attachPendingSubscription","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachPendingSubscriptionDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Manually attach a pending subscription to a user account","tags":["Admin"]}},"/api/admin/subscriptions":{"get":{"operationId":"AdminController_getSubscriptions","parameters":[{"name":"status","required":false,"in":"query","schema":{"type":"string"}},{"name":"type","required":false,"in":"query","schema":{"type":"string"}},{"name":"platform","required":false,"in":"query","schema":{"type":"string"}},{"name":"userId","required":false,"in":"query","schema":{"type":"string"}},{"name":"isFounder","required":false,"in":"query","schema":{"type":"string"}},{"name":"q","required":false,"in":"query","description":"Substring match (case-insensitive) against user email + first/last name + pending_email + pending_name","schema":{"type":"string"}},{"name":"dateFrom","required":false,"in":"query","description":"ISO date YYYY-MM-DD; filters on created_at","schema":{"type":"string"}},{"name":"dateTo","required":false,"in":"query","description":"ISO date YYYY-MM-DD; filters on created_at (inclusive)","schema":{"type":"string"}},{"name":"page","required":false,"in":"query","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List subscriptions with optional filters and pagination","tags":["Admin"]}},"/api/admin/subscriptions/{id}/details":{"get":{"description":"Returns the subscription row (including provider identifier columns), joined user/profile, and the timeline of subscription_events newest-first.","operationId":"AdminController_getSubscriptionDetails","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Subscription detail bundle for the troubleshooting modal","tags":["Admin"]}},"/api/admin/subscriptions/{id}/override":{"post":{"operationId":"AdminController_overrideSubscription","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Override subscription status for support workflows","tags":["Admin"]}},"/api/admin/subscriptions/{id}/refresh-from-stripe":{"post":{"description":"Fetches the latest state from Stripe for the given subscription and overwrites status, ends_at, stripe_price_id, stripe_product_id, stripe_current_period_start, stripe_cancel_at_period_end, and stripe_latest_invoice_id. Status is only applied for unambiguous Stripe states (active, trialing, canceled, incomplete_expired); anything else (paused, unpaid, past_due, incomplete) leaves the local status untouched and records statusSkipped on the timeline event, since guessing would silently grant or revoke entitlement. Only works for platform=stripe subscriptions that have a stripe_subscription_id. Returns the updated subscription row and a per-field diff of what changed.","operationId":"AdminController_refreshSubscriptionFromStripe","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"subscription":{"id":2102,"ends_at":"2027-04-30T01:59:52.000Z","stripe_price_id":"price_xxx"},"changes":{"ends_at":{"before":null,"after":"2027-04-30T01:59:52.000Z"},"stripe_price_id":{"before":null,"after":"price_xxx"}}}}}}}},"security":[{"bearer":[]}],"summary":"Refresh a Stripe subscription from the Stripe API","tags":["Admin"]}},"/api/admin/founder-stats":{"get":{"operationId":"AdminController_getFounderStats","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get founder count and remaining marketing slots","tags":["Admin"]}},"/api/admin/grant-access":{"post":{"operationId":"AdminController_grantAccess","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrantAccessDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Grant temporary premium access to a user","tags":["Admin"]}},"/api/admin/access-codes":{"get":{"operationId":"AdminController_getAccessCodes","parameters":[{"name":"page","required":false,"in":"query","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"string"}},{"name":"codeType","required":false,"in":"query","schema":{"type":"string"}},{"name":"used","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"codes":[{"id":1,"code":"ABC123","codeType":"standard","used":false,"created_at":"2026-01-01T00:00:00.000Z"}],"total":50,"page":1}}}}}},"security":[{"bearer":[]}],"summary":"List access codes with filtering","tags":["Admin"]},"post":{"operationId":"AdminController_createAccessCode","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAccessCodeDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"code":{"id":1,"code":"ABC123","codeType":"standard","used":false,"created_at":"2026-02-19T00:00:00.000Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Create a new access code","tags":["Admin"]}},"/api/admin/notification-logs":{"get":{"operationId":"AdminController_getNotificationLogs","parameters":[{"name":"userId","required":true,"in":"query","schema":{"type":"string"}},{"name":"type","required":true,"in":"query","schema":{"type":"string"}},{"name":"status","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"offset","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Query push notification logs for debugging","tags":["Admin"]}},"/api/admin/error-logs":{"get":{"operationId":"AdminController_getErrorLogs","parameters":[{"name":"userId","required":true,"in":"query","schema":{"type":"string"}},{"name":"errorCode","required":true,"in":"query","schema":{"type":"string"}},{"name":"action","required":true,"in":"query","schema":{"type":"string"}},{"name":"platform","required":true,"in":"query","schema":{"type":"string"}},{"name":"screen","required":true,"in":"query","schema":{"type":"string"}},{"name":"dateFrom","required":true,"in":"query","schema":{"type":"string"}},{"name":"dateTo","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"offset","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Query app error logs with filters","tags":["Admin"]}},"/api/admin/webhook-events":{"get":{"operationId":"AdminController_getWebhookEvents","parameters":[{"name":"eventSource","required":true,"in":"query","schema":{"type":"string"}},{"name":"eventType","required":true,"in":"query","schema":{"type":"string"}},{"name":"userId","required":true,"in":"query","schema":{"type":"string"}},{"name":"email","required":true,"in":"query","schema":{"type":"string"}},{"name":"customerExternalId","required":true,"in":"query","schema":{"type":"string"}},{"name":"dateFrom","required":true,"in":"query","schema":{"type":"string"}},{"name":"dateTo","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"offset","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Query webhook events with filters","tags":["Admin"]}},"/api/admin/webhook-events/{id}/reprocess":{"post":{"description":"Calls the same Stripe / RevenueCat handler the live webhook uses, with skipDedup=true so the existing webhook_events row is preserved. Idempotent — running multiple times will not duplicate subscriptions or re-tag GHL more than once. Also backfills customer_email and customer_external_id on the row when those columns are null.","operationId":"AdminController_reprocessWebhookEvent","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Replay a saved webhook event through the standard handler","tags":["Admin"]}},"/api/admin/marketing-events":{"get":{"operationId":"AdminController_getMarketingEvents","parameters":[{"name":"status","required":true,"in":"query","schema":{"type":"string"}},{"name":"eventName","required":true,"in":"query","schema":{"type":"string"}},{"name":"userId","required":true,"in":"query","schema":{"type":"string"}},{"name":"dateFrom","required":true,"in":"query","schema":{"type":"string"}},{"name":"dateTo","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"offset","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Query the AppsFlyer S2S outbox with filters","tags":["Admin"]}},"/api/admin/marketing-events/summary":{"get":{"description":"The at-a-glance \"is Track 2 actually live right now\" view — status counts, APPSFLYER_S2S_ENABLED/DRY_RUN, and the most recent sent_at.","operationId":"AdminController_getMarketingEventsSummary","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Outbox status counts + current config posture","tags":["Admin"]}},"/api/admin/marketing-events/{id}/retry":{"post":{"operationId":"AdminController_retryMarketingEvent","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Re-enqueue one failed/pending_identity marketing event","tags":["Admin"]}},"/api/admin/users/{id}/attribution-devices":{"get":{"operationId":"AdminController_getUserAttributionDevices","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"AppsFlyer-linked devices for one user — diagnostic for the marketing attribution panel","tags":["Admin"]}},"/api/admin/groups":{"get":{"operationId":"AdminController_getGroups","parameters":[{"name":"status","required":false,"in":"query","description":"Defaults to active. \"inactive\" is the past-groups list (GRP-04).","schema":{"enum":["active","inactive","all"],"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"groups":[{"id":1,"name":"January 2026 Cohort","memberCount":12},{"id":2,"name":"VIP Clients","memberCount":5}]}}}}}},"security":[{"bearer":[]}],"summary":"List groups with member counts","tags":["Admin"]},"post":{"operationId":"AdminController_createGroup","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGroupDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"group":{"id":3,"name":"February 2026 Cohort","created_at":"2026-02-19T00:00:00.000Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Create a new group","tags":["Admin"]}},"/api/admin/groups/assign-user":{"post":{"operationId":"AdminController_assignUserToGroup","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"User assigned to group"}}}}}},"security":[{"bearer":[]}],"summary":"Assign user (or pending user) to a group","tags":["Admin"]}},"/api/admin/groups/message":{"post":{"operationId":"AdminController_sendGroupMessage","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendGroupMessageDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"sentTo":15,"message":"Message sent"}}}}}},"security":[{"bearer":[]}],"summary":"Send message to all active members of a group","tags":["Admin"]}},"/api/admin/groups/{id}/members":{"get":{"operationId":"AdminController_getGroupMembers","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List members of a group","tags":["Admin"]}},"/api/admin/groups/{id}/members/{userId}":{"delete":{"operationId":"AdminController_removeGroupMember","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Remove a member from a group","tags":["Admin"]}},"/api/admin/groups/{id}/progress":{"get":{"operationId":"AdminController_getGroupProgress","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get progress stats, nutrition panel and chart data for a group","tags":["Admin"]}},"/api/admin/groups/{id}/available-users":{"get":{"operationId":"AdminController_getAvailableUsersForGroup","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get users not yet assigned to a group","tags":["Admin"]}},"/api/admin/groups/{id}/complete-program":{"post":{"operationId":"AdminController_completeProgramForGroup","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"affectedUsers":[{"id":5,"name":"Jane Doe","previousStatus":"active"},{"id":6,"name":"John Smith","previousStatus":"active"}]}}}}}},"security":[{"bearer":[]}],"summary":"Mark all active members of a group as program completed. Requires group name confirmation to prevent accidental bulk updates.","tags":["Admin"]}},"/api/admin/send-coach-note":{"post":{"operationId":"AdminController_sendCoachNote","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendCoachNoteDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Coach note sent"}}}}}},"security":[{"bearer":[]}],"summary":"Send coach note to a client (supports urgent flag)","tags":["Admin"]}},"/api/admin/update-macro-goals":{"post":{"operationId":"AdminController_updateMacroGoals","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUpdateMacroGoalsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Macro goals updated"}}}}}},"security":[{"bearer":[]}],"summary":"Update macro goals for a user (admin override)","tags":["Admin"]}},"/api/admin/ai-usage-stats":{"get":{"operationId":"AdminController_getAiUsageStats","parameters":[{"name":"days","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Aggregate AI token usage stats","tags":["Admin"]}},"/api/admin/ai-usage-by-feature":{"get":{"operationId":"AdminController_getAiUsageByFeature","parameters":[{"name":"days","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"AI token usage breakdown by feature","tags":["Admin"]}},"/api/admin/ai-usage-by-user":{"get":{"operationId":"AdminController_getAiUsageByUser","parameters":[{"name":"days","required":false,"in":"query","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Top users by AI token consumption","tags":["Admin"]}},"/api/admin/ai-usage-timeline":{"get":{"operationId":"AdminController_getAiUsageTimeline","parameters":[{"name":"days","required":false,"in":"query","schema":{"type":"string"}},{"name":"interval","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"AI token consumption over time","tags":["Admin"]}},"/api/admin/yesterday-activity/{userId}":{"get":{"operationId":"AdminController_getYesterdayActivity","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"foodLogs":[{"id":101,"food_name":"Grilled Chicken","calories":180,"protein":35,"meal_number":2}],"waterTotal":48}}}}}},"security":[{"bearer":[]}],"summary":"Get a user's food and water activity from yesterday","tags":["Admin"]}},"/api/admin/group/{groupId}/inches-lost-timeline":{"get":{"operationId":"AdminController_getInchesLostTimeline","parameters":[{"name":"groupId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":[{"date":"2025-06-01","avgInchesLost":0},{"date":"2026-01-01","avgInchesLost":12.5},{"date":"2026-02-19","avgInchesLost":18.2}]}}}}}},"security":[{"bearer":[]}],"summary":"Get aggregated inches-lost timeline for a group","tags":["Admin"]}},"/api/admin/rag/add":{"post":{"operationId":"AdminController_addRagEntry","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddRagDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"entry":{"id":101,"category":"nutrition","content":"Bariatric patients should prioritize protein at every meal...","created_at":"2026-02-19T00:00:00.000Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Add an entry to the RAG knowledge base","tags":["Admin"]}},"/api/admin/rag/content":{"get":{"operationId":"AdminController_getRagContent","parameters":[{"name":"category","required":false,"in":"query","schema":{"type":"string"}},{"name":"entryType","required":false,"in":"query","schema":{"type":"string"}},{"name":"search","required":false,"in":"query","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"entries":[{"id":1,"category":"nutrition","entry_type":"rule","content":"Protein first, always.","created_at":"2026-01-01T00:00:00.000Z"}],"total":85}}}}}},"security":[{"bearer":[]}],"summary":"Search/browse RAG knowledge base entries","tags":["Admin"]}},"/api/admin/rag/statistics":{"get":{"operationId":"AdminController_getRagStatistics","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"stats":{"total":85,"byCategory":{"nutrition":30,"mindset":15,"recipes":20,"exercise":10,"general":10}}}}}}}},"security":[{"bearer":[]}],"summary":"Get RAG knowledge base statistics by category","tags":["Admin"]}},"/api/admin/rag/process-transcript":{"post":{"operationId":"AdminController_processTranscript","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProcessTranscriptDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"processed":{"title":"Q&A Session Feb 2026","entryCount":12,"categories":["nutrition","mindset"]},"entryCount":12}}}}}},"security":[{"bearer":[]}],"summary":"Process a full transcript via AI (requires OpenAI)","tags":["Admin"]}},"/api/admin/rag/process-transcript-chunk":{"post":{"operationId":"AdminController_processTranscriptChunk","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProcessTranscriptChunkDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"processed":{"entries":[{"category":"nutrition","content":"..."}]}}}}}}},"security":[{"bearer":[]}],"summary":"Process a transcript chunk via AI (requires OpenAI)","tags":["Admin"]}},"/api/admin/rag/save-processed-transcript":{"post":{"operationId":"AdminController_saveProcessedTranscript","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveProcessedTranscriptDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"saved":12,"message":"Transcript saved"}}}}}},"security":[{"bearer":[]}],"summary":"Save processed transcript results to RAG (requires OpenAI)","tags":["Admin"]}},"/api/admin/rag/{id}":{"delete":{"operationId":"AdminController_deleteRagEntry","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Delete a RAG knowledge base entry","tags":["Admin"]}},"/api/admin/questionnaire/surveys":{"get":{"operationId":"AdminController_listSurveysAdmin","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List all surveys (admin view)","tags":["Admin"]},"post":{"operationId":"AdminController_createSurvey","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSurveyDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Create a new survey (e.g. a future H20-03 group-wide survey)","tags":["Admin"]}},"/api/admin/questionnaire/surveys/{id}":{"patch":{"operationId":"AdminController_updateSurvey","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSurveyDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update a survey — isActive is the visibility toggle: false makes every question in it unfetchable","tags":["Admin"]}},"/api/admin/questionnaire/questions":{"get":{"operationId":"AdminController_getQuestions","parameters":[{"name":"surveyId","required":false,"in":"query","description":"Scope to one survey; omit to list across all surveys","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List questions with options (admin view)","tags":["Admin"]},"post":{"operationId":"AdminController_createQuestion","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateQuestionDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Create a new question (with optional inline options)","tags":["Admin"]}},"/api/admin/questionnaire/questions/{id}":{"put":{"operationId":"AdminController_updateQuestion","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateQuestionDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update a question","tags":["Admin"]},"delete":{"operationId":"AdminController_deleteQuestion","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Soft-delete a question (set is_active=false)","tags":["Admin"]}},"/api/admin/questionnaire/questions/{id}/options":{"post":{"operationId":"AdminController_addOption","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOptionDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Add an option to a question","tags":["Admin"]}},"/api/admin/questionnaire/options/{id}":{"put":{"operationId":"AdminController_updateOption","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOptionDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update an option","tags":["Admin"]},"delete":{"operationId":"AdminController_deleteOption","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Soft-delete an option (set is_active=false)","tags":["Admin"]}},"/api/admin/rate-limits":{"get":{"operationId":"AdminController_getRateLimitConfigs","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List all AI rate limit configs (all tiers × features)","tags":["Admin"]}},"/api/admin/rate-limits/{id}":{"put":{"operationId":"AdminController_updateRateLimitConfig","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update per_hour/day/month/year limits for a tier+feature row","tags":["Admin"]}},"/api/admin/rate-limits/refresh":{"post":{"operationId":"AdminController_refreshRateLimitCache","parameters":[],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Bust rate limit config cache — changes go live within seconds","tags":["Admin"]}},"/api/admin/rate-limits/usage/{userId}":{"get":{"operationId":"AdminController_getRateLimitUsage","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get current AI usage counters for a specific user","tags":["Admin"]}},"/api/admin/ui-pools":{"get":{"operationId":"AdminController_getUiPools","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List all free-user UI pool configs (lifetime limits)","tags":["Admin"]}},"/api/admin/ui-pools/{id}":{"put":{"operationId":"AdminController_updateUiPool","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update max_uses for a UI pool","tags":["Admin"]}},"/api/admin/ui-pools/refresh-cache":{"post":{"operationId":"AdminController_refreshUiPoolCache","parameters":[],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Bust UI pool config cache","tags":["Admin"]}},"/api/admin/feedback":{"get":{"operationId":"AdminController_getFeedback","parameters":[{"name":"page","required":false,"in":"query","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"string"}},{"name":"type","required":false,"in":"query","schema":{"type":"string"}},{"name":"status","required":false,"in":"query","schema":{"type":"string"}},{"name":"userId","required":false,"in":"query","schema":{"type":"string"}},{"name":"dateFrom","required":false,"in":"query","description":"ISO date YYYY-MM-DD","schema":{"type":"string"}},{"name":"dateTo","required":false,"in":"query","description":"ISO date YYYY-MM-DD (inclusive)","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List user feedback submissions","tags":["Admin"]}},"/api/admin/feedback/{id}":{"get":{"operationId":"AdminController_getFeedbackById","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get a single feedback submission","tags":["Admin"]},"patch":{"operationId":"AdminController_updateFeedback","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update feedback status or admin notes","tags":["Admin"]}},"/api/admin/chat/repair-rooms":{"post":{"operationId":"AdminController_repairChatRooms","parameters":[],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Backfill any missing DM rooms and group chat memberships (idempotent)","tags":["Admin"]}},"/api/admin/notification-preference-types":{"get":{"operationId":"AdminController_getNotificationPreferenceTypes","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List all notification preference types (including inactive)","tags":["Admin"]}},"/api/admin/notification-preference-types/{id}":{"put":{"operationId":"AdminController_updateNotificationPreferenceType","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update a notification preference type label/status/role restrictions","tags":["Admin"]}},"/api/admin/subscribers":{"get":{"operationId":"AdminController_getSubscribers","parameters":[{"name":"page","required":false,"in":"query","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"string"}},{"name":"verified","required":false,"in":"query","description":"true | false","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List marketing subscribers with pagination","tags":["Admin"]}},"/api/admin/subscribers/email-templates":{"get":{"operationId":"AdminController_getEmailTemplates","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List available marketing email templates and their required variables","tags":["Admin"]}},"/api/admin/subscribers/send-email":{"post":{"operationId":"AdminController_sendMarketingEmail","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMarketingEmailDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"sent":38,"failed":0}}}}}},"security":[{"bearer":[]}],"summary":"Send a marketing email to all verified subscribers","tags":["Admin"]}},"/api/admin/bridge-logs":{"get":{"operationId":"AdminController_getBridgeLogs","parameters":[{"name":"keyName","required":true,"in":"query","schema":{"type":"string"}},{"name":"endpoint","required":true,"in":"query","schema":{"type":"string"}},{"name":"success","required":true,"in":"query","schema":{"type":"string"}},{"name":"errorType","required":true,"in":"query","schema":{"type":"string"}},{"name":"dateFrom","required":true,"in":"query","schema":{"type":"string"}},{"name":"dateTo","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"offset","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Query Lucille Bridge API interaction logs (admin only)","tags":["Admin"]}},"/api/admin/bridge-logs/purge":{"delete":{"operationId":"AdminController_purgeBridgeLogs","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Purge Lucille Bridge logs older than 30 days (admin only)","tags":["Admin"]}},"/api/admin/user/{id}/weight-progression":{"get":{"operationId":"AdminController_getUserWeightProgression","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get user weight progression with start/goal weight","tags":["Admin"]}},"/api/admin/user/{id}/trends/inches":{"get":{"operationId":"AdminController_getUserInchesTrends","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get user body measurement trends (total inches)","tags":["Admin"]}},"/api/admin/user/{id}/tracking-summary/{year}/{month}":{"get":{"operationId":"AdminController_getUserTrackingSummary","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"year","required":true,"in":"path","schema":{"type":"number"}},{"name":"month","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get daily tracking summary for a month","tags":["Admin"]}},"/api/admin/user/{id}/day-details/{date}":{"get":{"operationId":"AdminController_getUserDayDetails","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"date","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get detailed activity for a specific day","tags":["Admin"]}},"/api/admin/user/{id}/coach-goals":{"get":{"operationId":"AdminController_getCoachGoals","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get current coach goals for a client","tags":["Admin"]}},"/api/admin/user/{id}/coach-goals/update":{"post":{"operationId":"AdminController_saveCoachGoals","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Save/update coach goals for a client","tags":["Admin"]}},"/api/admin/user/{id}/coach-goals/history":{"get":{"operationId":"AdminController_getCoachGoalsHistory","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get coach goals history for a client","tags":["Admin"]}},"/api/admin/user/{id}/coach-notes/all":{"get":{"operationId":"AdminController_getAllCoachNotes","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get all coach notes for a client","tags":["Admin"]}},"/api/admin/user/{id}/client-notes":{"get":{"operationId":"AdminController_getClientNotes","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get client session/laser-coaching notes","tags":["Admin"]}},"/api/admin/user/{id}/food-logs-summary":{"get":{"operationId":"AdminController_getFoodLogsSummary","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get food logging stats (total entries, days logged, streak)","tags":["Admin"]}},"/api/admin/users/{id}/questionnaire":{"get":{"operationId":"AdminController_getUserQuestionnaire","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get questionnaire answers for a user","tags":["Admin"]}},"/api/admin/users/{id}/questionnaire/export":{"get":{"operationId":"AdminController_exportUserQuestionnaire","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Export questionnaire answers as text","tags":["Admin"]}},"/api/admin/user/{id}/measurements/latest":{"get":{"operationId":"AdminController_getUserLatestMeasurements","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get latest body measurements for a user","tags":["Admin"]}},"/api/admin/user/{id}/progress-photos":{"get":{"operationId":"AdminController_getUserProgressPhotos","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get progress photos for a user","tags":["Admin"]}},"/api/admin/agents":{"post":{"operationId":"AdminController_createAgent","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAgentDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Create a new agent","tags":["Admin"]},"get":{"operationId":"AdminController_listAgents","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List all agents with referral counts","tags":["Admin"]}},"/api/admin/agents/{id}":{"get":{"operationId":"AdminController_getAgent","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get agent details with QR codes and referral count","tags":["Admin"]},"patch":{"operationId":"AdminController_updateAgent","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAgentDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update agent (name, email, phone, is_active)","tags":["Admin"]}},"/api/admin/agents/{id}/qr-codes":{"post":{"operationId":"AdminController_createAgentQrCode","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAgentQrCodeDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Create a QR code for an agent","tags":["Admin"]},"get":{"operationId":"AdminController_listAgentQrCodes","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List QR codes for an agent","tags":["Admin"]}},"/api/admin/agents/{agentId}/qr-codes/{qrCodeId}":{"patch":{"operationId":"AdminController_updateAgentQrCode","parameters":[{"name":"agentId","required":true,"in":"path","schema":{"type":"string"}},{"name":"qrCodeId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAgentQrCodeDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update a QR code (label, trial_days, max_scans, is_active)","tags":["Admin"]},"delete":{"operationId":"AdminController_deleteAgentQrCode","parameters":[{"name":"agentId","required":true,"in":"path","schema":{"type":"string"}},{"name":"qrCodeId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""}},"security":[{"bearer":[]}],"summary":"Delete a QR code","tags":["Admin"]}},"/api/admin/notifications/policy-v21":{"post":{"deprecated":true,"description":"One-time blast. Sends via transactional email (not GHL). 100ms delay between sends. Returns sent/failed/skipped counts. Pass userIds to test with specific accounts before the full blast.","operationId":"AdminController_sendPolicyNotification","parameters":[],"requestBody":{"required":false,"content":{"application/json":{"schema":{"example":{"userIds":[1,2,3]},"properties":{"userIds":{"type":"array","items":{"type":"number"},"description":"Optional — omit to send to all active users"}}}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"sent":3280,"failed":12,"skipped":12,"total":3304}}}}}},"security":[{"bearer":[]}],"summary":"Deprecated (v2.1 copy) — use POST /admin/legal/notice-campaigns","tags":["Admin"]}},"/api/admin/user/{id}/health-data":{"delete":{"description":"Delegates to HealthDataDeletionService (HEALTH_DELETABLE_TABLES): health_samples, health_device_connections, health_sync_anchors, health_readiness_snapshots, bowel_movement_logs, digestive_symptom_logs, diary_entries, user_health_goals, user_source_preferences, and health_baselines. Clearing health_device_connections returns health_last_synced_at in GET /users/profile to null — mobile detects that and triggers a full re-sync.","operationId":"AdminController_deleteUserHealthData","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"health_samples":4821,"health_device_connections":2,"health_sync_anchors":12,"health_readiness_snapshots":90,"bowel_movement_logs":34,"digestive_symptom_logs":11,"diary_entries":8,"user_health_goals":1,"user_source_preferences":5,"health_baselines":3}}}}}},"security":[{"bearer":[]}],"summary":"Delete all health-domain data for a user","tags":["Admin"]}},"/api/admin/activity-logs":{"get":{"description":"Returns a paginated list ordered by created_at DESC. All filters are optional and AND-combined. Page size is capped at 200 server-side.","operationId":"ActivityLogAdminController_search","parameters":[{"name":"userId","required":false,"in":"query","schema":{"example":42,"type":"number"}},{"name":"activityType","required":false,"in":"query","schema":{"example":"food_log.create","type":"string"}},{"name":"targetType","required":false,"in":"query","schema":{"example":"food_log","type":"string"}},{"name":"targetId","required":false,"in":"query","schema":{"example":"101","type":"string"}},{"name":"actorRole","required":false,"in":"query","schema":{"enum":["user","admin","coach","system","bridge","anonymous"],"type":"string"}},{"name":"success","required":false,"in":"query","description":"true | false; omit for both","schema":{"example":"false","type":"string"}},{"name":"from","required":false,"in":"query","description":"ISO timestamp; inclusive lower bound on created_at","schema":{"example":"2026-04-21T00:00:00.000Z","type":"string"}},{"name":"to","required":false,"in":"query","description":"ISO timestamp; inclusive upper bound on created_at","schema":{"example":"2026-04-22T23:59:59.999Z","type":"string"}},{"name":"page","required":false,"in":"query","schema":{"example":1,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"example":50,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"page":1,"limit":50,"total":1,"data":[{"id":9123,"user_id":42,"activity_type":"food_log.create","actor_role":"user","target_type":"food_log","target_id":"101","http_method":"POST","http_path":"/api/food-log","status_code":201,"success":true,"duration_ms":87,"ip_address":"203.0.113.7","user_agent":"RewriteApp/1.4.0 (iOS 17.4)","details":{"food_log_id":101},"created_at":"2026-04-22T12:30:00.000Z"}]}}}}}},"security":[{"bearer":[]}],"summary":"Search activity logs with filters + pagination","tags":["Admin - Activity Log"]}},"/api/admin/activity-logs/stats":{"get":{"description":"Aggregated counts for a \"top actions\" dashboard widget. Returns total + failure count per activity_type.","operationId":"ActivityLogAdminController_stats","parameters":[{"name":"sinceHours","required":false,"in":"query","schema":{"example":24,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"example":20,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"sinceHours":24,"stats":[{"activity_type":"food_log.create","total":482,"failures":3},{"activity_type":"auth.login","total":310,"failures":22},{"activity_type":"weight.add","total":94,"failures":0}]}}}}}},"security":[{"bearer":[]}],"summary":"Counts per activity_type in a recent window","tags":["Admin - Activity Log"]}},"/api/admin/activity-logs/user/{userId}":{"get":{"operationId":"ActivityLogAdminController_userTrail","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"example":100,"type":"number"}},{"name":"sinceHours","required":false,"in":"query","description":"Only return rows created in the last N hours","schema":{"example":168,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"userId":42,"count":1,"data":[{"id":9123,"activity_type":"food_log.create","target_type":"food_log","target_id":"101","success":true,"created_at":"2026-04-22T12:30:00.000Z"}]}}}}}},"security":[{"bearer":[]}],"summary":"Recent activity for a single user (convenience endpoint)","tags":["Admin - Activity Log"]}},"/api/admin/activity-logs/target/{targetType}/{targetId}":{"get":{"operationId":"ActivityLogAdminController_targetTrail","parameters":[{"name":"targetType","required":true,"in":"path","schema":{"type":"string"}},{"name":"targetId","required":true,"in":"path","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"example":100,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"targetType":"food_log","targetId":"101","count":2,"data":[{"id":9200,"user_id":42,"activity_type":"food_log.delete","success":true,"created_at":"2026-04-22T13:05:00.000Z"},{"id":9123,"user_id":42,"activity_type":"food_log.create","success":true,"created_at":"2026-04-22T12:30:00.000Z"}]}}}}}},"security":[{"bearer":[]}],"summary":"Full history of a specific target row (e.g. a single food_log id)","tags":["Admin - Activity Log"]}},"/api/admin/activity-logs/export.csv":{"get":{"description":"Same filter surface as GET /. Streams matching rows to the client as CSV; capped at 50000 rows by default — narrow filters if you hit that. Common details.* keys (error, device) are broken out into columns; any other details keys land in a catch-all JSON column.","operationId":"ActivityLogAdminController_exportCsv","parameters":[{"name":"userId","required":false,"in":"query","schema":{"type":"number"}},{"name":"activityType","required":false,"in":"query","schema":{"type":"string"}},{"name":"targetType","required":false,"in":"query","schema":{"type":"string"}},{"name":"targetId","required":false,"in":"query","schema":{"type":"string"}},{"name":"actorRole","required":false,"in":"query","schema":{"type":"string"}},{"name":"success","required":false,"in":"query","description":"true | false; omit for both","schema":{"type":"string"}},{"name":"from","required":false,"in":"query","description":"ISO timestamp","schema":{"type":"string"}},{"name":"to","required":false,"in":"query","description":"ISO timestamp","schema":{"type":"string"}},{"name":"cap","required":false,"in":"query","description":"Max rows (default + max 50000)","schema":{"type":"number"}}],"responses":{"200":{"description":"text/csv; UTF-8, RFC 4180"}},"security":[{"bearer":[]}],"summary":"Export filtered activity logs as CSV (opens in Excel)","tags":["Admin - Activity Log"]}},"/api/admin/admin-activity-logs":{"get":{"operationId":"AdminActivityLogController_search","parameters":[{"name":"adminId","required":false,"in":"query","schema":{"type":"number"}},{"name":"actionType","required":false,"in":"query","schema":{"example":"lucille_seed_wipe","type":"string"}},{"name":"targetType","required":false,"in":"query","schema":{"example":"rewrite_rag","type":"string"}},{"name":"from","required":false,"in":"query","description":"ISO timestamp lower bound","schema":{"type":"string"}},{"name":"to","required":false,"in":"query","description":"ISO timestamp upper bound","schema":{"type":"string"}},{"name":"page","required":false,"in":"query","schema":{"example":1,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"example":50,"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Search admin_activity_log with filters + pagination","tags":["Admin - Admin Activity Log"]}},"/api/admin/admin-activity-logs/admin/{adminId}":{"get":{"operationId":"AdminActivityLogController_adminTrail","parameters":[{"name":"adminId","required":true,"in":"path","schema":{"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"example":100,"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"All actions performed by a specific admin user","tags":["Admin - Admin Activity Log"]}},"/api/admin/audit-logs":{"get":{"description":"Returns rows ordered by created_at DESC. All filters are optional and AND-combined. Page size capped at 200.","operationId":"AuditLogController_search","parameters":[{"name":"actionType","required":false,"in":"query","description":"rag_read | rag_write | bridge_access | admin_action | auth_failure | lucille_seed_wipe","schema":{"example":"lucille_seed_wipe","type":"string"}},{"name":"actor","required":false,"in":"query","description":"Exact actor string, e.g. \"admin\", \"user:42\", \"bridge_key:3\"","schema":{"example":"admin","type":"string"}},{"name":"resource","required":false,"in":"query","schema":{"example":"rewrite_rag","type":"string"}},{"name":"vaultType","required":false,"in":"query","schema":{"example":"user","type":"string"}},{"name":"wasAllowed","required":false,"in":"query","description":"true | false; omit for both","schema":{"type":"string"}},{"name":"from","required":false,"in":"query","description":"ISO timestamp lower bound on created_at","schema":{"type":"string"}},{"name":"to","required":false,"in":"query","description":"ISO timestamp upper bound on created_at","schema":{"type":"string"}},{"name":"page","required":false,"in":"query","schema":{"example":1,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"example":50,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"page":1,"limit":50,"total":1,"data":[{"id":12,"action_type":"lucille_seed_wipe","actor":"admin","vault_type":null,"resource":"rewrite_rag","detail":{"scoped":true,"user_ids_filter":[91],"affected_user_count":1,"deleted_rag_rows":2,"cleared_working_memory_blobs":1,"cleared_working_memory":true,"reason":"May 2026 contamination scrub"},"ip_address":null,"was_allowed":true,"created_at":"2026-05-15T09:26:11.000Z"}]}}}}}},"security":[{"bearer":[]}],"summary":"Search audit_log with filters + pagination","tags":["Admin - Audit Log"]}},"/api/admin/audit-logs/stats":{"get":{"operationId":"AuditLogController_stats","parameters":[{"name":"sinceHours","required":false,"in":"query","schema":{"example":24,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"example":20,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"sinceHours":24,"stats":[{"action_type":"rag_read","total":340,"denied":0},{"action_type":"bridge_access","total":82,"denied":1},{"action_type":"lucille_seed_wipe","total":2,"denied":0}]}}}}}},"security":[{"bearer":[]}],"summary":"Count per action_type in the last N hours — dashboard widget","tags":["Admin - Audit Log"]}},"/api/admin/rollout-cohort/no-plan-breakdown":{"get":{"description":"Partitions the noPlanChosen bucket into never_started / dropped_mid / completed_no_choice (the paywall opens AFTER the questionnaire, so questionnaire depth is the drop-off funnel), plus a per-question survival curve and time-to-drop, recency and verification splits. `total` reconciles exactly with rolloutCohort.noPlanChosen for the same cohortFrom.","operationId":"NoPlanAnalyticsController_getBreakdown","parameters":[{"name":"cohortFrom","required":false,"in":"query","description":"ISO date YYYY-MM-DD. Defaults to TRIAL_SYSTEM_LAUNCH_DATE, else the rollout floor 2026-07-27.","schema":{"type":"string"}},{"name":"timezone","required":false,"in":"query","description":"IANA zone for day boundaries.","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Where \"No Plan Chosen\" users actually dropped off","tags":["Admin - Rollout Cohort"]}},"/api/admin/rollout-cohort/no-plan-users":{"get":{"description":"Paginated, ordered by created_at DESC. All filters are optional and AND-combined, and mirror the breakdown buckets exactly so a chip click lands on the count the card showed. Page size capped at 200.","operationId":"NoPlanAnalyticsController_getUsers","parameters":[{"name":"limit","required":false,"in":"query","schema":{"type":"number"}},{"name":"page","required":false,"in":"query","schema":{"type":"number"}},{"name":"search","required":false,"in":"query","description":"Matches email, first or last name.","schema":{}},{"name":"signedUpWithin","required":false,"in":"query","schema":{"enum":["24h","7d","30d","older"],"type":"string"}},{"name":"droppedWithin","required":false,"in":"query","schema":{"enum":["same_session","under_24h","under_7d","later"],"type":"string"}},{"name":"phoneVerified","required":false,"in":"query","description":"true | false","schema":{}},{"name":"emailVerified","required":false,"in":"query","description":"true | false","schema":{}},{"name":"questionId","required":false,"in":"query","description":"Last question ANSWERED (abandoned right after it).","schema":{"type":"number"}},{"name":"stage","required":false,"in":"query","schema":{"enum":["never_started","dropped_mid","completed_no_choice"],"type":"string"}},{"name":"timezone","required":false,"in":"query","schema":{}},{"name":"cohortFrom","required":false,"in":"query","schema":{}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"The users behind any slice of the No Plan Chosen breakdown","tags":["Admin - Rollout Cohort"]}},"/api/admin/rollout-cohort/no-plan-users/export.csv":{"get":{"description":"Same filter surface as GET no-plan-users. Capped at 50000 rows — narrow filters if you hit that.","operationId":"NoPlanAnalyticsController_exportCsv","parameters":[],"responses":{"200":{"description":"text/csv; UTF-8, RFC 4180"}},"security":[{"bearer":[]}],"summary":"Export the filtered No Plan Chosen users as CSV (opens in Excel)","tags":["Admin - Rollout Cohort"]}},"/api/admin/subscriber-segments/founder-mismatch/breakdown":{"get":{"operationId":"SubscriberSegmentsController_getFounderMismatchBreakdown","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"is_founder flag vs. subscription_type=founder — who disagrees","tags":["Admin - Subscriber Segments"]}},"/api/admin/subscriber-segments/founder-mismatch/users":{"get":{"operationId":"SubscriberSegmentsController_getFounderMismatchUsers","parameters":[{"name":"which","required":true,"in":"query","schema":{"enum":["flag_only","type_only"],"type":"string"}},{"name":"search","required":false,"in":"query","schema":{"type":"string"}},{"name":"page","required":false,"in":"query","schema":{"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"tags":["Admin - Subscriber Segments"]}},"/api/admin/subscriber-segments/founder-mismatch/users/export.csv":{"get":{"operationId":"SubscriberSegmentsController_exportFounderMismatchCsv","parameters":[{"name":"which","required":true,"in":"query","schema":{"enum":["flag_only","type_only"],"type":"string"}},{"name":"search","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"tags":["Admin - Subscriber Segments"]}},"/api/admin/subscriber-segments/{segment}/breakdown":{"get":{"operationId":"SubscriberSegmentsController_getBreakdown","parameters":[{"name":"segment","required":true,"in":"path","schema":{"enum":["status-active","status-trialing","status-pastdue","status-canceled","status-expired","platform-stripe","platform-apple","platform-google"],"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"tags":["Admin - Subscriber Segments"]}},"/api/admin/subscriber-segments/{segment}/users":{"get":{"operationId":"SubscriberSegmentsController_getUsers","parameters":[{"name":"segment","required":true,"in":"path","schema":{"enum":["status-active","status-trialing","status-pastdue","status-canceled","status-expired","platform-stripe","platform-apple","platform-google"],"type":"string"}},{"name":"search","required":false,"in":"query","schema":{"type":"string"}},{"name":"page","required":false,"in":"query","schema":{"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"tags":["Admin - Subscriber Segments"]}},"/api/admin/subscriber-segments/{segment}/users/export.csv":{"get":{"operationId":"SubscriberSegmentsController_exportCsv","parameters":[{"name":"segment","required":true,"in":"path","schema":{"enum":["status-active","status-trialing","status-pastdue","status-canceled","status-expired","platform-stripe","platform-apple","platform-google"],"type":"string"}},{"name":"search","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"tags":["Admin - Subscriber Segments"]}},"/api/admin/revenue-drilldown/window-breakdown":{"get":{"operationId":"RevenueDrilldownController_getWindowBreakdown","parameters":[{"name":"from","required":true,"in":"query","schema":{"type":"string"}},{"name":"to","required":true,"in":"query","schema":{"type":"string"}},{"name":"timezone","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Platform + interval split of new revenue in a window","tags":["Admin - Revenue Drilldown"]}},"/api/admin/revenue-drilldown/window-subscriptions":{"get":{"operationId":"RevenueDrilldownController_getWindowSubscriptions","parameters":[{"name":"limit","required":false,"in":"query","schema":{"type":"number"}},{"name":"page","required":false,"in":"query","schema":{"type":"number"}},{"name":"search","required":false,"in":"query","schema":{}},{"name":"interval","required":false,"in":"query","schema":{"enum":["month","year","unknown"],"type":"string"}},{"name":"platform","required":false,"in":"query","schema":{"enum":["stripe","apple","google"],"type":"string"}},{"name":"timezone","required":false,"in":"query","schema":{}},{"name":"to","required":true,"in":"query","schema":{}},{"name":"from","required":true,"in":"query","schema":{}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"tags":["Admin - Revenue Drilldown"]}},"/api/admin/revenue-drilldown/window-subscriptions/export.csv":{"get":{"operationId":"RevenueDrilldownController_exportWindowCsv","parameters":[{"name":"to","required":true,"in":"query","schema":{}},{"name":"from","required":true,"in":"query","schema":{}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"tags":["Admin - Revenue Drilldown"]}},"/api/admin/revenue-drilldown/failed-payments-breakdown":{"get":{"operationId":"RevenueDrilldownController_getFailedPaymentsBreakdown","parameters":[{"name":"from","required":true,"in":"query","schema":{"type":"string"}},{"name":"to","required":true,"in":"query","schema":{"type":"string"}},{"name":"timezone","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"tags":["Admin - Revenue Drilldown"]}},"/api/admin/revenue-drilldown/failed-payments":{"get":{"operationId":"RevenueDrilldownController_getFailedPayments","parameters":[{"name":"limit","required":false,"in":"query","schema":{"type":"number"}},{"name":"page","required":false,"in":"query","schema":{"type":"number"}},{"name":"search","required":false,"in":"query","schema":{}},{"name":"timezone","required":false,"in":"query","schema":{}},{"name":"to","required":true,"in":"query","schema":{}},{"name":"from","required":true,"in":"query","schema":{}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"tags":["Admin - Revenue Drilldown"]}},"/api/admin/revenue-drilldown/failed-payments/export.csv":{"get":{"operationId":"RevenueDrilldownController_exportFailedPaymentsCsv","parameters":[{"name":"to","required":true,"in":"query","schema":{}},{"name":"from","required":true,"in":"query","schema":{}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"tags":["Admin - Revenue Drilldown"]}},"/api/admin/engagement-segments/logging-trend":{"get":{"operationId":"EngagementSegmentsController_getLoggingTrend","parameters":[{"name":"days","required":false,"in":"query","description":"Trend window in days (1-90, default 30)","schema":{"type":"string"}},{"name":"timezone","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Daily food/water/weight/exercise logger counts for the Logging rates trend chart","tags":["Admin - Engagement Segments"]}},"/api/admin/engagement-segments/{metric}/breakdown":{"get":{"operationId":"EngagementSegmentsController_getBreakdown","parameters":[{"name":"metric","required":true,"in":"path","schema":{"enum":["dau","wau","mau"],"type":"string"}},{"name":"timezone","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"DAU/WAU/MAU split by population — sums to the dashboard-summary-beta card total","tags":["Admin - Engagement Segments"]}},"/api/admin/engagement-segments/{metric}/users":{"get":{"operationId":"EngagementSegmentsController_getUsers","parameters":[{"name":"metric","required":true,"in":"path","schema":{"enum":["dau","wau","mau"],"type":"string"}},{"name":"population","required":true,"in":"query","schema":{"enum":["active-enrolled-clients","other-clients-members","subscribed-leads","free-other"],"type":"string"}},{"name":"search","required":false,"in":"query","schema":{"type":"string"}},{"name":"page","required":false,"in":"query","schema":{"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"type":"number"}},{"name":"timezone","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"tags":["Admin - Engagement Segments"]}},"/api/admin/engagement-segments/{metric}/users/export.csv":{"get":{"operationId":"EngagementSegmentsController_exportCsv","parameters":[{"name":"metric","required":true,"in":"path","schema":{"enum":["dau","wau","mau"],"type":"string"}},{"name":"population","required":true,"in":"query","schema":{"enum":["active-enrolled-clients","other-clients-members","subscribed-leads","free-other"],"type":"string"}},{"name":"search","required":true,"in":"query","schema":{"type":"string"}},{"name":"timezone","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"tags":["Admin - Engagement Segments"]}},"/api/admin/provider-truth":{"get":{"description":"Cached for 30 minutes because a refresh makes ~15 outbound calls across two rate-limited providers. Pass refresh=true to force a re-fetch. Each provider reports its own ok/error so one failing never blanks the other. Apple and Google are covered via RevenueCat — neither store has a list-all API.","operationId":"ProviderTruthController_getTruth","parameters":[{"name":"from","required":false,"in":"query","description":"ISO date YYYY-MM-DD. Defaults to 29 days before `to`.","schema":{"type":"string"}},{"name":"to","required":false,"in":"query","description":"ISO date YYYY-MM-DD. Defaults to today.","schema":{"type":"string"}},{"name":"refresh","required":false,"in":"query","description":"true to bypass the cache.","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Live subscription figures from Stripe + RevenueCat (no DB)","tags":["Admin - Provider Truth"]}},"/api/admin/provider-truth/stripe-reconciliation/export.csv":{"get":{"description":"The full contents of one reconciliation bucket (the JSON response caps each at 20). null_id = platform=stripe with no stripe_subscription_id, so no webhook can ever reach the row. phantom = an id Stripe has never heard of. stale = Stripe knows the id but calls it canceled/expired. missing = live at Stripe but absent from our entitled set.","operationId":"ProviderTruthController_exportCsv","parameters":[{"name":"bucket","required":true,"in":"query","schema":{"enum":["null_id","phantom","stale","missing","prepaid","rc_store","entitled_cancelled","cancelled_still_billing","unpaid_checkout"],"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Export the DB rows Stripe disagrees with","tags":["Admin - Provider Truth"]}},"/api/subscribers/subscribe":{"post":{"operationId":"SubscribersController_subscribe","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscribeDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Verification code sent"}}}}}},"summary":"Subscribe email to marketing list — sends OTP","tags":["Subscribers"]}},"/api/subscribers/verify":{"post":{"operationId":"SubscribersController_verify","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifySubscriberDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Subscribed successfully"}}}}}},"summary":"Verify OTP to confirm marketing subscription","tags":["Subscribers"]}},"/api/meal-notes":{"get":{"operationId":"MealNotesController_listForMeal","parameters":[{"name":"log_date","required":true,"in":"query","schema":{"example":"2026-04-24","type":"string"}},{"name":"meal_number","required":true,"in":"query","description":"1=breakfast, 2=lunch, 3=dinner, 4-6=snack","schema":{"example":2,"type":"number"}},{"name":"user_id","required":false,"in":"query","description":"When set by a coach/admin caller, returns notes for this client instead of the caller. Clients may only omit or pass their own id.","schema":{"type":"number"}},{"name":"scope","required":false,"in":"query","description":"'meal' = food_log_id IS NULL only; 'food' = food_log_id IS NOT NULL only; 'all' = both (default).","schema":{"default":"all","type":"string","enum":["meal","food","all"]}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"notes":[{"id":12,"user_id":5,"log_date":"2026-04-24","meal_number":2,"meal_type":"lunch","food_log_id":101,"body":"Ate out, estimated portions.","created_at":"2026-04-24T12:30:00.000Z","updated_at":"2026-04-24T12:30:00.000Z"}]}}}}}},"security":[{"bearer":[]}],"summary":"List meal notes for a meal. Clients see their own; coaches/admins pass user_id to read a client's.","tags":["Meal Notes"]},"post":{"operationId":"MealNotesController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMealNoteDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Create a meal note. Premium-gated (enrolled or paid).","tags":["Meal Notes"]}},"/api/meal-notes/by-day":{"get":{"operationId":"MealNotesController_listByDay","parameters":[{"name":"log_date","required":true,"in":"query","schema":{"example":"2026-04-24","type":"string"}},{"name":"user_id","required":false,"in":"query","description":"When set by a coach/admin caller, returns notes for this client instead of the caller.","schema":{"type":"number"}},{"name":"scope","required":false,"in":"query","schema":{"default":"all","type":"string","enum":["meal","food","all"]}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"log_date":"2026-04-24","user_id":5,"notes":[{"id":12,"meal_number":2,"body":"..."}],"byMeal":{"2":[{"id":12,"body":"..."}]}}}}}}},"security":[{"bearer":[]}],"summary":"List meal notes for a user across a whole day, grouped by meal_number.","tags":["Meal Notes"]}},"/api/meal-notes/{id}":{"patch":{"operationId":"MealNotesController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMealNoteDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Edit own meal note.","tags":["Meal Notes"]},"delete":{"operationId":"MealNotesController_softDelete","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Soft-delete own meal note.","tags":["Meal Notes"]}},"/api/health/capabilities/me":{"get":{"operationId":"HealthController_getMyCapabilities","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Capabilities for the current user — what health-data sources they have. Drives adaptive UI in the mobile app. Premium-only.","tags":["Health"]}},"/api/health/capabilities/{userId}":{"get":{"operationId":"HealthController_getUserCapabilities","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Capabilities for a specific user (coach role required). Drives device-badge cluster on the Clients page and adaptive drill-in tabs in the coach view. Premium-only + requires the target user to have set health_data_consent_at.","tags":["Health"]}},"/api/health/consent":{"get":{"operationId":"HealthController_getMyConsent","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get the current user's health-data sharing consent state (reads users.health_data_consent_at). Premium-only.","tags":["Health"]},"put":{"operationId":"HealthController_setMyConsent","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetHealthConsentDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Toggle the current user's health-data sharing consent. Sets users.health_data_consent_at to NOW() on grant (idempotent — already-true does not bump the timestamp) or null on revocation. The bridge HealthDataConsentGuard (REW-421) reads this column before returning per-user health data to Lucille. Premium-only.","tags":["Health"]}},"/api/health/sync":{"post":{"operationId":"HealthController_sync","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthSyncRequestDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Ingest a batch of health-platform samples for the current user (Apple HealthKit / Google Health Connect). Stores in health_samples, upserts sync anchors and device-connection rows, invalidates the capabilities cache. Up to 500 samples per request — clients paginate larger backfills. Premium-only.","tags":["Health"]}},"/api/health/connections/me":{"get":{"operationId":"HealthController_getMyConnections","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Connected health-platform sources for the current user. Drives the mobile Settings → Health Connections screen. Premium-only.","tags":["Health"]}},"/api/health/connections/{userId}":{"get":{"operationId":"HealthController_getUserConnections","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Connected health-platform sources for a specific client (coach role required). Powers the device-badge cluster on the Clients page. Premium-only + requires the target user to have set health_data_consent_at.","tags":["Health"]}},"/api/health/phase/me":{"get":{"operationId":"HealthController_getMyPhase","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Bariatric phase timeline for the current user — powers the \"Bariatric phase\" progress-bar widget on the dashboard. Returns the ordered 7-node display sequence (Pre-op → LRD → Liquid → Puree → Soft → Transition → Maintenance) with each node marked completed / current / upcoming. Non-bariatric users get is_bariatric=false and an empty phases array. Premium-only.","tags":["Health"]}},"/api/health/readiness/me":{"get":{"operationId":"HealthController_getMyReadiness","parameters":[{"name":"date","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Daily-readiness score for the current user — 0-100 composite of sleep / HRV / RHR / steps. Returns null score + band=\"insufficient_data\" when fewer than 2 components have data. Defaults to today; pass ?date=YYYY-MM-DD for a historical day. Premium-only.","tags":["Health"]}},"/api/health/readiness/history/me":{"get":{"operationId":"HealthController_getMyReadinessHistory","parameters":[{"name":"window","required":true,"in":"query","schema":{"type":"string"}},{"name":"date","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Readiness score history for the current user — powers the bar chart on the Daily Readiness detail screen. week=7 daily points, month=30 daily points, year=12 monthly-avg points. Sparse — only days with a stored snapshot are returned. Pass ?date=YYYY-MM-DD to anchor the window. Premium-only.","tags":["Health"]}},"/api/health/readiness/{userId}":{"get":{"operationId":"HealthController_getUserReadiness","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Daily-readiness score for a specific client (coach role). Same shape as /readiness/me. Premium-only + requires the target user to have set health_data_consent_at. Shared coaching inbox model — every coach can read any consented client, not just an assigned one.","tags":["Health"]}},"/api/health/metrics/me":{"get":{"operationId":"HealthController_getMyMetrics","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"30-day aggregate health metrics for the current user — powers the Sleep / Activity / HRV / Recovery dashboard cards. Each card returns current-window avg, prior-window avg, and a trend direction. Recovery avg is sourced from persisted daily readiness snapshots; all other metrics are computed from health_samples. 5-minute per-user cache; invalidated on sync. Premium-only.","tags":["Health"]}},"/api/health/metrics/{userId}":{"get":{"operationId":"HealthController_getClientMetrics","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"30-day aggregate health metrics for a specific client (coach role). Same shape as /metrics/me. Premium-only + requires the target user to have set health_data_consent_at. Shared coaching inbox model — every coach can read any consented client.","tags":["Health"]}},"/api/health/sparklines/me":{"get":{"operationId":"HealthController_getMySparklines","parameters":[{"name":"days","required":false,"in":"query","description":"Number of days to include (1-30). Defaults to 7.","schema":{"default":7,"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Daily time-series data for the Sleep / Activity / HRV / Recovery dashboard card sparklines. Returns arrays of (date, value) points for the last N days (default 7, max 30). Null per metric when the user has no data source connected for that type. 5-min Redis cache. Premium-only.","tags":["Health"]}},"/api/health/sparklines/{userId}":{"get":{"operationId":"HealthController_getClientSparklines","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}},{"name":"days","required":false,"in":"query","description":"Number of days to include (1-30). Defaults to 7.","schema":{"default":7,"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Daily sparkline data for a specific client (coach role). Same shape as /sparklines/me. Premium-only + requires health_data_consent_at.","tags":["Health"]}},"/api/health/data-export/me":{"get":{"operationId":"HealthController_exportMyHealthData","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Download every health-data row stored against the current user — samples, device connections, sync anchors, consent timeline. GDPR Article 20 portability. Self-contained JSON envelope. Capped at 200K samples / 5K devices / 5K anchors per request — response.truncated flags clipping. Premium-only; logged for audit.","tags":["Health"]}},"/api/health/data/me":{"delete":{"description":"Delegates to HealthDataDeletionService.deleteHealthData. Two independently-selectable groups: includeDeviceSynced covers health_samples, health_device_connections, health_sync_anchors, health_readiness_snapshots, user_source_preferences, health_baselines (the actual \"synced health data\" PP §2.3 refers to). Clearing health_device_connections returns health_last_synced_at in GET /users/profile to null, which mobile reads as \"not yet synced\". includeManualLogs covers bowel_movement_logs, digestive_symptom_logs, diary_entries, user_health_goals — never synced from a device. At least one must be true. If HealthKit/Health Connect permission is still granted at the OS level and includeDeviceSynced was selected, the next sync will re-pull that data — the mobile client is responsible for warning the user before they confirm.","operationId":"HealthController_deleteMyHealthData","parameters":[{"name":"includeDeviceSynced","required":false,"in":"query","description":"Delete device-synced data (samples, connections, anchors, readiness). Default true.","schema":{"type":"boolean"}},{"name":"includeManualLogs","required":false,"in":"query","description":"Delete manually-logged data (bowel movements, digestive symptoms, diary, goals). Default false.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"health_samples":4821,"health_device_connections":2,"health_sync_anchors":12,"health_readiness_snapshots":90,"user_source_preferences":5,"health_baselines":3}}}}}},"security":[{"bearer":[]}],"summary":"Delete the current user’s health data, scoped by category","tags":["Health"]}},"/api/health/report/export":{"post":{"description":"User selects which sections to include and a date range. Returns application/pdf binary. Generation time: 2–8 seconds. Sections: weight_body_composition, health_improvements, activity_movement, nutrition_logs, journey_timeline.","operationId":"HealthController_exportHealthReport","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportHealthReportDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Generate a configurable PDF health report","tags":["Health"]}},"/api/health/sleep/detail/me":{"get":{"operationId":"HealthController_getMySleepDetail","parameters":[{"name":"date","required":false,"in":"query","description":"Target date in YYYY-MM-DD format. Defaults to today in the timezone supplied via the x-timezone header (falls back to UTC).","schema":{"example":"2026-05-14","type":"string"}},{"name":"x-timezone","required":false,"in":"header","description":"IANA timezone string (e.g. \"America/New_York\"). Used to resolve \"today\" when no date param is supplied. Falls back to UTC.","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Full sleep detail for the current user for a given night. Returns last-night summary, goal % and status label, Lucille insight, 7-day weekly avg, stage breakdown (when mobile sends category), quality %, sleep onset, RHR avg, and bedtime consistency. Defaults to today in the user's timezone (x-timezone header). Premium-only.","tags":["Health"]}},"/api/health/blood_pressure/detail/me":{"get":{"operationId":"HealthController_getMyBloodPressureDetail","parameters":[{"name":"date","required":false,"in":"query","description":"Target date in YYYY-MM-DD format. Defaults to today in the timezone supplied via the x-timezone header (falls back to UTC).","schema":{"example":"2026-05-14","type":"string"}},{"name":"x-timezone","required":false,"in":"header","description":"IANA timezone string (e.g. \"America/New_York\"). Used to resolve \"today\" when no date param is supplied. Falls back to UTC.","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Blood pressure detail for the current user — the non-chart parts of the BP screen: latest reading card (systolic/diastolic, pulse, source, badge, trend vs previous reading), Lucille's Insights bullets, and the guidance callout. The History chart is served by /health/blood_pressure/trend/me. Defaults to today in the user's timezone (x-timezone header). Premium-only.","tags":["Health"]}},"/api/health/goals/me":{"get":{"operationId":"HealthController_getMyGoals","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get the current user's health goals (sleep hours, daily steps). Returns system defaults (sleep=7h, steps=8000) if the user has never set custom goals. Premium-only.","tags":["Health"]},"put":{"operationId":"HealthController_updateMyGoals","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateHealthGoalsDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Set or update the current user's health goals. Partial updates accepted — omit a field to leave it unchanged. Premium-only.","tags":["Health"]}},"/api/health/snapshot/me":{"post":{"operationId":"HealthController_triggerMySnapshot","parameters":[{"name":"date","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Manually trigger a readiness snapshot computation for the current user. Useful in development (the nightly scheduler skips non-production). Pass ?date=YYYY-MM-DD to compute for a specific date; defaults to yesterday UTC. Idempotent — safe to call multiple times for the same date.","tags":["Health"]}},"/api/health/sleep/trend/me":{"get":{"operationId":"HealthController_getMySleepTrend","parameters":[{"name":"window","required":false,"in":"query","description":"Time window. \"week\" = 7 daily points, \"month\" = 30 daily points, \"6months\" = ~26 weekly-average points, \"year\" = 12 monthly-average points. Defaults to \"week\".","schema":{"type":"string","enum":["week","month","6months","year"]}},{"name":"date","required":false,"in":"query","description":"End-date anchor in YYYY-MM-DD. Defaults to today in UTC. The window runs backwards from this date.","schema":{"example":"2026-05-19","type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Sleep duration trend for the current user. week=7 nightly hours, month=30 nightly hours, year=12 monthly-avg hours. Points are sparse — missing days/months are omitted. Goal field carries the active sleep goal in hours. Premium-only.","tags":["Health"]}},"/api/health/steps/trend/me":{"get":{"operationId":"HealthController_getMyStepsTrend","parameters":[{"name":"window","required":false,"in":"query","schema":{"type":"string","enum":["week","month","6months","year"]}},{"name":"date","required":false,"in":"query","schema":{"example":"2026-05-19","type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Daily step-count trend for the current user. week=7 daily totals, month=30 daily totals, year=12 monthly-avg daily steps. Goal field carries the active steps goal. Premium-only.","tags":["Health"]}},"/api/health/hrv/trend/me":{"get":{"operationId":"HealthController_getMyHrvTrend","parameters":[{"name":"window","required":false,"in":"query","schema":{"type":"string","enum":["week","month","6months","year"]}},{"name":"date","required":false,"in":"query","schema":{"example":"2026-05-19","type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"HRV (Heart Rate Variability) trend for the current user. week=7 daily-avg ms, month=30 daily-avg ms, year=12 monthly-avg ms. Higher is generally better. No goal (goal=null). Premium-only.","tags":["Health"]}},"/api/health/rhr/trend/me":{"get":{"operationId":"HealthController_getMyRhrTrend","parameters":[{"name":"window","required":false,"in":"query","schema":{"type":"string","enum":["week","month","6months","year"]}},{"name":"date","required":false,"in":"query","schema":{"example":"2026-05-19","type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Resting Heart Rate trend for the current user. week=7 daily-avg bpm, month=30 daily-avg bpm, year=12 monthly-avg bpm. Lower is generally better. No goal (goal=null). Premium-only.","tags":["Health"]}},"/api/health/active_minutes/trend/me":{"get":{"operationId":"HealthController_getMyActiveMinutesTrend","parameters":[{"name":"window","required":false,"in":"query","schema":{"type":"string","enum":["week","month","6months","year"]}},{"name":"date","required":false,"in":"query","schema":{"example":"2026-05-19","type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Active (exercise) minutes trend for the current user. week=7 daily totals, month=30 daily totals, year=12 monthly-avg daily minutes. Goal field carries the active_minutes_min goal. Premium-only.","tags":["Health"]}},"/api/health/stand_hours/trend/me":{"get":{"operationId":"HealthController_getMyStandHoursTrend","parameters":[{"name":"window","required":false,"in":"query","schema":{"type":"string","enum":["week","month","6months","year"]}},{"name":"date","required":false,"in":"query","schema":{"example":"2026-05-19","type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Stand hours trend for the current user. week=7 daily totals, month=30 daily totals, year=12 monthly-avg daily stand hours. Goal field carries the stand_hours_min goal. Premium-only.","tags":["Health"]}},"/api/health/blood_pressure/trend/me":{"get":{"operationId":"HealthController_getMyBloodPressureTrend","parameters":[{"name":"window","required":false,"in":"query","description":"Time window. \"month\" = ~30 daily points, \"6months\" = ~26 weekly-avg points, \"year\" = 12 monthly-avg points. Defaults to \"month\".","schema":{"type":"string","enum":["month","6months","year"]}},{"name":"date","required":false,"in":"query","description":"End-date anchor in YYYY-MM-DD. Defaults to today in UTC. The window runs backwards from this date.","schema":{"example":"2026-05-19","type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Blood pressure (systolic + diastolic) trend for the current user. month=~30 daily averages, 6months=~26 weekly averages, year=12 monthly averages. Each point carries both systolic and diastolic. Points are sparse — missing buckets are omitted. Premium-only.","tags":["Health"]}},"/api/health/activity/me":{"get":{"operationId":"HealthController_getMyActivity","parameters":[{"name":"date","required":false,"in":"query","description":"Target date in YYYY-MM-DD. Defaults to today in the user's timezone.","schema":{"example":"2026-05-19","type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Today's activity summary for the current user. Returns step/active-minute/stand-hour ring metrics (value, goal, pct) plus secondary stats (floors, distance, calories), a computed 0-20 strain score with coaching insight, and a pre-op vs now comparison table (null until pre-op baselines are configured). Premium-only.","tags":["Health"]}},"/api/health/more-metrics/me":{"get":{"operationId":"HealthController_getMyMoreMetrics","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"\"More Health Metrics\" screen data for the current user. Returns one card per clinical metric (glucose TIR, blood pressure, sleep quality, avg steps, RHR). Each card is null when the user has no connected device or insufficient data for that metric. Badges are derived from the current vs prior 30-day window. Pre-op comparisons are null until the baseline feature is built. Premium-only.","tags":["Health"]}},"/api/health/manual-entry":{"post":{"operationId":"HealthController_recordManualEntry","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManualEntryDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Record a manual blood pressure or resting heart rate reading. Stores in health_samples with platform=\"manual\". Use recorded_at to back-date a reading taken earlier in the day. Upserts on (user, type, source, timestamp) so re-submitting the same reading is safe. Premium-only.","tags":["Health"]}},"/api/health/metric-history/me":{"get":{"operationId":"HealthController_getMyMetricHistory","parameters":[{"name":"type","required":true,"in":"query","description":"Which health metric to retrieve history for.","schema":{"example":"blood_pressure","type":"string","enum":["blood_pressure","rhr","heart_rate","hrv","glucose","steps","weight","body_fat","bmi"]}},{"name":"from","required":false,"in":"query","description":"Start of date range (ISO 8601). Defaults to 90 days ago.","schema":{"example":"2026-01-01","type":"string"}},{"name":"to","required":false,"in":"query","description":"End of date range (ISO 8601). Defaults to today.","schema":{"example":"2026-06-12","type":"string"}},{"name":"limit","required":false,"in":"query","description":"Max rows to return (1–500). Defaults to 100.","schema":{"default":100,"example":50,"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Paginated history for a single health metric. Supported types: blood_pressure, rhr, heart_rate, hrv, glucose, steps, weight, body_fat, bmi. blood_pressure returns systolic/diastolic pairs. steps returns daily totals. All others return individual readings. Results ordered newest-first. Premium-only.","tags":["Health"]}},"/api/health/diary/me":{"get":{"description":"Returns all available health data points for the given local date: readiness score + insight, sleep summary, activity (steps, calories, distance, floors, stand hours, workouts), vitals (resting HR, HRV, blood glucose, blood pressure), and weight. All sections are nullable — if no data exists for a section the UI should hide that card. Pass the user's local date as `date` (YYYY-MM-DD).","operationId":"HealthController_getMyDiary","parameters":[{"name":"date","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiaryResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Daily health diary","tags":["Health"]}},"/api/journey/me":{"get":{"description":"Returns header stats (NSV count, anniversaries, weight lost), weight trend chart data, and the first page of the timeline feed. Use `trend_window` to select the chart range and `cursor` to paginate the timeline.","operationId":"JourneyController_getJourney","parameters":[{"name":"trend_window","required":false,"in":"query","description":"Weight chart range. Defaults to month.","schema":{"enum":["month","6months","year"],"type":"string"}},{"name":"cursor","required":false,"in":"query","description":"Opaque pagination cursor from previous response","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JourneyResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"My Journey page","tags":["Journey"]}},"/api/journey/me/export":{"get":{"description":"Generates a PDF of the full Journey page: stats header, weight trend chart, and complete timeline with photos embedded. Generation takes 3–10 seconds depending on how many photos exist. Responds with application/pdf binary.","operationId":"JourneyController_exportPdf","parameters":[],"responses":{"200":{"description":"PDF binary (application/pdf)"}},"security":[{"bearer":[]}],"summary":"Download Journey as PDF","tags":["Journey"]}},"/api/journey/weight-trend":{"get":{"description":"Called when user switches the Month / 6 months / Year tab.","operationId":"JourneyController_getWeightTrend","parameters":[{"name":"window","required":false,"in":"query","schema":{"enum":["month","6months","year"],"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WeightTrendDto"}}}}},"security":[{"bearer":[]}],"summary":"Weight trend chart data","tags":["Journey"]}},"/api/journey/timeline":{"get":{"description":"Fetches the next page of timeline entries. Pass `cursor` from previous response.","operationId":"JourneyController_getTimeline","parameters":[{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Paginated timeline feed","tags":["Journey"]}},"/api/journey/entries":{"post":{"description":"NSV entries automatically trigger an AI coaching response (stored asynchronously — poll the entry after ~2s if you need it immediately).","operationId":"JourneyController_createEntry","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJourneyEntryDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JourneyEntryResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Create a journey entry (NSV or misc)","tags":["Journey"]}},"/api/journey/entries/{id}":{"get":{"description":"Accepts the same id format emitted by /journey/me — e.g. \"e_4\" or raw \"4\".","operationId":"JourneyController_getEntry","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JourneyEntryResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Get a single journey entry","tags":["Journey"]},"put":{"operationId":"JourneyController_updateEntry","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateJourneyEntryDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JourneyEntryResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Edit a journey entry","tags":["Journey"]},"delete":{"operationId":"JourneyController_deleteEntry","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Entry deleted"}},"security":[{"bearer":[]}],"summary":"Delete a journey entry and its photos","tags":["Journey"]}},"/api/journey/milestones/{id}":{"delete":{"description":"Removes a single auto-generated milestone (weight loss marker, onederland, goal reached, etc.) by its numeric id. Any photos attached to the milestone are also deleted from storage. 404 when the milestone does not exist or belongs to another user.","operationId":"JourneyController_deleteMilestone","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"Milestone deleted"}},"security":[{"bearer":[]}],"summary":"Delete a computed milestone from the journey timeline.","tags":["Journey"]}},"/api/journey/entries/{id}/photos":{"post":{"description":"Accepts 1–5 images in a single request using the `photos` multipart field. Works for both manual entries (e_42) and computed milestones (wm_onederland, surgery, etc.) — pass the timeline item id directly. All files are uploaded in parallel.","operationId":"JourneyController_addPhoto","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"photos":{"type":"array","items":{"type":"string","format":"binary"},"description":"Up to 5 images per request. Repeat the field for multiple files."}}}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkAddPhotoResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Upload one or more photos to a journey entry or milestone (max 10 total)","tags":["Journey"]}},"/api/journey/entries/{id}/photos/{photoId}":{"delete":{"operationId":"JourneyController_removePhoto","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"photoId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Photo removed"}},"security":[{"bearer":[]}],"summary":"Remove a photo from a journey entry","tags":["Journey"]}},"/api/pc/team":{"get":{"operationId":"PcController_team","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Program Coordinators to show on the lead entry card","tags":["pc"]}},"/api/pc/conversation":{"get":{"operationId":"PcController_conversation","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"The caller's open conversation with the team, or null","tags":["pc"]}},"/api/pc/live":{"get":{"operationId":"PcController_live","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"The Program Coordinator shown to the caller as online now, if any","tags":["pc"]}},"/api/pc/conversation/messages":{"post":{"operationId":"PcController_send","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PcSendMessageDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Message the team; starts the conversation on the first call","tags":["pc"]}},"/api/pc/queue":{"get":{"operationId":"PcController_queue","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Requests waiting for a Program Coordinator: returned, overdue, new","tags":["pc"]}},"/api/pc/conversations/{roomId}/claim":{"post":{"operationId":"PcController_claim","parameters":[{"name":"roomId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PcClaimDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Accept a request, or take over another PC's conversation","tags":["pc"]}},"/api/pc/conversations/{roomId}/release":{"post":{"operationId":"PcController_release","parameters":[{"name":"roomId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PcReleaseDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Hand a conversation back to the pool","tags":["pc"]}},"/api/pc/conversations/{roomId}/archive":{"post":{"operationId":"PcController_archive","parameters":[{"name":"roomId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Archive a conversation whose lead has gone quiet","tags":["pc"]}},"/api/pc/conversations/{roomId}/unarchive":{"post":{"operationId":"PcController_unarchive","parameters":[{"name":"roomId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Move an archived conversation back to Active","tags":["pc"]}},"/api/pc/conversations/{roomId}/notes":{"get":{"operationId":"PcController_listNotes","parameters":[{"name":"roomId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Internal notes on a conversation, newest first","tags":["pc"]},"post":{"operationId":"PcController_addNote","parameters":[{"name":"roomId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PcNoteDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Add an internal note","tags":["pc"]}},"/api/pc/notes/{id}":{"patch":{"operationId":"PcController_editNote","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PcNoteDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Edit your own note","tags":["pc"]},"delete":{"operationId":"PcController_deleteNote","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Delete your own note","tags":["pc"]}},"/api/admin/pc/settings":{"get":{"operationId":"PcAdminController_getSettings","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Program Coordinator messaging settings","tags":["Admin"]},"patch":{"operationId":"PcAdminController_updateSettings","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PcSettingsDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Change how long a lead waits before another PC may take over","tags":["Admin"]}},"/api/users/{userId}/public-profile":{"get":{"operationId":"UsersController_getPublicProfile","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"user":{"id":5,"firstName":"Jane","lastName":"Doe","profileImageUrl":"https://d1abc2def3.cloudfront.net/profile-pictures/5/1740000000000-987654321.jpg?Policy=eyJTd...&Signature=abc123~XYZ&Key-Pair-Id=K1ABC2DEF3GH","memberSince":"2026-01-01T00:00:00.000Z","badges":[{"key":"top_chef","title":"Top Chef","description":"5 recipes created","icon":"top_chef","earnedAt":"2026-03-05T00:00:00.000Z"},{"key":"logging_queen","title":"Logging Bad Ass","description":"Logged three days in a row","icon":"logging_queen","earnedAt":"2026-03-08T00:00:00.000Z"}],"consistency":{"summary":{"points_total":11200,"level":7,"next_level":8,"next_level_threshold":13000,"points_to_next_level":1800},"achievements":[{"id":"9821","title":"Movement 4 days in a week","points":300,"entry_type":"goal","source_type":null,"earned_at":"2026-03-08T09:12:00.000Z"},{"id":"9740","title":"7-day streak","points":200,"entry_type":"streak_milestone","source_type":null,"earned_at":"2026-03-05T07:40:00.000Z"}]}}}}}}}},"security":[{"bearer":[]}],"summary":"Get public profile of another user","tags":["Users"]}},"/api/users/profile":{"get":{"operationId":"UsersController_getProfile","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"user":{"id":5,"email":"user@example.com","name":"Jane Doe","phone":"+1234567890","role":"lead","user_type":"premium","water_goal_oz":64,"created_at":"2026-01-01T00:00:00.000Z"},"profile":{"id":5,"user_id":5,"surgery_type":"gastric_sleeve","surgery_date":"2025-06-01","starting_weight":"220.0","goal_weight":"150.0","height_ft":5,"height_in":6,"dietary_restrictions":null,"profile_image_url":"https://d1234abcd.cloudfront.net/profile-pictures/5/1740000000000-987654321.jpg?Policy=...&Signature=...&Key-Pair-Id=K1234","needs_health_data_consent":false}}}}}}},"security":[{"bearer":[]}],"summary":"Get current user profile","tags":["Users"]},"patch":{"operationId":"UsersController_updateProfile","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProfileDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Profile updated successfully","profile":{"user_id":5,"surgery_type":"gastric_sleeve","goal_weight":"150.0"}}}}}}},"security":[{"bearer":[]}],"summary":"Update user profile","tags":["Users"]}},"/api/users/profile-picture":{"post":{"operationId":"UsersController_uploadProfilePicture","parameters":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"profilePicture":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"imageUrl":"https://d1234.cloudfront.net/profile-pictures/90/abc.jpg?..."}}}}}},"security":[{"bearer":[]}],"summary":"Upload profile picture","tags":["Users"]}},"/api/users/macro-goals":{"post":{"operationId":"UsersController_updateMacroGoals","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMacroGoalsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Macro goals updated successfully","macros":{"calories":1500,"protein":120,"carbs":150,"fat":50,"fiber":25}}}}}}},"security":[{"bearer":[]}],"summary":"Update macro nutrition goals","tags":["Users"]}},"/api/users/privacy-choices":{"get":{"operationId":"UsersController_getPrivacyChoices","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"privacyChoices":{"aiTrainingOptOut":false,"sensitivePiLimit":false,"termsAcceptedAt":"2026-03-01T00:00:00.000Z","termsVersion":"2.0","privacyAcceptedAt":"2026-03-01T00:00:00.000Z","privacyVersion":"2.0","healthDataConsentAt":"2026-03-01T00:00:00.000Z","healthDataConsentVersion":"2.0","aiDisclosureAcceptedAt":"2026-03-01T00:00:00.000Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Get current privacy and consent preferences","tags":["Users"]},"patch":{"operationId":"UsersController_updatePrivacyChoices","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePrivacyChoicesDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Privacy choices updated successfully","updatedFields":["sensitive_pi_limit"]}}}}}},"security":[{"bearer":[]}],"summary":"Update privacy preferences (AI opt-out, sensitive PI limit)","tags":["Users"]}},"/api/users/export":{"get":{"description":"Returns all personal data in structured JSON format. Required by Quebec Law 25 (data portability right) and PIPEDA (right of access). Rate-limited to 3 requests per day.","operationId":"UsersController_exportUserData","parameters":[],"responses":{"200":{"description":"User data export in JSON format"}},"security":[{"bearer":[]}],"summary":"Export all user data (data portability)","tags":["Users"]}},"/api/users/request-delete-otp":{"post":{"description":"Sends a 6-digit OTP to the email address. No authentication required.","operationId":"UsersController_requestDeleteOtp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestDeleteOtpDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"message":"If an account exists with that email, a deletion code has been sent."}}}}}},"security":[{"bearer":[]}],"summary":"Request account deletion OTP (Play Store flow)","tags":["Users"]}},"/api/users/delete-account":{"post":{"description":"Verifies OTP and soft-deletes the account. Optional feedback is saved.","operationId":"UsersController_deleteAccountByOtp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAccountByOtpDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"message":"Your account has been scheduled for deletion. You have 30 days to recover it by contacting support."}}}}}},"security":[{"bearer":[]}],"summary":"Delete account via OTP (Play Store flow)","tags":["Users"]}},"/api/users/account":{"delete":{"description":"Soft-deletes the authenticated user account. Login is blocked immediately. PII is hard-deleted after a 30-day grace period. Required by Apple App Store guidelines.","operationId":"UsersController_deleteAccount","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAccountDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"message":"Your account has been scheduled for deletion. You have 30 days to recover it by contacting support. You will be logged out now."}}}}}},"security":[{"bearer":[]}],"summary":"Delete account","tags":["Users"]}},"/api/food-log":{"get":{"operationId":"FoodLogController_getFoodLogs","parameters":[{"name":"date","required":false,"in":"query","schema":{"example":"2024-01-15","type":"string"}},{"name":"format","required":false,"in":"query","schema":{"enum":["raw","totals","both"],"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"example":50,"type":"number"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"date":"2026-02-19","entries":[{"id":101,"user_id":5,"food_name":"Grilled Chicken Breast","serving_size":"4 oz","calories":180,"protein":35,"carbs":0,"fat":3.5,"fiber":0,"sugar":0,"sodium":75,"meal_number":2,"log_time":"12:30:00","log_date":"2026-02-19","created_at":"2026-02-19T12:30:00.000Z"}],"totals":{"calories":180,"protein":35,"carbs":0,"fat":3.5,"fiber":0,"sodium":75,"sugar":0,"count":1,"foods":["Grilled Chicken Breast"]}}}}}}},"security":[{"bearer":[]}],"summary":"Get food logs for a date (raw entries, totals, or both)","tags":["Food Log"]},"post":{"description":"Body may be a single CreateFoodLogDto or a non-empty array of them. Single body returns { foodLog }; array body returns { foodLogs[] }. Items are persisted sequentially with the same per-row pipeline as the single path (FatSecret recompute, coach notify, badges, vault hooks). Fail-fast — a mid-array failure leaves earlier rows saved.","operationId":"FoodLogController_createFoodLog","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Food logged successfully","foodLog":{"id":101,"user_id":5,"food_name":"Grilled Chicken Breast","serving_size":"4 oz","calories":180,"protein":35,"carbs":0,"fat":3.5,"fiber":0,"sugar":0,"sodium":75,"meal_number":2,"log_time":"12:30:00","log_date":"2026-02-19","created_at":"2026-02-19T12:30:00.000Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Create one or many food log entries","tags":["Food Log"]}},"/api/food-log/{id}":{"delete":{"operationId":"FoodLogController_deleteFoodLog","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Food log deleted successfully"}}}}}},"security":[{"bearer":[]}],"summary":"Delete a food log entry","tags":["Food Log"]}},"/api/food-logs/{id}":{"delete":{"operationId":"FoodLogController_deleteFoodLogPlural","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"tags":["Food Log"]}},"/api/food-log/{id}/edit":{"patch":{"operationId":"FoodLogController_editFoodLog","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFoodLogDto"}},"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateFoodLogDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"foodLog":{"id":1,"food_name":"Grilled Chicken","calories":200,"protein":38,"carbs":0,"fat":4,"serving_size":"5 oz","food_id":"33691","serving_id":"57969","quantity":5,"unit":"oz","meal_number":2,"log_time":"08:00:00","log_date":"2026-03-16","images":["https://cdn.example.com/signed-url"]}}}}}}},"security":[{"bearer":[]}],"summary":"Edit any fields of a food log entry (partial update)","tags":["Food Log"]}},"/api/food-logs/{id}/edit":{"patch":{"operationId":"FoodLogController_editFoodLogPlural","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFoodLogDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"tags":["Food Log"]}},"/api/food-log/fatsecret-food/{foodId}":{"get":{"operationId":"FoodLogController_getFatSecretFood","parameters":[{"name":"foodId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"food":{"id":"33691","name":"Chicken Breast","brand":null,"image_url":null,"servingDescription":"100 g","servings":[{"serving_id":"57972","serving_description":"100 g","number_of_units":"100","measurement_description":"g","metric_serving_amount":"100","metric_serving_unit":"g","calories":"195","protein":"29.55","fat":"7.72","carbohydrate":"0"},{"serving_id":"57969","serving_description":"1 oz boneless, cooked","number_of_units":"1","measurement_description":"oz","metric_serving_amount":"28.35","metric_serving_unit":"g","calories":"55","protein":"8.38","fat":"2.19","carbohydrate":"0"}]}}}}}}},"security":[{"bearer":[]}],"summary":"Fetch a FatSecret food by id (servings for unit-aware edit)","tags":["Food Log"]}},"/api/food-log/preview-macros":{"post":{"operationId":"FoodLogController_previewMacros","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreviewMacrosDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"mode":"server_authoritative","food_id":"33691","name":"Chicken Breast","quantity":4,"unit":"oz","serving_id":"57969","region":null,"scale":1.134,"calories":221,"protein":33.5,"carbs":0,"fat":8.8,"fiber":0,"sugar":0,"sodium":84,"nutrition":[{"name":"Calories","value":221,"unit":"kcal"},{"name":"Protein","value":33.5,"unit":"g"},{"name":"Carbohydrates","value":0,"unit":"g"},{"name":"Fat","value":8.8,"unit":"g"},{"name":"Fiber","value":0,"unit":"g"},{"name":"Sodium","value":84,"unit":"mg"}],"fs_default":null}}}}}},"security":[{"bearer":[]}],"summary":"Preview server-authoritative macros for a (food_id, quantity, unit) tuple","tags":["Food Log"]}},"/api/macro-targets":{"get":{"operationId":"FoodLogController_getMacroTargets","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"targets":{"macros":{"calories":1500,"protein":120,"fat":50,"carbs":150,"fiber":25},"goals":{"water":{"goal_oz":64},"weight":{"start_weight":200,"goal_weight":160}}}}}}}}},"security":[{"bearer":[]}],"summary":"Get user macro nutrition targets","tags":["Food Log"]}},"/api/food-log/quick-add-search":{"get":{"description":"Trigram-similarity search over the user's own logged food_logs history and favorite meals, grouped by source and capped at 5 each — always available, no AI, no subscription/quota gate. Omitting `sources` runs all three legs (history, favourites, catalog) — the intended default. The catalog leg (AI/FatSecret pipeline) being enabled does NOT mean it runs: it only actually executes when history/favourites did NOT already return a confident match (a strong history similarity score, or any favourite hit) OR the query names a specific measured amount (e.g. \"200g chicken\", \"12 oz steak\") that a local logged row can't answer precisely; otherwise it is skipped for free and `fatsecretCatalogSkipped: true` is returned instead. When it does run, it is manually quota-checked and degrades to `fatsecretCatalog: null, fatsecretCatalogError: \"rate_limited\" | \"forbidden\" | \"unavailable\"` instead of failing the whole request when the user has no AI access left. Pass an explicit `sources` subset omitting \"catalog\" (e.g. `sources=history,favourites`) only to hard-guarantee zero AI quota spend for a call.","operationId":"FoodLogController_quickAddSearch","parameters":[{"name":"q","required":true,"in":"query","description":"Trigram-similarity match against the user's own logged food history and favourites.","schema":{"example":"chicken","type":"string"}},{"name":"sources","required":false,"in":"query","description":"Comma-separated: history, favourites, catalog. Omitted = all three (the default). Enabling catalog does not force it to run — see the endpoint description.","schema":{"type":"array","items":{"type":"string"}}},{"name":"timezone","required":false,"in":"query","description":"IANA timezone, used only by the catalog leg (region resolution) when requested.","schema":{"example":"America/New_York","type":"string"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"query":"chicken","history":[{"id":4821,"name":"Chicken Breast, Grilled","calories":140,"protein":26,"carbs":0,"fat":3,"serving_size":"3 oz","food_id":"9876","serving_id":"57972","quantity":1,"unit":"each","last_logged_at":"2026-07-30"}],"favourites":[],"fatsecretCatalogSkipped":true}}}}}},"security":[{"bearer":[]}],"summary":"Search own food history + favourites, falling back to the FatSecret/AI catalog when needed","tags":["Food Log"]}},"/api/food-log/fatsecret-search":{"post":{"deprecated":true,"description":"Deprecated — use GET /food-log/quick-add-search instead (sources=catalog).","operationId":"FoodLogController_fatsecretSearch","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FoodSearchDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"query":"grilled chicken","ingredientsFound":3,"isMultiIngredient":false,"options":[{"id":"9876","name":"Chicken Breast, Grilled","brand":null,"description":"1 serving (3 oz)","quantity":1,"calories":140,"protein":26,"fat":3,"carbs":0,"fiber":0,"sodium":65,"sugar":0,"servings":[{"serving_id":"57972","serving_description":"100 g","number_of_units":"100","measurement_description":"g","metric_serving_amount":"100","metric_serving_unit":"g","calories":"195","protein":"29.55","fat":"7.72","carbohydrate":"0"},{"serving_id":"57969","serving_description":"1 oz boneless, cooked","number_of_units":"1","measurement_description":"oz","metric_serving_amount":"28.35","metric_serving_unit":"g","calories":"55","protein":"8.38","fat":"2.19","carbohydrate":"0"}]}],"total":3,"aiResponse":"Great protein choice! Grilled chicken fits perfectly into your Rewrite plan."}}}}}},"security":[{"bearer":[]}],"summary":"Search FatSecret with AI parsing and coaching","tags":["Food Log"]}},"/api/food-log/autocomplete":{"get":{"description":"Substring-matches (`LIKE '%term%'`) against the user's own prior logged selections first (personal tier), then the community-wide top selections from the last 30 days (global tier). Prefix matches rank above non-prefix substring matches. Zero LLM calls. Returns an empty array for queries under 2 characters.","operationId":"FoodLogController_autocomplete","parameters":[{"name":"q","required":true,"in":"query","description":"Substring to match (≥2 chars)","schema":{"example":"chicken","type":"string"}},{"name":"limit","required":false,"in":"query","description":"Max suggestions (1–20, default 8)","schema":{"example":8,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"query":"chob","suggestions":[{"name":"Chobani Greek Yogurt 2% Plain","source":"personal","pickCount":12},{"name":"Chobani 0% Plain Greek Yogurt","source":"global","pickCount":47},{"name":"Chobani Vanilla Greek Yogurt","source":"global","pickCount":22}]}}}}}},"security":[{"bearer":[]}],"summary":"Personalized food-name autocomplete — driven by user + community search history","tags":["Food Log"]}},"/api/food-log/ingredient-alternatives":{"get":{"description":"The initial fatsecretSearch response for a multi-ingredient query ships 3 options per ingredient (primary + 2 alternatives). This endpoint returns additional options for a single ingredient — positions `offset..offset+limit-1` from FatSecret's search results — in the same FoodOption shape so the mobile client can append to the existing alternatives array without re-fetching the original search. Defaults: offset=3 (skip the 3 already shown), limit=5. Stateless; re-runs searchFoodsWithNutrition on each call.","operationId":"FoodLogController_ingredientAlternatives","parameters":[{"name":"searchTerm","required":true,"in":"query","description":"The search term for this ingredient (typically the `parsedIngredient` / `originalName` field from a prior fatsecretSearch response).","schema":{"example":"ribeye steak","type":"string"}},{"name":"quantity","required":false,"in":"query","description":"Quantity the user logged for this ingredient. Needed for correct macro scaling. If omitted, macros scale to FatSecret's default serving.","schema":{"example":30,"type":"number"}},{"name":"unit","required":false,"in":"query","description":"Unit for the quantity (oz / g / cup / tbsp / etc.).","schema":{"example":"oz","type":"string"}},{"name":"brand","required":false,"in":"query","description":"Optional brand hint — if the parser detected a brand in the original query, pass it here so FatSecret re-ranks toward branded matches.","schema":{"example":"chobani","type":"string"}},{"name":"offset","required":false,"in":"query","description":"Zero-based result offset. Defaults to 3 (skip the primary + 2 alternatives already shown). Clamped to [0, 20].","schema":{"example":3,"type":"number"}},{"name":"limit","required":false,"in":"query","description":"How many additional alternatives to return. Defaults to 5. Clamped to [1, 20].","schema":{"example":5,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"searchTerm":"ribeye steak","offset":3,"limit":5,"alternatives":[{"id":"...","name":"Grass-Fed Beef Ribeye Steak","brand":"Butcher Box","quantity":30,"unit":"oz","calories":2210,"protein":170,"fat":158},{"id":"...","name":"Beef Ribeye Steaks","brand":"Great American Steak Company","quantity":30,"unit":"oz","calories":2310,"protein":161,"fat":168}]}}}}}},"security":[{"bearer":[]}],"summary":"Paginate alternative matches for a multi-ingredient option (powers the mobile \"show more\" swap control)","tags":["Food Log"]}},"/api/food-log/barcode-lookup":{"post":{"operationId":"FoodLogController_barcodeLookup","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BarcodeLookupDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"barcode":"818290014894","options":[{"id":"12345","name":"Whole Milk Plain Greek Yogurt","brand":"Chobani","description":"1 container (150g)","quantity":1,"calories":170,"protein":17,"fat":9,"carbs":6,"fiber":0,"sodium":55,"sugar":4}],"total":1,"aiResponse":"Solid choice. Plain full-fat Greek yogurt is a Rewrite staple."}}}}}},"security":[{"bearer":[]}],"summary":"Look up food by UPC/EAN barcode","tags":["Food Log"]}},"/api/food-log/analyze-photo":{"post":{"operationId":"FoodLogController_analyzeFoodPhoto","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"decodedMeal":{"name":"Grilled Salmon with Vegetables","description":"Grilled salmon fillet with mixed steamed vegetables","macros":{"calories":350,"protein":42,"carbs":12,"fat":14,"fiber":4},"analysis":"This is a solid Rewrite-compliant meal. High protein and healthy fats.","verdict":"🟢 Great Choice","recommendation":"Keep this as a staple. Pair with sweet potato post-workout.","modifications":null,"rewriteCompliant":true,"ingredients":["salmon fillet","broccoli","zucchini","olive oil"],"portionSize":"6 oz salmon + 1 cup vegetables"}}}}}}},"security":[{"bearer":[]}],"summary":"Analyze food photo with AI vision","tags":["Food Log"]}},"/api/decode-meal":{"post":{"operationId":"FoodLogController_decodeMeal","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"decodedMeal":{"name":"Protein Oats","description":"Oatmeal with protein powder and banana","macros":{"calories":420,"protein":35,"carbs":52,"fat":8,"fiber":6},"analysis":"This breakfast is hitting your protein targets while keeping carbs moderate.","verdict":"🟢 Great Choice","recommendation":"Solid breakfast. Consider reducing banana size post-op.","modifications":"Swap banana for blueberries to lower sugar.","rewriteCompliant":true,"ingredients":["oats","protein powder","banana","almond milk"],"portionSize":"1 cup oats + 1 scoop protein"}}}}}}},"security":[{"bearer":[]}],"summary":"Decode a meal description with AI","tags":["Food Log"]}},"/api/food-log/analyze-label":{"post":{"operationId":"FoodLogController_analyzeLabelPhoto","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"examples":{"single":{"summary":"Single image (labelPhoto field) — backward-compatible flat response","value":{"success":true,"labelData":{"labelType":"both","productName":"Chobani Whole Milk Plain Greek Yogurt","servingSize":"1 container (150g)","servingsPerContainer":"1","nutritionFacts":{"calories":170,"protein":17,"carbs":6,"fat":9,"fiber":0,"sugar":4,"sodium":55},"ingredients":"Pasteurized Nonfat Milk, Cream."},"coaching":{"analysis":"Clean label. Two ingredients. This is Rewrite-approved.","verdict":"Great Choice — whole food, no junk","flaggedIngredients":[],"rewriteCompliant":true,"recommendation":"Keep this as a staple. Great protein source."}}},"multi":{"summary":"Multiple images (labelPhotos field) — results array keyed by id","value":{"success":true,"results":[{"id":"item-abc123","success":true,"labelData":{"labelType":"both","productName":"Chobani Whole Milk Plain Greek Yogurt","servingSize":"1 container (150g)","servingsPerContainer":"1","nutritionFacts":{"calories":170,"protein":17,"carbs":6,"fat":9,"fiber":0,"sugar":4,"sodium":55},"ingredients":"Pasteurized Nonfat Milk, Cream."},"coaching":{"analysis":"Clean label. Two ingredients. This is Rewrite-approved.","verdict":"Great Choice — whole food, no junk","flaggedIngredients":[],"rewriteCompliant":true,"recommendation":"Keep this as a staple. Great protein source."}}]}}}}}}}},"security":[{"bearer":[]}],"summary":"Analyze nutrition/ingredient label photo(s) with AI vision","tags":["Food Log"]}},"/api/badges":{"get":{"operationId":"BadgesController_getBadges","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get all badges with earned status for the current user","tags":["badges"]}},"/api/favorite-meals":{"post":{"operationId":"FavoriteMealsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFavoriteMealDto"}}}},"responses":{"201":{"description":"Favorite created"}},"security":[{"bearer":[]}],"summary":"Create a favorite meal bundle","tags":["Favorite Meals"]},"get":{"operationId":"FavoriteMealsController_list","parameters":[{"name":"sort","required":false,"in":"query","schema":{"default":"recent","type":"string","enum":["recent","most_used"]}},{"name":"favorite_type","required":false,"in":"query","description":"Filter by favorite type. Defaults to `bundle` so existing callers keep seeing named multi-item meals.","schema":{"type":"string","enum":["bundle","single_food"]}},{"name":"query","required":false,"in":"query","description":"Case-insensitive substring search over name, item food_name, and item brand.","schema":{"example":"chicken","type":"string"}},{"name":"page","required":false,"in":"query","description":"Page number (1-based)","schema":{"default":1,"example":1,"type":"number"}},{"name":"limit","required":false,"in":"query","description":"Items per page (max 100)","schema":{"default":20,"example":20,"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List the current user's favorite meals","tags":["Favorite Meals"]}},"/api/favorite-meals/suggestions":{"get":{"operationId":"FavoriteMealsController_suggestions","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Detect unsaved repeat-bundles (same 3+ items logged together on 2+ days in the last 14 days)","tags":["Favorite Meals"]}},"/api/favorite-meals/{id}":{"get":{"operationId":"FavoriteMealsController_get","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get a single favorite meal","tags":["Favorite Meals"]},"patch":{"operationId":"FavoriteMealsController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFavoriteMealDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Rename or edit a favorite meal","tags":["Favorite Meals"]},"delete":{"operationId":"FavoriteMealsController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Delete a favorite meal","tags":["Favorite Meals"]}},"/api/favorite-meals/{id}/log":{"post":{"operationId":"FavoriteMealsController_logFavorite","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogFavoriteMealDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"One-tap quick-log: insert one food_logs row per bundle item, bump log_count/last_logged_at","tags":["Favorite Meals"]}},"/api/planned-meals":{"get":{"operationId":"ScheduledMealsController_getPlannedMeals","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"meals":[{"id":1,"user_id":5,"food_name":"[PLANNED] Grilled Chicken","calories":180,"protein":35,"meal_number":2,"log_date":"2026-02-19","created_at":"2026-02-18T20:00:00.000Z"}]}}}}}},"security":[{"bearer":[]}],"summary":"List scheduled meals","tags":["Scheduled Meals"]},"post":{"operationId":"ScheduledMealsController_createPlannedMeal","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateScheduledMealDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"meal":{"id":1,"user_id":5,"food_name":"[PLANNED] Grilled Chicken","calories":180,"protein":35,"meal_number":2,"log_date":"2026-02-19"}}}}}}},"security":[{"bearer":[]}],"summary":"Schedule a meal for a future day","tags":["Scheduled Meals"]}},"/api/planned-meals/{id}":{"delete":{"operationId":"ScheduledMealsController_deletePlannedMeal","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Planned meal deleted"}}}}}},"security":[{"bearer":[]}],"summary":"Delete a scheduled meal","tags":["Scheduled Meals"]}},"/api/planned-meals/{id}/consume":{"post":{"operationId":"ScheduledMealsController_consumePlannedMeal","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsumeScheduledMealDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Planned meal consumed and logged","foodLog":{"id":101,"user_id":5,"food_name":"Grilled Chicken","calories":180,"protein":35,"log_date":"2026-02-19"}}}}}}},"security":[{"bearer":[]}],"summary":"Log a scheduled meal (turns it into a normal food log)","tags":["Scheduled Meals"]}},"/api/save-planned-meal":{"post":{"operationId":"LegacySaveScheduledMealController_createPlannedMeal","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateScheduledMealDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"meal":{"id":1,"user_id":5,"food_name":"[PLANNED] Grilled Chicken","calories":180,"meal_number":2,"log_date":"2026-02-19"}}}}}}},"security":[{"bearer":[]}],"summary":"Schedule a meal (legacy route)","tags":["Scheduled Meals"]}},"/api/meal-plans":{"get":{"operationId":"MealPlansController_getPlans","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List the user's meal plans","tags":["Meal Plans"]},"post":{"operationId":"MealPlansController_createPlan","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMealPlanDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Create a meal plan, optionally with its first items","tags":["Meal Plans"]}},"/api/meal-plans/{id}":{"get":{"operationId":"MealPlansController_getPlan","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get a meal plan and its items","tags":["Meal Plans"]},"patch":{"operationId":"MealPlansController_renamePlan","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenameMealPlanDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Rename a meal plan","tags":["Meal Plans"]},"delete":{"operationId":"MealPlansController_deletePlan","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Delete a meal plan and its grocery list","tags":["Meal Plans"]}},"/api/meal-plans/{id}/items":{"post":{"operationId":"MealPlansController_addItems","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddMealPlanItemsDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Add one or more recipes or foods to a meal plan","tags":["Meal Plans"]}},"/api/meal-plans/{id}/items/{itemId}":{"patch":{"operationId":"MealPlansController_updateItem","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"itemId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMealPlanItemDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Change how many servings of an item the plan makes","tags":["Meal Plans"]},"delete":{"operationId":"MealPlansController_removeItem","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"itemId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Remove an item from a meal plan","tags":["Meal Plans"]}},"/api/meal-plans/{id}/grocery-list":{"post":{"operationId":"MealPlansController_buildGroceryList","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Build (or rebuild) the plan's grocery list with Lucille","tags":["Meal Plans"]}},"/api/grocery-lists":{"get":{"operationId":"GroceryListsController_getLists","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List the user's grocery lists","tags":["Meal Plans"]}},"/api/grocery-lists/shared/{shareId}/save":{"post":{"operationId":"GroceryListsController_saveShared","parameters":[{"name":"shareId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Save a copy of a grocery list shared in chat","tags":["Meal Plans"]}},"/api/grocery-lists/{id}":{"get":{"operationId":"GroceryListsController_getDetail","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get a grocery list and its items","tags":["Meal Plans"]},"patch":{"operationId":"GroceryListsController_rename","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenameGroceryListDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Rename a grocery list","tags":["Meal Plans"]},"delete":{"operationId":"GroceryListsController_deleteList","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Delete a grocery list","tags":["Meal Plans"]}},"/api/grocery-lists/{id}/items":{"post":{"operationId":"GroceryListsController_addItem","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddGroceryItemDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Add a line by hand","tags":["Meal Plans"]}},"/api/grocery-lists/{id}/items/{itemId}":{"patch":{"operationId":"GroceryListsController_updateItem","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"itemId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateGroceryItemDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Tick, untick or edit a line","tags":["Meal Plans"]},"delete":{"operationId":"GroceryListsController_deleteItem","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"itemId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Remove a line","tags":["Meal Plans"]}},"/api/grocery-lists/{id}/share":{"post":{"operationId":"GroceryListsController_share","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareGroceryListDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Post a copy of the list into a group chat","tags":["Meal Plans"]}},"/api/cookbook":{"get":{"operationId":"CookbookController_getRecipes","parameters":[{"name":"view","required":false,"in":"query","schema":{"enum":["my","community","favorites"],"type":"string"}},{"name":"mealType","required":false,"in":"query","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"Sort order. newest=by created_at desc (default), top_rated=by average rating desc, trending=by recent engagement (7 days) desc. Only meaningful for community and favorites views.","schema":{"enum":["newest","top_rated","trending"],"type":"string"}},{"name":"page","required":false,"in":"query","schema":{"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"type":"number"}},{"name":"userId","required":false,"in":"query","description":"Filter community view to a specific user's public recipes. Only valid with view=community.","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List recipes (own, community, or favorites)","tags":["Cookbook"]}},"/api/cookbook/categories":{"get":{"operationId":"CookbookController_getCategories","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"categories":[{"name":"breakfast","count":3},{"name":"lunch","count":5},{"name":"dinner","count":4},{"name":"snack","count":2}]}}}}}},"security":[{"bearer":[]}],"summary":"Get recipe categories with counts","tags":["Cookbook"]}},"/api/cookbook/search":{"get":{"operationId":"CookbookController_searchRecipes","parameters":[{"name":"view","required":false,"in":"query","schema":{"enum":["my","community"],"type":"string"}},{"name":"mealType","required":false,"in":"query","schema":{"type":"string"}},{"name":"search","required":false,"in":"query","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"recipes":[{"id":1,"title":"High-Protein Chicken Bowl","meal_type":"lunch","calories":420,"protein":48}]}}}}}},"security":[{"bearer":[]}],"summary":"Search recipes with filters","tags":["Cookbook"]}},"/api/cookbook/recipes":{"post":{"operationId":"CookbookController_saveRecipe","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveRecipeDto"}},"multipart/form-data":{"schema":{"$ref":"#/components/schemas/SaveRecipeDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"recipe":{"id":1,"user_id":5,"title":"High-Protein Chicken Bowl","meal_type":"lunch","calories":420,"protein":48,"carbs":30,"fat":12,"imageUrl":"https://cdn.example.com/signed-url","created_at":"2026-02-01T00:00:00.000Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Save a recipe to cookbook","tags":["Cookbook"]}},"/api/cookbook/recipes/{id}":{"patch":{"operationId":"CookbookController_updateRecipe","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRecipeDto"}},"multipart/form-data":{"schema":{"$ref":"#/components/schemas/UpdateRecipeDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Recipe updated successfully","recipe":{"id":1,"user_id":5,"title":"High-Protein Chicken Bowl (Updated)","meal_type":"lunch","calories":420,"protein":48,"carbs":30,"fat":12,"imageUrl":"https://cdn.example.com/signed-url","created_at":"2026-02-01T00:00:00.000Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Update an existing recipe","tags":["Cookbook"]},"delete":{"operationId":"CookbookController_deleteRecipe","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Recipe deleted"}}}}}},"security":[{"bearer":[]}],"summary":"Delete a recipe from cookbook","tags":["Cookbook"]},"get":{"operationId":"CookbookController_getRecipeById","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"recipe":{"id":1,"title":"High-Protein Chicken Bowl","createdBy":{"userId":5,"firstName":"Jane","lastName":"D.","profileImageUrl":null},"averageRating":4.2,"ratingCount":11,"myRating":5,"commentCount":3,"comments":[{"id":10,"body":"Love this!","createdAt":"2026-01-01T00:00:00.000Z","user":{"userId":7,"firstName":"Tom","lastName":"K.","profileImageUrl":null}}]}}}}}}},"security":[{"bearer":[]}],"summary":"Get a single recipe with creator info, ratings and comments","tags":["Cookbook"]}},"/api/cookbook/recipes/{id}/community":{"patch":{"operationId":"CookbookController_toggleCommunity","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Recipe is now visible in the community","isCommunity":true}}}}}},"security":[{"bearer":[]}],"summary":"Toggle community visibility of a recipe","tags":["Cookbook"]}},"/api/cookbook/generate":{"post":{"operationId":"CookbookController_generateRecipe","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateRecipeDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"recipe":{"title":"Lemon Herb Grilled Chicken","meal_type":"dinner","calories":380,"protein":45,"carbs":8,"fat":16,"ingredients":["chicken breast","lemon","rosemary","olive oil","garlic"],"instructions":["Marinate chicken...","Grill 6 min per side..."]}}}}}}},"security":[{"bearer":[]}],"summary":"AI-generate a recipe from description","tags":["Cookbook"]}},"/api/cookbook/customize":{"post":{"operationId":"CookbookController_customizeRecipe","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomizeRecipeDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"recipe":{"title":"Low-Carb Chicken Bowl (Customized)","meal_type":"lunch","calories":360,"protein":48,"carbs":12,"fat":14,"modifications":"Replaced rice with cauliflower rice to reduce carbs."}}}}}}},"security":[{"bearer":[]}],"summary":"AI-customize an existing recipe","tags":["Cookbook"]}},"/api/cookbook/format":{"post":{"operationId":"CookbookController_formatRecipeText","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FormatRecipeDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"recipe":{"title":"Simple Egg White Omelette","meal_type":"breakfast","servings":1,"calories":120,"protein":24,"carbs":2,"fat":2,"ingredients":["4 egg whites","spinach","mushrooms","salt","pepper"],"instructions":["Whisk egg whites...","Pour into pan...","Add fillings and fold..."]}}}}}}},"security":[{"bearer":[]}],"summary":"AI-format raw recipe text into structured recipe","tags":["Cookbook"]}},"/api/cookbook/rate":{"post":{"operationId":"CookbookController_rateRecipe","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateRecipeDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Rating saved"}}}}}},"security":[{"bearer":[]}],"summary":"Rate and provide feedback on a recipe","tags":["Cookbook"]}},"/api/cookbook/meal-ideas":{"post":{"operationId":"CookbookController_generateMealIdeas","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateMealIdeasDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"ideas":[{"title":"Grilled Salmon Bowl","description":"Salmon over quinoa with roasted veggies","calories":480,"protein":42},{"title":"Turkey Lettuce Wraps","description":"Ground turkey in butter lettuce with avocado","calories":320,"protein":36}]}}}}}},"security":[{"bearer":[]}],"summary":"Generate AI meal ideas from text input","tags":["Cookbook"]}},"/api/cookbook/meal-ideas-photo":{"post":{"operationId":"CookbookController_generateMealIdeasFromPhoto","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/LucilleActionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"ideas":[{"title":"Stir-Fry Chicken with Broccoli","description":"Using your chicken and broccoli","calories":350,"protein":40},{"title":"Chicken and Rice Bowl","description":"Simple protein bowl with your ingredients","calories":420,"protein":38}]}}}}}},"security":[{"bearer":[]}],"summary":"Generate meal ideas from a photo of ingredients","tags":["Cookbook"]}},"/api/cookbook/analyze-photo":{"post":{"operationId":"CookbookController_analyzePhoto","parameters":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/LucilleActionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"ingredients":["chicken breast","broccoli","olive oil","garlic"],"suggestions":[{"title":"Garlic Chicken with Broccoli","calories":340,"protein":42},{"title":"Sheet Pan Chicken Dinner","calories":380,"protein":44}]}}}}}},"security":[{"bearer":[]}],"summary":"Analyze food photo and generate recipes","tags":["Cookbook"]}},"/api/cookbook/photo-to-order":{"post":{"operationId":"CookbookController_photoToOrder","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/LucilleActionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"recommendation":"Order the grilled salmon (no sauce) with steamed vegetables instead of fries. Ask for dressing on the side. This hits your protein target and stays Rewrite-compliant."}}}}}},"security":[{"bearer":[]}],"summary":"Analyze menu photo for personalized ordering guidance","tags":["Cookbook"]}},"/api/cookbook/recipes/share":{"post":{"operationId":"CookbookController_shareRecipe","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Recipe shared"}}}}}},"security":[{"bearer":[]}],"summary":"Track recipe share","tags":["Cookbook"]}},"/api/cookbook/photo-shares":{"post":{"operationId":"CookbookController_sharePhotoRecipe","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Photo recipe shared"}}}}}},"security":[{"bearer":[]}],"summary":"Track photo recipe share","tags":["Cookbook"]}},"/api/cookbook/recipes/{id}/ratings":{"post":{"operationId":"CookbookController_upsertRating","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertRatingDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Rating saved","averageRating":4.2,"ratingCount":12}}}}}},"security":[{"bearer":[]}],"summary":"Submit or update a rating (1–5) for a community recipe","tags":["Cookbook"]},"delete":{"operationId":"CookbookController_deleteRating","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Rating removed"}}}}}},"security":[{"bearer":[]}],"summary":"Remove own rating from a community recipe","tags":["Cookbook"]}},"/api/cookbook/recipes/{id}/comments":{"get":{"operationId":"CookbookController_getComments","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"page","required":false,"in":"query","schema":{"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"comments":[{"id":10,"body":"Love this!","createdAt":"2026-01-01T00:00:00.000Z","user":{"userId":7,"firstName":"Tom","lastName":"K.","profileImageUrl":null},"replies":[{"id":14,"body":"Agreed!","createdAt":"2026-01-01T01:00:00.000Z","parentId":10,"user":{"userId":9,"firstName":"Amy","lastName":"S.","profileImageUrl":null}}]}],"total":1,"page":1,"limit":20}}}}}},"security":[{"bearer":[]}],"summary":"List comments on a community recipe (paginated)","tags":["Cookbook"]},"post":{"operationId":"CookbookController_addComment","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddCommentDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"comment":{"id":11,"body":"Great recipe!","createdAt":"2026-01-01T00:00:00.000Z","user":{"userId":5,"firstName":"Jane","lastName":"D.","profileImageUrl":null}}}}}}}},"security":[{"bearer":[]}],"summary":"Add a comment to a community recipe","tags":["Cookbook"]}},"/api/cookbook/comments/{commentId}/replies":{"post":{"operationId":"CookbookController_addReply","parameters":[{"name":"commentId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddCommentDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"comment":{"id":15,"body":"Agreed!","createdAt":"2026-01-01T00:00:00.000Z","parentId":10,"user":{"userId":9,"firstName":"Amy","lastName":"S.","profileImageUrl":null}}}}}}}},"security":[{"bearer":[]}],"summary":"Reply to a comment on a community recipe","tags":["Cookbook"]}},"/api/cookbook/comments/{commentId}":{"delete":{"operationId":"CookbookController_deleteComment","parameters":[{"name":"commentId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Comment deleted"}}}}}},"security":[{"bearer":[]}],"summary":"Delete own comment on a community recipe (cascades replies)","tags":["Cookbook"]}},"/api/cookbook/recipes/{id}/favorites":{"post":{"operationId":"CookbookController_addFavorite","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Recipe added to favorites","isFavorite":true}}}}}},"security":[{"bearer":[]}],"summary":"Add a community recipe to favorites","tags":["Cookbook"]},"delete":{"operationId":"CookbookController_removeFavorite","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Recipe removed from favorites","isFavorite":false}}}}}},"security":[{"bearer":[]}],"summary":"Remove a community recipe from favorites","tags":["Cookbook"]}},"/api/weight":{"post":{"operationId":"WeightController_saveWeight","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveWeightDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":{"id":42,"user_id":5,"weight":"185.5","recorded_at":"2026-02-19T08:00:00.000Z","created_at":"2026-02-19T08:00:00.000Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Save a weight entry","tags":["Weight & Measurements"]}},"/api/weight/latest":{"get":{"operationId":"WeightController_getLatestWeight","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":{"id":42,"user_id":5,"weight":"185.5","recorded_at":"2026-02-19T08:00:00.000Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Get most recent weight entry","tags":["Weight & Measurements"]}},"/api/weight/{id}":{"patch":{"operationId":"WeightController_updateWeight","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWeightDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":{"id":42,"user_id":5,"weight":"186.0","recorded_at":"2026-02-20T08:00:00.000Z","created_at":"2026-02-19T08:00:00.000Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Edit an existing weight entry","tags":["Weight & Measurements"]}},"/api/weight/history":{"get":{"description":"Optional ?months= filters the chart window (e.g. 1, 6, 12). Optional ?aggregate=monthly collapses entries to one averaged point per calendar month, for compact \"all time\" charting.","operationId":"WeightController_getWeightHistory","parameters":[{"name":"months","required":true,"in":"query","schema":{"type":"string"}},{"name":"aggregate","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"startWeight":220,"goalWeight":150,"currentWeight":185.5,"totalLost":34.5,"history":[{"id":1,"weight":"220.0","recorded_at":"2025-06-01T00:00:00.000Z"},{"id":42,"weight":"185.5","recorded_at":"2026-02-19T08:00:00.000Z"}]}}}}}},"security":[{"bearer":[]}],"summary":"Get full weight history with start/goal weights","tags":["Weight & Measurements"]}},"/api/measurements":{"post":{"operationId":"WeightController_saveMeasurements","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveMeasurementsDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":{"id":10,"user_id":5,"chest":"38.0","waist":"32.5","hips":"40.0","arms":"13.0","thighs":"22.0","recorded_at":"2026-02-19T08:00:00.000Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Save body measurements","tags":["Weight & Measurements"]},"get":{"operationId":"WeightController_getMeasurementsHistory","parameters":[{"name":"months","required":true,"in":"query","schema":{"type":"string"}},{"name":"field","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":[{"recorded_at":"2026-01-10T08:00:00.000Z","neck":14,"arm":12,"chest":38,"waist":32,"abdomen":33,"hips":40,"thigh":22,"calf":15},{"recorded_at":"2026-02-17T08:00:00.000Z","neck":13.5,"arm":11.5,"chest":37,"waist":31,"abdomen":32,"hips":39,"thigh":21.5,"calf":14.5}]}}}}}},"security":[{"bearer":[]}],"summary":"Get body measurement history with optional filters","tags":["Weight & Measurements"]}},"/api/measurements/latest":{"get":{"operationId":"WeightController_getLatestMeasurements","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":{"current":{"chest":"38.0","waist":"32.5","hips":"40.0","arms":"13.0","thighs":"22.0"},"baseline":{"chest":"42.0","waist":"38.0","hips":"44.0","arms":"14.5","thighs":"26.0"},"changes":{"chest":-4,"waist":-5.5,"hips":-4,"arms":-1.5,"thighs":-4,"total":-19}}}}}}}},"security":[{"bearer":[]}],"summary":"Get latest measurements with baseline changes","tags":["Weight & Measurements"]}},"/api/measurements/combined":{"get":{"operationId":"WeightController_getCombinedMeasurements","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":{"chest":"38.0","waist":"32.5","hips":"40.0","arms":"13.0","thighs":"22.0","recorded_at":"2026-02-19T08:00:00.000Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Get best-available composite measurements","tags":["Weight & Measurements"]}},"/api/measurements/inches":{"get":{"operationId":"WeightController_getInchesHistory","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":[{"date":"2025-06-01","totalInches":164},{"date":"2026-02-19","totalInches":145}]}}}}}},"security":[{"bearer":[]}],"summary":"Get total inches over time for charting","tags":["Weight & Measurements"]}},"/api/measurements/progress":{"get":{"operationId":"WeightController_getMeasurementsProgress","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":{"weight":{"current":185.5,"start":220,"goal":150,"lost":34.5},"measurements":{"current":{"waist":"32.5","hips":"40.0"},"changes":{"waist":-5.5,"hips":-4}},"inchesLost":19}}}}}}},"security":[{"bearer":[]}],"summary":"Full progress page: weight + measurements + changes","tags":["Weight & Measurements"]}},"/api/progress-photos":{"get":{"operationId":"ProgressController_getPhotos","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":[{"id":1,"user_id":5,"image_url":"https://d1234abcd.cloudfront.net/progress-photos/5/1740000000000-123456789.jpg?Policy=...&Signature=...&Key-Pair-Id=K1234","photo_date":"2026-02-19","notes":"Week 4 check-in","weight":185.5,"weight_recorded_at":"2026-02-19T08:00:00.000Z","created_at":"2026-02-19T10:00:00.000Z"}]}}}}}},"security":[{"bearer":[]}],"summary":"List all progress photos","tags":["Progress & Tracking"]},"post":{"operationId":"ProgressController_createPhoto","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["photo"],"properties":{"photo":{"type":"string","format":"binary"},"notes":{"type":"string","example":"Week 4 check-in"}}}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":{"id":1,"user_id":5,"image_url":"https://d1234.cloudfront.net/progress-photos/5/abc.jpg?...","photo_date":"2026-02-24","notes":"Week 4 check-in","weight":184.2,"weight_recorded_at":"2026-02-23T07:30:00.000Z","created_at":"2026-02-24T10:00:00.000Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Add a progress photo (multipart/form-data)","tags":["Progress & Tracking"]}},"/api/progress-photos/{id}":{"delete":{"operationId":"ProgressController_deletePhoto","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Photo deleted"}}}}}},"security":[{"bearer":[]}],"summary":"Delete a progress photo","tags":["Progress & Tracking"]}},"/api/weekly-summary":{"get":{"operationId":"ProgressController_getWeeklySummary","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"period":{"from":"2026-02-13","to":"2026-02-19"},"mealsLoggedPercent":71,"foodsLogged":45,"mealsLoggedCount":42,"mostLoggedEmotion":{"vibe":"motivated","emoji":"💪","count":4}}}}}}},"security":[{"bearer":[]}],"summary":"Get 7-day insights: meals logged %, foods logged, top emotion","tags":["Progress & Tracking"]}},"/api/logging-consistency":{"get":{"operationId":"ProgressController_getLoggingConsistency","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"streak":5,"longestStreak":14,"totalDaysLogged":42,"lastLogDate":"2026-02-19","consistencyPercent":75}}}}}},"security":[{"bearer":[]}],"summary":"Get logging streak and consistency stats","tags":["Progress & Tracking"]}},"/api/logging-goal":{"post":{"operationId":"ProgressController_generateLoggingGoal","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":{"id":1,"goalText":"Your most logged meal is Breakfast. Your least logged meal is Dinner. Let's try to get dinner logs increased by 50%!","targetMeal":"dinner","targetIncreasePercent":50,"progressPercent":0,"progressLabel":"Just started!","setDate":"2025-12-03T00:00:00.000Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Generate a new logging goal targeting the least-logged meal","tags":["Progress & Tracking"]}},"/api/logging-calendar/{year}/{month}":{"get":{"operationId":"ProgressController_getLoggingCalendar","parameters":[{"name":"year","required":true,"in":"path","schema":{"type":"number"}},{"name":"month","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"year":2026,"month":2,"days":{"2026-02-01":{"logged":true,"calories":1420},"2026-02-02":{"logged":false},"2026-02-19":{"logged":true,"calories":1600}}}}}}}},"security":[{"bearer":[]}],"summary":"Get calendar view of logging activity","tags":["Progress & Tracking"]}},"/api/day-details/{date}":{"get":{"operationId":"ProgressController_getDayDetails","parameters":[{"name":"date","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"date":"2026-02-19","foodLogs":[{"id":101,"food_name":"Grilled Chicken","calories":180,"protein":35,"meal_number":2,"log_time":"12:30:00"}],"waterLogs":[{"id":1,"amount_oz":16,"log_date":"2026-02-19"}],"weightEntries":[{"id":1,"user_id":5,"weight":"185.5","recorded_at":"2026-02-19T08:00:00.000Z"}],"measurements":[],"progressPhotos":[{"id":1,"user_id":5,"image_url":"https://d1234abcd.cloudfront.net/progress-photos/5/1740000000000-123456789.jpg?Policy=...&Signature=...&Key-Pair-Id=K1234","photo_date":"2026-02-19","notes":"Week 4 check-in","weight":185.5,"weight_recorded_at":"2026-02-19T08:00:00.000Z"}],"vibeLogs":[{"id":1,"vibe":"motivated","emoji":"💪","timestamp":"2026-02-19T09:00:00.000Z"}],"supplements":[{"supplement":{"id":1,"name":"Vitamin D","type":"preset"},"log":{"id":5,"taken":true}},{"supplement":{"id":2,"name":"Iron","type":"preset"},"log":null}],"totals":{"calories":1420,"protein":110,"carbs":130,"fat":45,"fiber":22,"water":64},"measurementSummary":{"weight":185.5,"measurements":null}}}}}}},"security":[{"bearer":[]}],"summary":"Get all activity for a specific date","tags":["Progress & Tracking"]}},"/api/vibe-log":{"post":{"operationId":"ProgressController_createVibeLog","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateVibeLogDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":{"id":1,"user_id":5,"vibe":"motivated","emoji":"💪","timestamp":"2026-02-19T09:00:00.000Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Log current vibe/mood","tags":["Progress & Tracking"]}},"/api/vibe-log/current":{"get":{"operationId":"ProgressController_getCurrentVibe","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":{"id":1,"vibe":"motivated","emoji":"💪","timestamp":"2026-02-19T09:00:00.000Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Get today's vibe, or null if not yet logged","tags":["Progress & Tracking"]}},"/api/meals/history":{"get":{"operationId":"ProgressController_getMealHistory","parameters":[{"name":"page","required":false,"in":"query","schema":{"example":1,"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"example":20,"type":"string"}},{"name":"unique","required":false,"in":"query","description":"Return only unique entries by food_name","schema":{"example":false,"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":[{"id":101,"user_id":5,"food_name":"Grilled Chicken","serving_size":"4 oz","calories":180,"protein":35,"carbs":0,"fat":4,"fiber":0,"sugar":null,"sodium":null,"meal_number":2,"log_time":"12:30:00","log_date":"2026-03-10","is_planned":false,"created_at":"2026-03-10T12:30:00.000Z"}],"total":120,"page":1,"limit":20,"totalPages":6}}}}}},"security":[{"bearer":[]}],"summary":"Get paginated meal history","tags":["Progress & Tracking"]}},"/api/vibe-log/history":{"get":{"operationId":"ProgressController_getVibeHistory","parameters":[{"name":"limit","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":[{"id":1,"vibe":"motivated","emoji":"💪","timestamp":"2026-02-19T09:00:00.000Z"},{"id":2,"vibe":"happy","emoji":"😊","timestamp":"2026-02-18T10:00:00.000Z"}]}}}}}},"security":[{"bearer":[]}],"summary":"Get vibe history","tags":["Progress & Tracking"]}},"/api/vibe-log/trend":{"get":{"description":"Shaped like GET /journey/weight-trend. window = month (30 days) | 6months (180) | year (365); one point per logged day, empty days omitted. Each point is the average mood score (1–5) for the day plus the vibe logged most often that day. `breakdown` counts each vibe over the window, most frequent first.","operationId":"ProgressController_getVibeTrend","parameters":[{"name":"window","required":false,"in":"query","description":"Same windows as GET /journey/weight-trend: month = last 30 days, 6months = last 180, year = last 365. Defaults to \"month\".","schema":{"type":"string","enum":["month","6months","year"]}},{"name":"date","required":false,"in":"query","description":"End-date anchor in YYYY-MM-DD (inclusive). Defaults to today in the user's timezone.","schema":{"example":"2026-10-26","type":"string"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"metric":"vibes","window":"month","unit":"score","scale":{"min":1,"max":5},"points":[{"date":"2026-10-20","value":4,"vibe":"motivated","emoji":"💪"},{"date":"2026-10-22","value":2,"vibe":"tired","emoji":"😴"},{"date":"2026-10-25","value":5,"vibe":"happy","emoji":"😊"}],"goal":null,"starting_value":4,"current_value":5,"change":1,"average":3.7,"days_logged":3,"goal_progress_pct":null,"top_vibe":{"vibe":"happy","emoji":"😊","count":1},"breakdown":[{"vibe":"happy","emoji":"😊","count":1},{"vibe":"tired","emoji":"😴","count":1},{"vibe":"motivated","emoji":"💪","count":1}]}}}}}},"security":[{"bearer":[]}],"summary":"Vibes/mood trend for sharing","tags":["Progress & Tracking"]}},"/api/admin/promotions":{"get":{"operationId":"AdminPromotionsController_list","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List all promotions (any coach+)","tags":["Admin - Promotions"]},"post":{"operationId":"AdminPromotionsController_create","parameters":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreatePromotionDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Create a promotion (admin only)","tags":["Admin - Promotions"]}},"/api/admin/promotions/{id}":{"patch":{"operationId":"AdminPromotionsController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/UpdatePromotionDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update a promotion (admin only)","tags":["Admin - Promotions"]},"delete":{"operationId":"AdminPromotionsController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Delete a promotion (admin only)","tags":["Admin - Promotions"]}},"/api/promotions/active":{"get":{"operationId":"PromotionsController_active","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Currently-active promotions for the logged-in user (empty if none)","tags":["Promotions"]}},"/api/campaigns/active":{"get":{"operationId":"CampaignsController_active","parameters":[{"name":"country","required":false,"in":"query","description":"ISO 3166-1 alpha-2 device country. Overrides the stored profile country for eligibility.","schema":{"example":"US","type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Campaigns the logged-in user is eligible for, one resolved variant each (empty if none)","tags":["Campaigns"]}},"/api/campaigns/{id}/events":{"post":{"operationId":"CampaignsController_recordEvent","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordCampaignEventDto"}}}},"responses":{"204":{"description":""}},"security":[{"bearer":[]}],"summary":"Record shown/dismissed/clicked. `shown` drives the cooldown and impression cap.","tags":["Campaigns"]}},"/api/campaigns/{id}/claim":{"post":{"operationId":"CampaignsController_claim","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"country","required":false,"in":"query","description":"ISO 3166-1 alpha-2 device country. Overrides the stored profile country for eligibility.","schema":{"example":"US","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimCampaignDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Claim the offer. Idempotent — a repeat claim returns the existing grant with already_claimed: true. Eligibility is re-checked here; 409 means the user no longer qualifies. Send `address` when the campaign has `requires_address`.","tags":["Campaigns"]},"get":{"operationId":"CampaignsController_myClaim","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"This user's claim for the campaign, or null","tags":["Campaigns"]}},"/api/campaigns/audience-tags":{"get":{"operationId":"CampaignsController_audienceTags","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Entitlement tag vocabulary for targeting campaign variants (admin panel builds its checkboxes from this)","tags":["Campaigns"]}},"/api/campaigns/dynamic-audience-tags":{"get":{"operationId":"CampaignsController_dynamicAudienceTags","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Slug-carrying targeting tags (claimed:, eligible:) — a separate endpoint so the existing audience-tags response shape stays an array","tags":["Campaigns"]}},"/api/campaigns/eligibility-transition-types":{"get":{"operationId":"CampaignsController_eligibilityTransitionTypes","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Subscription transitions a campaign can gate a claim on (admin panel builds its checkboxes from this)","tags":["Campaigns"]}},"/api/admin/campaigns/{id}/claims":{"get":{"operationId":"AdminCampaignClaimsController_list","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"status","required":false,"in":"query","schema":{"enum":["pending","approved","fulfilled","rejected","revoked"],"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Claims for a campaign, oldest first (fulfilment queue)","tags":["Admin - Campaigns"]}},"/api/admin/campaigns/{id}/claims/summary":{"get":{"operationId":"AdminCampaignClaimsController_summary","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Claim counts by status","tags":["Admin - Campaigns"]}},"/api/admin/campaigns/{id}/claims/{claimId}":{"patch":{"operationId":"AdminCampaignClaimsController_update","parameters":[{"name":"claimId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCampaignClaimDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update a claim — status, fulfilment method/reference, notes (admin only)","tags":["Admin - Campaigns"]}},"/api/admin/campaigns":{"get":{"operationId":"AdminCampaignsController_list","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"All campaigns with their variants (coach+)","tags":["Admin - Campaigns"]},"post":{"operationId":"AdminCampaignsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCampaignDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Create a campaign, without variants (admin only)","tags":["Admin - Campaigns"]}},"/api/admin/campaigns/{id}/variants":{"post":{"operationId":"AdminCampaignsController_createVariant","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateCampaignVariantDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Add a variant, with optional hero/badge images (admin only)","tags":["Admin - Campaigns"]}},"/api/admin/campaigns/{id}/variants/{variantId}":{"patch":{"operationId":"AdminCampaignsController_updateVariant","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"variantId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/UpdateCampaignVariantDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update a variant; images optional (admin only)","tags":["Admin - Campaigns"]},"delete":{"operationId":"AdminCampaignsController_removeVariant","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"variantId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Delete a variant and its images (admin only)","tags":["Admin - Campaigns"]}},"/api/admin/campaigns/{id}/preview":{"post":{"operationId":"AdminCampaignsController_preview","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreviewCampaignDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"What GET /campaigns/active would return for a given user — targeting QA without impersonation (coach+)","tags":["Admin - Campaigns"]}},"/api/admin/campaigns/{id}":{"get":{"operationId":"AdminCampaignsController_findOne","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"One campaign with its variants (coach+)","tags":["Admin - Campaigns"]},"patch":{"operationId":"AdminCampaignsController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCampaignDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update a campaign (admin only). `slug` is immutable.","tags":["Admin - Campaigns"]},"delete":{"operationId":"AdminCampaignsController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Delete a campaign — cascades to variants, events and claims (admin only)","tags":["Admin - Campaigns"]}},"/api/log/water":{"get":{"operationId":"WaterVitaminsController_getWaterLogs","parameters":[{"name":"date","required":false,"in":"query","schema":{"example":"2025-01-15","type":"string"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"logs":[{"id":1,"user_id":5,"amount_oz":16,"log_date":"2026-02-19","created_at":"2026-02-19T08:00:00.000Z"},{"id":2,"user_id":5,"amount_oz":20,"log_date":"2026-02-19","created_at":"2026-02-19T12:00:00.000Z"}],"totalOz":36}}}}}},"security":[{"bearer":[]}],"summary":"Get water logs for a date","tags":["Water & Vitamins"]},"post":{"operationId":"WaterVitaminsController_logWater","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogWaterDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":{"id":3,"user_id":5,"amount_oz":16,"log_date":"2026-02-19","created_at":"2026-02-19T15:00:00.000Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Log water intake","tags":["Water & Vitamins"]}},"/api/log/water/trend":{"get":{"description":"Daily series for the shareable trend card, shaped like GET /journey/weight-trend. window = month (30 days) | 6months (180) | year (365); one point per logged day, empty days omitted. Headline stats (starting/current/goal/change/average/goal_progress_pct) sit beside the points.","operationId":"WaterVitaminsController_getWaterTrend","parameters":[{"name":"window","required":false,"in":"query","description":"Same windows as GET /journey/weight-trend: month = last 30 days, 6months = last 180, year = last 365. Defaults to \"month\".","schema":{"type":"string","enum":["month","6months","year"]}},{"name":"date","required":false,"in":"query","description":"End-date anchor in YYYY-MM-DD (inclusive). Defaults to today in the user's timezone.","schema":{"example":"2026-10-26","type":"string"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"metric":"water","window":"month","unit":"oz","points":[{"date":"2026-10-20","value":48},{"date":"2026-10-21","value":64},{"date":"2026-10-23","value":72}],"goal":64,"starting_value":48,"current_value":72,"change":24,"average":61.3,"days_logged":3,"goal_progress_pct":67}}}}}},"security":[{"bearer":[]}],"summary":"Water intake trend (oz/day) for sharing","tags":["Water & Vitamins"]}},"/api/log/water/{id}":{"delete":{"operationId":"WaterVitaminsController_deleteWaterLog","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Water log deleted","totalOz":36}}}}}},"security":[{"bearer":[]}],"summary":"Delete a water log entry","tags":["Water & Vitamins"]}},"/api/log/movement":{"get":{"operationId":"WaterVitaminsController_getMovementLogs","parameters":[{"name":"date","required":false,"in":"query","schema":{"example":"2026-02-24","type":"string"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"logs":[{"id":1,"user_id":5,"duration_minutes":30,"log_date":"2026-02-24","created_at":"2026-02-24T08:00:00.000Z"}],"totalMinutes":45}}}}}},"security":[{"bearer":[]}],"summary":"Get movement logs for a date","tags":["Water & Vitamins"]},"post":{"operationId":"WaterVitaminsController_logMovement","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogMovementDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":{"id":1,"user_id":5,"duration_minutes":30,"log_date":"2026-02-24","created_at":"2026-02-24T08:00:00.000Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Log movement/exercise duration","tags":["Water & Vitamins"]}},"/api/log/movement/trend":{"get":{"description":"Daily series for the shareable trend card, shaped like GET /journey/weight-trend. window = month (30 days) | 6months (180) | year (365); one point per logged day, empty days omitted. Headline stats (starting/current/goal/change/average/goal_progress_pct) sit beside the points. Manually logged movement only — synced steps/active minutes live on GET /health/{steps|active_minutes}/trend/me.","operationId":"WaterVitaminsController_getMovementTrend","parameters":[{"name":"window","required":false,"in":"query","description":"Same windows as GET /journey/weight-trend: month = last 30 days, 6months = last 180, year = last 365. Defaults to \"month\".","schema":{"type":"string","enum":["month","6months","year"]}},{"name":"date","required":false,"in":"query","description":"End-date anchor in YYYY-MM-DD (inclusive). Defaults to today in the user's timezone.","schema":{"example":"2026-10-26","type":"string"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogTrendDto"}}}}},"security":[{"bearer":[]}],"summary":"Movement trend (logged minutes/day) for sharing","tags":["Water & Vitamins"]}},"/api/log/movement/{id}":{"delete":{"operationId":"WaterVitaminsController_deleteMovementLog","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Movement log deleted","totalMinutes":15}}}}}},"security":[{"bearer":[]}],"summary":"Delete a movement log entry","tags":["Water & Vitamins"]}},"/api/log/vitamins":{"post":{"operationId":"WaterVitaminsController_logVitamins","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogVitaminsDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":[{"id":5,"user_id":5,"supplement_id":1,"taken":true,"log_date":"2026-02-19"}]}}}}}},"security":[{"bearer":[]}],"summary":"Log supplement/vitamin intake (bulk)","tags":["Water & Vitamins"]},"get":{"operationId":"WaterVitaminsController_getVitaminLog","parameters":[{"name":"date","required":false,"in":"query","schema":{"example":"2026-02-19","type":"string"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"date":"2026-02-19","vitamins":[{"supplement":{"id":1,"user_id":null,"name":"Bariatric multivitamins","type":"preset","recommended_dose":null},"log":{"id":5,"taken":true,"log_date":"2026-02-19"}},{"supplement":{"id":2,"user_id":null,"name":"Iron","type":"preset","recommended_dose":null},"log":null},{"supplement":{"id":8,"user_id":5,"name":"MyVitamin","type":"custom","recommended_dose":"1 cap"},"log":null}]}}}}}},"security":[{"bearer":[]}],"summary":"Get vitamin checklist + taken status for a date","tags":["Water & Vitamins"]}},"/api/log/vitamins/trend":{"get":{"description":"Daily series for the shareable trend card, shaped like GET /journey/weight-trend. window = month (30 days) | 6months (180) | year (365); one point per logged day, empty days omitted. Headline stats (starting/current/goal/change/average/goal_progress_pct) sit beside the points. Value is how many vitamins were ticked as taken that day, not which ones.","operationId":"WaterVitaminsController_getVitaminTrend","parameters":[{"name":"window","required":false,"in":"query","description":"Same windows as GET /journey/weight-trend: month = last 30 days, 6months = last 180, year = last 365. Defaults to \"month\".","schema":{"type":"string","enum":["month","6months","year"]}},{"name":"date","required":false,"in":"query","description":"End-date anchor in YYYY-MM-DD (inclusive). Defaults to today in the user's timezone.","schema":{"example":"2026-10-26","type":"string"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogTrendDto"}}}}},"security":[{"bearer":[]}],"summary":"Vitamin intake trend (number taken/day) for sharing","tags":["Water & Vitamins"]}},"/api/log/vitamins/{supplementId}":{"put":{"operationId":"WaterVitaminsController_toggleVitaminLog","parameters":[{"name":"supplementId","required":true,"in":"path","schema":{"type":"number"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToggleVitaminLogDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"log":{"id":5,"user_id":5,"supplement_id":1,"taken":true,"log_date":"2026-02-19","created_at":"2026-02-19T09:00:00.000Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Toggle taken status for a supplement (upsert)","tags":["Water & Vitamins"]}},"/api/log/vitamins/{logId}":{"delete":{"operationId":"WaterVitaminsController_deleteVitaminLog","parameters":[{"name":"logId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Vitamin log deleted"}}}}}},"security":[{"bearer":[]}],"summary":"Delete a specific vitamin log entry","tags":["Water & Vitamins"]}},"/api/vitamins/presets":{"get":{"operationId":"VitaminsCatalogController_getPresets","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"presets":[{"id":1,"user_id":null,"name":"Bariatric multivitamins","type":"preset","recommended_dose":null},{"id":2,"user_id":null,"name":"Iron","type":"preset","recommended_dose":null},{"id":3,"user_id":null,"name":"Calcium","type":"preset","recommended_dose":null},{"id":4,"user_id":null,"name":"B12","type":"preset","recommended_dose":null},{"id":5,"user_id":null,"name":"B1","type":"preset","recommended_dose":null},{"id":6,"user_id":null,"name":"Vitamin A","type":"preset","recommended_dose":null},{"id":7,"user_id":null,"name":"Omega-3","type":"preset","recommended_dose":null}]}}}}}},"security":[{"bearer":[]}],"summary":"Get curated list of common vitamins/supplements","tags":["Water & Vitamins"]}},"/api/vitamins":{"get":{"operationId":"VitaminsCatalogController_getSupplements","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"supplements":[{"id":1,"user_id":null,"name":"Bariatric multivitamins","type":"preset","recommended_dose":null,"created_at":"2026-01-01T00:00:00.000Z"},{"id":2,"user_id":null,"name":"Iron","type":"preset","recommended_dose":null,"created_at":"2026-01-01T00:00:00.000Z"},{"id":8,"user_id":5,"name":"MyVitamin","type":"custom","recommended_dose":"1 cap","created_at":"2026-02-10T00:00:00.000Z"}]}}}}}},"security":[{"bearer":[]}],"summary":"Get full supplement list: all presets (user_id=null) + user's custom supplements","tags":["Water & Vitamins"]},"post":{"operationId":"VitaminsCatalogController_createSupplement","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSupplementDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"supplement":{"id":3,"user_id":5,"name":"MyVitamin","type":"custom","recommended_dose":"1 cap","created_at":"2026-02-10T00:00:00.000Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Add a supplement to user's list","tags":["Water & Vitamins"]}},"/api/vitamins/{id}":{"delete":{"operationId":"VitaminsCatalogController_deleteSupplement","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Supplement deleted"}}}}}},"security":[{"bearer":[]}],"summary":"Soft-delete a custom supplement (hides it from UI; logs are preserved). Presets (user_id=null) return 404.","tags":["Water & Vitamins"]}},"/api/bowel-movements":{"post":{"operationId":"BowelMovementController_create","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBowelMovementDto"}}}},"responses":{"201":{"description":"Created"}},"security":[{"bearer":[]}],"summary":"Log a bowel movement","tags":["Bowel Movements"]},"get":{"operationId":"BowelMovementController_list","parameters":[{"name":"from","required":false,"in":"query","description":"Inclusive lower bound on logged_at (ISO).","schema":{"example":"2026-04-01T00:00:00.000Z","type":"string"}},{"name":"to","required":false,"in":"query","description":"Inclusive upper bound on logged_at (ISO).","schema":{"example":"2026-04-30T23:59:59.999Z","type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List bowel movements (optional from / to range)","tags":["Bowel Movements"]}},"/api/bowel-movements/insights":{"get":{"operationId":"BowelMovementController_insights","parameters":[{"name":"period","required":true,"in":"query","schema":{"type":"string"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"AI-generated trend insights based on bowel movement history","tags":["Bowel Movements"]}},"/api/bowel-movements/trend":{"get":{"operationId":"BowelMovementController_trend","parameters":[{"name":"period","required":false,"in":"query","description":"Aggregation window. week=7d, month=30d, 6months=180d.","schema":{"default":"month","type":"string","enum":["week","month","6months"]}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Scatter chart data — entries with bristol_type + name, filtered by period","tags":["Bowel Movements"]}},"/api/bowel-movements/summary":{"get":{"operationId":"BowelMovementController_summary","parameters":[{"name":"days","required":false,"in":"query","description":"Aggregation window in days. Defaults to 7. Capped at 90.","schema":{"example":7,"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Aggregate summary (count, avg/day, Bristol distribution, flag counts)","tags":["Bowel Movements"]}},"/api/bowel-movements/{id}":{"patch":{"operationId":"BowelMovementController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBowelMovementDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Edit own bowel movement entry","tags":["Bowel Movements"]},"delete":{"operationId":"BowelMovementController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Soft-delete own bowel movement entry","tags":["Bowel Movements"]}},"/api/digestive-symptoms":{"post":{"operationId":"DigestiveSymptomController_create","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDigestiveSymptomDto"}}}},"responses":{"201":{"description":"Created"}},"security":[{"bearer":[]}],"summary":"Log a digestive symptom entry","tags":["Digestive Symptoms"]},"get":{"operationId":"DigestiveSymptomController_list","parameters":[{"name":"from","required":false,"in":"query","description":"Inclusive lower bound on logged_at (ISO).","schema":{"example":"2026-07-01T00:00:00.000Z","type":"string"}},{"name":"to","required":false,"in":"query","description":"Inclusive upper bound on logged_at (ISO).","schema":{"example":"2026-07-01T23:59:59.999Z","type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List digestive symptom entries (optional from / to range)","tags":["Digestive Symptoms"]}},"/api/digestive-symptoms/summary":{"get":{"operationId":"DigestiveSymptomController_summary","parameters":[{"name":"days","required":false,"in":"query","description":"Aggregation window in days. Defaults to 7. Capped at 90.","schema":{"example":7,"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Aggregate summary (count, avg/day, per-symptom counts)","tags":["Digestive Symptoms"]}},"/api/digestive-symptoms/insights":{"get":{"operationId":"DigestiveSymptomController_insights","parameters":[{"name":"period","required":true,"in":"query","schema":{"type":"string"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"AI-generated trend insights based on user digestive + food log data","tags":["Digestive Symptoms"]}},"/api/digestive-symptoms/stats":{"get":{"operationId":"DigestiveSymptomController_stats","parameters":[{"name":"period","required":false,"in":"query","description":"Aggregation window. week=7d, month=30d, 6months=180d.","schema":{"default":"month","type":"string","enum":["week","month","6months"]}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Most common symptoms bar chart — sorted counts by period","tags":["Digestive Symptoms"]}},"/api/digestive-symptoms/calendar":{"get":{"operationId":"DigestiveSymptomController_calendar","parameters":[{"name":"year","required":true,"in":"query","schema":{"example":2026,"type":"number"}},{"name":"month","required":true,"in":"query","description":"1–12","schema":{"example":4,"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Symptom frequency calendar heatmap — per-day counts for a given month","tags":["Digestive Symptoms"]}},"/api/digestive-symptoms/{id}":{"patch":{"operationId":"DigestiveSymptomController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDigestiveSymptomDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Edit own digestive symptom entry","tags":["Digestive Symptoms"]},"delete":{"operationId":"DigestiveSymptomController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Soft-delete own digestive symptom entry","tags":["Digestive Symptoms"]}},"/api/ai/respond":{"post":{"operationId":"AiCoachController_respond","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AiRespondDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"response":"Great question! Based on your progress, I recommend increasing your protein intake to 130g daily.","sessionId":"6b1f2c7e-3d4a-4f8b-9c21-0a5e7d8f1b32","routerDecision":{"intent_mode":"chat","tone_profile":"warm_direct","anchor_type":"protein","is_crisis":false},"recipeIntent":{"detected":false,"confidence":0},"extra":{"card":{"type":"recipe","v":1,"status":"done","payload":{"title":"Garlic Chicken with Broccoli","macros":{"calories":340,"protein":42}}}}}}}}}},"security":[{"bearer":[]}],"summary":"Send message to AI coach (requires OpenAI)","tags":["AI Coach"]}},"/api/ai/daily-fix":{"post":{"operationId":"AiCoachController_dailyFix","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Good morning Jane! You're on a 5-day logging streak. Today focus on hitting 120g protein and 64oz water. You've got this!","date":"2026-02-19","timezone":"America/New_York"}}}}}},"security":[{"bearer":[]}],"summary":"Get personalized daily coaching message","tags":["AI Coach"]}},"/api/ai/free-usage":{"get":{"operationId":"AiCoachController_getFreeUsage","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":[{"poolName":"ai_chat","displayName":"AI Chat","used":4,"max":5},{"poolName":"meal_ideas","displayName":"Meal Ideas","used":1,"max":5},{"poolName":"food_photo","displayName":"Food Photo","used":0,"max":5}]}}}}},"security":[{"bearer":[]}],"summary":"Get free-tier UI pool usage (lifetime counters). Returns empty array for non-free users.","tags":["AI Coach"]}},"/api/chat-history":{"get":{"operationId":"AiCoachController_getChatHistory","parameters":[{"name":"page","required":false,"in":"query","schema":{"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"type":"number"}},{"name":"search","required":false,"in":"query","schema":{"type":"string"}},{"name":"responseType","required":false,"in":"query","description":"Filter by response_type. Defaults to \"chat\" only. Pass \"all\" to include daily_fix and other types.","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"conversations":[{"id":2,"sessionId":"6b1f2c7e-3d4a-4f8b-9c21-0a5e7d8f1b32","userMessage":"[Menu Fix]","aiResponse":"Looked at that menu. Best call is the Grilled Salmon — 420 cal, 38g protein.","responseType":"chat","attachments":[{"url":"https://cdn.example.com/lucille-chat/42/1754563-8821.jpg?Policy=...&Signature=...&Key-Pair-Id=...","mime":"image/jpeg","name":"IMG_4821.jpg"}],"card":{"type":"menu_fix","v":1,"status":"done","clientTurnId":"c1f0e9d8-...","payload":{"recommendations":[],"avoid":[],"phaseBlocked":false}},"createdAt":"2026-02-19T10:00:01.000Z"},{"id":1,"sessionId":"6b1f2c7e-3d4a-4f8b-9c21-0a5e7d8f1b32","userMessage":"How much protein?","aiResponse":"Great question!...","responseType":"chat","attachments":[],"card":null,"createdAt":"2026-02-19T10:00:00.000Z"}],"pagination":{"page":1,"limit":20,"total":42,"totalPages":3}}}}}}},"security":[{"bearer":[]}],"summary":"Get paginated chat history","tags":["AI Coach"]}},"/api/ai/diagnose-chat":{"post":{"operationId":"AiCoachController_diagnoseChat","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"response":"Coach reply text...","diagnosticNote":"Check PM2 logs for [CHAT PERF] user=123 breakdown"}}}}}},"security":[{"bearer":[]}],"summary":"[Diagnostic] Admin-only. Run chat respond() for any user ID and return the AI response. Timing logs appear in PM2 under [CHAT PERF].","tags":["AI Coach"]}},"/api/communication/notes":{"get":{"operationId":"CommunicationController_getNotes","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":[{"id":1,"content":"Great progress this week! Keep focusing on protein.","coach_name":"Lucille","created_at":"2026-02-17T10:00:00.000Z","read_at":null,"is_urgent":false,"reaction":null},{"id":2,"content":"Your logging streak is amazing — 14 days!","coach_name":"Lucille","created_at":"2026-02-10T09:00:00.000Z","read_at":"2026-02-10T12:00:00.000Z","is_urgent":false,"reaction":"❤️"}]}}}}}},"security":[{"bearer":[]}],"summary":"Get all coach notes with macro context","tags":["Communication"]}},"/api/communication/reaction":{"post":{"operationId":"CommunicationController_reactToNote","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReactToNoteDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Reaction saved"}}}}}},"security":[{"bearer":[]}],"summary":"React to a coach note","tags":["Communication"]}},"/api/coach-notes/latest":{"get":{"operationId":"CommunicationController_getLatestCoachNote","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":{"id":1,"content":"Great progress this week! Keep focusing on protein.","coach_name":"Lucille","is_urgent":false,"created_at":"2026-02-17T10:00:00.000Z"}}}}}}},"security":[{"bearer":[]}],"summary":"Get latest unread coach note","tags":["Communication"]}},"/api/coach-notes/{noteId}/mark-read":{"post":{"operationId":"CommunicationController_markNoteAsRead","parameters":[{"name":"noteId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Note marked as read"}}}}}},"security":[{"bearer":[]}],"summary":"Mark a coach note as read","tags":["Communication"]}},"/api/communication/group-messages":{"get":{"operationId":"CommunicationController_getGroupMessages","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":[{"id":1,"content":"Welcome to the January 2026 cohort! Check-in starts Monday.","sender_name":"Lucille","group_name":"January 2026 Cohort","sent_at":"2026-01-06T09:00:00.000Z","read_at":"2026-01-06T10:00:00.000Z"}]}}}}}},"security":[{"bearer":[]}],"summary":"Get all group messages (full inbox)","tags":["Communication"]}},"/api/group-messages":{"get":{"operationId":"CommunicationController_getUnreadGroupMessages","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":[{"id":2,"content":"Reminder: log your meals today to keep your streak!","sender_name":"Lucille","group_name":"January 2026 Cohort","sent_at":"2026-02-19T08:00:00.000Z","read_at":null}],"unreadCount":3}}}}}},"security":[{"bearer":[]}],"summary":"Get unread group messages (dashboard preview)","tags":["Communication"]}},"/api/group-messages/{messageId}/mark-read":{"post":{"operationId":"CommunicationController_markGroupMessageRead","parameters":[{"name":"messageId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Message marked as read"}}}}}},"security":[{"bearer":[]}],"summary":"Mark a group message as read","tags":["Communication"]}},"/api/unread-count":{"get":{"operationId":"CommunicationController_getUnreadCount","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"count":5}}}}}},"security":[{"bearer":[]}],"summary":"Get total unread notification count","tags":["Communication"]}},"/api/questionnaire/surveys":{"get":{"operationId":"QuestionnaireController_listSurveys","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"The surveys you may open: the onboarding questionnaire, plus coaching surveys released to a group you are a client of or that you already started (V2-06)","tags":["Questionnaire"]}},"/api/questionnaire/questions":{"get":{"operationId":"QuestionnaireController_getQuestionsWithAnswers","parameters":[{"name":"surveyKey","required":false,"in":"query","description":"Survey key (defaults to \"intake\")","schema":{"type":"string"}}],"responses":{"200":{"description":"Returns questions grouped by section with user answers"}},"security":[{"bearer":[]}],"summary":"Get all questions with options and user answers for a survey","tags":["Questionnaire"]}},"/api/questionnaire/answer":{"post":{"operationId":"QuestionnaireController_saveAnswer","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveAnswerDto"}}}},"responses":{"200":{"description":"Answer saved or updated (upsert)"}},"security":[{"bearer":[]}],"summary":"Save or update an answer to a question","tags":["Questionnaire"]}},"/api/questionnaire-responses":{"get":{"operationId":"QuestionnaireController_getResponses","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"responses":[]}}}}}},"security":[{"bearer":[]}],"summary":"Get all questionnaire responses (legacy)","tags":["Questionnaire"]}},"/api/questionnaire/complete":{"post":{"operationId":"QuestionnaireController_markComplete","parameters":[],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompleteQuestionnaireDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Questionnaire completed"}}}}}},"security":[{"bearer":[]}],"summary":"Mark a survey as completed — defaults to \"intake\" if surveyKey is omitted","tags":["Questionnaire"]}},"/api/questionnaire/premium-pitch":{"get":{"operationId":"QuestionnaireController_getPremiumPitch","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"pitch":"Since tracking felt overwhelming after surgery for you, our AI coach..."}}}}}},"security":[{"bearer":[]}],"summary":"Personalized paywall pitch generated from the user's own questionnaire answers — feature-focused only, no outcome claims.","tags":["Questionnaire"]}},"/api/coach/groups/{groupId}/surveys":{"get":{"operationId":"SurveyReleaseController_list","parameters":[{"name":"groupId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Every active coaching survey, with its release to this group and how many current clients have done it, started it, or not started it.","tags":["Coach — Group surveys"]}},"/api/coach/groups/{groupId}/surveys/{surveyId}/release":{"put":{"operationId":"SurveyReleaseController_release","parameters":[{"name":"groupId","required":true,"in":"path","schema":{"type":"number"}},{"name":"surveyId","required":true,"in":"path","schema":{"type":"number"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReleaseSurveyDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Release a coaching survey to this group, now or from 08:00 on releaseOn in your timezone. Current clients see it from then and get one push. 409 ALREADY_RELEASED when a date is sent for a live release.","tags":["Coach — Group surveys"]},"delete":{"operationId":"SurveyReleaseController_withdraw","parameters":[{"name":"groupId","required":true,"in":"path","schema":{"type":"number"}},{"name":"surveyId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Withdraw a release, or cancel one still scheduled. Clients who started the survey keep it; keptBy counts this group's.","tags":["Coach — Group surveys"]}},"/api/announcements":{"get":{"operationId":"AnnouncementController_list","parameters":[{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Live posts for you: pinned first (on the first page), then newest first, 20 a page.","tags":["Announcements"]}},"/api/announcements/unread-count":{"get":{"operationId":"AnnouncementController_unreadCount","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"How many live posts you can see (total), and how many you haven't seen (count).","tags":["Announcements"]}},"/api/announcements/read":{"post":{"operationId":"AnnouncementController_markRead","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkAnnouncementsReadDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Mark posts as seen. Ids you cannot see are skipped.","tags":["Announcements"]}},"/api/announcements/{id}":{"get":{"operationId":"AnnouncementController_get","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"One live post. 404 when it is gone, ended, or not for you.","tags":["Announcements"]}},"/api/coach/announcements/options":{"get":{"operationId":"AnnouncementAdminController_options","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"The audiences you may post to, and the active coaching groups.","tags":["Coach — Announcements"]}},"/api/coach/announcements":{"get":{"operationId":"AnnouncementAdminController_list","parameters":[{"name":"status","required":false,"in":"query","schema":{"default":"live","type":"string","enum":["scheduled","live","ended"]}},{"name":"groupId","required":false,"in":"query","description":"Only group posts that include this group.","schema":{"type":"number"}},{"name":"audience","required":false,"in":"query","description":"Only all-clients posts. Ignored when groupId is set.","schema":{"type":"string","enum":["all_clients"]}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Posts in one state (scheduled, live, ended), any author, with read counts; optionally one group's posts or the all-clients posts.","tags":["Coach — Announcements"]},"post":{"operationId":"AnnouncementAdminController_create","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateAnnouncementDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Post now or at publishAt, to groups (or all clients, admins only), with up to 4 images. The audience gets one push when it goes live.","tags":["Coach — Announcements"]}},"/api/coach/announcements/{id}":{"get":{"operationId":"AnnouncementAdminController_get","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"One post, in any state but deleted.","tags":["Coach — Announcements"]},"patch":{"operationId":"AnnouncementAdminController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/UpdateAnnouncementDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Edit a post (its author or an admin). Audience and publish time only before it goes out: 409 ALREADY_PUBLISHED after.","tags":["Coach — Announcements"]},"delete":{"operationId":"AnnouncementAdminController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Take a post down for everyone (its author or an admin).","tags":["Coach — Announcements"]}},"/api/location/update":{"post":{"operationId":"LocationController_updateLocation","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLocationDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"location":{"lat":40.7128,"lng":-74.006,"name":"New York, NY"},"isTraveling":false,"travelContext":null}}}}}},"security":[{"bearer":[]}],"summary":"Update GPS location and detect travel status","tags":["Location"]}},"/api/admin/users/{id}/program-end-date":{"patch":{"operationId":"ClientStatusAdminController_setEndDate","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetProgramEndDateDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Set a new program end date: moves a current client's date, or makes a past client current again. Logged.","tags":["Admin — Client status"]}},"/api/admin/users/{id}/program-dates":{"patch":{"operationId":"ClientStatusAdminController_updateProgramDates","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProgramDatesDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Set program start/end dates for a client (admin only, logged)","tags":["Admin — Client status"]}},"/api/admin/users/{id}/status-history":{"get":{"operationId":"ClientStatusAdminController_history","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Every current/past change for a client: who, when, before and after","tags":["Admin — Client status"]}},"/api/app-sessions":{"post":{"operationId":"AppSessionsController_record","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordAppSessionsDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Record finished foreground sessions (client time in app, V2-02). Stored for clients only; resending is harmless.","tags":["App sessions"]}},"/api/admin/assignments/catalog":{"get":{"operationId":"AssignmentsAdminController_list","parameters":[{"name":"includeInactive","required":false,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"The catalogue, in order","tags":["Admin — Assignments"]},"post":{"operationId":"AssignmentsAdminController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogItemDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Add an assignment. It is not issued to anyone until a coach issues it. Logged.","tags":["Admin — Assignments"]}},"/api/admin/assignments/catalog/{catalogId}":{"get":{"operationId":"AssignmentsAdminController_get","parameters":[{"name":"catalogId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"tags":["Admin — Assignments"]},"patch":{"operationId":"AssignmentsAdminController_update","parameters":[{"name":"catalogId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogItemDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Edit an assignment. Issued copies keep what they were sent with. Logged.","tags":["Admin — Assignments"]},"delete":{"operationId":"AssignmentsAdminController_deactivate","parameters":[{"name":"catalogId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Deactivate an assignment (soft). Issued copies stay. Logged.","tags":["Admin — Assignments"]}},"/api/coach/assignments/catalog":{"get":{"operationId":"AssignmentsCoachController_listCatalog","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Active assignments a coach can issue","tags":["Coach — Assignments"]}},"/api/coach/assignments/overview":{"get":{"operationId":"AssignmentsCoachController_overview","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Every live assignment held by a current client, bundled the way it was issued, and the ticks waiting on a coach's Confirm","tags":["Coach — Assignments"]}},"/api/coach/assignments/issue":{"post":{"operationId":"AssignmentsCoachController_issue","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IssueAssignmentDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Issue an assignment to groups (their current members), clients, or both, with a due date. Skips past clients, other programs, and anyone who already holds it.","tags":["Coach — Assignments"]}},"/api/coach/assignments/issues/{issueId}":{"patch":{"operationId":"AssignmentsCoachController_update","parameters":[{"name":"issueId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateIssueDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Change the due date or the named coach","tags":["Coach — Assignments"]}},"/api/coach/assignments/due-dates":{"patch":{"operationId":"AssignmentsCoachController_moveDueDates","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoveDueDatesDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"A new due date for a bundle's issues: open ones move, finished ones keep theirs (19 Oct 2026)","tags":["Coach — Assignments"]}},"/api/coach/assignments/issues/{issueId}/withdraw":{"post":{"operationId":"AssignmentsCoachController_withdraw","parameters":[{"name":"issueId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Withdraw an issued assignment. It can be issued again later.","tags":["Coach — Assignments"]}},"/api/coach/assignments/steps/{stepId}/tick":{"post":{"operationId":"AssignmentsCoachController_tick","parameters":[{"name":"stepId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Tick any step. A coach tick counts as confirmed (ASG-04).","tags":["Coach — Assignments"]}},"/api/coach/assignments/steps/{stepId}/untick":{"post":{"operationId":"AssignmentsCoachController_untick","parameters":[{"name":"stepId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UntickStepDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Untick any step, the client's or the activity stream's included (ASG-04). An optional note tells the client what is missing, with a push.","tags":["Coach — Assignments"]}},"/api/coach/assignments/steps/{stepId}/confirm":{"post":{"operationId":"AssignmentsCoachController_confirm","parameters":[{"name":"stepId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Confirm a step the client or the activity stream ticked","tags":["Coach — Assignments"]}},"/api/coach/clients/{id}/assignments":{"get":{"operationId":"AssignmentsCoachController_forClient","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"A client's checklist (ASG-01): the fixed rows, live assignments, who ticked what, and uploads","tags":["Coach — Assignments"]}},"/api/coach/groups/{groupId}/assignments":{"get":{"operationId":"AssignmentsCoachController_forGroup","parameters":[{"name":"groupId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"A group's rollup (ASG-05): current members against the assignments they hold. Names, status and dates only.","tags":["Coach — Assignments"]}},"/api/assignments/mine":{"get":{"operationId":"AssignmentsController_mine","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"My assignments, with their steps and my uploads","tags":["Assignments"]}},"/api/assignments/steps/{stepId}/tick":{"post":{"operationId":"AssignmentsController_tick","parameters":[{"name":"stepId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Tick a step I can tick myself","tags":["Assignments"]}},"/api/assignments/steps/{stepId}/untick":{"post":{"operationId":"AssignmentsController_untick","parameters":[{"name":"stepId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Untick my own tick, until my coach confirms it","tags":["Assignments"]}},"/api/assignments/steps/{stepId}/uploads":{"post":{"operationId":"AssignmentsController_upload","parameters":[{"name":"stepId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Attach work to a step that takes a file. Off until ASSIGNMENT_UPLOADS_ENABLED=true.","tags":["Assignments"]}},"/api/assignments/uploads/{uploadId}":{"delete":{"operationId":"AssignmentsController_removeUpload","parameters":[{"name":"uploadId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Remove one of my uploads","tags":["Assignments"]}},"/api/coach/attendance/pending":{"get":{"operationId":"AttendanceCoachController_pending","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Calls you hosted in the last 14 days with someone still unmarked","tags":["Coach — Attendance"]}},"/api/coach/attendance/events/{eventId}":{"get":{"operationId":"AttendanceCoachController_get","parameters":[{"name":"eventId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"A call's roster and marks","tags":["Coach — Attendance"]},"put":{"operationId":"AttendanceCoachController_save","parameters":[{"name":"eventId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveAttendanceDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Save marks for a call: attended, missed, or null to clear. One save, one transaction; returns the roster and how many were marked missed.","tags":["Coach — Attendance"]}},"/api/coach/clients/{id}/attendance":{"get":{"operationId":"AttendanceCoachController_forClient","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"A client's calls: totals, no-show streak, the latest marks","tags":["Coach — Attendance"]}},"/api/coach/groups/{groupId}/attendance":{"get":{"operationId":"AttendanceCoachController_forGroup","parameters":[{"name":"groupId","required":true,"in":"path","schema":{"type":"number"}},{"name":"from","required":false,"in":"query","description":"A calendar date, inclusive","schema":{"example":"2026-09-01","type":"string"}},{"name":"to","required":false,"in":"query","description":"A calendar date, inclusive","schema":{"example":"2026-10-13","type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"A group's current members against its calls in the window (default the last 8 weeks)","tags":["Coach — Attendance"]}},"/api/admin/attendance/report":{"get":{"operationId":"AttendanceAdminController_report","parameters":[{"name":"from","required":false,"in":"query","description":"A calendar date, inclusive","schema":{"example":"2026-09-01","type":"string"}},{"name":"to","required":false,"in":"query","description":"A calendar date, inclusive","schema":{"example":"2026-10-13","type":"string"}},{"name":"clientId","required":false,"in":"query","schema":{"type":"number"}},{"name":"groupId","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Every mark in the window (default the last 90 days), by client or group, with streaks","tags":["Admin — Attendance"]}},"/api/admin/attendance/export":{"get":{"operationId":"AttendanceAdminController_export","parameters":[{"name":"from","required":false,"in":"query","description":"A calendar date, inclusive","schema":{"example":"2026-09-01","type":"string"}},{"name":"to","required":false,"in":"query","description":"A calendar date, inclusive","schema":{"example":"2026-10-13","type":"string"}},{"name":"clientId","required":false,"in":"query","schema":{"type":"number"}},{"name":"groupId","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"The report as CSV","tags":["Admin — Attendance"]}},"/api/coach/dashboard-summary":{"get":{"operationId":"CoachController_getDashboardSummary","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"clientHealthAlerts":5,"foodLogsCompleted":19,"foodLogsCompletedPct":76,"alertBreakdown":{"critical":2,"medium":5,"low":3}}}}}}},"security":[{"bearer":[]}],"summary":"Mobile Coach API #1 — Home \"Coaching\" card + Coaching tab alert card. Open alert counts by priority + today's food-log completion stats, across every consented client (shared coaching inbox — every coach sees every client).","tags":["Coach"]}},"/api/coach/client-alerts":{"get":{"operationId":"CoachController_getClientAlerts","parameters":[{"name":"filter","required":false,"in":"query","description":"'all' or a clientId","schema":{"default":"all","type":"string"}},{"name":"sort","required":false,"in":"query","description":"priority (default) | recent","schema":{"default":"priority","type":"string","enum":["priority","recent"]}},{"name":"assigned","required":false,"in":"query","description":"all (default) | me | unassigned","schema":{"default":"all","type":"string","enum":["all","me","any","unassigned"]}},{"name":"cursor","required":false,"in":"query","description":"Opaque cursor from a previous page. Omit for first page.","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"default":8,"type":"number"}},{"name":"all","required":false,"in":"query","description":"true: every row in one response, cursor and limit ignored, nextCursor null","schema":{"default":false,"type":"boolean"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Mobile Coach API #2 — Client Alerts page. Cursor-paged, open alerts across every client (shared coaching inbox).","tags":["Coach"]}},"/api/coach/client-alerts/{alertId}/assignee":{"patch":{"operationId":"CoachController_assignClientAlert","parameters":[{"name":"alertId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignAlertDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"ALR-02 — assign an open alert to a coach (or unassign with null). The coach gets a push unless they assigned it themselves. Every coach still sees it.","tags":["Coach"]}},"/api/coach/coaches":{"get":{"operationId":"CoachController_listCoaches","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Active coaches, for assigning an alert","tags":["Coach"]}},"/api/coach/stats":{"get":{"operationId":"CoachController_getMyStats","parameters":[{"name":"period","required":false,"in":"query","schema":{"default":"7d","type":"string","enum":["7d","30d","all"]}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"CMP-05 — the caller's own stats for 7d / 30d / all: responses to clients (first coach message after a client message), median response time, who is waiting now, reviews given, and their pinned clients: active vs dormant (7 days) and time in app.","tags":["Coach"]}},"/api/coach/past-clients":{"get":{"operationId":"CoachController_getPastClients","parameters":[{"name":"search","required":false,"in":"query","description":"Case-insensitive substring match on name","schema":{"type":"string"}},{"name":"cursor","required":false,"in":"query","description":"Opaque cursor from a previous page. Omit for first page.","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"default":20,"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"GRP-04 — clients whose program has ended, names only: a coach's access ends with the program (V2-02). Cursor-paged, newest end first.","tags":["Coach"]}},"/api/coach/clients/{id}/mark-past":{"post":{"operationId":"CoachController_markClientPast","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkClientPastDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"End a current client's program today (V2-02). They become past: member tier, 1:1 coach rooms read-only, group chat kept. Only an admin can make them current again.","tags":["Coach"]}},"/api/coach/clients/{id}/alerts":{"get":{"operationId":"CoachController_getClientDetailAlerts","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Mobile Coach API #5 — Client Detail \"Alerts\" tab. Full (unpaged) rich alert cards for one client, with action button captions.","tags":["Coach"]}},"/api/coach/clients/{id}/alerts/{alertId}/dismiss":{"patch":{"operationId":"CoachController_dismissClientAlert","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"alertId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Manually dismiss an open client alert.","tags":["Coach"]}},"/api/coach/clients/{id}/chat-room":{"post":{"operationId":"CoachController_getOrCreateClientChatRoom","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Find-or-create the 1:1 coach–client DM room for this client, so the app can navigate straight to it (chat header button, alert card \"Message\" action).","tags":["Coach"]}},"/api/coach/client-directory":{"get":{"operationId":"CoachController_getClientDirectory","parameters":[{"name":"filter","required":false,"in":"query","description":"'all' (default) | 'mine' (CMP-07, this coach's pinned clients) | a clientId","schema":{"default":"all","type":"string"}},{"name":"sort","required":false,"in":"query","description":"priority (default) | recent | name","schema":{"default":"priority","type":"string","enum":["priority","recent","name"]}},{"name":"search","required":false,"in":"query","description":"Case-insensitive substring match on name","schema":{"default":"","type":"string"}},{"name":"cursor","required":false,"in":"query","description":"Opaque cursor from a previous page. Omit for first page.","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"default":8,"type":"number"}},{"name":"all","required":false,"in":"query","description":"true: every row in one response, cursor and limit ignored, nextCursor null","schema":{"default":false,"type":"boolean"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Mobile Coach API #3 — Client Directory page. Cursor-paged, searchable roster of every client (shared coaching inbox).","tags":["Coach"]}},"/api/coach/clients/{id}/overlay":{"patch":{"operationId":"CoachController_setClientOverlay","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetOverlayDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"CMP-06/CMP-07 — set this coach's own colour tag and/or pin for a client. Never a restriction — every coach still sees every client. A pin also routes the client's alerts: while one coach has them pinned, new alerts are assigned to that coach (ALR-02, 12 Oct 2026).","tags":["Coach"]}},"/api/coach/clients/{id}/detail":{"get":{"operationId":"CoachController_getMobileClientDetail","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Mobile Coach API #4 — Client Detail page: identity header + Client info tab + Notes tab + Medical Overrides. Deliberately not GET /coach/clients/:id — that path already serves the admin web dashboard with a different response shape.","tags":["Coach"]}},"/api/coach/clients/{id}/questionnaire":{"get":{"operationId":"CoachController_getClientQuestionnaire","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"survey","required":false,"in":"query","description":"Survey key. Defaults to \"intake\".","schema":{"example":"intake","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"survey":{"key":"intake","name":"Intake"},"completed":true,"completedAt":"2026-09-01T14:20:00.000Z","answeredCount":2,"sections":[{"section":1,"answers":[{"questionId":10,"questionKey":"current_weight","question":"What is your current weight?","answerType":"numeric","values":["220"],"display":"220","answeredAt":"2026-09-01T14:05:00.000Z"},{"questionId":14,"questionKey":"goals","question":"What are your goals?","answerType":"multi_choice","values":["Lose weight","More energy"],"display":"Lose weight, More energy","answeredAt":"2026-09-01T14:07:00.000Z"}]}]}}}}}},"security":[{"bearer":[]}],"summary":"Mobile Coach API — the client's questionnaire answers, grouped by section in survey order. Defaults to the onboarding (intake) survey. Each answer is flattened to display strings (`values`, plus `display` joined with \", \"). Unanswered questions are omitted. Requires coach_data_consent_at.","tags":["Coach"]}},"/api/coach/clients/{id}/nutrition":{"get":{"operationId":"CoachController_getClientNutrition","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"date","required":false,"in":"query","description":"YYYY-MM-DD, local date. Defaults to today.","schema":{"type":"string"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Mobile Coach API #6 — Food & Health tab, Food half. One day's macro totals-vs-goal + per-meal breakdown.","tags":["Coach"]}},"/api/coach/clients/{id}/food-preferences":{"get":{"description":"Saved meals, favorite meals, favorite recipes, and the user's own onboarding answer on food allergies, hard no's and non-negotiables. Read-only. Opened by the `food_preferences` prize consent only (a `food_logs` share does not open it), for the coach the meal plan is assigned to, while it is active — or by general coaching consent for a current client. Field list is provisional pending product-owner confirmation.","operationId":"CoachController_getClientFoodPreferences","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"A user's saved food preferences, for a custom meal plan prize","tags":["Coach"]}},"/api/coach/clients/{id}/health-insights":{"get":{"operationId":"CoachController_getClientHealthInsights","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Mobile Coach API #7 — Food & Health tab, Health half. Weight chart, vibe check, food adherence summary, and sleep/steps/HRV/readiness sparkline cards.","tags":["Coach"]}},"/api/coach/clients/{id}/notes":{"post":{"operationId":"CoachController_createClientNote","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCoachNoteDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Create a coach note for a client","tags":["Coach"]}},"/api/coach/clients/{id}/notes/{noteId}":{"patch":{"operationId":"CoachController_updateClientNote","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"noteId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCoachNoteDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Edit one of the requesting coach's own notes on a client","tags":["Coach"]},"delete":{"operationId":"CoachController_deleteClientNote","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"noteId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Delete one of the requesting coach's own notes on a client","tags":["Coach"]}},"/api/coach/clients/{id}/overrides":{"post":{"operationId":"CoachController_setClientOverride","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetOverrideDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Activate or update a medical override on behalf of a client. Same override_type enum and idempotent upsert-by-type as the self-serve and admin endpoints — this route just adds this coach's assignment scoping. Stamps source=\"coach_set\".","tags":["Coach"]},"delete":{"operationId":"CoachController_deactivateClientOverride","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeactivateOverrideDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Deactivate a client's active medical override of the given type.","tags":["Coach"]}},"/api/coach/triage":{"get":{"operationId":"CoachController_getTriage","parameters":[{"name":"severity","required":false,"in":"query","description":"critical | warning | info","schema":{"type":"string","enum":["critical","warning","info"]}},{"name":"alert_type","required":false,"in":"query","description":"e.g. 'late_night_eating', 'crisis'","schema":{"type":"string"}},{"name":"age_minutes_max","required":false,"in":"query","description":"Only rows newer than N minutes","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Daily-rounds triage feed for the coach dashboard. Aggregates crisis-flagged conversations + VaultHooks coaching-priority pattern detections (underfueling, late-night eating, meal skipping, weight plateau, sodium spike, shake over-reliance, etc.) into a single severity-sorted feed. Each row includes client name + surgery_date + active medical overrides so the UI can render phase context without extra round-trips. Cached per-coach for 60s.","tags":["Coach"]}},"/api/coach/clients":{"get":{"operationId":"CoachController_getClients","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}},{"name":"page","required":false,"in":"query","description":"Page number (1-indexed). Default 1.","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","description":"Page size (1-100). Default 50.","schema":{"type":"string"}},{"name":"search","required":false,"in":"query","description":"Match against id, email, first/last name.","schema":{"type":"string"}},{"name":"program_status","required":false,"in":"query","description":"pending | active | completed | dropped","schema":{"type":"string"}},{"name":"is_enrolled","required":false,"in":"query","description":"true | false","schema":{"type":"string"}},{"name":"logged_today","required":false,"in":"query","description":"true → logged at least one non-planned food entry today; false → none.","schema":{"type":"string"}},{"name":"has_weight","required":false,"in":"query","description":"true → has at least one weight tracking entry; false → none.","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"clients":[{"id":5,"email":"jane@example.com","firstName":"Jane","lastName":"Doe","todayFoodLogs":4,"latestWeight":162.4,"latestVibe":"🙂"}],"total":84,"page":1,"limit":50}}}}}},"security":[{"bearer":[]}],"summary":"List active clients with paginated daily summary","tags":["Coach"]}},"/api/coach/clients/{id}":{"get":{"operationId":"CoachController_getClientDetail","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"canViewClientData":true,"client":{"id":5,"name":"Jane Doe","email":"jane@example.com","macros":{"calories":1500,"protein":120,"carbs":150,"fat":50},"recentLogs":[{"date":"2026-02-19","calories":1420,"protein":110,"logCount":4}],"notes":[{"id":1,"content":"Great progress this week!","created_at":"2026-02-17T10:00:00.000Z"}],"profile":{"surgery_type":"gastric_sleeve","surgery_date":"2025-06-01"}}}}}}}},"security":[{"bearer":[]}],"summary":"Get detailed client view with macros, logs, notes","tags":["Coach"]}},"/api/coach/dashboard-stats":{"get":{"operationId":"CoachController_getDashboardStats","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"totalClients":15,"activeToday":8,"avgCalories":1350,"loggedTodayPercent":53,"streakLeaders":[{"name":"Jane Doe","streak":14},{"name":"John Smith","streak":9}]}}}}}},"security":[{"bearer":[]}],"summary":"Get coach dashboard statistics","tags":["Coach"]}},"/api/coach/generate-note":{"post":{"operationId":"CoachController_generateNote","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateNoteDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"note":"Jane, you crushed it this week! Your 5-day logging streak shows real commitment. Your protein is averaging 112g — just 8g shy of your 120g goal. Focus on adding a protein shake or Greek yogurt to close that gap. Keep going!"}}}}}},"security":[{"bearer":[]}],"summary":"AI-generate a personalized coach note (requires OpenAI)","tags":["Coach"]}},"/api/coach/groups":{"get":{"operationId":"CoachController_getGroups","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"groups":[{"id":1,"name":"January 2026 Cohort","description":null,"type":"coaching","startDate":"2026-01-12","endDate":"2026-05-03","memberCount":12,"week":6,"totalWeeks":16}]}}}}}},"security":[{"bearer":[]}],"summary":"List active groups with their current-client counts and, for a cohort, its week","tags":["Coach"]}},"/api/coach/food-logs/{logId}/review":{"patch":{"operationId":"CoachController_reviewFoodLog","parameters":[{"name":"logId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewFoodLogDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Review saved"}}}}}},"security":[{"bearer":[]}],"summary":"Add coach feedback to a food log entry","tags":["Coach"]}},"/api/coach/clients/{id}/diary/gratitude":{"get":{"operationId":"CoachController_getClientGratitude","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"month","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"403":{"description":"Client has not consented to coach data sharing"}},"security":[{"bearer":[]}],"summary":"Get client morning gratitude entries (consent required)","tags":["Coach"]}},"/api/coach/clients/{id}/diary/inventory":{"get":{"description":"Nightly Inventory is self_only — coaches get completion/lock state per entry, never the written content. See DiaryService.getInventorySummary.","operationId":"CoachController_getClientInventory","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"month","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"403":{"description":"Client has not consented to coach data sharing"}},"security":[{"bearer":[]}],"summary":"Get client nightly inventory completion state (consent required)","tags":["Coach"]}},"/api/coach/clients/{id}/diary/trend/{metric}":{"get":{"operationId":"CoachController_getClientDiaryTrend","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"metric","required":true,"in":"path","schema":{"type":"string"}},{"name":"window","required":false,"in":"query","description":"month | 6months | year","schema":{"type":"string","enum":["month","6months","year"]}},{"name":"date","required":false,"in":"query","description":"End-date anchor, YYYY-MM-DD","schema":{"example":"2026-10-26","type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Mobile Coach API — \"View trends\" on a Client diary section. metric: water | movement | vitamins | vibes. Byte-for-byte mirror of the client's own GET /log/{metric}/trend (GET /vibe-log/trend for vibes), bucketed in the client's stored timezone. Vibes count every log, as the diary shows them. Requires coach_data_consent_at.","tags":["Coach"]}},"/api/coach/clients/{id}/diary/bowel-movements/trend":{"get":{"operationId":"CoachController_getClientBowelMovementTrend","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"period","required":false,"in":"query","description":"Aggregation window. week=7d, month=30d, 6months=180d.","schema":{"default":"month","type":"string","enum":["week","month","6months"]}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Mobile Coach API — Poop tab of a client's trends. Byte-for-byte mirror of GET /bowel-movements/trend (period: week | month | 6months). Requires coach_data_consent_at.","tags":["Coach"]}},"/api/coach/clients/{id}/diary/digestive-symptoms/stats":{"get":{"operationId":"CoachController_getClientDigestiveSymptomStats","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"period","required":false,"in":"query","description":"Aggregation window. week=7d, month=30d, 6months=180d.","schema":{"default":"month","type":"string","enum":["week","month","6months"]}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Mobile Coach API — Digestion tab of a client's trends. Byte-for-byte mirror of GET /digestive-symptoms/stats (period: week | month | 6months). Requires coach_data_consent_at.","tags":["Coach"]}},"/api/coach/clients/{id}/diary/digestive-symptoms/calendar":{"get":{"operationId":"CoachController_getClientDigestiveSymptomCalendar","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"year","required":true,"in":"query","schema":{"example":2026,"type":"number"}},{"name":"month","required":true,"in":"query","description":"1–12","schema":{"example":4,"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Mobile Coach API — Digestion tab's calendar. Byte-for-byte mirror of GET /digestive-symptoms/calendar (year, month). Requires coach_data_consent_at.","tags":["Coach"]}},"/api/coach/clients/{id}/day-details":{"get":{"operationId":"CoachController_getClientDayDetails","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"date","required":true,"in":"query","description":"YYYY-MM-DD, local date","schema":{"type":"string"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Mobile Coach API — \"Client diary\" drill-in from the Food & Health tab. Full day activity for one date: food/water/movement/weight/measurements/photos/vibe/bowel/digestive/supplements (same payload as the client's own day-details view), plus real gratitude/inventory entry content (diary.gratitudeEntry / diary.inventoryEntry) alongside the existing diary completion-status fields. Consent required (coach_data_consent_at).","tags":["Coach"]}},"/api/coach/clients/{id}/food-logs/{logId}/review":{"patch":{"operationId":"CoachController_reviewClientFoodLog","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"logId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewFoodLogDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Mobile Coach API alias for the existing (admin-web) PATCH /coach/food-logs/:logId/review — same underlying action (saves coach_feedback/reviewed_by/reviewed_at on the food log, sends a chat message + push to the client), reached via the /coach/clients/:id/... convention this API otherwise uses everywhere. Additionally verifies the log actually belongs to clientId — the bare route has no such check since it has no client id in its path at all. 404 (not the bare route's 403) if the log belongs to a different client. Requires coach_data_consent_at.","tags":["Coach"]},"delete":{"operationId":"CoachController_withdrawClientFoodLogReview","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"logId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"CMP-09 — withdraw a food log review: the feedback leaves the client's diary and its chat card is deleted (so it also leaves the AI context). Kept in the review history. Only the coach who wrote it, or an admin. No push.","tags":["Coach"]}},"/api/coach/clients/{id}/day-reviews":{"get":{"operationId":"CoachController_getClientDayReviews","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"from","required":true,"in":"query","schema":{"example":"2026-10-01","type":"string"}},{"name":"to","required":true,"in":"query","description":"At most 62 days after `from`","schema":{"example":"2026-10-31","type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"CMP-09 — the reviewed days in a range (at most 62 days), for the diary calendar: the day review and who wrote it, and how many entries have a review.","tags":["Coach"]}},"/api/coach/clients/{id}/review-history":{"get":{"operationId":"CoachController_getClientReviewHistory","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"date","required":true,"in":"query","description":"The diary day","schema":{"example":"2026-10-12","type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"CMP-09 — every review given, edited or withdrawn on one diary day (day and entries), newest first, with who did it.","tags":["Coach"]}},"/api/coach/clients/{id}/food-logs/day-review/{date}":{"patch":{"operationId":"CoachController_reviewClientDay","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"date","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewDayDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"message":"Day reviewed","logDate":"2026-08-05"}}}}}},"security":[{"bearer":[]}],"summary":"Day-level counterpart to food-logs/:logId/review (REW-702) — coach feedback on a client's whole day of logging instead of one entry. Day and per-entry reviews coexist and both surface to the client. Saves coach_feedback/reviewed_by/reviewed_at on a food_day_reviews row keyed by (clientId, date) — re-reviewing the same day updates that row rather than creating a duplicate. Also sends a chat message + push to the client. Requires coach_data_consent_at.","tags":["Coach"]},"delete":{"operationId":"CoachController_withdrawClientDayReview","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"date","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"CMP-09 — withdraw a day review: removed from the client's diary, its chat card deleted (so it also leaves the AI context), kept in the review history. Only the coach who wrote it, or an admin. No push.","tags":["Coach"]}},"/api/coach/clients/{id}/health/trend/{metric}":{"get":{"operationId":"CoachController_getClientHealthTrend","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"metric","required":true,"in":"path","schema":{"type":"string"}},{"name":"window","required":false,"in":"query","description":"\"week\"=7 daily points, \"month\"=30 daily points (or 6months for blood_pressure), \"year\"=12 monthly-average points. Defaults to \"week\".","schema":{"type":"string","enum":["week","month","year","6months"]}},{"name":"date","required":false,"in":"query","description":"End-date anchor, YYYY-MM-DD","schema":{"example":"2026-05-19","type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Mobile Coach API — \"More\" drill-in on the Sleep/Steps/HRV cards from health-insights (#7). metric: sleep | steps | hrv | rhr | active_minutes | stand_hours | blood_pressure. window: week/month/year (blood_pressure also accepts 6months). Requires health_data_consent_at.","tags":["Coach"]}},"/api/coach/clients/{id}/health/readiness-history":{"get":{"operationId":"CoachController_getClientReadinessHistory","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"window","required":false,"in":"query","description":"\"week\"=7 daily points, \"month\"=30 daily points (or 6months for blood_pressure), \"year\"=12 monthly-average points. Defaults to \"week\".","schema":{"type":"string","enum":["week","month","year","6months"]}},{"name":"date","required":false,"in":"query","description":"End-date anchor, YYYY-MM-DD","schema":{"example":"2026-05-19","type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Mobile Coach API — \"More\" drill-in on the Readiness card from health-insights (#7). window: week/month/year. Requires health_data_consent_at.","tags":["Coach"]}},"/api/coach/clients/{id}/health/weight-history":{"get":{"operationId":"CoachController_getClientWeightHistory","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"months","required":false,"in":"query","description":"Filters the chart window, e.g. 1, 6, 12","schema":{"type":"string"}},{"name":"aggregate","required":false,"in":"query","description":"'monthly' collapses to one point per month","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Mobile Coach API — Weight card \"More\" opens the client's own Trends sheet in coach mode, which reads /weight/history + /measurements directly (not the #7 summary shape). Byte-for-byte mirror of GET /weight/history, including months/aggregate params. Requires coach_data_consent_at. Replaces the earlier custom-shaped weight-trend endpoint.","tags":["Coach"]}},"/api/coach/clients/{id}/health/measurements":{"get":{"operationId":"CoachController_getClientMeasurements","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"months","required":false,"in":"query","description":"Defaults to 3","schema":{"type":"string"}},{"name":"field","required":false,"in":"query","description":"Filter to one measurement field","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Mobile Coach API — the other half of the Weight card's Trends sheet (dual-axis weight vs body measurements graph). Byte-for-byte mirror of GET /measurements. Requires coach_data_consent_at.","tags":["Coach"]}},"/api/coach/clients/{id}/health/vibe-history":{"get":{"operationId":"CoachController_getClientVibeHistory","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"window","required":false,"in":"query","description":"\"week\"=7 daily points, \"month\"=30 daily points (or 6months for blood_pressure), \"year\"=12 monthly-average points. Defaults to \"week\".","schema":{"type":"string","enum":["week","month","year","6months"]}},{"name":"date","required":false,"in":"query","description":"End-date anchor, YYYY-MM-DD","schema":{"example":"2026-05-19","type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Mobile Coach API — \"More\" drill-in on the Vibe check card from health-insights (#7). Historical mood entries for charting, respecting each entry's own share_with_coach flag. window: week/month/year. Requires coach_data_consent_at.","tags":["Coach"]}},"/api/coach/clients/{id}/health/sleep/detail":{"get":{"operationId":"CoachController_getClientSleepDetail","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"date","required":false,"in":"query","description":"YYYY-MM-DD, defaults to today","schema":{"type":"string"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Sleep sheet: quality stat cards, stage breakdown, weekly average, insight. Mirrors GET /health/sleep/detail/me. Requires health_data_consent_at.","tags":["Coach"]}},"/api/coach/clients/{id}/health/activity":{"get":{"operationId":"CoachController_getClientActivity","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"date","required":false,"in":"query","description":"YYYY-MM-DD, defaults to today","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Steps sheet: steps/active minutes/stand hours vs goals, floors, distance, calories, strain score. Mirrors GET /health/activity/me. Requires health_data_consent_at.","tags":["Coach"]}},"/api/coach/clients/{id}/health/more-metrics":{"get":{"operationId":"CoachController_getClientMoreMetrics","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"HRV sheet: glucose/blood pressure/sleep quality/steps/RHR cards. Mirrors GET /health/more-metrics/me. Requires health_data_consent_at.","tags":["Coach"]}},"/api/coach/clients/{id}/health/readiness":{"get":{"operationId":"CoachController_getClientReadiness","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"date","required":false,"in":"query","description":"YYYY-MM-DD, defaults to today","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Readiness sheet: today's score, band, and the 5-component breakdown (sleep/hrv/rhr/steps/strain). Distinct from readiness-history (#16), which is trend-only. Mirrors GET /health/readiness/me. Requires health_data_consent_at.","tags":["Coach"]}},"/api/coach/clients/{id}/health/blood_pressure/detail":{"get":{"operationId":"CoachController_getClientBpDetail","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"date","required":false,"in":"query","description":"YYYY-MM-DD, defaults to today","schema":{"type":"string"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Blood pressure card/sheet detail. Mirrors GET /health/blood_pressure/detail/me. Requires health_data_consent_at.","tags":["Coach"]}},"/api/coach/clients/{id}/health/metric-history":{"get":{"operationId":"CoachController_getClientMetricHistory","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"type","required":true,"in":"query","description":"Which health metric to retrieve history for.","schema":{"example":"blood_pressure","type":"string","enum":["blood_pressure","rhr","heart_rate","hrv","glucose","steps","weight","body_fat","bmi"]}},{"name":"from","required":false,"in":"query","description":"Start of date range (ISO 8601). Defaults to 90 days ago.","schema":{"example":"2026-01-01","type":"string"}},{"name":"to","required":false,"in":"query","description":"End of date range (ISO 8601). Defaults to today.","schema":{"example":"2026-06-12","type":"string"}},{"name":"limit","required":false,"in":"query","description":"Max rows to return (1–500). Defaults to 100.","schema":{"default":100,"example":50,"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Raw per-reading history for one metric type (blood_pressure, rhr, heart_rate, hrv, glucose, steps, weight, body_fat, bmi), with source attribution. Mirrors GET /health/metric-history/me. Requires health_data_consent_at.","tags":["Coach"]}},"/api/coach/clients/{id}/health/diary":{"get":{"operationId":"CoachController_getClientHealthDiary","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"date","required":true,"in":"query","description":"YYYY-MM-DD","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"The wearable/device side of one day — sleep stages, activity, vitals, weight. NOT redundant with day-details (#17): that's what the client logged, this is what the device recorded. Mirrors GET /health/diary/me. Requires health_data_consent_at.","tags":["Coach"]}},"/api/coach/clients/{id}/health/capabilities":{"get":{"operationId":"CoachController_getClientHealthCapabilities","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Which metrics have a connected data source for this client — use to distinguish \"0 steps\" from \"no tracker connected\" rather than hardcoding hasSource. Path alias for the already-live GET /health/capabilities/:userId (same service, same guard) added for /coach/clients/:id/health/... path consistency. Requires health_data_consent_at.","tags":["Coach"]}},"/api/admin/coach-stats":{"get":{"operationId":"CoachStatsAdminController_list","parameters":[{"name":"period","required":false,"in":"query","schema":{"default":"7d","type":"string","enum":["7d","30d","all"]}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Every active coach's response, review and My clients numbers for the period","tags":["Admin — Coach stats"]}},"/api/admin/coach-stats/{coachId}":{"get":{"operationId":"CoachStatsAdminController_one","parameters":[{"name":"coachId","required":true,"in":"path","schema":{"type":"number"}},{"name":"period","required":false,"in":"query","schema":{"default":"7d","type":"string","enum":["7d","30d","all"]}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"One coach's stats, with their My clients list","tags":["Admin — Coach stats"]}},"/api/diary/inventory/questions":{"get":{"operationId":"DiaryController_getInventoryQuestions","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get all active inventory questions (ordered by sort_order)","tags":["Diary"]}},"/api/diary/inventory":{"post":{"operationId":"DiaryController_createInventory","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInventoryDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Create or update nightly inventory for a date","tags":["Diary"]},"get":{"operationId":"DiaryController_getInventoryList","parameters":[{"name":"page","required":false,"in":"query","schema":{"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"type":"number"}},{"name":"month","required":false,"in":"query","description":"YYYY-MM","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List inventory entries (paginated, optional month filter)","tags":["Diary"]}},"/api/diary/inventory/search":{"get":{"operationId":"DiaryController_searchInventory","parameters":[{"name":"q","required":true,"in":"query","schema":{"type":"string"}},{"name":"page","required":false,"in":"query","schema":{"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Search inventory entries by note text","tags":["Diary"]}},"/api/diary/inventory/{date}":{"get":{"operationId":"DiaryController_getInventoryByDate","parameters":[{"name":"date","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get inventory entry for a specific date","tags":["Diary"]}},"/api/diary/gratitude":{"post":{"operationId":"DiaryController_createGratitude","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGratitudeDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Create or update morning gratitude for a date","tags":["Diary"]},"get":{"operationId":"DiaryController_getGratitudeList","parameters":[{"name":"page","required":false,"in":"query","schema":{"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"type":"number"}},{"name":"month","required":false,"in":"query","description":"YYYY-MM","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List gratitude entries (paginated, optional month filter)","tags":["Diary"]}},"/api/diary/gratitude/{id}":{"put":{"operationId":"DiaryController_updateGratitude","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateGratitudeDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update gratitude entry content or share to community","tags":["Diary"]}},"/api/diary/gratitude/{date}":{"get":{"operationId":"DiaryController_getGratitudeByDate","parameters":[{"name":"date","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get gratitude entry for a specific date","tags":["Diary"]}},"/api/medical-overrides":{"get":{"operationId":"MedicalOverridesController_list","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List the user's medical overrides (active + history)","tags":["Medical Overrides"]},"post":{"operationId":"MedicalOverridesController_set","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetOverrideDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Activate or update a medical override","tags":["Medical Overrides"]},"delete":{"operationId":"MedicalOverridesController_deactivate","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeactivateOverrideDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Deactivate an active medical override","tags":["Medical Overrides"]}},"/api/admin/users/{userId}/medical-overrides":{"get":{"operationId":"MedicalOverridesAdminController_list","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List a target user's medical overrides (active + history). Used by the admin web Overview tab.","tags":["Medical Overrides — Admin/Coach"]},"post":{"operationId":"MedicalOverridesAdminController_set","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetOverrideDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Activate or update a medical override on behalf of a client. Stamps source=\"coach_set\" and created_by=staff user id.","tags":["Medical Overrides — Admin/Coach"]},"delete":{"operationId":"MedicalOverridesAdminController_deactivate","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeactivateOverrideDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Deactivate a target user's active medical override. Soft delete — history is retained.","tags":["Medical Overrides — Admin/Coach"]}},"/api/scheduling/webhooks/ghl-calendar":{"post":{"operationId":"GhlCalendarWebhookController_handleGhlCalendarWebhook","parameters":[{"name":"x-ghl-signature","required":true,"in":"header","schema":{"type":"string"}},{"name":"x-wh-signature","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"summary":"GHL calendar webhook — appointment create/update/cancel","tags":["scheduling"]}},"/api/scheduling/events":{"get":{"operationId":"SchedulingController_listEvents","parameters":[{"name":"from","required":true,"in":"query","schema":{"type":"string"}},{"name":"to","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Events in a date range the viewer may see (calendar page, Home \"What's on\" card)","tags":["scheduling"]}},"/api/scheduling/events/{id}":{"get":{"operationId":"SchedulingController_getEvent","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Event detail — access-resolved, join_url/resources/recording redacted when locked","tags":["scheduling"]}},"/api/scheduling/events/{id}/rsvp":{"post":{"operationId":"SchedulingController_rsvp","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RsvpDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"RSVP to an event occurrence — closes at the event start time","tags":["scheduling"]}},"/api/scheduling/bookable-coaches":{"get":{"operationId":"SchedulingController_listBookableCoaches","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Coaches with an active 1:1 calendar mapping — the booking screen's coach picker","tags":["scheduling"]}},"/api/scheduling/coaching/eligibility":{"get":{"operationId":"SchedulingController_getCoaching1on1Eligibility","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Whether this client can book a 1:1 this week, and which session is in the way if not — asked before the slot picker is offered","tags":["scheduling"]}},"/api/scheduling/coach/{coachId}/free-slots":{"get":{"operationId":"SchedulingController_getCoachFreeSlots","parameters":[{"name":"coachId","required":true,"in":"path","schema":{"type":"number"}},{"name":"startDate","required":true,"in":"query","schema":{"example":"2026-09-01T00:00:00Z","type":"string"}},{"name":"endDate","required":true,"in":"query","schema":{"example":"2026-09-07T00:00:00Z","type":"string"}},{"name":"timezone","required":false,"in":"query","schema":{"example":"America/New_York","type":"string"}},{"name":"excludeEventId","required":false,"in":"query","description":"The 1:1 being rescheduled. Its own week is not counted against the one-per-week limit.","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Bookable slots from a coach's calendar, live, minus what they already have on. `{ slots, source }` — `source` is 'live', 'mirror' (GHL unreachable, last swept copy) or 'manual' (app-side hours).","tags":["scheduling"]}},"/api/scheduling/coach/{coachId}/book":{"post":{"operationId":"SchedulingController_bookCoaching","parameters":[{"name":"coachId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookCoaching1on1Dto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Book a 1:1 coaching session against GHL","tags":["scheduling"]}},"/api/scheduling/events/{id}/reschedule":{"post":{"operationId":"SchedulingController_rescheduleOwnSession","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RescheduleDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Reschedule own 1:1 session — up to 1 hour before start, system-enforced","tags":["scheduling"]}},"/api/scheduling/events/{id}/cancel":{"post":{"operationId":"SchedulingController_cancelOwnSession","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Cancel own 1:1 session","tags":["scheduling"]}},"/api/scheduling/sales/free-slots":{"get":{"operationId":"SchedulingController_getSalesFreeSlots","parameters":[{"name":"startDate","required":true,"in":"query","schema":{"example":"2026-09-01T00:00:00Z","type":"string"}},{"name":"endDate","required":true,"in":"query","schema":{"example":"2026-09-07T00:00:00Z","type":"string"}},{"name":"timezone","required":false,"in":"query","schema":{"example":"America/New_York","type":"string"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Live GHL free-slots read for the sales calendar, falling back to the mirrored copy (non-client users welcome). Returns `{ slots, source }`.","tags":["scheduling"]}},"/api/scheduling/sales/eligibility":{"get":{"operationId":"SchedulingController_getSalesCallEligibility","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Whether this user may book a discovery call, and why not","tags":["scheduling"]}},"/api/scheduling/sales/book":{"post":{"operationId":"SchedulingController_bookSalesCall","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookSalesCallDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Book a sales call — GHL's own automation on that calendar sends the confirmation","tags":["scheduling"]}},"/api/scheduling/sales/{id}/reschedule":{"post":{"operationId":"SchedulingController_rescheduleSalesCall","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RescheduleDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Move own sales call — up to 1 hour before it starts","tags":["scheduling"]}},"/api/scheduling/sales/{id}/cancel":{"post":{"operationId":"SchedulingController_cancelSalesCall","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Cancel own sales call","tags":["scheduling"]}},"/api/scheduling/recordings":{"get":{"operationId":"SchedulingController_listRecordings","parameters":[{"name":"category","required":false,"in":"query","schema":{"type":"string","enum":["workshop","coaching_1on1","group_coaching","community"]}},{"name":"search","required":false,"in":"query","schema":{"type":"string"}},{"name":"playedFilter","required":false,"in":"query","schema":{"type":"string","enum":["all","played","unplayed"]}},{"name":"page","required":false,"in":"query","schema":{"default":1,"type":"number"}},{"name":"pageSize","required":false,"in":"query","schema":{"default":20,"type":"number"}},{"name":"includeLinks","required":false,"in":"query","schema":{"default":false,"type":"boolean"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Paged/filtered recordings list, scoped to the viewer's visible categories","tags":["scheduling"]}},"/api/scheduling/recordings/{id}/playback":{"get":{"operationId":"SchedulingController_getPlayback","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Signed playback URL + resume position; marks played on first access","tags":["scheduling"]}},"/api/scheduling/events/{id}/recording-link/opened":{"post":{"operationId":"SchedulingController_markRecordingLinkOpened","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Mark an event's manual recording link played for the viewer (first open counts; repeats are no-ops)","tags":["scheduling"]}},"/api/scheduling/recordings/{id}/position":{"post":{"operationId":"SchedulingController_updatePosition","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePlaybackPositionDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Persist resume position — does not affect the played signal","tags":["scheduling"]}},"/api/scheduling/timezone":{"get":{"operationId":"SchedulingController_getTimezone","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Resolved timezone for Calendar Settings (auto-learned, or manually overridden)","tags":["scheduling"]},"delete":{"operationId":"SchedulingController_clearTimezone","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Follow the device again — undoes a manual Calendar Settings pick","tags":["scheduling"]},"put":{"operationId":"SchedulingController_setTimezone","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetTimezoneDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Manually set the calendar timezone — freezes future header-learning","tags":["scheduling"]}},"/api/coach/scheduling/events":{"post":{"operationId":"CoachSchedulingController_createEvent","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCoachingEventDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Create a single (non-recurring) event","tags":["coach-scheduling"]}},"/api/coach/scheduling/events/{id}":{"patch":{"operationId":"CoachSchedulingController_editOccurrence","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EditOccurrenceDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Edit one occurrence — detaches it from its series, if any","tags":["coach-scheduling"]}},"/api/coach/scheduling/events/{id}/recording":{"put":{"operationId":"CoachSchedulingController_setRecordingUrl","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetEventRecordingUrlDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Set the recording link on an event that has ended, or clear it with `null`. Manual until Zoom recordings are pulled automatically","tags":["coach-scheduling"]}},"/api/coach/scheduling/coaches":{"get":{"operationId":"CoachSchedulingController_listCoaches","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Every active coach — the add-event facilitator picker","tags":["coach-scheduling"]}},"/api/coach/scheduling/events/{id}/invitations":{"get":{"operationId":"CoachSchedulingController_getInvitations","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Who is invited to this event — individuals and cohorts, name-resolved","tags":["coach-scheduling"]},"put":{"operationId":"CoachSchedulingController_setInvitations","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetEventInvitationsDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Replace the invite list. Both arrays are the full intended state; `{}` clears it","tags":["coach-scheduling"]}},"/api/coach/scheduling/events/{id}/invitations/{invitationId}":{"delete":{"operationId":"CoachSchedulingController_removeInvitation","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"invitationId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Uninvite one person or cohort","tags":["coach-scheduling"]}},"/api/coach/scheduling/events/{id}/resources":{"get":{"operationId":"CoachSchedulingController_listResources","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Files attached to an event, signed. The coach's view — visibility windows do not hide anything here","tags":["coach-scheduling"]},"post":{"operationId":"CoachSchedulingController_uploadResource","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/UploadEventResourceDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Attach a file to an event, or to every later occurrence of its series with ","tags":["coach-scheduling"]}},"/api/coach/scheduling/events/{id}/resources/{resourceId}":{"delete":{"operationId":"CoachSchedulingController_removeResource","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"resourceId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Remove an attached file, and the stored object behind it","tags":["coach-scheduling"]}},"/api/coach/scheduling/events/{id}/cancel":{"post":{"operationId":"CoachSchedulingController_cancelEvent","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelEventDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Cancel an event. `scope: series` cancels every later occurrence too. Routes through GHL cancellation first for a 1:1 session","tags":["coach-scheduling"]}},"/api/coach/scheduling/1on1":{"post":{"operationId":"CoachSchedulingController_scheduleCoaching1on1","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleCoaching1on1Dto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Schedule a 1:1 for a client — books it in GHL, ignoring the coach's free-slot windows","tags":["coach-scheduling"]}},"/api/coach/scheduling/events/{id}/reschedule":{"post":{"operationId":"CoachSchedulingController_rescheduleSession","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RescheduleDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Reschedule a 1:1 session from the coach side","tags":["coach-scheduling"]}},"/api/coach/scheduling/series":{"post":{"operationId":"CoachSchedulingController_createSeries","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEventSeriesDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Create a recurring series — materializes occurrences ~10 weeks ahead","tags":["coach-scheduling"]}},"/api/coach/scheduling/series/{id}":{"patch":{"operationId":"CoachSchedulingController_editSeries","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EditEventSeriesDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Edit the whole series — re-applies to future, not-yet-individually-edited occurrences only","tags":["coach-scheduling"]}},"/api/coach/scheduling/my-upcoming-sessions":{"get":{"operationId":"CoachSchedulingController_getMyUpcomingSessions","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Everything the coach is running in the next 30 days","tags":["coach-scheduling"]}},"/api/coach/scheduling/recordings":{"post":{"operationId":"CoachSchedulingController_createRecording","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRecordingDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Register an uploaded recording against its event — rejected for coaching_1on1","tags":["coach-scheduling"]}},"/api/coach/scheduling/working-hours":{"get":{"operationId":"CoachSchedulingController_getWeeklyHours","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"The coach's weekly hours — unused; see the note above","tags":["coach-scheduling"]},"put":{"operationId":"CoachSchedulingController_setWeeklyHours","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetWeeklyHoursDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"UNUSED — no app build calls this, and no availability read consults what it writes (see the note above this block). Full-replaces the weekly hours and raises coach_hours_conflict WorkItems for any now-out-of-hours future 1:1 bookings; the GHL copy is pushed only while GHL_AVAILABILITY_SYNC_ENABLED is on.","tags":["coach-scheduling"]}},"/api/coach/scheduling/working-hours/effective":{"get":{"operationId":"CoachSchedulingController_getEffectiveWeeklyHours","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"The coach's effective availability and which source governs it. source: 'manual' — no active GHL mapping, weeklyHours reconstructed from the tables above. source: 'ghl' — an active coaching_1on1 mapping; weeklyHours is always [] (GHL's real availability has no reconstructable weekly-hours shape once you account for personal/round-robin calendar types), upcomingSlots carries the next real bookable windows instead. Additive: GET working-hours is unchanged and stays the source for the fallback path.","tags":["coach-scheduling"]}},"/api/coach/scheduling/working-hours/overrides":{"get":{"operationId":"CoachSchedulingController_getOverrides","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Date-specific availability overrides — unused; see the note above","tags":["coach-scheduling"]},"post":{"operationId":"CoachSchedulingController_setOverride","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetOverrideDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"UNUSED — see the note above this block. Adds or replaces a date-specific override; app-side only, never mirrored to GHL.","tags":["coach-scheduling"]}},"/api/coach/scheduling/working-hours/overrides/{date}":{"delete":{"operationId":"CoachSchedulingController_removeOverride","parameters":[{"name":"date","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"UNUSED — see the note above this block. Removes a date-specific override.","tags":["coach-scheduling"]}},"/api/coach/scheduling/calendar-free-slots":{"get":{"operationId":"CoachSchedulingController_getCalendarFreeSlots","parameters":[{"name":"category","required":true,"in":"query","schema":{"type":"string","enum":["coaching_1on1","group_coaching","sales","workshop","community","reward"]}},{"name":"startDate","required":true,"in":"query","schema":{"example":"2026-09-01T00:00:00Z","type":"string"}},{"name":"endDate","required":true,"in":"query","schema":{"example":"2026-09-22T00:00:00Z","type":"string"}},{"name":"groupId","required":false,"in":"query","schema":{"type":"number"}},{"name":"invitedGroupIds","required":false,"in":"query","description":"Cohorts on the event's invite list — how Group Coaching names its audience.","schema":{"type":"array","items":{"type":"number"}}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"What a category's GHL calendar has open, live, for the event authoring form","tags":["coach-scheduling"]}},"/api/coach/scheduling/calendar-mappings":{"get":{"operationId":"CoachSchedulingController_listCalendarMappings","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List GHL calendar mappings, each with how its sweep is faring","tags":["coach-scheduling"]},"put":{"operationId":"CoachSchedulingController_upsertCalendarMapping","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertCalendarMappingDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Create or update a GHL calendar mapping for a category/coach/group","tags":["coach-scheduling"]}},"/api/coach/scheduling/calendar-mappings/{id}":{"delete":{"operationId":"CoachSchedulingController_deleteCalendarMapping","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Deactivate a GHL calendar mapping","tags":["coach-scheduling"]}},"/api/coach/scheduling/admin/events":{"get":{"operationId":"CoachSchedulingController_listAdminEvents","parameters":[{"name":"category","required":false,"in":"query","schema":{"type":"string","enum":["workshop","coaching_1on1","group_coaching","community","sales_call","streak_huddle","reward_session","reward_chat","live_ama"]}},{"name":"coachId","required":false,"in":"query","description":"Primary or co-facilitating coach","schema":{"type":"number"}},{"name":"clientId","required":false,"in":"query","description":"The RSVP holder for a 1:1 or sales call","schema":{"type":"number"}},{"name":"from","required":false,"in":"query","schema":{"example":"2026-09-01T00:00:00Z","type":"string"}},{"name":"to","required":false,"in":"query","schema":{"example":"2026-10-01T00:00:00Z","type":"string"}},{"name":"syncStatus","required":false,"in":"query","description":"pending = calendar resolved but GHL owes a write; stale = GHL has the appointment but a local edit is outstanding; not_applicable = no GHL calendar resolved for this event","schema":{"type":"string","enum":["synced","pending","stale","not_applicable"]}},{"name":"page","required":false,"in":"query","schema":{"default":1,"type":"number"}},{"name":"pageSize","required":false,"in":"query","schema":{"default":50,"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Every scheduled event, filterable by coach/client/category/date/GHL sync status — ops reporting, not viewer-scoped","tags":["coach-scheduling"]}},"/api/coach/scheduling/admin/ghl-sync-health":{"get":{"operationId":"CoachSchedulingController_getGhlSyncHealth","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Is the GHL sync healthy: feature flags, what each sweep still owes GHL, when each job last ran, and the newest webhook received","tags":["coach-scheduling"]}},"/api/coach/scheduling/admin/events/{id}/ghl":{"get":{"operationId":"CoachSchedulingController_getEventGhlState","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"One session's GHL fields and where each attendee's GHL appointment and tag stand","tags":["coach-scheduling"]}},"/api/coach/scheduling/admin/users/{userId}/ghl":{"get":{"operationId":"CoachSchedulingController_getUserGhlState","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"One user's GHL contact id and the RSVPs still owed a GHL write","tags":["coach-scheduling"]}},"/api/admin/scheduling/past-events":{"post":{"operationId":"AdminPastEventsController_importOne","parameters":[{"name":"x-client","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportPastEventDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Import one past session with its recording link (admin only)","tags":["admin-scheduling"]}},"/api/admin/scheduling/past-events/bulk":{"post":{"operationId":"AdminPastEventsController_importMany","parameters":[{"name":"x-client","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportPastEventsDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Import many past sessions (CSV upload). Each row stands alone: a bad row is reported, the rest still import (admin only)","tags":["admin-scheduling"]}},"/api/admin/scheduling/past-events/{id}":{"get":{"operationId":"AdminPastEventsController_getOne","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"One imported past session's full fields, for the edit form (admin only)","tags":["admin-scheduling"]},"patch":{"operationId":"AdminPastEventsController_editOne","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"x-client","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EditPastEventDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Edit one imported past session — title, times, coach, group, links, description, recording (admin only)","tags":["admin-scheduling"]}},"/api/rewards/admin/reward-slots/demand":{"get":{"description":"Open requests only: not yet notified, no call booked, the prize claimed and not delivered, expired or revoked. Most-waited prize first, with the oldest request (REW-1010).","operationId":"RewardSlotsAdminController_demand","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Users waiting on \"Notify me\", per call prize","tags":["Rewards (admin)"]}},"/api/rewards/admin/reward-slots":{"get":{"description":"The bookable window (2 hours to 14 days out), slots of `minutes` (default 30), from each coach's GHL reward calendar — or their in-app working hours when GHL has no answer or is off (then the reward coach pool supplies too). Includes a per-coach count, and `by_length`: open slots for each call length the active call prizes book (e.g. 15 and 30), with those prizes.","operationId":"RewardSlotsAdminController_list","parameters":[{"name":"minutes","required":false,"in":"query","description":"Slot length (a prize's `slot_minutes`); default 30.","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Open reward slots across every coach, soonest first","tags":["Rewards (admin)"]}},"/api/rewards/grants/{id}/slots":{"get":{"description":"Slots of the prize's length (`slot_minutes`, default 30), 2 hours to 14 days out, across every coach supplying reward slots, soonest first. When the prize already has a call booked, `booking` is set and `slots` is empty. When nothing is open, `substitute` is a comparable prize to take instead (or null) and the app offers \"Notify me\"; `notify_me` shows whether the user already asked.","operationId":"RewardBookingController_slots","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Open reward slots for a claimed call prize","tags":["Rewards"]}},"/api/rewards/grants/{id}/book":{"post":{"description":"Creates the call (and its GHL appointment on the coach's reward calendar) and assigns the coach. The optional `focus` and `note` go on the event and the appointment for the coach. 409 `SLOT_UNAVAILABLE` when the slot was just taken or is no longer offered — list again; 409 `ALREADY_BOOKED` when the prize has a live booking.","operationId":"RewardBookingController_book","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookRewardSlotDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Book a claimed call prize into a reward slot","tags":["Rewards"]}},"/api/rewards/grants/{id}/cancel-booking":{"post":{"description":"Any time before the call starts. Cancels the call (and its GHL appointment), unassigns the coach and returns the prize to \"book a slot\" — rebook through GET slots / POST book. 409 `NOT_BOOKED` with no booked call, `CALL_STARTED` once it has begun.","operationId":"RewardBookingController_cancelBooking","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Cancel the booked call for a prize","tags":["Rewards"]}},"/api/rewards/grants/{id}/notify-me":{"post":{"description":"For a claimed call prize with no open slot. A push (`reward_slot_open`) goes out once slots open; asking again after that re-arms it. 409 `ALREADY_BOOKED` when a call is booked.","operationId":"RewardBookingController_notifyMe","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Ask to be pushed when a reward slot opens","tags":["Rewards"]}},"/api/rewards/grants/{id}/substitute":{"post":{"description":"Only while no slot is open (409 `SLOTS_OPEN` otherwise). Swaps the prize for the `substitute` from GET slots and delivers it — returns a claim result (`next_action`, usually `open_chat`). 409 `NO_SUBSTITUTE` when nothing qualifies.","operationId":"RewardBookingController_substitute","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Take the comparable prize instead of the call","tags":["Rewards"]}},"/api/rewards/grants/{id}/chat-days":{"get":{"description":"For a prize claimed with `next_action: book_chat`. Each coach with a reward calendar, and the days (in `timezone`, up to 21 days out) they have reward time open; \"first available\" is any of those days. Once a day is booked, `booking` is set and `coaches` is empty.","operationId":"RewardBookingController_chatDays","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"timezone","required":false,"in":"query","description":"The app's calendar zone; days are dates in it. Defaults to the profile's.","schema":{"example":"America/New_York","type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Coaches and open days for a claimed 24-hour coaching chat","tags":["Rewards"]}},"/api/rewards/grants/{id}/book-chat":{"post":{"description":"Final: there is no cancel. Puts the coach's 15-minute marker on their reward calendar (and in GHL) at their first open reward slot that day; the chat opens at the start of `date` in `timezone`, straight away when that is today. 409 `DAY_UNAVAILABLE` when the coach (or every coach, for first available) has nothing open that day any more; 409 `ALREADY_BOOKED`.","operationId":"RewardBookingController_bookChat","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookRewardChatDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Book the day of a 24-hour coaching chat","tags":["Rewards"]}},"/api/rewards/grants/{id}/chat-notify-me":{"post":{"description":"For a 24-hour coaching chat with no day booked. A push (`reward_chat`) goes out once any coach has a day open; asking again after that re-arms it. 409 `ALREADY_BOOKED` once a day is booked.","operationId":"RewardBookingController_chatNotifyMe","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Ask to be pushed when a coaching chat day opens","tags":["Rewards"]}},"/api/rewards/grants/{id}/chat-substitute":{"post":{"description":"Only while no coach has a day open (409 `DAYS_OPEN` otherwise). Swaps the prize for the `substitute` from GET chat-days and delivers it — returns a claim result (`next_action`, usually `open_chat`). 409 `NO_SUBSTITUTE` when nothing qualifies, `ALREADY_BOOKED` once a day is booked.","operationId":"RewardBookingController_chatSubstitute","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Take the comparable prize instead of the coaching chat","tags":["Rewards"]}},"/api/coach/reward-tasks/{id}/didnt-happen":{"post":{"description":"From when the booked call starts until it auto-delivers (3 hours after it ends). `no_show`: delivered with delivery_outcome 'no_show', consent revoked, and the user gets the no-show push instead of \"is in\". `coach_unavailable`: the call is cancelled and the prize goes back to booking, with an apology push. 409 NOT_A_CALL, NOT_BOOKED, CALL_NOT_STARTED or ALREADY_DELIVERED.","operationId":"CoachRewardCallsController_didntHappen","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RewardCallDidntHappenDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Mark this coach's booked reward call as didn't happen","tags":["Coach"]}},"/api/rewards/admin/grants/{id}/didnt-happen":{"post":{"description":"From when the booked call starts until it auto-delivers (3 hours after it ends). `no_show`: delivered with delivery_outcome 'no_show', consent revoked, and the user gets the no-show push instead of \"is in\". `coach_unavailable`: the call is cancelled and the prize goes back to booking, with an apology push. 409 NOT_A_CALL, NOT_BOOKED, CALL_NOT_STARTED or ALREADY_DELIVERED.","operationId":"RewardCallsAdminController_didntHappen","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RewardCallDidntHappenDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Mark a booked reward call as didn't happen","tags":["Rewards (admin)"]}},"/api/admin/scheduling/events/{id}/sales-intake":{"get":{"operationId":"AdminSalesIntakeController_getForEvent","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"What the lead wrote when booking this discovery call, if it's a sales call","tags":["admin-scheduling"]}},"/api/admin/users/{id}/sales-intakes":{"get":{"operationId":"AdminSalesIntakeController_listForUser","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"A user's discovery-call intakes, newest first","tags":["admin-scheduling"]}},"/api/rewards/wallet":{"get":{"description":"Dollar credit earned at levels 10, 15 and 20 by paid users, members and clients (BE-7.1). While store credit is switched off (`store_credit_enabled: false` on /rewards/me) this is `enabled: false` with a zero balance and no entries — the app shows no wallet.","operationId":"RewardsController_getWallet","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"enabled":true,"balance_cents":500,"currency":"USD","entries":[{"id":1,"amount_cents":500,"currency":"USD","reason":"level_up","level":10,"created_at":"2026-10-14T09:00:00.000Z"}]}}}}}},"security":[{"bearer":[]}],"summary":"The store-credit wallet: balance and history","tags":["Rewards"]}},"/api/rewards/huddle":{"get":{"description":"Shows the date to everyone, even before the invite list is locked — the status field is `pending_snapshot` until then, never a guess.","operationId":"RewardsController_getHuddle","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"scheduled_at":"2026-10-05T18:00:00.000Z","status":"invited","used_banked_pass":false}}}}}},"security":[{"bearer":[]}],"summary":"The next monthly Streak Huddle, and whether this user is invited","tags":["Rewards"]}},"/api/rewards/me":{"get":{"description":"Feeds both the dashboard consistency card and the Journey progress tab. Opening this endpoint settles a broken streak, credits pending smart-scale points, and picks the day's featured tasks — all idempotent.","operationId":"RewardsController_getMe","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"timezone":"America/New_York","points_total":4740,"level":7,"next_level":8,"level_threshold":3700,"next_level_threshold":5000,"points_to_next_level":260,"streak":{"current_days":12,"previous_streak_days":null,"best_days":21,"rest_days_banked":1,"last_active_date":"2026-08-25"},"today":{"date":"2026-08-25","points_earned":30,"daily_cap":100,"bonuses":[{"id":"log_mood","label":"Check in with how you feel","action":"log_mood","source_type":"mood","points":20,"completed":false}]},"streak_rewards":{"steps":[{"days":7,"points":200,"state":"completed"},{"days":14,"points":500,"state":"next"},{"days":30,"points":2000,"state":"upcoming"}],"days_to_next":2,"next_points":500},"goals":[],"pending_reward":null,"pending_level_up":null,"pending_huddle_seat":null,"credited_now":[],"store_credit_enabled":false,"claim_mode":"real"}}}}}},"security":[{"bearer":[]}],"summary":"Points, level, streak, today's tasks, goals and anything pending","tags":["Rewards"]}},"/api/rewards/config":{"get":{"description":"Served rather than hardcoded so caps and thresholds can be retuned without an app release. Cache aggressively — it changes rarely.","operationId":"RewardsController_getConfig","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Level thresholds, streak milestones, caps and points per source","tags":["Rewards"]}},"/api/rewards/goals":{"get":{"description":"`recommended` is ordered by Lucille when `ranked_by` is `lucille` (catalogue order when `default`), and each entry may carry her one-line `reason`. `top_picks` is the first `max_active` of that slate while the user has no active goal, and empty otherwise: the app shows it in place of the empty \"goals are on the way\" state.","operationId":"RewardsController_getGoals","parameters":[{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Active goals plus the recommendation slate","tags":["Rewards"]}},"/api/rewards/goals/{id}/activate":{"post":{"description":"Returns 409 at the active cap so the app can prompt for a swap rather than evicting a goal the user did not choose to drop.","operationId":"RewardsController_activateGoal","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"409":{"description":"Already at the active-goal cap"}},"security":[{"bearer":[]}],"summary":"Activate a goal from the recommendation slate","tags":["Rewards"]}},"/api/rewards/goals/{id}":{"delete":{"description":"Progress is paused and preserved, never wiped — re-adding the goal later picks up where the user left off.","operationId":"RewardsController_deactivateGoal","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Remove an active goal","tags":["Rewards"]}},"/api/rewards/grants":{"get":{"operationId":"RewardsController_getGrants","parameters":[{"name":"status","required":false,"in":"query","schema":{"default":"all","type":"string","enum":["unclaimed","claimed","all"]}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Rewards this user has been granted","tags":["Rewards"]}},"/api/rewards/grants/{id}/claim":{"post":{"description":"Returns `expired` or `already_claimed` as a normal 200 result — both are states the app has copy for, and turning them into errors makes the claim button look broken. `next_action` tells the app where to go next.","operationId":"RewardsController_claimGrant","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"status":"claimed","next_action":"collect_address","grant":{"id":12,"reward_type":"branded_water_bottle"}}}}}}},"security":[{"bearer":[]}],"summary":"Claim a granted reward","tags":["Rewards"]}},"/api/rewards/grants/{id}/consent":{"post":{"description":"Only reached when `claim` returned `status: \"consent_needed\"` — some prizes (a coach call, a custom meal plan) need a coach to see something they don't otherwise have access to. Declining never voids the prize — either answer completes the claim the same way.","operationId":"RewardsController_recordGrantConsent","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordConsentDecisionDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Answer a pending data-access consent ask, then complete the claim","tags":["Rewards"]}},"/api/rewards/grants/{id}/consent/withdraw":{"post":{"description":"PP v2.4 §2A: \"You may withdraw the consent at any time before delivery.\" Ends the coach's prize-scoped access immediately. The prize is kept. Idempotent — returns `withdrawn: false` when access had already ended (expired, delivered or withdrawn). 404 if the prize never asked for consent.","operationId":"RewardsController_withdrawGrantConsent","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Stop sharing data for a prize before it is delivered","tags":["Rewards"]}},"/api/rewards/grants/{id}/consent/resume":{"post":{"description":"After stopping, or after declining at claim (REW-1117). Allowed while the prize is still being delivered (`consent.resumable`), any number of times. Starts now for the usual TTL, or for a booked call or chat whose access has not begun, when it begins (`starts_later`). Idempotent: `shared: false` when sharing is already on or due to start. 404 if the prize never asked for consent; 409 `NOT_SHAREABLE` once it is delivered.","operationId":"RewardsController_resumeGrantConsent","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Share data for a prize again","tags":["Rewards"]}},"/api/rewards/grants/{id}/options":{"get":{"description":"Any grant with `trigger_days` set — the app shows the menu without calling `claim` first; most prizes auto-resolve and never have options. Filtered to what this user is eligible for right now, same rules as everywhere else. Once a pick is made (waiting on its consent answer) the menu is that one option, with `chosen: true`.","operationId":"RewardsController_getGrantOptions","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"The menu for a choice-based prize (streak-day milestones, e.g. day 100/365)","tags":["Rewards"]}},"/api/rewards/grants/{id}/choose":{"post":{"description":"Re-validates the choice is still eligible (stock, subscription state, etc.) at the moment of picking, not just when the menu was fetched. Returns `expired` past the claim-by date. A pick holds: choosing the same option again continues its claim, a different one is a 400.","operationId":"RewardsController_chooseGrantOption","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChooseGrantOptionDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Pick one option on a choice-based prize, then complete the claim","tags":["Rewards"]}},"/api/rewards/grants/{id}/preview":{"post":{"description":"Staff only (coach, PC or admin flag; `/rewards/me.claim_mode` is `preview`); anyone else gets a 403. Answers like claim, choose and consent in one call, with `preview: true`: send `catalogue_id` for a choice prize's pick and `consent` to answer a `consent_needed` step. Nothing is reserved, booked, opened or delivered, and no consent is recorded. Afterwards the grant lists as claimed with `staff_preview: true`, and booking or picking for it is refused with `STAFF_PREVIEW_ONLY`.","operationId":"RewardsController_previewGrantClaim","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreviewGrantClaimDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"A staff member's preview of claiming a prize: nothing is claimed or delivered","tags":["Rewards"]}},"/api/rewards/grants/{id}/calls":{"get":{"description":"For a claimed community-call prize (`next_action: pick_call`): the scheduled calls starting over an hour from now, and the one picked, if any. There is no deadline to pick (29 Oct 2026); `pick_by` is the claim-by date, kept for older apps.","operationId":"RewardsController_communityCalls","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Community calls this prize can be used on","tags":["Rewards"]}},"/api/rewards/grants/{id}/call":{"post":{"description":"Switching is allowed until an hour before the picked call starts (409 `TOO_LATE` after). 409 `CALL_UNAVAILABLE` for a call that can no longer be picked.","operationId":"RewardsController_pickCommunityCall","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PickCommunityCallDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Pick, or switch, the community call a prize is a seat on","tags":["Rewards"]}},"/api/rewards/level-ups/{id}/ack":{"post":{"description":"Without this the modal re-fires on every app open — the ack is what makes the celebration happen exactly once.","operationId":"RewardsController_ackLevelUp","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Mark a level-up celebration as shown","tags":["Rewards"]}},"/api/rewards/huddle/seats/{id}/celebrated":{"post":{"description":"Acks `pending_huddle_seat` from `/rewards/me`, and every older seat with it, so the celebration happens once.","operationId":"RewardsController_celebrateHuddleSeat","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Mark a Streak Huddle seat celebration as shown","tags":["Rewards"]}},"/api/rewards/recap":{"get":{"description":"Tiles are an array, not fixed keys: which six are worth showing differs per user, so the server picks and the client draws what it is handed.","operationId":"RewardsController_getRecap","parameters":[{"name":"month","required":false,"in":"query","description":"Month to recap. Defaults to the month just gone.","schema":{"example":"2026-07","type":"string"}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Monthly recap tiles","tags":["Rewards"]}},"/api/rewards/badges":{"get":{"description":"Level badges (level_<n>) and streak badges (streak_<days>), each with tier and earned date (BE-3.1). The classic badges stay on GET /badges.","operationId":"RewardsController_getBadges","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Earned rewards badges, oldest first","tags":["Rewards"]}},"/api/rewards/ladder":{"get":{"description":"The whole path, not only what is earned: per level (L1–50, further for a user past it) the points needed, badge, cosmetics, floor-prize cosmetic, store credit and prize; per streak milestone the points, badge, cosmetics and choice prize. Each carries this user's state (earned / unlocked, completed / current / upcoming). A prize ahead of the user is a preview of what the waterfall would pick for them now; once granted it is the grant, still unrevealed until claimed. Read-only.","operationId":"RewardsController_getLadder","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Every level and streak milestone with what it gives, and where this user is on it","tags":["Rewards"]}},"/api/rewards/grants/{id}/spotlight":{"post":{"description":"Day-100 streak grants only (BE-5.2). Opting in puts you in the weekly spotlight post; declining means a private message from a coach instead. Can be changed until it has been handled. Returns the grant, with `spotlight` and `share_card`.","operationId":"RewardsController_setSpotlight","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpotlightChoiceDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Opt in or out of the day-100 public spotlight","tags":["Rewards"]}},"/api/rewards/cosmetics/equip":{"patch":{"description":"One per kind — equipping a frame takes off the one that was on. 400 for an unknown or still-locked cosmetic. Returns the full cosmetics list (BE-3.2).","operationId":"RewardsController_equipCosmetic","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EquipCosmeticDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Equip or take off an unlocked cosmetic","tags":["Rewards"]}},"/api/rewards/ledger":{"get":{"description":"Cursor-paginated. No screen consumes this yet; it is the append-only event stream the points total is derived from.","operationId":"RewardsController_getLedger","parameters":[{"name":"cursor","required":false,"in":"query","description":"Opaque cursor from the previous page","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"minimum":1,"maximum":100,"example":50,"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Award history, newest first","tags":["Rewards"]}},"/api/rewards/admin/ladder":{"get":{"description":"Built from the same code the awards use and the live config (REW-998): per level the points needed, badge, cosmetics reaching it unlocks, the floor-prize cosmetic, and store credit cents; per streak milestone the points, badge and cosmetics (year_one at 365).","operationId":"RewardsAdminController_ladder","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"The badge, cosmetic and store-credit ladder, L1–50 and streak days","tags":["Rewards (admin)"]}},"/api/rewards/admin/config/export":{"get":{"operationId":"RewardsAdminController_exportConfig","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Export the effective rewards config, level table, and active level-up offers","tags":["Rewards (admin)"]}},"/api/rewards/admin/catalogue":{"get":{"operationId":"RewardsAdminController_getCatalogue","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Prize catalogue","tags":["Rewards (admin)"]}},"/api/rewards/admin/catalogue/{id}":{"patch":{"description":"Copy (title, description, fulfilment class) stays migration-controlled on purpose — this is only the operational fields. `host_coach_id` must be an active coach.","operationId":"RewardsAdminController_updateCatalogueEntry","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRewardCatalogueDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update a prize's numbers — cost, ranking, free-days amount, active flag, host coach","tags":["Rewards (admin)"]}},"/api/rewards/admin/goal-catalogue":{"get":{"operationId":"RewardsAdminController_getGoalCatalogue","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"The pre-approved goal pool","tags":["Rewards (admin)"]}},"/api/rewards/admin/goal-catalogue/{id}":{"patch":{"description":"Label and rule shape stay migration-controlled — a new rule shape needs a new evaluator in RewardsGoalsService regardless of who edits the row, and this is not the place to hand-edit rule_params JSON.","operationId":"RewardsAdminController_updateGoalCatalogueEntry","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateGoalCatalogueDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update a goal's points, length, or visibility","tags":["Rewards (admin)"]}},"/api/rewards/admin/offers":{"get":{"operationId":"RewardsAdminController_getOffers","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"All prize offers, live or not","tags":["Rewards (admin)"]},"post":{"description":"`quantity_available` is a total for everyone, ever; coach prizes carry none (decided 2026-10-28), so keep it for physical goods. A typo'd `eligibility` key (e.g. `requires_active_subscrption`) is rejected with a 400 here, not silently ignored — this route only, not the global default.","operationId":"RewardsAdminController_createOffer","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertRewardOfferDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Create a prize offer","tags":["Rewards (admin)"]}},"/api/rewards/admin/offers/{id}":{"patch":{"operationId":"RewardsAdminController_updateOffer","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertRewardOfferDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update a prize offer","tags":["Rewards (admin)"]}},"/api/rewards/admin/users/{userId}/summary":{"get":{"description":"Read-only — unlike the app's own `/rewards/me`, this never settles a broken streak, credits pending points, or seeds an existing-user's history as a side effect of being viewed. An admin opening someone's profile must never change what they see.","operationId":"RewardsAdminController_getUserSummary","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"A user's points/level/streak/goals/grants/rollout status, for the admin user-details page","tags":["Rewards (admin)"]}},"/api/rewards/admin/users/{userId}/preview-prize":{"get":{"description":"Dry run of the claim-time waterfall (BE-4.1, for ADM-4.2): the user's resolved audience, the selected prize (or null), and every offer at that level with selected / skipped (+ reason) / not_reached. With `trigger_days` (100, 365) instead of `level`, the streak-milestone choice menu: each option offered or skipped (+ reason), prize null (REW-1001). Read-only — reserves and grants nothing.","operationId":"RewardsAdminController_previewPrize","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}},{"name":"level","required":true,"in":"query","schema":{"type":"number"}},{"name":"trigger_days","required":true,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Which prize this user would get at a level if they claimed now, and why","tags":["Rewards (admin)"]}},"/api/rewards/admin/users/{userId}/recalculate":{"post":{"description":"The recovery path the append-only ledger exists for. Safe to run at any time; it is the only supported way to fix a total that has gone wrong.","operationId":"RewardsAdminController_recalculate","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Rebuild a user's total and level from the ledger","tags":["Rewards (admin)"]}},"/api/rewards/admin/users/{userId}/bonus-rest-days":{"post":{"description":"Banks 1–3 bonus rest days, e.g. after an app bug broke a streak. They protect future missed days; past days are not rewritten. Stops at `streak.bonus_rest_days_cap` — `added` may be less than `count` (`capped: true`). The reason is kept in the activity log.","operationId":"RewardsAdminController_grantBonusRestDays","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrantBonusRestDaysDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Give a user bonus rest days (support goodwill)","tags":["Rewards (admin)"]}},"/api/rewards/admin/users/{userId}/points":{"post":{"description":"Adds 1–5000 points to a client, member or lead in the rewards rollout. They land on the user's next app open as `credited_now` (\"+500 · reason\") and count toward levels, so a level crossed brings its prize. Gifts only add — points can never be taken back. Send a fresh `request_id` per submit: the same one again returns `gifted: false` and gives nothing.","operationId":"RewardsAdminController_giftPointsToUser","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GiftPointsDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Give a user points (admins and coaches)","tags":["Rewards (admin)"]}},"/api/rewards/admin/groups/{groupId}/points":{"post":{"description":"The same gift as `users/:userId/points`, to each active member. Staff and members outside the rewards rollout are skipped and listed in `skipped` ({ user_id, reason }). Re-sending the same `request_id` only gives it to members who did not get it yet (`already_gifted`).","operationId":"RewardsAdminController_giftPointsToGroup","parameters":[{"name":"groupId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GiftPointsDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Give every member of a group points (admins and coaches)","tags":["Rewards (admin)"]}},"/api/rewards/admin/grants":{"get":{"description":"`status=manual_review` (default) or `awaiting_delivery` — claimed, fulfilled coach-time or consent-scoped prizes not yet marked delivered. Each row has the user, prize, reason, assigned coach and consent state. Returns { items, total, page, limit }.","operationId":"RewardsAdminController_fulfilmentQueue","parameters":[{"name":"status","required":false,"in":"query","description":"`manual_review`: waiting on a human. `awaiting_delivery` (derived, not a stored status): claimed and fulfilled, coach-time or consent-scoped, not yet marked delivered.","schema":{"default":"manual_review","type":"string","enum":["manual_review","awaiting_delivery"]}},{"name":"page","required":false,"in":"query","schema":{"default":1,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"maximum":100,"default":25,"type":"number"}},{"name":"user_id","required":false,"in":"query","description":"Only this user's grants.","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Fulfilment queue: grants waiting on a human, oldest claim first","tags":["Rewards (admin)"]}},"/api/rewards/admin/grants/{id}/fulfil":{"post":{"description":"Sets status fulfilled and delivered_at, and ends the prize's data-access consent (revoke_reason \"delivered\"). 404 unknown grant; 409 already delivered, or pending / expired / revoked. Returns the row in the queue's shape.","operationId":"RewardsAdminController_fulfilGrant","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FulfilGrantDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Mark a claimed prize delivered","tags":["Rewards (admin)"]}},"/api/rewards/admin/coach-pool":{"get":{"description":"Every active coach, with in_pool, when they were last assigned a prize, and how many they still owe. Pool coaches first, least recently assigned first.","operationId":"RewardsAdminController_getCoachPool","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"The async reward-prize coach pool checklist (ADM-6.1)","tags":["Rewards (admin)"]},"put":{"description":"Replaces the pool with exactly these coaches. New Log Review, meal-plan and messaging prizes rotate to the pool coach assigned least recently; an empty pool falls back to REWARD_COACH_TIME_COACH_ID. 400 if an id is not an active coach.","operationId":"RewardsAdminController_setCoachPool","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetCoachPoolDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Save the coach pool checklist","tags":["Rewards (admin)"]}},"/api/rewards/admin/store-credit":{"get":{"description":"Amounts are the settings `store_credit.level_<10|15|20>_cents` (placeholders until the client signs off).","operationId":"RewardsAdminController_getStoreCredit","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Store credit: on/off and the amount per level","tags":["Rewards (admin)"]},"put":{"description":"Off by default until the merch store launches. On: paid users, members and clients reaching level 10, 15 or 20 from now on are credited (nothing is back-filled), and the app shows the wallet.","operationId":"RewardsAdminController_setStoreCredit","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetStoreCreditEnabledDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Switch the store-credit wallet on or off","tags":["Rewards (admin)"]}},"/api/rewards/admin/users/{userId}/wallet":{"get":{"description":"The real ledger, shown even while store credit is switched off.","operationId":"RewardsAdminController_getUserWallet","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"A user's store-credit wallet","tags":["Rewards (admin)"]}},"/api/rewards/admin/spotlights":{"get":{"description":"Unhandled only unless include_handled=true. Oldest answer first.","operationId":"RewardsAdminController_listSpotlights","parameters":[{"name":"choice","required":false,"in":"query","schema":{"default":"opted_in","type":"string","enum":["opted_in","declined"]}},{"name":"include_handled","required":false,"in":"query","description":"Include ones already posted / messaged.","schema":{"default":false,"type":"boolean"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Day-100 spotlight answers: opt-ins for the weekly post, or decliners for a coach message","tags":["Rewards (admin)"]}},"/api/rewards/admin/spotlights/{id}/handled":{"post":{"description":"The action must match the answer. 404 unknown; 400 unanswered or mismatched; 409 already handled.","operationId":"RewardsAdminController_markSpotlightHandled","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpotlightHandledDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Mark a spotlight answer handled: posted (opt-in) or messaged (decliner)","tags":["Rewards (admin)"]}},"/api/rewards/admin/grants/expire-stale":{"post":{"description":"Also runs nightly. Releases the stock an unclaimed grant was holding, so a coach slot claimed and never used does not stay reserved.","operationId":"RewardsAdminController_expireStale","parameters":[],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Expire unclaimed grants past their claim-by date","tags":["Rewards (admin)"]}},"/api/rewards/admin/settings":{"get":{"operationId":"RewardsAdminController_listSettings","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Every numeric knob — points, caps, streak rules, goal limits, EOD hour","tags":["Rewards (admin)"]}},"/api/rewards/admin/settings/{key}":{"patch":{"operationId":"RewardsAdminController_updateSetting","parameters":[{"name":"key","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSettingDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update one numeric setting by its key (e.g. \"caps.daily_points_cap\")","tags":["Rewards (admin)"]}},"/api/rewards/admin/source-type-config":{"get":{"operationId":"RewardsAdminController_listSourceTypeConfig","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Points per log type — the \"make food-log worth 15\" screen","tags":["Rewards (admin)"]}},"/api/rewards/admin/source-type-config/{sourceType}":{"patch":{"operationId":"RewardsAdminController_updateSourceTypeConfig","parameters":[{"name":"sourceType","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSourceTypeConfigDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update one log type's base and bonus points","tags":["Rewards (admin)"]}},"/api/rewards/admin/streak-milestones":{"get":{"operationId":"RewardsAdminController_listStreakMilestones","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Consecutive-day milestone payouts","tags":["Rewards (admin)"]},"post":{"operationId":"RewardsAdminController_upsertStreakMilestone","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertStreakMilestoneDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Add or update a milestone at a given day count","tags":["Rewards (admin)"]}},"/api/rewards/admin/streak-milestones/{id}":{"delete":{"operationId":"RewardsAdminController_deleteStreakMilestone","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Remove a milestone","tags":["Rewards (admin)"]}},"/api/rewards/admin/level-thresholds":{"get":{"operationId":"RewardsAdminController_listLevelThresholds","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Cumulative points required per level (1-10); levels past this extend by the gap-increment setting","tags":["Rewards (admin)"]}},"/api/rewards/admin/level-thresholds/{level}":{"patch":{"operationId":"RewardsAdminController_updateLevelThreshold","parameters":[{"name":"level","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLevelThresholdDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update the points required for one published level","tags":["Rewards (admin)"]}},"/api/rewards/admin/rollout":{"get":{"operationId":"RewardsAdminController_getRollout","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Current rollout mode and allowlist","tags":["Rewards (admin)"]},"patch":{"description":"Layered on top of REWARDS_ENABLED, not a replacement — the env kill switch always wins even if this is wide open.","operationId":"RewardsAdminController_updateRolloutMode","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRolloutModeDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Set the rollout mode: off / staff_only / allowlist / all","tags":["Rewards (admin)"]}},"/api/rewards/admin/rollout/allowlist":{"post":{"operationId":"RewardsAdminController_addToAllowlist","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddRolloutAllowlistDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Add a user to the \"small group\" rollout stage","tags":["Rewards (admin)"]}},"/api/rewards/admin/rollout/allowlist/{userId}":{"delete":{"operationId":"RewardsAdminController_removeFromAllowlist","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Remove a user from the rollout allowlist","tags":["Rewards (admin)"]}},"/api/rewards/admin/settings/refresh-cache":{"post":{"description":"Every write above already busts the cache on its own — this is the belt-and-braces \"Refresh Cache\" button for peace of mind after a bulk edit, or if a bust is ever suspected to have failed.","operationId":"RewardsAdminController_refreshCache","parameters":[],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Force-refresh the cached rewards config","tags":["Rewards (admin)"]}},"/api/rewards/admin/amas":{"get":{"operationId":"RewardsAdminController_listLiveAmas","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Every live AMA with Shenelle, newest first, with its seat count","tags":["Rewards (admin)"]},"post":{"description":"A `live_ama` calendar event. Holders of the Level 20 prize pick one (like the community call); only its host, staff and seat holders see it. Never announced, never synced to GHL.","operationId":"RewardsAdminController_scheduleLiveAma","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleLiveAmaDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Schedule a live AMA","tags":["Rewards (admin)"]}},"/api/rewards/admin/amas/{id}":{"patch":{"description":"A move keeps every seat and pushes each seat holder the new time. cancelled: true is final; seat holders go back to picking and are told. Refused once it has started.","operationId":"RewardsAdminController_updateLiveAma","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLiveAmaDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Edit a live AMA: time, length, host, link, title, or cancel","tags":["Rewards (admin)"]}},"/api/rewards/admin/amas/{id}/seats":{"get":{"operationId":"RewardsAdminController_liveAmaSeats","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Who picked a live AMA, with each seat holder's email","tags":["Rewards (admin)"]}},"/api/rewards/admin/huddles":{"get":{"operationId":"RewardsAdminController_listHuddles","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Every scheduled Streak Huddle","tags":["Rewards (admin)"]},"post":{"description":"The invite-list snapshot fires automatically once the configured lead time (default 7 days) is reached — nothing further to trigger by hand. Without `join_url` it follows the standing link (PUT huddles/standing-link).","operationId":"RewardsAdminController_scheduleHuddle","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleHuddleDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Schedule a Streak Huddle","tags":["Rewards (admin)"]}},"/api/rewards/admin/huddles/standing-link":{"get":{"operationId":"RewardsAdminController_getStandingHuddleLink","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"The standing Zoom link every Huddle uses unless it has its own","tags":["Rewards (admin)"]},"put":{"description":"https only. Every upcoming Huddle that follows the standing link — and its calendar event — moves to the new one in the same transaction; Huddles with their own link, and past ones, keep theirs. Returns how many moved.","operationId":"RewardsAdminController_setStandingHuddleLink","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetStandingHuddleLinkDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Set or clear the standing Zoom link","tags":["Rewards (admin)"]}},"/api/rewards/admin/huddles/{id}":{"patch":{"description":"Its calendar event follows every change (BE-5.1). join_url gives it its own link (null: none; omitted leaves it; never served to a user who isn't invited); use_standing_link: true puts it back on the standing link. scheduled_at reschedules (the snapshot moves too if it has not run). cancelled: true cancels the Huddle and its event; final. Once the invite list is taken, invitees get a push when it moves or is cancelled.","operationId":"RewardsAdminController_updateHuddle","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateHuddleDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Edit a Huddle: join link, reschedule, or cancel","tags":["Rewards (admin)"]}},"/api/rewards/admin/huddles/{id}/invites":{"get":{"description":"Enriched with email so the join link can be sent out by hand — there is no in-app notification for a Huddle invite today.","operationId":"RewardsAdminController_listHuddleInvites","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"A huddle's invite list, with each invitee's email","tags":["Rewards (admin)"]}},"/api/rewards/admin/huddles/{id}/invites/{userId}/attend":{"post":{"description":"The only thing that consumes a banked grace pass — there is no automatic attendance signal, so this is always a manual call.","operationId":"RewardsAdminController_markHuddleAttended","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Mark a user as having actually attended","tags":["Rewards (admin)"]}},"/api/rewards/admin/huddles/{id}/run-snapshot":{"post":{"description":"Same effect as the hourly cron catching it once `snapshot_at` arrives — just on demand. Refuses on an already-locked huddle; the snapshot never re-runs.","operationId":"RewardsAdminController_runHuddleSnapshot","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Force this huddle's invite-list snapshot to run now","tags":["Rewards (admin)"]}},"/api/rewards/admin/qa/scenarios":{"get":{"operationId":"RewardsQaController_listScenarios","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List the available one-click scenario presets","tags":["Rewards (QA)"]}},"/api/rewards/admin/qa/reset/{userId}":{"post":{"description":"Deletes every rewards-owned row for this user — ledger, streak, grants, goals, Huddle invites/passes, level-ups. Nothing outside rewards is touched.","operationId":"RewardsQaController_reset","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Wipe a user's rewards state back to a clean slate","tags":["Rewards (QA)"]}},"/api/rewards/admin/qa/scenario/{userId}":{"post":{"description":"Composes the qa/* primitives — does not reset the user first, except existing_user_historical_seed, which structurally needs to. Safe to layer onto a real user's existing state: a facet-only scenario (e.g. huddle_invited) only ever touches its own facet. See GET qa/scenarios for the full list.","operationId":"RewardsQaController_applyScenario","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QaApplyScenarioDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Apply a one-click scenario preset","tags":["Rewards (QA)"]}},"/api/rewards/admin/qa/points/{userId}":{"post":{"operationId":"RewardsQaController_setPoints","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QaSetPointsDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Set a user's points_total directly","tags":["Rewards (QA)"]}},"/api/rewards/admin/qa/streak/{userId}":{"post":{"operationId":"RewardsQaController_setStreak","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QaSetStreakDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Set a user's streak state directly","tags":["Rewards (QA)"]}},"/api/rewards/admin/qa/bonus-rest-days/{userId}":{"post":{"operationId":"RewardsQaController_setBonusRestDays","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QaSetBonusRestDaysDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Set a user's banked bonus rest days directly","tags":["Rewards (QA)"]}},"/api/rewards/admin/qa/grants/{userId}":{"post":{"operationId":"RewardsQaController_setGrant","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QaSetGrantDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Replace a user's grants with one, in a given state","tags":["Rewards (QA)"]}},"/api/rewards/admin/qa/goals/{userId}":{"post":{"operationId":"RewardsQaController_setGoal","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QaSetGoalDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Upsert one goal for a user into a given state","tags":["Rewards (QA)"]}},"/api/rewards/admin/qa/huddle-invite/{userId}":{"post":{"operationId":"RewardsQaController_setHuddleInvite","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QaSetHuddleInviteDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Set a user's Huddle invite status against a fresh test Huddle","tags":["Rewards (QA)"]}},"/api/rewards/admin/qa/huddle-pass/{userId}":{"post":{"operationId":"RewardsQaController_bankPass","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Grant a user an unused Huddle banked pass","tags":["Rewards (QA)"]}},"/api/rewards/admin/qa/activity-days/{userId}":{"post":{"description":"Writes the one non-rewards table `computeHistoricalStreak()` reads, so `POST qa/historical-seed/:userId` has real history to walk. Not cleared by `reset` — re-running this clears its own prior seed first.","operationId":"RewardsQaController_seedActivityDays","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QaSeedActivityDaysDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Seed real activity (water_logs) across the last N days","tags":["Rewards (QA)"]}},"/api/rewards/admin/qa/historical-seed/{userId}":{"post":{"description":"The same inline seed a real first `/rewards/me` read would trigger — not a bypass of its gates. No-ops (with a reason) if user_points_state already exists for this user (call qa/reset first) or if their account postdates REWARDS_LAUNCH_DATE.","operationId":"RewardsQaController_runHistoricalSeed","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QaRunHistoricalSeedDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Run the one-time existing-user seed right now","tags":["Rewards (QA)"]}},"/api/coach/reward-tasks":{"get":{"description":"Claimed prizes assigned to the caller and not yet delivered: user, prize, due date (end of the access window), the DM room to reply in, and consent state. `voice_notes_allowed` on Log Reviews follows the reward_tasks.log_review_voice_notes setting (default: text only).","operationId":"CoachRewardTasksController_list","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Reward prizes this coach owes, oldest claim first","tags":["Coach"]}},"/api/coach/reward-tasks/{id}/complete":{"post":{"description":"Only for a prize assigned to the caller (404 otherwise). Marks it delivered and ends its data-access consent. 409 if already delivered or not deliverable.","operationId":"CoachRewardTasksController_complete","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FulfilGrantDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Mark a reward prize delivered","tags":["Coach"]}},"/api/coach/groups/{groupId}/overview":{"get":{"operationId":"GroupOverviewController_get","parameters":[{"name":"groupId","required":true,"in":"path","schema":{"type":"number"}},{"name":"period","required":false,"in":"query","schema":{"default":"7d","type":"string","enum":["7d","30d","start"]}},{"name":"x-timezone","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"A cohort's logging (days with 3+ meals), calls, assignments and group-chat posts, and each current member on each. Logging counts only clients who share data with coaches.","tags":["Coach — Group overview"]}},"/api/coach/groups/{groupId}/chat-room":{"post":{"operationId":"GroupOverviewController_chatRoom","parameters":[{"name":"groupId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Find or create the group's chat room so the app can open it. 403 NOT_IN_GROUP_CHAT when you aren't a member of the group; nobody is added.","tags":["Coach — Group overview"]}},"/api/coach/recordings/waiting":{"get":{"operationId":"RecordingLogController_waiting","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Your group, community and workshop calls from the last 14 days that ended with no recording link: `{ total, events }`, newest first, at most 5","tags":["Coach — Recordings"]}},"/api/coach/recordings/{eventId}/opens":{"get":{"operationId":"RecordingLogController_opens","parameters":[{"name":"eventId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Who opened the call's recording. A group call lists its whole roster with each person's attendance; a community call or workshop lists only who opened it","tags":["Coach — Recordings"]}},"/api/coach/recordings/{eventId}/visibility":{"put":{"operationId":"RecordingLogController_setVisibility","parameters":[{"name":"eventId","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetRecordingVisibilityDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Hide the recording from everyone but coaches and admins, or show it again. Showing one that was never announced sends the \"Recording ready\" notice. 400 NO_1ON1_RECORDINGS on a 1:1 or discovery call","tags":["Coach — Recordings"]}},"/api/admin/recordings":{"get":{"operationId":"RecordingAdminController_list","parameters":[{"name":"q","required":false,"in":"query","description":"Matches the call title","schema":{"type":"string"}},{"name":"category","required":false,"in":"query","schema":{"type":"string","enum":["group_coaching","community","workshop"]}},{"name":"hidden","required":false,"in":"query","description":"true: hidden only; false: visible only","schema":{"type":"boolean"}},{"name":"from","required":false,"in":"query","description":"Call date, inclusive","schema":{"example":"2026-09-01","type":"string"}},{"name":"to","required":false,"in":"query","description":"Call date, inclusive","schema":{"example":"2026-10-13","type":"string"}},{"name":"page","required":false,"in":"query","schema":{"default":1,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"default":25,"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Every recording, newest call first: audience, link, hidden state and open count","tags":["Admin — Recordings"]}},"/api/admin/recordings/{eventId}/opens":{"get":{"operationId":"RecordingAdminController_opens","parameters":[{"name":"eventId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"The recording's open log, as coaches see it","tags":["Admin — Recordings"]}},"/api/shenellebot/embed":{"post":{"operationId":"ShenellebotController_embed","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"response":"Here's your coaching response..."}}}}}},"summary":"Public AI coaching widget embed endpoint","tags":["Shenellebot"]}},"/api/bridge/health":{"get":{"operationId":"BridgeController_health","parameters":[],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Bridge health check — verifies key validity and returns system status","tags":["Lucille Bridge"]}},"/api/bridge/aggregate-stats":{"get":{"operationId":"BridgeController_aggregateStats","parameters":[{"name":"days","required":true,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"PII-stripped aggregate coaching statistics","tags":["Lucille Bridge"]}},"/api/bridge/admin/dashboard-summary":{"get":{"description":"Mirrors /api/admin/dashboard-summary under bridge auth. Requires the `metrics` scope.","operationId":"BridgeController_adminDashboardSummary","parameters":[{"name":"days","required":true,"in":"query","schema":{"type":"string"}},{"name":"from","required":true,"in":"query","schema":{"type":"string"}},{"name":"to","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Cross-platform engagement summary for service consumers (Lucille).","tags":["Lucille Bridge"]}},"/api/bridge/admin/dashboard-summary-beta":{"get":{"description":"Mirrors /api/admin/dashboard-summary-beta under bridge auth. Requires the `metrics` scope.","operationId":"BridgeController_adminDashboardSummaryBeta","parameters":[{"name":"days","required":true,"in":"query","schema":{"type":"string"}},{"name":"from","required":true,"in":"query","schema":{"type":"string"}},{"name":"to","required":true,"in":"query","schema":{"type":"string"}},{"name":"cohortFrom","required":true,"in":"query","schema":{"type":"string"}},{"name":"timezone","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Fixed cross-platform engagement summary for service consumers (Lucille).","tags":["Lucille Bridge"]}},"/api/bridge/knowledge/search":{"post":{"operationId":"BridgeController_searchKnowledge","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchKnowledgeDto"}}}},"responses":{"201":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Semantic search across allowed vault tiers","tags":["Lucille Bridge"]}},"/api/bridge/knowledge/ingest":{"post":{"operationId":"BridgeController_ingestKnowledge","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestKnowledgeDto"}}}},"responses":{"201":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Ingest a single knowledge entry from Mac Lucille into app vault","tags":["Lucille Bridge"]}},"/api/bridge/knowledge/ingest-batch":{"post":{"operationId":"BridgeController_ingestBatch","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestBatchDto"}}}},"responses":{"201":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Batch ingest knowledge entries from Mac Lucille","tags":["Lucille Bridge"]}},"/api/bridge/community-wisdom":{"get":{"operationId":"BridgeController_communityWisdom","parameters":[{"name":"limit","required":true,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"PII-stripped community success patterns and wisdom","tags":["Lucille Bridge"]}},"/api/bridge/domain-catalog":{"get":{"operationId":"BridgeController_domainCatalog","parameters":[],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Browse domain knowledge categories and entry counts","tags":["Lucille Bridge"]}},"/api/bridge/coaching-philosophy":{"get":{"description":"Serves docs/coaching-philosophy-for-mac.md as the single source of truth for Rewrite coaching rules. Includes a SHA256 hash so Mac Lucille can detect changes and invalidate its local cache without comparing full content.","operationId":"BridgeController_coachingPhilosophy","parameters":[],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Canonical Rewrite coaching philosophy (markdown, for Mac Lucille prompt injection)","tags":["Lucille Bridge"]}},"/api/bridge/users-with-medical-overrides":{"get":{"description":"Returns user_id + override_types[] for every user with at least one active override. Used by Mac Lucille to surface override status at the top of each client block in the coach brief. No names, emails, or health data.","operationId":"BridgeController_usersWithMedicalOverrides","parameters":[],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"PII-minimized list of users with active medical overrides","tags":["Lucille Bridge"]}},"/api/bridge/user-activity-feed":{"get":{"operationId":"BridgeController_userActivityFeed","parameters":[{"name":"since","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Recent user activity signals for Lucille CTA consumption (PII-minimized)","tags":["Lucille Bridge"]}},"/api/bridge/escalation-events":{"get":{"operationId":"BridgeController_escalationEvents","parameters":[{"name":"since","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Recent AI coach escalation events for Lucille signal routing","tags":["Lucille Bridge"]}},"/api/bridge/group-messages":{"get":{"operationId":"BridgeController_groupMessages","parameters":[{"name":"group_id","required":true,"in":"query","schema":{"type":"number"}},{"name":"room_id","required":true,"in":"query","schema":{"type":"number"}},{"name":"room_types","required":true,"in":"query","schema":{"type":"string"}},{"name":"since","required":true,"in":"query","schema":{"type":"string"}},{"name":"before_id","required":true,"in":"query","schema":{"type":"number"}},{"name":"limit","required":true,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Recent in-app chat messages (group + coach DM rooms) for Lucille feedback + coach-signal monitoring","tags":["Lucille Bridge"]}},"/api/bridge/groups":{"get":{"operationId":"BridgeController_groups","parameters":[],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"App coaching groups with start/end dates, for Lucille \"going quiet\" alert windowing","tags":["Lucille Bridge"]}},"/api/bridge/user-working-memory/{userId}":{"get":{"operationId":"BridgeController_getUserWorkingMemory","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Per-user working memory blob for coach dashboard. Requires the user to have set health_data_consent_at (REW-421).","tags":["Lucille Bridge"]}},"/api/bridge/coach-working-memory/{coachId}":{"get":{"operationId":"BridgeController_getCoachWorkingMemory","parameters":[{"name":"coachId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Per-coach caseload working memory for dashboard","tags":["Lucille Bridge"]}},"/api/bridge/user/seed-coaching-context":{"post":{"operationId":"BridgeController_seedCoachingContext","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeedCoachingContextDto"}}}},"responses":{"201":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Seed coaching context for a user (mid-program onboarding or Zoom/WhatsApp sync)","tags":["Lucille Bridge"]}},"/api/bridge/resolve-user":{"post":{"operationId":"BridgeController_resolveUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveUserDto"}}}},"responses":{"201":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Resolve app userId from email or phone (minimal data return)","tags":["Lucille Bridge"]}},"/api/bridge/new-user-signups":{"get":{"operationId":"BridgeController_newUserSignups","parameters":[{"name":"days","required":true,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Clients who signed up or gave coaching consent in the last N days — used by Lucille for GHL identity matching","tags":["Lucille Bridge"]}},"/api/bridge/match-coaching-client":{"post":{"operationId":"BridgeController_matchCoachingClient","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatchCoachingClientDto"}}}},"responses":{"201":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Match a GHL contact to an app user by email, phone, or name","tags":["Lucille Bridge"]}},"/api/bridge/client-profile/{userId}":{"get":{"description":"Returns profile, macro targets, activity stats, detected patterns, and AI coach summary for a specific user. Used when an app user becomes a coaching client and opts to share their historical data with the coaching team. Requires the user to have set health_data_consent_at (REW-421).","operationId":"BridgeController_getClientProfile","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Export full client profile from app for Lucille/coaching consumption","tags":["Lucille Bridge"]}},"/api/bridge/user-activity/{userId}":{"get":{"operationId":"BridgeController_getUserActivity","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}},{"name":"days","required":true,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Per-user activity feed — food logs, patterns, engagement for a specific client. Requires the user to have set health_data_consent_at (REW-421).","tags":["Lucille Bridge"]}},"/api/bridge/user-health-samples/{userId}":{"get":{"operationId":"BridgeController_getUserHealthSamples","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}},{"name":"metric","required":true,"in":"query","schema":{"type":"string"}},{"name":"since","required":true,"in":"query","schema":{"type":"string"}},{"name":"until","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"cursor","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Per-user health sample query — time-windowed reads of health_samples rows for one (user, sample_type). Cursor-paginated for stable iteration. Used by Lucille for coach-prep, daily-fix prompt assembly, and AI chat context that needs raw signal beyond the daily summary. Requires the user to have set coach_data_consent_at AND health_data_consent_at.","tags":["Lucille Bridge"]}},"/api/bridge/user-health-summary/{userId}":{"get":{"operationId":"BridgeController_getUserHealthSummary","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}},{"name":"date","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Per-user daily health summary — aggregates every sample type for one (user, date) into latest / sum / avg / min / max as appropriate. Single-round-trip query so daily-fix prompt assembly stays snappy. Requires the user to have set coach_data_consent_at AND health_data_consent_at.","tags":["Lucille Bridge"]}},"/api/bridge/user-health-devices/{userId}":{"get":{"operationId":"BridgeController_getUserHealthDevices","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Connected health-platform sources for a specific user — device registry view. Mirrors the in-app endpoint but exposed to Lucille so the company AI can build coach-attribution strings ('your client's Withings scale read 187 lbs this morning'). Requires coaching AND health-data consent.","tags":["Lucille Bridge"]}},"/api/bridge/user-health-alerts/{userId}":{"get":{"operationId":"BridgeController_getUserHealthAlerts","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}},{"name":"since_minutes","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Recent coaching-priority pattern detections for a specific user. Same source as /escalation-events (rewrite_rag rows tagged 'coaching_priority') but scoped to one user — what Lucille hits when assembling per-client coaching context rather than a global feed. Includes ALL coaching-priority insights, not just health-derived ones. Requires coaching AND health-data consent.","tags":["Lucille Bridge"]}},"/api/bridge/group-health-rollup/{groupId}":{"get":{"operationId":"BridgeController_getGroupHealthRollup","parameters":[{"name":"groupId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Aggregate health rollup for a coaching group. Counts + denominators only — NEVER per-user data. Returns total members, phase distribution, coverage rates per metric (with denominators for honest rendering), active-in-last-7-days count, and members- needing-attention count. Used by Lucille for cohort-level briefings and weekly group summaries.","tags":["Lucille Bridge"]}},"/api/bridge/coach-caseload-health/{coachId}":{"get":{"operationId":"BridgeController_getCoachCaseloadHealth","parameters":[{"name":"coachId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"A coach's caseload-level health rollup. Same shape as the group rollup but scoped via coach_client_assignments instead of group_memberships. Used for \"your N clients today\" coach-side summaries (briefings, end-of-day reports).","tags":["Lucille Bridge"]}},"/api/bridge/user-conversations/{userId}":{"get":{"description":"Returns paginated conversation messages between the user and the in-app AI coach (App Lucille). Used for diagnosing user-reported issues like \"Lucille called me by another name\" or \"she referenced foods I never ate\". Returns raw message text — no PII scrubbing (coach scope already implies authorized access to user-level data). Supports filtering by time window, response type (chat/daily_fix/crisis), and session_id.","operationId":"BridgeController_getUserConversations","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Retrieve user's App Lucille chat history for diagnostic review","tags":["Lucille Bridge"]}},"/api/bridge/meal-notes":{"get":{"description":"Returns all active (non-deleted) meal notes for (user_id, log_date). scope: 'meal' (meal-level only), 'food' (entry-level only), 'all' (default). Requires the user to have set health_data_consent_at (REW-421).","operationId":"BridgeController_getMealNotesForUserDay","parameters":[{"name":"user_id","required":true,"in":"query","schema":{"type":"number"}},{"name":"log_date","required":true,"in":"query","schema":{"type":"string"}},{"name":"scope","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Per-user per-day meal notes for Lucille coaching context","tags":["Lucille Bridge"]}},"/api/bridge/meal-notes/recent":{"get":{"description":"Returns up to `limit` notes (cap 500) created after `since` (ISO timestamp). Omitting `since` defaults to the last 7 days.","operationId":"BridgeController_getRecentMealNotes","parameters":[{"name":"since","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Recent meal notes across users (windowed) — for Lucille hourly vault rebuild","tags":["Lucille Bridge"]}},"/api/bridge/research/cohort-query":{"post":{"description":"Query anonymized aggregate data across user cohorts. All data is stripped of PII and aggregated to cohort level. Supports filtering by surgery type, months post-surgery, activity level, and detected patterns. Returns aggregate nutrition stats, pattern distribution, food search trends, and restaurant mention frequency. Pass `includeHealth: true` to also receive the inline health slice (sleep / steps / resting HR / HRV / glucose-in-range / weight loss) — same shape as /research/health-aggregates. For internal research use — no individual user data is ever returned.","operationId":"BridgeController_researchCohortQuery","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResearchCohortQueryDto"}}}},"responses":{"201":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Anonymized cohort analysis — aggregate nutrition, weight, and behavioral data","tags":["Lucille Bridge"]}},"/api/bridge/research/food-intelligence":{"get":{"operationId":"BridgeController_researchFoodIntelligence","parameters":[{"name":"days","required":true,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Anonymized food behavior intelligence — search trends, restaurant habits, barcode data","tags":["Lucille Bridge"]}},"/api/bridge/research/retention-curves":{"get":{"operationId":"BridgeController_researchRetentionCurves","parameters":[],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Anonymized retention and engagement curves — cohort-level logging consistency over time","tags":["Lucille Bridge"]}},"/api/bridge/research/health-aggregates":{"post":{"operationId":"BridgeController_researchHealthAggregates","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResearchCohortQueryDto"}}}},"responses":{"201":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Anonymized health-data aggregates across a research cohort. Same filter shape as /research/cohort-query, different aggregate set: sleep / steps / resting HR / HRV / glucose-in-range / weight loss. No per-user data leaves the endpoint — counts + denominators only. Use case: comparing cohort-level health outcomes across surgery types, post-op windows, or activity levels.","tags":["Lucille Bridge"]}},"/api/bridge/research/comorbidity-outcomes":{"get":{"operationId":"BridgeController_researchComorbidityOutcomes","parameters":[],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Per-condition outcome proxies across the entire active user base. For each condition users opted into at signup (T2D, hypertension, sleep apnea, joint pain), returns a heuristic % of contributors meeting a documented threshold. NOT a clinical-grade remission rate — naming is deliberately conservative so downstream consumers don't accidentally make medical claims. Anonymized aggregate only.","tags":["Lucille Bridge"]}},"/api/bridge/feedback":{"get":{"description":"Used by Lucille to poll new feedback, classify into buckets (billing / tech / program-access / bug / general), and create Linear tickets. Each row includes the submitter email so replies can be drafted without an extra round trip.","operationId":"BridgeController_listFeedback","parameters":[],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"List user-submitted feedback (suggestions, bug reports, contact requests)","tags":["Lucille Bridge"]}},"/api/bridge/feedback/{id}":{"patch":{"description":"After Lucille classifies a feedback item and creates a Linear ticket, it should PATCH status=reviewed and admin_notes=\"LINEAR: REW-XXX\" so the admin inbox stays clean and the link to the ticket is discoverable from the dashboard.","operationId":"BridgeController_updateFeedback","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFeedbackDto"}}}},"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Update feedback status / admin notes","tags":["Lucille Bridge"]}},"/api/bridge/users/search":{"get":{"description":"Fallback for resolve-user when the email exact-match misses. Lucille tokenizes the email local-part (e.g. \"ninifetchini\" → \"nini\", \"fetch\") and queries each token here; the candidate set gets returned for human disambiguation. Limit clamped to 50.","operationId":"BridgeController_searchUsers","parameters":[],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Fuzzy search users by email / first name / last name fragments","tags":["Lucille Bridge"]}},"/api/bridge/user-subscription/{userId}":{"get":{"description":"Returns the most-relevant subscription row (active > trialing > past_due > canceled > expired), with the cross-store identifiers support agents cite to Apple / Google / Stripe. For per-purchase Google Play GPA.* order ids use /events.","operationId":"BridgeController_getUserSubscription","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"Get current subscription for a user — platform, status, transaction ids","tags":["Lucille Bridge"]}},"/api/bridge/user-subscription/{userId}/events":{"get":{"description":"Surfaces store-specific identifiers from event metadata when present — Google Play GPA.* order ids, Apple original_transaction_id. Use this when answering \"what was the Google order id for the purchase on date X?\".","operationId":"BridgeController_getUserSubscriptionEvents","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"number"}},{"name":"limit","required":true,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bridge-key":[]}],"summary":"List subscription events for a user (per-purchase identifiers, status changes)","tags":["Lucille Bridge"]}},"/api/admin/bridge-keys":{"get":{"operationId":"BridgeAdminController_listKeys","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List all bridge keys (raw key never shown)","tags":["Bridge Key Admin"]},"post":{"operationId":"BridgeAdminController_createKey","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBridgeKeyDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Create a new bridge key — returns raw key ONCE","tags":["Bridge Key Admin"]}},"/api/admin/bridge-keys/{id}":{"patch":{"operationId":"BridgeAdminController_updateKey","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBridgeKeyDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update a bridge key — name, scopes, IPs, rate limit","tags":["Bridge Key Admin"]},"delete":{"operationId":"BridgeAdminController_deleteKey","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Permanently delete a bridge key","tags":["Bridge Key Admin"]}},"/api/admin/bridge-keys/{id}/revoke":{"post":{"operationId":"BridgeAdminController_revokeKey","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Revoke (deactivate) a bridge key","tags":["Bridge Key Admin"]}},"/api/admin/bridge-keys/{id}/renew":{"post":{"operationId":"BridgeAdminController_renewKey","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Renew a bridge key expiration (extends by 90 days from now)","tags":["Bridge Key Admin"]}},"/api/admin/bridge-keys/audit-trail":{"get":{"operationId":"BridgeAdminController_auditTrail","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Recent bridge access audit trail","tags":["Bridge Key Admin"]}},"/api/admin/bridge-keys/security-alerts":{"get":{"operationId":"BridgeAdminController_securityAlerts","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Recent denied/failed bridge access attempts","tags":["Bridge Key Admin"]}},"/api/admin/lucille-seed/stats":{"get":{"operationId":"LucilleSeedAdminController_stats","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Per-user counts of `source=lucille_seed` rows in rewrite_rag. Use this before AND after a wipe to confirm the scope. Includes a signals-only sub-count so Phase H scope is visible.","tags":["Lucille Seed Admin"]}},"/api/admin/lucille-seed/wipe":{"post":{"operationId":"LucilleSeedAdminController_wipeSeed","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WipeSeedDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"DESTRUCTIVE — delete `source=lucille_seed` rows from rewrite_rag and optionally clear affected users' working_memory blobs. Requires explicit confirmation string + reason. Returns counts. Pass `signalsOnly=true` to scope the wipe to coaching-signal entries only (Phase H, May 2026).","tags":["Lucille Seed Admin"]}},"/api/errors/log":{"post":{"operationId":"ErrorLogsController_logError","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogErrorDto"}}}},"responses":{"201":{"description":""}},"summary":"Log a client-side app error (no auth required)","tags":["Error Logs"]}},"/api/feedback":{"post":{"operationId":"FeedbackController_submit","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitFeedbackDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Submit a suggestion, bug report, or get-in-touch message","tags":["Feedback"]}},"/api/legal/current":{"get":{"operationId":"LegalController_getCurrent","parameters":[],"responses":{"200":{"description":""}},"summary":"Current version + URL for each policy kind (privacy, terms, ai_disclosure)","tags":["Legal"]}}},"info":{"title":"Rewrite Lifestyle API","description":"Health & lifestyle coaching platform API","version":"1.0","contact":{}},"tags":[],"servers":[],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"JWT","type":"http"},"bridge-key":{"type":"apiKey","in":"header","name":"X-Bridge-Key"}},"schemas":{"LoginDto":{"type":"object","properties":{"email":{"type":"string","example":"user@example.com"},"password":{"type":"string","example":"password123"}},"required":["email","password"]},"RegisterDto":{"type":"object","properties":{"email":{"type":"string","example":"user@example.com"},"password":{"type":"string","example":"Password1"},"accessCode":{"type":"string","example":"ABC123"},"firstName":{"type":"string","example":"Jane"},"lastName":{"type":"string","example":"Doe"},"phone":{"type":"string","example":"+15551234567"},"country":{"type":"string","example":"US","description":"ISO 3166-1 alpha-2 country code. Falls back to a code derived from the request timezone if omitted (back-compat for older app versions)."},"isEnrolled":{"type":"boolean","example":false},"termsAccepted":{"type":"boolean","example":true,"description":"User must accept Terms & Conditions to register"},"privacyAccepted":{"type":"boolean","example":true,"description":"User must accept Privacy Policy to register"},"aiDisclosureAccepted":{"type":"boolean","example":true,"description":"User must acknowledge AI-powered features disclosure"},"ageConfirmed":{"type":"boolean","example":true,"description":"User confirms they are 18 years of age or older"},"healthDataConsentAccepted":{"type":"boolean","example":true,"description":"User must consent to collection and use of health and wellness data"},"coachDataConsentAccepted":{"type":"boolean","example":true,"description":"User consents to sharing their data with their assigned coach"}},"required":["email","password","accessCode","phone","termsAccepted","privacyAccepted","aiDisclosureAccepted","ageConfirmed","healthDataConsentAccepted"]},"RegisterOpenDto":{"type":"object","properties":{"email":{"type":"string","example":"user@example.com"},"password":{"type":"string","example":"Password1"},"firstName":{"type":"string","example":"Jane"},"lastName":{"type":"string","example":"Doe"},"phone":{"type":"string","example":"+15551234567"},"country":{"type":"string","example":"US","description":"ISO 3166-1 alpha-2 country code. Falls back to a code derived from the request timezone if omitted (back-compat for older app versions)."},"termsAccepted":{"type":"boolean","example":true,"description":"User must accept Terms & Conditions to register"},"privacyAccepted":{"type":"boolean","example":true,"description":"User must accept Privacy Policy to register"},"aiDisclosureAccepted":{"type":"boolean","example":true,"description":"User must acknowledge AI-powered features disclosure"},"ageConfirmed":{"type":"boolean","example":true,"description":"User confirms they are 18 years of age or older"},"healthDataConsentAccepted":{"type":"boolean","example":true,"description":"User must consent to collection and use of health and wellness data"},"referralToken":{"type":"string","description":"HMAC-signed referral token from agent QR code"}},"required":["email","password","phone","termsAccepted","privacyAccepted","aiDisclosureAccepted","ageConfirmed","healthDataConsentAccepted"]},"ChangePasswordDto":{"type":"object","properties":{"currentPassword":{"type":"string"},"newPassword":{"type":"string"}},"required":["currentPassword","newPassword"]},"ForgotPasswordDto":{"type":"object","properties":{"email":{"type":"string","example":"user@example.com"}},"required":["email"]},"VerifyOtpDto":{"type":"object","properties":{"email":{"type":"string","example":"user@example.com"},"otp":{"type":"string","example":"123456"}},"required":["email","otp"]},"ResetPasswordDto":{"type":"object","properties":{"email":{"type":"string","example":"user@example.com"},"otp":{"type":"string","example":"123456"},"newPassword":{"type":"string","example":"NewPassword1"}},"required":["email","otp","newPassword"]},"SendCheckoutOtpDto":{"type":"object","properties":{"email":{"type":"string","example":"user@example.com"}},"required":["email"]},"VerifyCheckoutOtpDto":{"type":"object","properties":{"email":{"type":"string","example":"user@example.com"},"otp":{"type":"string","example":"123456"}},"required":["email","otp"]},"CreateCheckoutDto":{"type":"object","properties":{"priceId":{"type":"string","example":"price_123"},"email":{"type":"string","example":"user@example.com"},"firstName":{"type":"string","example":"Jane"},"lastName":{"type":"string","example":"Doe"}},"required":["priceId","email"]},"RegisterDeviceDto":{"type":"object","properties":{"appsflyerId":{"type":"string","example":"1234567890123-1234567"},"platform":{"type":"string","enum":["ios","android"]},"appVersion":{"type":"string","example":"3.5.1"},"bundleId":{"type":"string","example":"rewritelifestyle.ai.app"}},"required":["appsflyerId","platform"]},"UpdatePreferencesDto":{"type":"object","properties":{"new_direct_message":{"type":"boolean"},"new_group_message":{"type":"boolean"},"new_chat_group_message":{"type":"boolean"},"community_zoom_reminder":{"type":"boolean"},"workshop_reminder":{"type":"boolean"},"free_workshop_reminder":{"type":"boolean"},"recipe_comment":{"type":"boolean"},"recipe_comment_reply":{"type":"boolean"},"missed_meal_reminder":{"type":"boolean"},"vibe_check_reminder":{"type":"boolean"},"weight_measurement_reminder":{"type":"boolean"},"progress_photo_reminder":{"type":"boolean"},"inventory_reminder":{"type":"boolean"},"gratitude_reminder":{"type":"boolean"},"daily_points_summary":{"type":"boolean"},"streak_reminder":{"type":"boolean"},"reward_slot_open":{"type":"boolean"},"reward_chat":{"type":"boolean"},"reward_delivered":{"type":"boolean"},"reward_reopened":{"type":"boolean"},"streak_huddle":{"type":"boolean"},"coaching_session_reminder_advance":{"type":"boolean"},"coaching_session_reminder_24h":{"type":"boolean"},"coaching_session_reminder_1h":{"type":"boolean"},"coaching_session_reminder_15m":{"type":"boolean"},"recording_ready":{"type":"boolean"},"peer_dm_notifications":{"type":"boolean"},"pc_lead_requests":{"type":"boolean"},"marketing_notification":{"type":"boolean"},"marketing_email":{"type":"boolean"},"marketing_sms":{"type":"boolean"}}},"PushCampaignTargetingDto":{"type":"object","properties":{"platforms":{"type":"array","description":"Restrict to platforms (OneSignal device_type). Empty = all.","items":{"type":"string","enum":["ios","android","web"]}},"appVersion":{"type":"string","description":"App version, e.g. \"2.4.1\""},"appVersionOp":{"type":"string","description":"Comparison operator for appVersion. Default \"=\".","enum":["=","!=",">","<",">=","<="]},"role":{"type":"string","description":"Filter by user role tag (e.g. \"client\", \"lead\")"},"subscriptionTier":{"type":"string","description":"Filter by subscription_tier tag"},"isFounder":{"type":"string","description":"Filter by is_founder tag (\"true\" / \"false\")"},"isEnrolled":{"type":"string","description":"Filter by is_enrolled tag (\"true\" / \"false\")"},"includedSegments":{"description":"OneSignal segment names (created in dashboard). Combined with filters via OR.","type":"array","items":{"type":"string"}}}},"PreviewPushCampaignDto":{"type":"object","properties":{"targeting":{"$ref":"#/components/schemas/PushCampaignTargetingDto"}},"required":["targeting"]},"SendPushCampaignDto":{"type":"object","properties":{"title":{"type":"string","description":"Notification heading. Max 80 chars recommended."},"body":{"type":"string","description":"Notification body. Max 200 chars recommended."},"data":{"type":"object","description":"Optional deep-link payload delivered to the app (e.g. { screen: \"Subscription\" })"},"targeting":{"$ref":"#/components/schemas/PushCampaignTargetingDto"},"expectedRecipientCount":{"type":"number","description":"Recipient count the admin saw in the preview. Used as a sanity check — if OneSignal returns a much larger audience the send is rejected."},"testUserId":{"type":"number","description":"Send only to this one user ID, bypassing targeting entirely — for trying out a campaign before sending it to a real audience. When set, `targeting` may be empty."}},"required":["title","body","targeting"]},"MarkRoomsReadDto":{"type":"object","properties":{"roomIds":{"example":[12,40],"type":"array","items":{"type":"number"}}},"required":["roomIds"]},"MediaUrlsDto":{"type":"object","properties":{"keys":{"example":["chat/12/abc.jpg"],"type":"array","items":{"type":"string"}}},"required":["keys"]},"RoomMuteDto":{"type":"object","properties":{"duration":{"type":"string","enum":["8h","1w","always","off"],"description":"How long to mute this group's push notifications. 'off' unmutes.","example":"8h"}},"required":["duration"]},"PeerSettingsDto":{"type":"object","properties":{"allowRequests":{"type":"boolean","description":"Whether other clients may send this client a message request."}},"required":["allowRequests"]},"PeerUserDto":{"type":"object","properties":{"userId":{"type":"number","description":"The other client."}},"required":["userId"]},"PeerReportDto":{"type":"object","properties":{"roomId":{"type":"number"},"reason":{"type":"string","enum":["harassment","threats","sexual_content","spam","medical_misinformation","inappropriate_content","safety_concern","other"]},"freeText":{"type":"string"},"messageIds":{"description":"The messages the report is about. Omitted, the latest few in the thread are attached.","type":"array","items":{"type":"number"}}},"required":["roomId","reason"]},"ResolvePeerReportDto":{"type":"object","properties":{"action":{"type":"string","enum":["dismiss","warn","suspend","ban"]},"suspendDays":{"type":"number","description":"For `suspend`: how many days. Defaults to 7."},"note":{"type":"string","description":"For `ban`: the reason kept on the account."}},"required":["action"]},"PeerLegalHoldDto":{"type":"object","properties":{"hold":{"type":"boolean","description":"True keeps the report and its evidence until released."}},"required":["hold"]},"OpenStaffDmDto":{"type":"object","properties":{"userId":{"type":"number","description":"The other coach."}},"required":["userId"]},"ChatSettingsDto":{"type":"object","properties":{"peerDmEnabled":{"type":"boolean","description":"Whether client-to-client DMs are switched on for every client"}},"required":["peerDmEnabled"]},"ChatPinsUpdateDto":{"type":"object","properties":{"pin":{"description":"Room ids to pin, newest first. They go on top; one already pinned keeps its place.","example":[12,7],"type":"array","items":{"type":"number"}},"unpin":{"description":"Room ids to unpin, including pins for rooms no longer in the list.","example":[3],"type":"array","items":{"type":"number"}}}},"UpdateWorkItemDto":{"type":"object","properties":{"status":{"type":"string","enum":["resolved","dismissed"]},"assigneeId":{"type":"object","nullable":true,"description":"A coach id to assign to, or null to unassign"}}},"AddProgramTagDto":{"type":"object","properties":{"tag":{"type":"string","example":"4m enrolled"},"programType":{"type":"string","enum":["twelve_month_intensive","four_month_intensive","bootcamp_30_day","alumni"]}},"required":["tag","programType"]},"DismissProvisioningDto":{"type":"object","properties":{}},"EditProvisioningDto":{"type":"object","properties":{"programType":{"type":"string","enum":["four_month_intensive","twelve_month_intensive"]},"endDate":{"type":"string","example":"2027-02-01"}}},"ValidateCodeDto":{"type":"object","properties":{"code":{"type":"string","example":"ABC123"}},"required":["code"]},"AdminUpdateUserDto":{"type":"object","properties":{"email":{"type":"string","example":"user@example.com"},"role":{"type":"string","example":"client","enum":["client","coach","lead","member","pc"]},"is_admin":{"type":"boolean","example":false},"is_super_admin":{"type":"boolean","example":false},"can_view_dashboard_beta":{"type":"boolean","example":false},"is_active":{"type":"boolean","example":true},"is_enrolled":{"type":"boolean","example":true},"is_bnpl":{"type":"boolean","example":false},"can_handle_leads":{"type":"boolean","example":false},"reward_pool_member":{"type":"boolean","example":false},"is_internal":{"type":"boolean","example":false},"email_verified":{"type":"boolean","example":true},"phone_verified":{"type":"boolean","example":true},"is_founder":{"type":"boolean","example":false,"description":"Toggling true claims a founder slot (sets users.is_founder + increments counter); toggling false releases it. Idempotent."},"timezone":{"type":"string","example":"America/New_York"},"water_goal_oz":{"type":"number","example":64},"first_name":{"type":"string","example":"Jane"},"last_name":{"type":"string","example":"Doe"},"phone":{"type":"string","example":"+1-555-000-0000"},"city":{"type":"string","example":"Austin"},"state":{"type":"string","example":"TX"},"country":{"type":"string","example":"US","description":"ISO 3166-1 alpha-2 country code"},"postal_code":{"type":"string","example":"78701"},"birth_date":{"type":"string","example":"1985-06-15"},"occupation":{"type":"string","example":"Teacher"},"activity_level":{"type":"string","example":"moderate"},"surgery_type":{"type":"string","example":"gastric_sleeve"},"surgery_date":{"type":"string","example":"2022-03-10"},"starting_weight":{"type":"number","example":280.5},"goal_weight":{"type":"number","example":160},"program_status":{"type":"string","example":"active","enum":["pending","active","completed","dropped"]}}},"EnrollClientDto":{"type":"object","properties":{"programType":{"type":"string","example":"four_month_intensive","description":"Program type — see GET /api/admin/programs for valid values"},"startDate":{"type":"string","example":"2025-01-06"},"endDate":{"type":"string","example":"2025-05-06"}},"required":["programType","startDate"]},"UpdateEnrollmentStatusDto":{"type":"object","properties":{"status":{"type":"string","enum":["completed","dropped"]},"endDate":{"type":"string","description":"Defaults to today if omitted and no end_date is already set."}},"required":["status"]},"PublishPolicyVersionDto":{"type":"object","properties":{"kind":{"type":"string","enum":["privacy","terms","ai_disclosure"]},"version":{"type":"string","example":"2.2"},"effectiveAt":{"type":"string","example":"2026-10-01T00:00:00Z"},"url":{"type":"string","example":"https://rewritelifestyle.ai/legal/privacy-2.2"}},"required":["kind","version","effectiveAt","url"]},"PreviewPolicyNoticeDto":{"type":"object","properties":{"subject":{"type":"string"},"bodyHtml":{"type":"string"},"bodyText":{"type":"string"},"effectiveAt":{"type":"string"},"sampleFirstName":{"type":"string","example":"Alex"}}},"CreatePolicyNoticeCampaignDto":{"type":"object","properties":{"key":{"type":"string","example":"pp-terms-v3.0","description":"Unique name for this notice"},"subject":{"type":"string","example":"Changes to our Privacy Policy and Terms, effective {{effectiveDate}}"},"bodyHtml":{"type":"string","description":"HTML body. The client's copy, as supplied. `{{firstName}}` and `{{effectiveDate}}` are filled in per recipient (no name: \"Hi {{firstName}},\" becomes \"Hi,\"). Must link to both the Privacy Policy and the Terms."},"bodyText":{"type":"string","description":"Plain-text body. The client's copy, as supplied. `{{firstName}}` and `{{effectiveDate}}` are filled in per recipient (no name: \"Hi {{firstName}},\" becomes \"Hi,\"). Must link to both the Privacy Policy and the Terms."},"effectiveAt":{"type":"string","example":"2026-12-03T08:00:00.000Z","description":"When the new version takes effect. At least 30 days after the notice is sent."}},"required":["key","subject","bodyHtml","bodyText","effectiveAt"]},"UpdatePolicyNoticeCampaignDto":{"type":"object","properties":{"subject":{"type":"string"},"bodyHtml":{"type":"string"},"bodyText":{"type":"string"},"effectiveAt":{"type":"string","example":"2026-12-03T08:00:00.000Z"}}},"PolicyNoticeTestDto":{"type":"object","properties":{"emails":{"example":["you@example.com","po@example.com"],"description":"1–10 sample addresses","type":"array","items":{"type":"string"}},"sampleFirstName":{"type":"string","example":"Alex","description":"Name used for {{firstName}} (default \"Test\")"}},"required":["emails"]},"PolicyNoticeExcludeDto":{"type":"object","properties":{"userIds":{"example":[12,34],"type":"array","items":{"type":"string"}},"reason":{"type":"string","example":"test / internal account"}},"required":["userIds","reason"]},"PolicyNoticeIncludeDto":{"type":"object","properties":{"userIds":{"example":[12],"type":"array","items":{"type":"string"}}},"required":["userIds"]},"PolicyNoticeSendBatchDto":{"type":"object","properties":{"userIds":{"example":[12,34],"description":"1–500 reviewed users, all still pending","type":"array","items":{"type":"string"}},"confirm":{"type":"boolean","example":true,"description":"Must be true: this emails these users."}},"required":["userIds","confirm"]},"PolicyNoticeRetryFailedDto":{"type":"object","properties":{"userIds":{"example":[12],"description":"Only these users; omit to requeue every failed send","type":"array","items":{"type":"string"}}}},"PolicyNoticeReportDto":{"type":"object","properties":{"note":{"type":"string","example":"Final report after the send finished on 2 Nov 2026"}}},"StartPolicyNoticeDto":{"type":"object","properties":{"confirm":{"type":"boolean","example":true,"description":"Must be true: this emails every live account."}},"required":["confirm"]},"ComplianceLookupDto":{"type":"object","properties":{"email":{"type":"string","example":"jane@example.com","description":"Email of a deleted account to re-link"}},"required":["email"]},"ComplianceLegalHoldDto":{"type":"object","properties":{"userId":{"type":"number","example":42,"description":"Live account"},"subjectRef":{"type":"string","description":"Deleted account (compliance_subject_archive.subject_ref)"},"hold":{"type":"boolean","example":true,"description":"true places the hold, false lifts it"}},"required":["hold"]},"BackfillExpiredClientsDto":{"type":"object","properties":{"userIds":{"example":[101,204,318],"description":"Explicit list of user ids whose program has already ended but who are still on client access. Never a blind historical sweep — the caller supplies exactly who to fix.","type":"array","items":{"type":"string"}}},"required":["userIds"]},"AdminCreateUserDto":{"type":"object","properties":{}},"AdminResetPasswordDto":{"type":"object","properties":{}},"AttachPendingSubscriptionDto":{"type":"object","properties":{}},"GrantAccessDto":{"type":"object","properties":{}},"CreateAccessCodeDto":{"type":"object","properties":{"codeType":{"type":"string","example":"client","enum":["client","coach","lead"]},"email":{"type":"string","example":"user@example.com"}},"required":["codeType","email"]},"CreateGroupDto":{"type":"object","properties":{"name":{"type":"string","example":"January 2025 Cohort"},"description":{"type":"string","example":"New year weight loss group"},"startDate":{"type":"string","example":"2025-01-15"},"endDate":{"type":"string","example":"2025-04-15"},"programId":{"type":"number","example":2,"description":"Which program this group cohort represents"}},"required":["name"]},"AssignUserDto":{"type":"object","properties":{"groupId":{"type":"number","example":1},"userId":{"type":"string","example":"5","description":"User ID or \"pending_<accessCodeId>\""}},"required":["groupId","userId"]},"SendGroupMessageDto":{"type":"object","properties":{"groupId":{"type":"number","example":1},"subject":{"type":"string","example":"Weekly check-in"},"content":{"type":"string","example":"Great work this week everyone!"}},"required":["groupId","subject","content"]},"SendCoachNoteDto":{"type":"object","properties":{"clientId":{"type":"number","example":5},"noteContent":{"type":"string","example":"Great progress on your protein intake today!"},"noteType":{"type":"string","example":"nutrition_alert"},"isUrgent":{"type":"boolean","example":false},"isVisibleToClient":{"type":"boolean","example":true}},"required":["clientId","noteContent"]},"AdminUpdateMacroGoalsDto":{"type":"object","properties":{"userId":{"type":"number","example":5},"calories":{"type":"number","example":1500},"protein":{"type":"number","example":100},"carbs":{"type":"number","example":150},"fat":{"type":"number","example":50},"fiber":{"type":"number","example":25},"reason":{"type":"string","example":"Post-surgery stage 3 adjustment"}},"required":["userId","calories","protein","carbs","fat","fiber","reason"]},"AddRagDto":{"type":"object","properties":{"title":{"type":"string","example":"Protein intake guidelines for stage 2"},"content":{"type":"string","example":"During stage 2 recovery, patients should aim for..."},"category":{"type":"string","example":"food_rules"},"tags":{"type":"string","example":"protein,stage2,recovery"}},"required":["title","content","category"]},"ProcessTranscriptDto":{"type":"object","properties":{"transcript":{"type":"string","example":"Full transcript text here..."},"source":{"type":"string","example":"coaching-call-2025-01"}},"required":["transcript","source"]},"ProcessTranscriptChunkDto":{"type":"object","properties":{"text":{"type":"string","example":"Chunk of transcript text..."},"source":{"type":"string","example":"coaching-call-2025-01"}},"required":["text","source"]},"SaveProcessedTranscriptDto":{"type":"object","properties":{"processed":{"type":"object"},"parentTranscriptId":{"type":"string","example":"uuid-parent-id"}},"required":["processed"]},"CreateSurveyDto":{"type":"object","properties":{"key":{"type":"string","example":"healthy_2_0","description":"Stable key — question.survey_id resolution and app clients reference this, not the id"},"name":{"type":"string","example":"Healthy 2.0"},"isActive":{"type":"boolean","example":true,"description":"Defaults to true — an inactive survey's questions become unfetchable immediately"}},"required":["key","name"]},"UpdateSurveyDto":{"type":"object","properties":{"name":{"type":"string","example":"Healthy 2.0 (renamed)"},"isActive":{"type":"boolean","example":false,"description":"The visibility toggle — false makes every question in this survey unfetchable"}}},"CreateOptionDto":{"type":"object","properties":{"optionText":{"type":"string","example":"Gastric Sleeve"},"optionValue":{"type":"string","example":"gastric_sleeve"},"displayOrder":{"type":"number","example":1}},"required":["optionText"]},"CreateQuestionDto":{"type":"object","properties":{"surveyId":{"type":"number","example":1,"description":"Survey this question belongs to — defaults to the intake survey if omitted"},"questionText":{"type":"string","example":"What type of bariatric surgery did you have?"},"questionKey":{"type":"string","example":"surgery_type"},"answerType":{"type":"string","example":"single_choice"},"minItems":{"type":"number","example":5,"description":"Minimum items required — only meaningful for answerType \"repeatable_list\""},"allowsCustomInput":{"type":"boolean","example":false},"section":{"type":"number","example":1},"displayOrder":{"type":"number","example":1},"isRequired":{"type":"boolean","example":true},"conditionalOnQuestionId":{"type":"number","example":12,"description":"ID of question this depends on"},"conditionalOnOptionIds":{"example":[1,2,3],"description":"Option IDs that make this question visible","type":"array","items":{"type":"string"}},"options":{"description":"Initial options to create with the question","type":"array","items":{"$ref":"#/components/schemas/CreateOptionDto"}}},"required":["questionText","questionKey","answerType"]},"UpdateQuestionDto":{"type":"object","properties":{"surveyId":{"type":"number","example":1},"questionText":{"type":"string","example":"Updated question text"},"questionKey":{"type":"string","example":"updated_key"},"answerType":{"type":"string","example":"multi_choice"},"minItems":{"type":"number","example":5},"allowsCustomInput":{"type":"boolean","example":true},"section":{"type":"number","example":2},"displayOrder":{"type":"number","example":5},"isRequired":{"type":"boolean","example":false},"isActive":{"type":"boolean","example":true},"conditionalOnQuestionId":{"type":"number","example":12},"conditionalOnOptionIds":{"example":[1,2,3],"type":"array","items":{"type":"string"}}}},"UpdateOptionDto":{"type":"object","properties":{"optionText":{"type":"string","example":"Gastric Sleeve (VSG)"},"optionValue":{"type":"string","example":"gastric_sleeve"},"displayOrder":{"type":"number","example":1},"isActive":{"type":"boolean","example":true}}},"SendMarketingEmailDto":{"type":"object","properties":{"templateKey":{"type":"string","example":"app_launch","description":"Template key: app_launch | new_feature | newsletter | promotional | custom"},"variables":{"type":"object","example":{"appStoreUrl":"https://apps.apple.com/...","playStoreUrl":"https://play.google.com/..."},"description":"Variables required by the chosen template"}}},"CreateAgentDto":{"type":"object","properties":{"name":{"type":"string","example":"John Smith"},"email":{"type":"string","example":"john@agency.com"},"phone":{"type":"string","example":"+15551234567"}},"required":["name","email"]},"UpdateAgentDto":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"is_active":{"type":"boolean"}}},"CreateAgentQrCodeDto":{"type":"object","properties":{"label":{"type":"string","example":"June conference"},"trial_days":{"type":"number","example":7,"description":"Trial days granted to referred users"},"max_scans":{"type":"number","example":500,"description":"Maximum number of signups. Null = unlimited."}}},"UpdateAgentQrCodeDto":{"type":"object","properties":{"label":{"type":"string"},"trial_days":{"type":"number"},"max_scans":{"type":"number"},"is_active":{"type":"boolean"}}},"SubscribeDto":{"type":"object","properties":{"email":{"type":"string","example":"user@example.com"}},"required":["email"]},"VerifySubscriberDto":{"type":"object","properties":{"email":{"type":"string","example":"user@example.com"},"otp":{"type":"string","example":"482931"}},"required":["email","otp"]},"CreateMealNoteDto":{"type":"object","properties":{"log_date":{"type":"string","example":"2026-04-24","description":"Diary date (YYYY-MM-DD) the meal belongs to"},"meal_number":{"type":"number","example":2,"description":"1=breakfast, 2=lunch, 3=dinner, 4-6=snack"},"meal_type":{"type":"string","example":"lunch","description":"Optional label mirroring mobile UI"},"food_log_id":{"type":"number","example":101,"description":"Optional — attach the note to a specific food_logs row instead of the whole meal"},"body":{"type":"string","example":"Ate out, estimated portions.","description":"Note content (plain text, max 2000 chars)"}},"required":["log_date","meal_number","body"]},"UpdateMealNoteDto":{"type":"object","properties":{"body":{"type":"string","example":"Felt nauseous after this one."}},"required":["body"]},"SetHealthConsentDto":{"type":"object","properties":{"consent":{"type":"boolean","description":"Whether the user consents to share their health data with their coach. Setting true creates the consent timestamp; setting false revokes it."}},"required":["consent"]},"HealthSyncSampleDto":{"type":"object","properties":{"sample_type":{"type":"string","enum":["weight","body_fat_percentage","lean_body_mass","bmi","waist_circumference","dietary_energy","dietary_protein","dietary_carbs","dietary_fat","dietary_sugar","dietary_fiber","dietary_water","steps","active_energy_burned","apple_exercise_time","exercise_session","distance","floors_climbed","stand_hour","sleep_session","heart_rate","resting_heart_rate","hrv","blood_glucose","blood_pressure_systolic","blood_pressure_diastolic"],"description":"Normalized sample type key"},"value":{"type":"object","description":"Primary numeric value. Nullable for category samples whose meaning is in metadata (e.g. workout type without a primary number).","nullable":true},"unit":{"type":"string","description":"Unit string — lbs, kg, kcal, mg/dL, bpm, oz, g, min, count, etc."},"start_date":{"type":"string","description":"Sample window start (ISO 8601)"},"end_date":{"type":"string","description":"Sample window end (ISO 8601). Equals start_date for point-in-time samples."},"source_bundle_id":{"type":"string","description":"App or device that originally wrote this sample to the platform hub. Used for cross-source dedup priority. Examples: com.apple.health.1234ABCD, com.withings.wiscale2, com.dexcom.cgm.G7"},"source_name":{"type":"string","description":"Human-readable display name. Examples: \"Apple Watch\", \"Withings Body+\""},"category":{"type":"object","description":"Sub-type for category-type samples. sleep_session: \"session\" (full-night summary), \"light\", \"deep\", \"rem\", \"asleep\", \"awake\", \"out_of_bed\", \"unknown\". exercise_session: \"running\", \"cycling\", \"swimming\", etc. Null / omitted for scalar samples (weight, steps, hrv, etc.).","nullable":true},"metadata":{"type":"object","description":"Type-specific structured payload — sleep stages, workout details, glucose-meter context, blood-pressure pairing, etc."}},"required":["sample_type","unit","start_date","end_date","source_bundle_id"]},"HealthSyncAnchorDto":{"type":"object","properties":{"sample_type":{"type":"string","enum":["weight","body_fat_percentage","lean_body_mass","bmi","waist_circumference","dietary_energy","dietary_protein","dietary_carbs","dietary_fat","dietary_sugar","dietary_fiber","dietary_water","steps","active_energy_burned","apple_exercise_time","exercise_session","distance","floors_climbed","stand_hour","sleep_session","heart_rate","resting_heart_rate","hrv","blood_glucose","blood_pressure_systolic","blood_pressure_diastolic"]},"last_anchor":{"type":"string","description":"Timestamp of the most recent sample successfully read on the client (ISO 8601)"},"hk_anchor":{"type":"object","description":"iOS-only — opaque token from HKAnchoredObjectQuery for efficient resumption. Null on Android.","nullable":true}},"required":["sample_type","last_anchor"]},"HealthSyncRequestDto":{"type":"object","properties":{"platform":{"type":"string","enum":["apple_health","health_connect"]},"samples":{"description":"Up to 500 samples per request. Larger batches must be paginated by the client.","type":"array","items":{"$ref":"#/components/schemas/HealthSyncSampleDto"}},"anchors":{"description":"Anchor positions per (sample_type, platform). Server upserts these so the client can resume from where it left off after a reinstall or device switch.","type":"array","items":{"$ref":"#/components/schemas/HealthSyncAnchorDto"}},"preferred_sources":{"type":"object","description":"Platform-resolved preferred source per sample type. Key = sample_type (e.g. \"steps\"), value = source_bundle_id the platform considers authoritative (e.g. \"com.apple.health.9B9B995F-...\"). When present, the backend records these in user_source_preferences so all aggregate queries (step totals, HRV, sleep) return the same numbers the user sees in Apple Health / Google Fit. Omit types for which the platform has no clear preference.","additionalProperties":{"type":"string"},"example":{"steps":"com.apple.health.9B9B995F-8827-4CC1-BDF5-4D8E9BCE948A","heart_rate":"com.apple.health.9B9B995F-8827-4CC1-BDF5-4D8E9BCE948A","weight":"com.yolanda.fitindex"}}},"required":["platform","samples","anchors"]},"ReportDateRangeDto":{"type":"object","properties":{"preset":{"type":"string","enum":["last_30_days","last_3_months","all_time","custom"]},"from":{"type":"string","example":"2025-11-11","description":"Required when preset = \"custom\""},"to":{"type":"string","example":"2026-05-14","description":"Required when preset = \"custom\""}},"required":["preset"]},"ExportHealthReportDto":{"type":"object","properties":{"sections":{"type":"array","description":"Sections to include in the PDF. At least one required.","example":["weight_body_composition","health_improvements"],"items":{"type":"string","enum":["weight_body_composition","activity_movement","sleep","mood","nutrition_logs","daily_readiness","blood_glucose","blood_pressure","poop","digestive_symptoms","journey_timeline","health_improvements"]}},"date_range":{"$ref":"#/components/schemas/ReportDateRangeDto"}},"required":["sections","date_range"]},"UpdateHealthGoalsDto":{"type":"object","properties":{"sleep_hours":{"type":"number","description":"Target sleep hours per night (1–12). Default 7.0.","minimum":1,"maximum":12,"example":7},"steps":{"type":"number","description":"Daily step goal (500–50 000). Default 8 000.","minimum":500,"maximum":50000,"example":8000},"active_minutes_min":{"type":"number","description":"Daily active minutes goal (10–300). Default 60.","minimum":10,"maximum":300,"example":60},"stand_hours_min":{"type":"number","description":"Daily stand hours goal (1–16). Default 8.","minimum":1,"maximum":16,"example":8}}},"BloodPressureValueDto":{"type":"object","properties":{"systolic":{"type":"number","description":"Systolic pressure (mmHg).","minimum":60,"maximum":300,"example":120},"diastolic":{"type":"number","description":"Diastolic pressure (mmHg).","minimum":30,"maximum":200,"example":80}},"required":["systolic","diastolic"]},"RhrValueDto":{"type":"object","properties":{"bpm":{"type":"number","description":"Resting heart rate (bpm).","minimum":30,"maximum":220,"example":62}},"required":["bpm"]},"ManualEntryDto":{"type":"object","properties":{"metric":{"type":"string","enum":["blood_pressure","rhr"],"description":"Which metric is being entered manually.","example":"blood_pressure"},"blood_pressure":{"description":"Required when metric = \"blood_pressure\".","allOf":[{"$ref":"#/components/schemas/BloodPressureValueDto"}]},"rhr":{"description":"Required when metric = \"rhr\".","allOf":[{"$ref":"#/components/schemas/RhrValueDto"}]},"recorded_at":{"type":"string","description":"ISO 8601 timestamp of when the reading was taken. Defaults to now. Use this when recording a reading from earlier in the day.","example":"2026-05-19T08:30:00.000Z"},"is_baseline":{"type":"boolean","description":"When true, this reading is stored as the pre-op baseline for the metric in addition to being written to health_samples. Only valid for blood_pressure and rhr. Use this for the \"Add a manual entry\" flow on the More Metrics pre-op baseline prompt.","example":false}},"required":["metric"]},"DiaryReadinessDto":{"type":"object","properties":{"score":{"type":"object"},"band":{"type":"object"},"insight":{"type":"object"}}},"DiarySleepStagesDto":{"type":"object","properties":{"light_min":{"type":"object"},"deep_min":{"type":"object"},"rem_min":{"type":"object"},"awake_min":{"type":"object"}}},"DiarySleepDto":{"type":"object","properties":{"total_min":{"type":"number"},"bedtime":{"type":"object"},"wake_time":{"type":"object"},"quality_pct":{"type":"object"},"stages":{"$ref":"#/components/schemas/DiarySleepStagesDto"}},"required":["total_min","stages"]},"DiaryWorkoutDto":{"type":"object","properties":{"type":{"type":"object"},"duration_min":{"type":"number"},"start_time":{"type":"string"}},"required":["duration_min","start_time"]},"DiaryActivityDto":{"type":"object","properties":{"steps":{"type":"object"},"active_calories":{"type":"object"},"distance_km":{"type":"object"},"floors_climbed":{"type":"object"},"stand_hours":{"type":"object"},"workouts":{"type":"array","items":{"$ref":"#/components/schemas/DiaryWorkoutDto"}}},"required":["workouts"]},"DiaryBloodPressureDto":{"type":"object","properties":{"systolic":{"type":"object"},"diastolic":{"type":"object"}}},"DiaryVitalsDto":{"type":"object","properties":{"resting_heart_rate":{"type":"object"},"hrv":{"type":"object"},"blood_glucose":{"type":"object"},"blood_pressure":{"$ref":"#/components/schemas/DiaryBloodPressureDto"}}},"DiaryResponseDto":{"type":"object","properties":{"date":{"type":"string"},"readiness":{"$ref":"#/components/schemas/DiaryReadinessDto"},"sleep":{"$ref":"#/components/schemas/DiarySleepDto"},"activity":{"$ref":"#/components/schemas/DiaryActivityDto"},"vitals":{"$ref":"#/components/schemas/DiaryVitalsDto"},"weight_lbs":{"type":"object"},"computed_at":{"type":"string"}},"required":["date","activity","vitals","computed_at"]},"JourneyStatsDto":{"type":"object","properties":{"nsv_count":{"type":"number","description":"Number of NSV entries created by the user"},"anniversary_count":{"type":"number","description":"Number of surgery anniversaries reached (years)"},"weight_lost_lbs":{"type":"object","description":"Total weight lost since starting weight (lbs). Null if no starting weight."}},"required":["nsv_count","anniversary_count","weight_lost_lbs"]},"WeightTrendPointDto":{"type":"object","properties":{"date":{"type":"string","example":"2026-03-15"},"weight":{"type":"number","example":184.2}},"required":["date","weight"]},"WeightTrendDto":{"type":"object","properties":{"window":{"type":"string","enum":["month","6months","year"]},"points":{"type":"array","items":{"$ref":"#/components/schemas/WeightTrendPointDto"}},"starting_weight":{"type":"object","nullable":true},"current_weight":{"type":"object","nullable":true},"goal_weight":{"type":"object","nullable":true},"weight_change":{"type":"object","nullable":true},"goal_progress_pct":{"type":"object","nullable":true,"description":"Percentage of (starting→goal) gap closed. Null when inputs missing."}},"required":["window","points","starting_weight","current_weight","goal_weight","weight_change","goal_progress_pct"]},"JourneyPhotoDto":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"}},"required":["id","url"]},"TimelineItemDto":{"type":"object","properties":{"id":{"type":"string","description":"Unique item ID. Pattern: \"e_{id}\" for DB entries, \"surgery\", \"anniversary_{n}\", \"wm_{n|key}\", \"hm_{key}\""},"type":{"type":"string","enum":["nsv","misc","movement","weight","goal_completion","monthly_recap","level_up","streak_milestone","reward_earned","streak_huddle","surgery","anniversary","weight_log","weight_milestone","health_milestone"]},"title":{"type":"string"},"body":{"type":"object","nullable":true},"entry_date":{"type":"string","example":"2024-11-22"},"weight_at_time_lbs":{"type":"object","nullable":true},"ai_coaching_response":{"type":"object","nullable":true,"description":"AI-generated coaching message. Present only on NSV entries."},"photos":{"type":"array","items":{"$ref":"#/components/schemas/JourneyPhotoDto"}},"can_edit":{"type":"boolean","description":"Whether the current user can edit/delete this item"},"is_auto_generated":{"type":"boolean","description":"True for backend-computed items (surgery, anniversary, weight_milestone, health_milestone, weight_log). False for user-created entries (nsv, misc, movement). Use this to decide icon treatment, edit affordance visibility, and photo suggestion flow."},"anniversary_year":{"type":"object","nullable":true,"description":"Years since surgery. Set only on anniversary items."},"milestone_subtype":{"type":"object","nullable":true,"description":"Subtype for milestone items — use this to pick the right icon/badge. weight_milestone subtypes: \"weight_loss\" | \"onederland\" | \"goal_achieved\". health_milestone subtypes: \"first_10k_steps\" | \"first_30min_active\" | \"first_workout\". Null for all other item types."}},"required":["id","type","title","body","entry_date","weight_at_time_lbs","ai_coaching_response","photos","can_edit","is_auto_generated","anniversary_year","milestone_subtype"]},"TimelinePageDto":{"type":"object","properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/TimelineItemDto"}},"has_more":{"type":"boolean"},"next_cursor":{"type":"object","nullable":true}},"required":["entries","has_more","next_cursor"]},"JourneyResponseDto":{"type":"object","properties":{"user_id":{"type":"number"},"stats":{"$ref":"#/components/schemas/JourneyStatsDto"},"weight_trend":{"$ref":"#/components/schemas/WeightTrendDto"},"timeline":{"$ref":"#/components/schemas/TimelinePageDto"},"computed_at":{"type":"string"}},"required":["user_id","stats","weight_trend","timeline","computed_at"]},"CreateJourneyEntryDto":{"type":"object","properties":{"type":{"type":"string","enum":["nsv","misc","movement","weight"],"example":"nsv"},"title":{"type":"string","example":"Ran my first 5K!","maxLength":255},"body":{"type":"string","example":"Never thought I could do this...","maxLength":500},"entry_date":{"type":"string","example":"2024-11-22","description":"Date the milestone happened (YYYY-MM-DD)"},"weight_at_time_lbs":{"type":"number","example":215,"description":"Weight on that day (lbs)"}},"required":["type","title","entry_date"]},"JourneyEntryResponseDto":{"type":"object","properties":{"id":{"type":"number"},"type":{"type":"string","enum":["nsv","misc","movement","weight","goal_completion","monthly_recap","level_up","streak_milestone","reward_earned","streak_huddle"]},"title":{"type":"string"},"body":{"type":"object","nullable":true},"entry_date":{"type":"string"},"weight_at_time_lbs":{"type":"object","nullable":true},"ai_coaching_response":{"type":"object","nullable":true},"photos":{"type":"array","items":{"$ref":"#/components/schemas/JourneyPhotoDto"}},"created_at":{"type":"string"},"updated_at":{"type":"string"},"points_awarded":{"type":"object","nullable":true}},"required":["id","type","title","body","entry_date","weight_at_time_lbs","ai_coaching_response","photos","created_at","updated_at"]},"UpdateJourneyEntryDto":{"type":"object","properties":{"title":{"type":"string","example":"Updated title","maxLength":255},"body":{"type":"string","maxLength":500},"entry_date":{"type":"string","example":"2024-11-22"},"weight_at_time_lbs":{"type":"number"}}},"UploadedPhotoDto":{"type":"object","properties":{"photo_id":{"type":"string"},"url":{"type":"string"}},"required":["photo_id","url"]},"BulkAddPhotoResponseDto":{"type":"object","properties":{"uploaded":{"type":"array","items":{"$ref":"#/components/schemas/UploadedPhotoDto"}},"total_photos":{"type":"number"}},"required":["uploaded","total_photos"]},"PcSendMessageDto":{"type":"object","properties":{"content":{"type":"string","description":"Message text","maxLength":4000},"client_id":{"type":"string","description":"Client-generated id, for idempotent retries"},"preferred_pc_id":{"type":"number","description":"The PC the lead was shown as online (`GET /pc/live`) when they started the conversation. That PC is told first and personally; the conversation still goes to the whole pool."}},"required":["content"]},"PcClaimDto":{"type":"object","properties":{"expectedOwnerId":{"type":"number","description":"Who the caller saw holding the conversation: null for a request in the pool, a PC id for a takeover. The claim only succeeds if that is still true, so two PCs acting on the same conversation cannot silently take it from each other. Omitted by builds that predate it.","nullable":true}}},"PcReleaseDto":{"type":"object","properties":{"note":{"type":"string","description":"Optional handover note, saved as an internal note","maxLength":1000}}},"PcNoteDto":{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":1000}},"required":["body"]},"PcSettingsDto":{"type":"object","properties":{"takeoverAfterMinutes":{"type":"number","description":"Minutes a lead can wait unanswered before every PC is told about it","minimum":10,"maximum":20160}},"required":["takeoverAfterMinutes"]},"UpdateProfileDto":{"type":"object","properties":{"firstName":{"type":"string","example":"Jane"},"lastName":{"type":"string","example":"Doe"},"email":{"type":"string","example":"user@example.com"},"phone":{"type":"string","example":"+15551234567"},"city":{"type":"string","example":"Miami"},"state":{"type":"string","example":"FL"},"country":{"type":"string","example":"US","description":"ISO 3166-1 alpha-2 country code"},"postalCode":{"type":"string","example":"33101"},"unitSystem":{"type":"string","example":"metric","enum":["metric","imperial"]},"coachDataConsent":{"type":"boolean","example":true,"description":"Grant or revoke consent to share data with assigned coach (MP-1)"}}},"MacroValuesDto":{"type":"object","properties":{}},"WaterGoalDto":{"type":"object","properties":{}},"WeightGoalDto":{"type":"object","properties":{}},"UpdateMacroGoalsDto":{"type":"object","properties":{"macros":{"$ref":"#/components/schemas/MacroValuesDto"},"water":{"$ref":"#/components/schemas/WaterGoalDto"},"weight":{"$ref":"#/components/schemas/WeightGoalDto"},"reason":{"type":"string","example":"Adjusted based on new weight goal"},"updated_by_user":{"type":"boolean","example":true,"default":true}}},"UpdatePrivacyChoicesDto":{"type":"object","properties":{"aiTrainingOptOut":{"type":"boolean","example":true,"description":"Opt out of AI profiling: stops personal health data (food logs, weight, patterns) from being sent to OpenAI for personalized coaching. Tracking features still work — data is saved locally but not analyzed by AI. Required by Quebec Law 25 (profiling opt-out) and CCPA/CPRA (ADMT opt-out)."},"sensitivePiLimit":{"type":"boolean","example":true,"description":"CCPA: Limit use of sensitive personal information (disables location tracking and travel context in AI)"},"privacyAccepted":{"type":"boolean","example":true,"description":"Accept the current Privacy Policy version. Required for existing users whose privacy_version does not match the current required version. Must be true — sending false returns 400."},"termsAccepted":{"type":"boolean","example":true,"description":"Accept the current Terms & Conditions version. Required for existing users whose terms_version does not match the current required version. Must be true — sending false returns 400."},"healthDataConsentAccepted":{"type":"boolean","example":true,"description":"Re-consent to health and wellness data collection under the current PP version. Required for existing users whose health_data_consent_version is outdated. Must be true — sending false returns 400."}}},"RequestDeleteOtpDto":{"type":"object","properties":{"email":{"type":"string","example":"user@example.com"}},"required":["email"]},"DeleteAccountByOtpDto":{"type":"object","properties":{"email":{"type":"string","example":"user@example.com"},"otp":{"type":"string","example":"482910","description":"6-digit OTP sent to email"},"feedback":{"type":"string","example":"Switching to a different app"}},"required":["email","otp"]},"DeleteAccountDto":{"type":"object","properties":{"confirmDeletion":{"type":"boolean","description":"Must be true to confirm account deletion","example":true}},"required":["confirmDeletion"]},"CreateFoodLogDto":{"type":"object","properties":{"foodName":{"type":"string","example":"Chicken Breast"},"description":{"type":"string","example":"1/4 pizza"},"servingSize":{"type":"string","example":"4 oz"},"food_id":{"type":"string","example":"33691","description":"FatSecret food_id"},"serving_id":{"type":"string","example":"57972","description":"FatSecret serving_id"},"quantity":{"type":"number","example":3,"description":"Quantity in the chosen unit"},"unit":{"type":"string","example":"oz","description":"Unit label the user chose"},"entry_mode":{"type":"string","enum":["serving","advanced"],"example":"serving","description":"Entry mode used in the log sheet: \"serving\" = native FatSecret serving picker, \"advanced\" = manual/advanced editor."},"calories":{"type":"number","example":180},"protein":{"type":"number","example":35},"carbs":{"type":"number","example":0},"fat":{"type":"number","example":3.5},"fiber":{"type":"number","example":0},"sugar":{"type":"number","example":0},"sodium":{"type":"number","example":75},"meal_number":{"type":"number","example":1},"logDate":{"type":"string","example":"2024-01-15"},"logTime":{"type":"string","example":"07:30:00"},"nutrition":{"description":"Detailed nutrition breakdown","example":[{"name":"Sugar","value":4,"unit":"g"},{"name":"Sodium","value":170,"unit":"mg"}],"type":"array","items":{"type":"string"}},"my_food_id":{"type":"number","example":42,"description":"Reference to a saved \"My Foods\" entry. When set, missing fields are hydrated from the saved food and last_logged_at is bumped on the source row."},"note":{"type":"string","description":"Optional meal note to attach to this food log entry at create time. Premium-gated (enrolled or paid subscription). Ignored on PATCH.","example":"Ate out, estimated portions."},"manual_macros":{"type":"boolean","description":"Opt-out flag. When true, the server will NOT recompute macros from FatSecret and will trust client-supplied calories/protein/carbs/fat/fiber as-is. Use this for user-overridden portions where the client has authoritative numbers (e.g. label-scan, photo analysis with explicit nutrition_facts override). Default false.","example":false},"image_refs":{"description":"Existing S3 keys to attach to this log (from a previously logged food). Used when logging from the recently-logged list where an image ref already exists. In FormData requests, send as a JSON-encoded string: '[\"key1\",\"key2\"]'. Max 3.","example":["food-log-images/42/1234567890.jpg"],"type":"array","items":{"type":"string"}}},"required":["foodName"]},"PreviewMacrosDto":{"type":"object","properties":{"food_id":{"type":"string","example":"12345","description":"FatSecret food id"},"quantity":{"type":"number","example":70},"unit":{"type":"string","example":"g","description":"Unit label (g, oz, each, large, …)"},"serving_id":{"type":"string","example":"57969","description":"FatSecret serving_id the user picked from the food's servings array. When provided, the recompute uses THAT serving's per-serving nutrition and metric reference instead of the FS default. Required for foods whose servings are not linear scalings of each other (raw vs cooked, with-skin vs without, piece vs volume) — without it, preview anchors on the wrong baseline. When the id does not match any serving the food currently exposes, falls back to the FS default and logs a warning."},"region":{"type":"string","example":"US","description":"FatSecret region override. Currently advisory only — getFoodDetails is region-agnostic, so the value is echoed back in the response but does not affect the FS lookup. Matches createFoodLog/editFoodLog behavior. Reserved for future region-scoped lookups."}},"required":["food_id","quantity","unit"]},"FoodSearchDto":{"type":"object","properties":{"query":{"type":"string","example":"6 eggs and 2 slices of ham"},"timezone":{"type":"string","example":"America/New_York"}},"required":["query"]},"BarcodeLookupDto":{"type":"object","properties":{"barcode":{"type":"string","example":"818290014894","description":"UPC or EAN barcode string"}},"required":["barcode"]},"NutritionEntryDto":{"type":"object","properties":{}},"FavoriteMealItemDto":{"type":"object","properties":{"food_name":{"type":"string","example":"Chobani 0% Plain Greek Yogurt"},"serving_size":{"type":"string","example":"1 cup"},"description":{"type":"string","example":"non-fat, plain"},"calories":{"type":"number","example":130},"protein":{"type":"number","example":23},"carbs":{"type":"number","example":9},"fat":{"type":"number","example":0},"fiber":{"type":"number","example":0},"sugar":{"type":"number","example":6},"sodium":{"type":"number","example":75},"nutrition":{"description":"Detailed nutrition breakdown (matches food_logs.nutrition)","type":"array","items":{"$ref":"#/components/schemas/NutritionEntryDto"}},"fatsecret_food_id":{"type":"string","example":"12345","description":"FatSecret food ID if the item came from a search; kept only inside the favorite for future re-enrichment."},"serving_id":{"type":"string","example":"67890","description":"FatSecret serving ID for the chosen serving on this item."},"brand":{"type":"string","example":"Optimum Nutrition"},"barcode":{"type":"string","example":"818290014894"},"notes":{"type":"string","example":"Mixed with cold water, two scoops"},"serving_qty":{"type":"number","example":30},"serving_unit":{"type":"string","example":"g"}},"required":["food_name"]},"CreateFavoriteMealDto":{"type":"object","properties":{"name":{"type":"string","example":"Morning Yogurt Bowl"},"favorite_type":{"type":"string","enum":["bundle","single_food"],"default":"bundle","description":"Set 'single_food' to save a one-tap reusable item (items[] must have exactly one entry). Omit or 'bundle' for a named multi-item meal."},"meal_number":{"type":"number","example":1,"description":"1=breakfast, 2=lunch, 3=dinner, 4-6=snack"},"meal_type":{"type":"string","enum":["breakfast","lunch","dinner","snack"],"example":"breakfast"},"items":{"type":"array","items":{"$ref":"#/components/schemas/FavoriteMealItemDto"}}},"required":["name","items"]},"UpdateFavoriteMealDto":{"type":"object","properties":{"name":{"type":"string","example":"Morning Yogurt Bowl"},"favorite_type":{"type":"string","enum":["bundle","single_food"],"default":"bundle","description":"Set 'single_food' to save a one-tap reusable item (items[] must have exactly one entry). Omit or 'bundle' for a named multi-item meal."},"meal_number":{"type":"number","example":1,"description":"1=breakfast, 2=lunch, 3=dinner, 4-6=snack"},"meal_type":{"type":"string","enum":["breakfast","lunch","dinner","snack"],"example":"breakfast"},"items":{"type":"array","items":{"$ref":"#/components/schemas/FavoriteMealItemDto"}}}},"LogFavoriteMealDto":{"type":"object","properties":{"log_date":{"type":"string","example":"2026-04-21","description":"Local date YYYY-MM-DD"},"log_time":{"type":"string","example":"07:30:00","description":"Local time HH:MM:SS"},"timezone":{"type":"string","example":"America/New_York"},"meal_number":{"type":"number","example":1,"description":"Override the favorite's default meal slot"}}},"CreateScheduledMealDto":{"type":"object","properties":{"foodName":{"type":"string","example":"Grilled Chicken with Rice"},"servingSize":{"type":"string","example":"full portion"},"calories":{"type":"number","example":350},"protein":{"type":"number","example":35},"carbs":{"type":"number","example":40},"fat":{"type":"number","example":8},"fiber":{"type":"number","example":5},"meal_number":{"type":"number","example":1,"description":"Meal slot number (1=breakfast, 2=lunch, 3=dinner, etc.)"},"plannedDate":{"type":"string","example":"2024-01-20"},"timezone":{"type":"string","example":"America/New_York"},"nutrition":{"description":"Detailed nutrition breakdown","example":[{"name":"Sugar","value":4,"unit":"g"},{"name":"Sodium","value":170,"unit":"mg"}],"type":"array","items":{"type":"string"}}},"required":["foodName","plannedDate"]},"ConsumeScheduledMealDto":{"type":"object","properties":{"portionAmount":{"type":"number","example":1,"description":"Portion multiplier (informational only, macros should be pre-calculated by UI)"},"portionUnit":{"type":"string","example":"serving"},"log_date":{"type":"string","example":"2024-03-17","description":"Log date in YYYY-MM-DD format"},"log_time":{"type":"string","example":"12:30:00","description":"Log time in HH:MM:SS format"},"calories":{"type":"number","example":350},"protein":{"type":"number","example":25.5},"carbs":{"type":"number","example":30},"fat":{"type":"number","example":12},"fiber":{"type":"number","example":4},"sugar":{"type":"number","example":5},"sodium":{"type":"number","example":480},"meal_number":{"type":"number","example":1,"description":"Meal slot number (1=breakfast, 2=lunch, 3=dinner, etc.)"},"nutrition":{"description":"Detailed nutrition breakdown","example":[{"name":"Sugar","value":4,"unit":"g"},{"name":"Sodium","value":170,"unit":"mg"}],"type":"array","items":{"type":"string"}}}},"MealPlanItemInputDto":{"type":"object","properties":{"source":{"type":"string","enum":["recipe","food"]},"name":{"type":"string","example":"Lemon Chicken Traybake"},"servingSize":{"type":"string","example":"1 serving"},"servings":{"type":"number","example":4,"description":"How many servings to plan. Defaults to the recipe yield, or 1 for a food."},"recipeYield":{"type":"number","example":2,"description":"Recipes: how many servings the ingredient list makes."},"ingredients":{"description":"Recipes: ingredient lines for recipeYield servings.","type":"array","items":{"type":"string"}},"calories":{"type":"number","example":420},"protein":{"type":"number","example":35},"carbs":{"type":"number","example":30},"fat":{"type":"number","example":12},"fiber":{"type":"number","example":5},"nutrition":{"type":"array","items":{"$ref":"#/components/schemas/NutritionEntryDto"}},"foodRef":{"type":"object","description":"Foods: search-result identity (foodId, servingId, quantity, unit) for reopening the log flow."}},"required":["source","name"]},"CreateMealPlanDto":{"type":"object","properties":{"name":{"type":"string","example":"This week"},"items":{"description":"Optional first items, so \"new plan\" and \"add\" are one call.","type":"array","items":{"$ref":"#/components/schemas/MealPlanItemInputDto"}}},"required":["name"]},"RenameMealPlanDto":{"type":"object","properties":{"name":{"type":"string","example":"Next week"}},"required":["name"]},"AddMealPlanItemsDto":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MealPlanItemInputDto"}}},"required":["items"]},"UpdateMealPlanItemDto":{"type":"object","properties":{"servings":{"type":"number","example":3}},"required":["servings"]},"RenameGroceryListDto":{"type":"object","properties":{"name":{"type":"string","example":"Weekend shop"}},"required":["name"]},"AddGroceryItemDto":{"type":"object","properties":{"name":{"type":"string","example":"Kitchen roll"},"amount":{"type":"object","example":2,"nullable":true},"unit":{"type":"string","enum":["g","kg","ml","l","count","tsp","tbsp","pack","can","jar","bottle","bunch","clove","slice","oz","lb","fl_oz","cup"],"nullable":true},"section":{"type":"string","enum":["produce","meat_seafood","dairy_eggs","bakery","pantry","spices","frozen","beverages","other"]}},"required":["name"]},"UpdateGroceryItemDto":{"type":"object","properties":{"name":{"type":"string","example":"Bananas"},"amount":{"type":"object","example":5,"nullable":true},"unit":{"type":"string","enum":["g","kg","ml","l","count","tsp","tbsp","pack","can","jar","bottle","bunch","clove","slice","oz","lb","fl_oz","cup"],"nullable":true},"isChecked":{"type":"boolean","example":true},"mealPlanItemId":{"type":"number","example":12,"description":"With isChecked: tick or untick only this meal plan item's part of the line"}}},"ShareGroceryListDto":{"type":"object","properties":{"roomId":{"type":"number","example":42,"description":"Group chat room to post the list into"}},"required":["roomId"]},"SaveRecipeDto":{"type":"object","properties":{"title":{"type":"string","example":"Protein-Packed Chicken Bowl"},"description":{"type":"string","example":"A healthy post-surgery meal"},"mealType":{"type":"string","example":"dinner"},"calories":{"type":"number","example":350},"protein":{"type":"number","example":35},"carbs":{"type":"number","example":40},"fat":{"type":"number","example":12},"fiber":{"type":"number","example":5},"ingredients":{"type":"object","example":["4 oz chicken breast","1 cup brown rice"]},"instructions":{"type":"object","example":["Cook chicken for 10 min","Prepare rice"]},"journeyStage":{"type":"string","example":"early-post-op"},"customizationNotes":{"type":"string"},"userTimezone":{"type":"string","example":"Asia/Calcutta"},"localTimestamp":{"type":"string","example":"2026-02-18T11:23:34.331Z"},"servings":{"type":"number","example":2},"prepTime":{"type":"string","example":"15 min"},"cookTime":{"type":"string","example":"30 min"},"source":{"type":"string","example":"ai-generated"},"id":{"type":"object","description":"Recipe ID (ignored on create, used for re-saves)"},"userId":{"type":"number","description":"User ID (ignored, extracted from JWT)"},"macros":{"type":"object","description":"Nested macros object from AI-generated recipes"},"tags":{"type":"object","description":"Recipe tags"},"createdAt":{"type":"string","description":"Creation timestamp from client"},"original_recipe_id":{"type":"object","description":"Original recipe ID if customized"},"creator_name":{"type":"string","description":"Creator name for community recipes"},"image":{"type":"string"},"meal_type":{"type":"string"},"prep_time":{"type":"string"},"surgeryStage":{"type":"string"},"dietaryRestrictions":{"type":"array","items":{"type":"string"}},"isCommunity":{"type":"boolean","example":true,"description":"Share recipe in the community feed. Defaults to true."},"ai_note":{"type":"string","description":"Personalized AI coaching note (not shown in community feed)"},"nutrition":{"description":"Full nutrition breakdown. Merged into macros so all entries are preserved.","example":[{"name":"Sugar","value":1,"unit":"g"},{"name":"Sodium","value":150,"unit":"mg"}],"type":"array","items":{"type":"string"}}},"required":["title"]},"UpdateRecipeDto":{"type":"object","properties":{"title":{"type":"string","example":"Protein-Packed Chicken Bowl"},"description":{"type":"string","example":"A healthy post-surgery meal"},"mealType":{"type":"string","example":"dinner"},"calories":{"type":"number","example":350},"protein":{"type":"number","example":35},"carbs":{"type":"number","example":40},"fat":{"type":"number","example":12},"fiber":{"type":"number","example":5},"ingredients":{"type":"object","example":["4 oz chicken breast","1 cup brown rice"]},"instructions":{"type":"object","example":["Cook chicken for 10 min","Prepare rice"]},"journeyStage":{"type":"string","example":"early-post-op"},"servings":{"type":"number","example":2},"prepTime":{"type":"string","example":"15 min"},"cookTime":{"type":"string","example":"30 min"},"isCommunity":{"type":"boolean","example":true},"ai_note":{"type":"string","description":"Personalized AI coaching note (not shown in community feed)"},"nutrition":{"description":"Full nutrition breakdown. Merged into macros so all entries are preserved.","example":[{"name":"Sugar","value":1,"unit":"g"},{"name":"Sodium","value":150,"unit":"mg"}],"type":"array","items":{"type":"string"}}}},"GenerateRecipeDto":{"type":"object","properties":{"description":{"type":"string","example":"High protein breakfast with eggs"}},"required":["description"]},"OriginalRecipe":{"type":"object","properties":{}},"CustomizeRecipeDto":{"type":"object","properties":{"originalRecipe":{"$ref":"#/components/schemas/OriginalRecipe"},"customizationRequest":{"type":"string","example":"Make it keto-friendly"}},"required":["originalRecipe","customizationRequest"]},"FormatRecipeDto":{"type":"object","properties":{"recipeText":{"type":"string","example":"Take 2 chicken breasts, season with salt..."},"title":{"type":"string","example":"Easy Chicken Stir Fry"},"description":{"type":"string","example":"A quick stir fry with chicken and veggies"},"mealType":{"type":"string","example":"dinner"},"servings":{"type":"number","example":2}}},"RateRecipeDto":{"type":"object","properties":{"recipeId":{"type":"number","example":42},"rating":{"type":"number","example":5},"feedback":{"type":"string","example":"Loved the simplicity"}},"required":["recipeId","rating"]},"GenerateMealIdeasDto":{"type":"object","properties":{"userInput":{"type":"string","example":"I want something high-protein for dinner"}},"required":["userInput"]},"LucilleActionDto":{"type":"object","properties":{"source":{"type":"string","example":"lucille","description":"Set to \"lucille\" when the call originates in the Lucille chat. The result is then persisted as a chat turn and returned as a `card`."},"sessionId":{"type":"string","description":"Current Lucille chat session id, if the client tracks one."},"clientTurnId":{"type":"string","description":"Client-generated id for the optimistic chat bubble. Echoed back inside `card` so the app can reconcile it."},"userMessage":{"type":"string","description":"What the user typed alongside the photo."}}},"UpsertRatingDto":{"type":"object","properties":{"rating":{"type":"number","description":"Rating value between 1 and 5","minimum":1,"maximum":5}},"required":["rating"]},"AddCommentDto":{"type":"object","properties":{"body":{"type":"string","description":"Comment text"}},"required":["body"]},"SaveWeightDto":{"type":"object","properties":{"weight":{"type":"number","example":185.5},"date":{"type":"string","example":"2025-01-15"}},"required":["weight"]},"UpdateWeightDto":{"type":"object","properties":{"weight":{"type":"number","example":186},"date":{"type":"string","example":"2025-01-15"}}},"SaveMeasurementsDto":{"type":"object","properties":{"date":{"type":"string","example":"2025-01-15"},"neck":{"type":"number","example":14.5},"arm":{"type":"number","example":12},"chest":{"type":"number","example":38},"waist":{"type":"number","example":32},"abdomen":{"type":"number","example":34},"hips":{"type":"number","example":38},"thigh":{"type":"number","example":22},"calf":{"type":"number","example":15}}},"LocationDto":{"type":"object","properties":{}},"CreateVibeLogDto":{"type":"object","properties":{"vibe":{"type":"string","example":"motivated","enum":["happy","motivated","neutral","sad","stressed","tired","excited","confident","grateful"]},"logDate":{"type":"string","example":"2025-01-15"},"emoji":{"type":"string","example":"😊"},"note":{"type":"string","example":"Feeling great today"},"share_with_coach":{"type":"boolean","example":true},"location":{"$ref":"#/components/schemas/LocationDto"},"locationLat":{"type":"number","example":37.7749},"locationLng":{"type":"number","example":-122.4194},"locationName":{"type":"string","example":"San Francisco, CA"}},"required":["vibe"]},"CreatePromotionDto":{"type":"object","properties":{"title":{"type":"string","example":"Spring sale banner"},"forward_url":{"type":"string","example":"https://rewritelifestyle.com/spring-sale"},"start_at":{"type":"string","example":"2026-05-01T00:00:00.000Z"},"end_at":{"type":"string","example":"2026-05-15T23:59:59.000Z"},"priority":{"type":"number","default":0},"is_active":{"type":"boolean","default":true},"countries":{"example":["US","IN"],"type":"array","items":{"type":"string"}},"roles":{"example":["member"],"type":"array","items":{"type":"string"}},"subscription_statuses":{"example":["active","trialing"],"type":"array","items":{"type":"string"}},"group_ids":{"example":[12,34],"type":"array","items":{"type":"string"}}},"required":["forward_url","start_at","end_at"]},"UpdatePromotionDto":{"type":"object","properties":{"title":{"type":"string","example":"Spring sale banner"},"forward_url":{"type":"string","example":"https://rewritelifestyle.com/spring-sale"},"start_at":{"type":"string","example":"2026-05-01T00:00:00.000Z"},"end_at":{"type":"string","example":"2026-05-15T23:59:59.000Z"},"priority":{"type":"number","default":0},"is_active":{"type":"boolean","default":true},"countries":{"example":["US","IN"],"type":"array","items":{"type":"string"}},"roles":{"example":["member"],"type":"array","items":{"type":"string"}},"subscription_statuses":{"example":["active","trialing"],"type":"array","items":{"type":"string"}},"group_ids":{"example":[12,34],"type":"array","items":{"type":"string"}}}},"RecordCampaignEventDto":{"type":"object","properties":{"type":{"type":"string","enum":["shown","dismissed","clicked"],"example":"shown"},"variant_id":{"type":"number","example":2,"description":"Variant the user actually saw"},"metadata":{"type":"object","example":{"screen":"home","app_version":"3.4.1"}}},"required":["type"]},"CampaignClaimAddressDto":{"type":"object","properties":{"recipient_name":{"type":"string","example":"Jordan Rivera"},"address_line1":{"type":"string","example":"221B Baker Street"},"address_line2":{"type":"string","example":"Apt 4"},"city":{"type":"string","example":"Austin"},"state":{"type":"string","example":"TX"},"postal_code":{"type":"string","example":"78701"},"country":{"type":"string","example":"US","description":"ISO-3166-1 alpha-2"},"phone":{"type":"string","example":"+15125550143"}}},"ClaimCampaignDto":{"type":"object","properties":{"address":{"description":"Required when the campaign has `requires_address`, rejected otherwise.","allOf":[{"$ref":"#/components/schemas/CampaignClaimAddressDto"}]}}},"UpdateCampaignClaimDto":{"type":"object","properties":{"status":{"type":"string","enum":["pending","approved","fulfilled","rejected","revoked"]},"fulfilment_method":{"type":"string","example":"drop_ship","description":"e.g. drop_ship, coupon"},"fulfilment_reference":{"type":"string","example":"WITHINGS-8H3K-2211"},"admin_notes":{"type":"string","example":"Shipped 12 Sep, refund window cleared"}}},"CreateCampaignDto":{"type":"object","properties":{"slug":{"type":"string","example":"smart-scale-giveaway"},"name":{"type":"string","example":"Smart Scale Giveaway"},"template":{"type":"string","enum":["offer_sheet_v1"],"default":"offer_sheet_v1"},"placement":{"type":"string","enum":["home_bottom_sheet","paywall","subscription_tab"],"default":"home_bottom_sheet"},"start_at":{"type":"string","example":"2026-09-01T00:00:00.000Z"},"end_at":{"type":"string","example":"2026-09-14T23:59:59.000Z"},"is_active":{"type":"boolean","default":true},"priority":{"type":"number","default":0,"description":"Higher wins when several campaigns match"},"dismissible":{"type":"boolean","default":true},"cooldown_hours":{"type":"object","example":48,"description":"Hours to wait after a `shown` event before showing again. Null = no cooldown."},"max_impressions":{"type":"object","example":3,"description":"Total `shown` events per user before the campaign stops appearing."},"claimable":{"type":"boolean","default":false,"description":"Whether POST /campaigns/:id/claim is accepted"},"max_claims":{"type":"object","example":500,"description":"Global stock cap. Null = unlimited."},"suppress_after_claim":{"type":"boolean","default":true},"requires_address":{"type":"boolean","default":false,"description":"Whether claiming requires a shipping address. Only for campaigns that ship a physical item — the address is stored separately and deleted after fulfilment."},"eligibility_rule":{"type":"string","enum":["none","subscription_transition"],"description":"Extra gate on claiming, beyond variant audience targeting. Omit or use `none` for no gate."},"eligibility_rule_params":{"type":"object","additionalProperties":true,"description":"Parameters for `eligibility_rule`. For `subscription_transition`: { transition_types: string[] (from GET /campaigns/eligibility-transition-types), since: \"campaign_start\" | \"last_n_days\", window_days?: number, extension_reasons?: string[] }"},"countries":{"example":["US"],"description":"ISO-3166-1 alpha-2. Empty = anywhere.","type":"array","items":{"type":"string"}},"allowed_user_ids":{"example":[90,91],"description":"Restrict the campaign to these user ids, on top of every other filter — a listed user still has to pass `countries` and still has to match a variant audience. Omit or send an empty array for no restriction. For internal testing and one-off cohorts; ids differ between environments.","type":"array","items":{"type":"string"}},"excluded_states":{"example":["QC"],"description":"Excluded subdivisions, matched against profile.state — which nothing currently populates, so this filter is inert today. Restrict via `countries` instead.","type":"array","items":{"type":"string"}}},"required":["slug","name","start_at","end_at"]},"CampaignExtraMediaDto":{"type":"object","properties":{"role":{"type":"string","example":"background"},"s3_key":{"type":"string","example":"campaigns/1/1757000000000-123.png"},"alt":{"type":"object","example":"Withings Body Smart scale"}},"required":["role","s3_key"]},"CreateCampaignVariantDto":{"type":"object","properties":{"name":{"type":"string","example":"Annual Stripe — extend","description":"Internal label, never shown"},"priority":{"type":"number","default":0,"description":"Highest matching variant wins"},"is_active":{"type":"boolean","default":true},"audience_all":{"example":["subscribed","interval:year"],"type":"array","items":{"type":"string"}},"audience_any":{"example":["free","no_subscription"],"type":"array","items":{"type":"string"}},"audience_none":{"example":["claimed:smart-scale-giveaway"],"type":"array","items":{"type":"string"}},"pill_text":{"type":"object","example":"LIMITED TIME"},"headline":{"type":"string","example":"Get a free smart scale, on us."},"body":{"type":"object"},"primary_cta_label":{"type":"object","example":"Extend and claim"},"primary_cta_action_type":{"type":"string","enum":["deeplink","url","in_app_route","checkout","dismiss","none"]},"primary_cta_action_value":{"type":"object","example":"rewrite://subscription/extend"},"secondary_cta_label":{"type":"object","example":"Maybe later"},"secondary_cta_action_type":{"type":"string","enum":["deeplink","url","in_app_route","checkout","dismiss","none"]},"secondary_cta_action_value":{"type":"object"},"footnote":{"type":"object","example":"One scale per person, US only."},"terms_label":{"type":"object","example":"T&Cs apply."},"terms_url":{"type":"object","example":"https://rewritelifestyle.com/offers/smart-scale"},"extra_media":{"description":"Extra images beyond hero/badge, as JSON. Keys must already be uploaded.","type":"array","items":{"$ref":"#/components/schemas/CampaignExtraMediaDto"}},"extra":{"type":"object","description":"Template-specific passthrough, as JSON. Sent to the client verbatim."}},"required":["name","headline"]},"UpdateCampaignVariantDto":{"type":"object","properties":{"name":{"type":"string","example":"Annual Stripe — extend","description":"Internal label, never shown"},"priority":{"type":"number","default":0,"description":"Highest matching variant wins"},"is_active":{"type":"boolean","default":true},"audience_all":{"example":["subscribed","interval:year"],"type":"array","items":{"type":"string"}},"audience_any":{"example":["free","no_subscription"],"type":"array","items":{"type":"string"}},"audience_none":{"example":["claimed:smart-scale-giveaway"],"type":"array","items":{"type":"string"}},"pill_text":{"type":"object","example":"LIMITED TIME"},"headline":{"type":"string","example":"Get a free smart scale, on us."},"body":{"type":"object"},"primary_cta_label":{"type":"object","example":"Extend and claim"},"primary_cta_action_type":{"type":"string","enum":["deeplink","url","in_app_route","checkout","dismiss","none"]},"primary_cta_action_value":{"type":"object","example":"rewrite://subscription/extend"},"secondary_cta_label":{"type":"object","example":"Maybe later"},"secondary_cta_action_type":{"type":"string","enum":["deeplink","url","in_app_route","checkout","dismiss","none"]},"secondary_cta_action_value":{"type":"object"},"footnote":{"type":"object","example":"One scale per person, US only."},"terms_label":{"type":"object","example":"T&Cs apply."},"terms_url":{"type":"object","example":"https://rewritelifestyle.com/offers/smart-scale"},"extra_media":{"description":"Extra images beyond hero/badge, as JSON. Keys must already be uploaded.","type":"array","items":{"$ref":"#/components/schemas/CampaignExtraMediaDto"}},"extra":{"type":"object","description":"Template-specific passthrough, as JSON. Sent to the client verbatim."},"remove_hero":{"type":"boolean","description":"Delete the current hero image"},"remove_badge":{"type":"boolean","description":"Delete the current badge image"}}},"PreviewCampaignDto":{"type":"object","properties":{"user_id":{"type":"number","example":1234,"description":"User to evaluate targeting against"},"country":{"type":"string","example":"US","description":"Simulated device country"}},"required":["user_id"]},"UpdateCampaignDto":{"type":"object","properties":{"name":{"type":"string","example":"Smart Scale Giveaway"},"template":{"type":"string","enum":["offer_sheet_v1"],"default":"offer_sheet_v1"},"placement":{"type":"string","enum":["home_bottom_sheet","paywall","subscription_tab"],"default":"home_bottom_sheet"},"start_at":{"type":"string","example":"2026-09-01T00:00:00.000Z"},"end_at":{"type":"string","example":"2026-09-14T23:59:59.000Z"},"is_active":{"type":"boolean","default":true},"priority":{"type":"number","default":0,"description":"Higher wins when several campaigns match"},"dismissible":{"type":"boolean","default":true},"cooldown_hours":{"type":"object","example":48,"description":"Hours to wait after a `shown` event before showing again. Null = no cooldown."},"max_impressions":{"type":"object","example":3,"description":"Total `shown` events per user before the campaign stops appearing."},"claimable":{"type":"boolean","default":false,"description":"Whether POST /campaigns/:id/claim is accepted"},"max_claims":{"type":"object","example":500,"description":"Global stock cap. Null = unlimited."},"suppress_after_claim":{"type":"boolean","default":true},"requires_address":{"type":"boolean","default":false,"description":"Whether claiming requires a shipping address. Only for campaigns that ship a physical item — the address is stored separately and deleted after fulfilment."},"eligibility_rule":{"type":"string","enum":["none","subscription_transition"],"description":"Extra gate on claiming, beyond variant audience targeting. Omit or use `none` for no gate."},"eligibility_rule_params":{"type":"object","additionalProperties":true,"description":"Parameters for `eligibility_rule`. For `subscription_transition`: { transition_types: string[] (from GET /campaigns/eligibility-transition-types), since: \"campaign_start\" | \"last_n_days\", window_days?: number, extension_reasons?: string[] }"},"countries":{"example":["US"],"description":"ISO-3166-1 alpha-2. Empty = anywhere.","type":"array","items":{"type":"string"}},"allowed_user_ids":{"example":[90,91],"description":"Restrict the campaign to these user ids, on top of every other filter — a listed user still has to pass `countries` and still has to match a variant audience. Omit or send an empty array for no restriction. For internal testing and one-off cohorts; ids differ between environments.","type":"array","items":{"type":"string"}},"excluded_states":{"example":["QC"],"description":"Excluded subdivisions, matched against profile.state — which nothing currently populates, so this filter is inert today. Restrict via `countries` instead.","type":"array","items":{"type":"string"}}}},"LogWaterDto":{"type":"object","properties":{"amountOz":{"type":"number","example":16},"logDate":{"type":"string","example":"2025-01-15"}},"required":["amountOz"]},"LogTrendPointDto":{"type":"object","properties":{"date":{"type":"string","example":"2026-10-24"},"value":{"type":"number","description":"Daily total: oz, minutes, or vitamins taken.","example":56}},"required":["date","value"]},"LogTrendDto":{"type":"object","properties":{"metric":{"type":"string","enum":["water","movement","vitamins"]},"window":{"type":"string","enum":["month","6months","year"]},"unit":{"type":"string","description":"water→oz, movement→minutes, vitamins→count.","example":"oz"},"points":{"description":"One point per logged day, oldest first. Days with nothing logged are omitted.","type":"array","items":{"$ref":"#/components/schemas/LogTrendPointDto"}},"starting_value":{"type":"object","nullable":true,"description":"First logged day in the window."},"current_value":{"type":"object","nullable":true,"description":"Latest logged day in the window."},"goal":{"type":"object","nullable":true,"description":"Daily goal. water = the user's water_goal_oz; null otherwise."},"change":{"type":"object","nullable":true,"description":"current_value − starting_value."},"average":{"type":"object","nullable":true,"description":"Average across logged days."},"days_logged":{"type":"number","description":"Days in the window with at least one entry."},"goal_progress_pct":{"type":"object","nullable":true,"description":"Percent of logged days at or above goal. Null without a goal."}},"required":["metric","window","unit","points","starting_value","current_value","goal","change","average","days_logged","goal_progress_pct"]},"LogMovementDto":{"type":"object","properties":{"durationMinutes":{"type":"number","example":30},"logDate":{"type":"string","example":"2026-02-24"}},"required":["durationMinutes"]},"SupplementEntry":{"type":"object","properties":{"id":{"type":"number","example":1},"name":{"type":"string","example":"Vitamin D"},"recommendedDose":{"type":"string","example":"1000 IU"},"taken":{"type":"boolean","example":true}},"required":["name","taken"]},"LogVitaminsDto":{"type":"object","properties":{"supplements":{"type":"array","items":{"$ref":"#/components/schemas/SupplementEntry"}},"logDate":{"type":"string","example":"2025-01-15"}},"required":["supplements"]},"ToggleVitaminLogDto":{"type":"object","properties":{"taken":{"type":"boolean","example":true},"logDate":{"type":"string","example":"2026-02-19"}},"required":["taken"]},"CreateSupplementDto":{"type":"object","properties":{"name":{"type":"string","example":"Vitamin D"},"recommendedDose":{"type":"string","example":"1000 IU"},"type":{"type":"string","example":"preset","enum":["preset","custom"]}},"required":["name"]},"BowelMovementFlagsDto":{"type":"object","properties":{"blood":{"type":"boolean","description":"Visible blood in stool"},"pain":{"type":"boolean","description":"Pain during or around the BM"},"urgency":{"type":"boolean","description":"Strong urgency / rush"},"incomplete":{"type":"boolean","description":"Felt incomplete"}}},"CreateBowelMovementDto":{"type":"object","properties":{"logged_at":{"type":"string","example":"2026-04-30T08:15:00.000Z","description":"ISO timestamp of the BM. Defaults to now. Accepts past timestamps for backfill (REW-410 pattern)."},"bristol_type":{"type":"number","example":4,"description":"Bristol Stool Scale 1-7. Optional — frequency-only logging is allowed."},"notes":{"type":"string","example":"Slight cramping after lunch."},"flags":{"description":"Optional structured flags","allOf":[{"$ref":"#/components/schemas/BowelMovementFlagsDto"}]}}},"UpdateBowelMovementDto":{"type":"object","properties":{"logged_at":{"type":"string","example":"2026-04-30T08:15:00.000Z","description":"ISO timestamp of the BM. Defaults to now. Accepts past timestamps for backfill (REW-410 pattern)."},"bristol_type":{"type":"number","example":4,"description":"Bristol Stool Scale 1-7. Optional — frequency-only logging is allowed."},"notes":{"type":"string","example":"Slight cramping after lunch."},"flags":{"description":"Optional structured flags","allOf":[{"$ref":"#/components/schemas/BowelMovementFlagsDto"}]}}},"CreateDigestiveSymptomDto":{"type":"object","properties":{"logged_at":{"type":"string","example":"2026-07-01T08:15:00.000Z","description":"ISO timestamp. Defaults to now."},"symptoms":{"type":"array","example":["bloating","nausea"],"description":"One or more symptoms from the allowed list.","items":{"type":"string","enum":["dumping","gas","bloating","nausea","cramping","vomiting","reflux","food_stuck"]}},"notes":{"type":"string","example":"Happened within 30 mins of eating."}},"required":["symptoms"]},"UpdateDigestiveSymptomDto":{"type":"object","properties":{"logged_at":{"type":"string","example":"2026-07-01T08:15:00.000Z","description":"ISO timestamp. Defaults to now."},"symptoms":{"type":"array","example":["bloating","nausea"],"description":"One or more symptoms from the allowed list.","items":{"type":"string","enum":["dumping","gas","bloating","nausea","cramping","vomiting","reflux","food_stuck"]}},"notes":{"type":"string","example":"Happened within 30 mins of eating."}}},"AiRespondDto":{"type":"object","properties":{"message":{"type":"string","example":"How much protein should I eat today?"},"sessionId":{"type":"string","example":"session-abc-123"},"session_id":{"type":"string","example":"session-abc-123","description":"Snake-case alias for sessionId"},"timezone":{"type":"string","example":"America/New_York"},"capabilities":{"example":["recipe_set"],"description":"Chat cards this client can render beyond the original set. Older builds send nothing and get today's behaviour. Known: \"recipe_set\" (up to three recipes in one card).","type":"array","items":{"type":"string"}}},"required":["message"]},"ReactToNoteDto":{"type":"object","properties":{"noteId":{"type":"number","example":42},"reaction":{"type":"string","example":"motivated","enum":["happy","motivated","neutral","sad","angry"]}},"required":["noteId","reaction"]},"SaveAnswerDto":{"type":"object","properties":{"questionId":{"type":"number","example":5,"description":"ID of the question being answered"},"selectedOptionId":{"type":"object","example":20,"description":"Single selected option ID (for single_choice questions)"},"selectedOptionIds":{"type":"object","example":[10,12,15],"description":"Array of selected option IDs (for multi_choice questions)"},"customAnswer":{"type":"object","example":"220","description":"Custom text answer (for free_text, numeric, date, or \"Other\" specify)"},"customAnswerList":{"type":"object","example":["Family dinners","Late-night snacking","Stress eating"],"description":"Array answer (for repeatable_list questions)"}},"required":["questionId"]},"CompleteQuestionnaireDto":{"type":"object","properties":{"surveyKey":{"type":"string","example":"healthy_2_0","description":"Survey key being completed — defaults to \"intake\" for backward compatibility with existing clients that never sent this."}}},"ReleaseSurveyDto":{"type":"object","properties":{"releaseOn":{"type":"string","example":"2026-10-20","description":"Release on this date, at 08:00 in your timezone (x-timezone). Omit to release now. Today or later."}}},"MarkAnnouncementsReadDto":{"type":"object","properties":{"ids":{"example":[1,2,3],"type":"array","items":{"type":"string"}}},"required":["ids"]},"CreateAnnouncementDto":{"type":"object","properties":{"title":{"type":"string","example":"Week 6 call moves to Thursday"},"body":{"type":"string","example":"This week only, the group call is on Thursday at 6pm."},"audience":{"type":"string","enum":["all_clients","groups"],"description":"all_clients is for admins only."},"groupIds":{"example":[12,34],"description":"Required when audience is groups: active coaching groups.","type":"array","items":{"type":"string"}},"publishAt":{"type":"string","example":"2026-10-20T07:30:00.000Z","description":"Publish at this instant. Omit, or send \"now\" or an empty value, to publish now."},"expiresOn":{"type":"string","example":"2026-10-31","description":"Last day the post shows, in your timezone (x-timezone). Omit for no end date."},"pinned":{"type":"boolean","default":false}},"required":["title","body","audience"]},"UpdateAnnouncementDto":{"type":"object","properties":{"title":{"type":"string"},"body":{"type":"string"},"audience":{"type":"string","enum":["all_clients","groups"],"description":"Before publish only."},"groupIds":{"description":"Before publish only.","type":"array","items":{"type":"string"}},"publishAt":{"type":"string","description":"Before publish only. \"now\" publishes straight away."},"expiresOn":{"type":"string","description":"YYYY-MM-DD, or an empty string for no end date."},"pinned":{"type":"boolean"},"keepImageIds":{"example":[5,3],"description":"The existing images to keep, in order; new files follow them. Omit to leave the images as they are. An empty value removes them all.","type":"array","items":{"type":"string"}}}},"UpdateLocationDto":{"type":"object","properties":{"latitude":{"type":"number","example":37.7749},"longitude":{"type":"number","example":-122.4194},"accuracy":{"type":"number","example":10.5},"locationName":{"type":"string","example":"San Francisco, CA"}},"required":["latitude","longitude"]},"SetProgramEndDateDto":{"type":"object","properties":{"endDate":{"type":"string","example":"2027-02-01","description":"Today or later"},"programType":{"type":"string","enum":["twelve_month_intensive","four_month_intensive","bootcamp_30_day","alumni"],"description":"Required only for a client with no program on record"},"note":{"type":"string","description":"Why, kept in the status history"}},"required":["endDate"]},"UpdateProgramDatesDto":{"type":"object","properties":{"program_start_date":{"type":"string","example":"2025-01-06"},"program_end_date":{"type":"string","example":"2025-04-06"}}},"AppSessionDto":{"type":"object","properties":{"startedAt":{"type":"string","example":"2026-10-12T08:01:00.000Z"},"endedAt":{"type":"string","example":"2026-10-12T08:07:30.000Z"}},"required":["startedAt","endedAt"]},"RecordAppSessionsDto":{"type":"object","properties":{"sessions":{"type":"array","items":{"$ref":"#/components/schemas/AppSessionDto"}}},"required":["sessions"]},"AssignmentStepDto":{"type":"object","properties":{"label":{"type":"string","example":"Complete the stress worksheet"},"mode":{"type":"string","enum":["auto","client","coach","client_or_coach"]},"autoSignal":{"type":"string","enum":["group_photo","group_message","diary_entry","coach_dm"],"nullable":true,"description":"Required for `auto`; on `client` it pre-ticks"},"allowsUpload":{"type":"boolean"},"routesToCoach":{"type":"boolean","description":"Completing it opens a work item for the named coach (ASG-06)"}},"required":["label","mode"]},"CatalogItemDto":{"type":"object","properties":{"code":{"type":"string","example":"A12"},"title":{"type":"string"},"instructions":{"type":"string"},"programTypes":{"type":"array","description":"Empty means every program","items":{"type":"string","enum":["twelve_month_intensive","four_month_intensive","bootcamp_30_day","alumni"]}},"steps":{"type":"array","items":{"$ref":"#/components/schemas/AssignmentStepDto"}},"sortOrder":{"type":"number"},"isActive":{"type":"boolean"}}},"IssueAssignmentDto":{"type":"object","properties":{"catalogId":{"type":"number"},"dueDate":{"type":"string","example":"2026-10-20","description":"A calendar date"},"groupIds":{"description":"Every current member of each group","type":"array","items":{"type":"number"}},"clientIds":{"type":"array","items":{"type":"number"}}},"required":["catalogId","dueDate"]},"UpdateIssueDto":{"type":"object","properties":{"dueDate":{"type":"string","example":"2026-10-27"},"assignedCoachId":{"type":"object","nullable":true,"description":"The named coach for routing (A11); null clears it"}}},"MoveDueDatesDto":{"type":"object","properties":{"issueIds":{"description":"A bundle's issues; finished ones keep their date","type":"array","items":{"type":"number"}},"dueDate":{"type":"string","example":"2026-10-31"}},"required":["issueIds","dueDate"]},"UntickStepDto":{"type":"object","properties":{"note":{"type":"string","example":"Share your list of 5 trigger foods in the group","description":"What was missing. Shown to the client under the step, with a push, until it is ticked again."}}},"AttendanceMarkDto":{"type":"object","properties":{"clientId":{"type":"number"},"status":{"type":"string","enum":["attended","missed"],"nullable":true,"description":"Null clears the mark"},"note":{"type":"object","nullable":true,"maxLength":500,"description":"Staff-only note with the mark. Left out keeps the note; null or empty clears it"}},"required":["clientId","status"]},"SaveAttendanceDto":{"type":"object","properties":{"marks":{"type":"array","items":{"$ref":"#/components/schemas/AttendanceMarkDto"}}},"required":["marks"]},"AssignAlertDto":{"type":"object","properties":{"assigneeId":{"type":"object","nullable":true,"example":12,"description":"An active coach's user id, or null to unassign"}},"required":["assigneeId"]},"MarkClientPastDto":{"type":"object","properties":{"outcome":{"type":"string","enum":["completed","dropped"],"description":"Finished the program, or left it"},"note":{"type":"string","description":"Why, kept in the status history. No health detail."}},"required":["outcome"]},"SetOverlayDto":{"type":"object","properties":{"colourCode":{"type":"string","enum":["red","amber","green"],"nullable":true},"isPinned":{"type":"boolean"},"notificationPriority":{"type":"object","nullable":true}}},"CreateCoachNoteDto":{"type":"object","properties":{"content":{"type":"string","description":"Free-text note body"}},"required":["content"]},"UpdateCoachNoteDto":{"type":"object","properties":{"content":{"type":"string","description":"Free-text note body"}},"required":["content"]},"SetOverrideDto":{"type":"object","properties":{"date":{"type":"string","example":"2026-12-25"},"ranges":{"description":"Empty array = unavailable that day","type":"array","items":{"$ref":"#/components/schemas/OverrideRangeDto"}}},"required":["date","ranges"]},"DeactivateOverrideDto":{"type":"object","properties":{}},"GenerateNoteDto":{"type":"object","properties":{"clientId":{"type":"number","example":5},"context":{"type":"string","example":"Focus on protein intake progress"}},"required":["clientId"]},"ReviewFoodLogDto":{"type":"object","properties":{"feedback":{"type":"string","example":"Good protein choice, consider adding more fiber"}},"required":["feedback"]},"ReviewDayDto":{"type":"object","properties":{"feedback":{"type":"string","example":"Solid day overall — protein was a bit light at dinner."}},"required":["feedback"]},"InventoryResponseDto":{"type":"object","properties":{"questionId":{"type":"number","example":1,"description":"ID from GET /diary/inventory/questions"},"boolAnswer":{"type":"object","example":true,"nullable":true,"description":"null for text-only questions (input_type = \"text\")"},"note":{"type":"object","example":"Felt frustrated at work","nullable":true}},"required":["questionId"]},"CreateInventoryDto":{"type":"object","properties":{"date":{"type":"string","example":"2026-03-22"},"responses":{"description":"One response per active question — must match all active question IDs exactly","type":"array","items":{"$ref":"#/components/schemas/InventoryResponseDto"}}},"required":["date","responses"]},"CreateGratitudeDto":{"type":"object","properties":{"date":{"type":"string","example":"2026-03-22"},"content":{"type":"string","example":"1. My health\n2. A good night's sleep\n3. My daughter's laugh"}},"required":["date","content"]},"UpdateGratitudeDto":{"type":"object","properties":{"content":{"type":"string","example":"1. My family\n2. Good weather"},"shared":{"type":"boolean","example":true,"description":"Mark entry as shared to community group"}}},"RsvpDto":{"type":"object","properties":{"status":{"type":"string","enum":["yes","maybe","no"]}},"required":["status"]},"BookCoaching1on1Dto":{"type":"object","properties":{"startTime":{"type":"string"},"endTime":{"type":"string"},"title":{"type":"string"}},"required":["startTime","endTime"]},"RescheduleDto":{"type":"object","properties":{"startTime":{"type":"string"},"endTime":{"type":"string"}},"required":["startTime","endTime"]},"BookSalesCallDto":{"type":"object","properties":{"startTime":{"type":"string"},"endTime":{"type":"string"},"title":{"type":"string"},"struggles":{"type":"string","description":"What the booker says they are struggling with"},"extraNotes":{"type":"string","description":"Anything else they want the team to know beforehand"},"consentProgramOptions":{"type":"boolean","description":"Understands the call covers program options and cost"},"consentReschedulePolicy":{"type":"boolean","description":"Understands the 24-hour reschedule and no-show policy"},"consentSms":{"type":"boolean","description":"Happy to be texted about the call"},"marketingOptIn":{"type":"boolean","description":"Optional: wants more information about the programs"}},"required":["startTime","endTime"]},"UpdatePlaybackPositionDto":{"type":"object","properties":{"positionSeconds":{"type":"number"}},"required":["positionSeconds"]},"SetTimezoneDto":{"type":"object","properties":{"timezone":{"type":"string","example":"America/New_York"}},"required":["timezone"]},"EventParagraphDto":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string"},"italic":{"type":"boolean"}},"required":["id","text"]},"CreateCoachingEventDto":{"type":"object","properties":{"category":{"type":"string","enum":["workshop","coaching_1on1","group_coaching","community"]},"groupId":{"type":"number"},"coachId":{"type":"number","description":"Primary facilitator running this event."},"coFacilitatorIds":{"description":"Additional facilitators, equal-weight with coachId, which stays the primary. Allowed on every category.","type":"array","items":{"type":"number"}},"title":{"type":"string"},"subtitle":{"type":"string"},"description":{"type":"array","items":{"$ref":"#/components/schemas/EventParagraphDto"}},"startAt":{"type":"string"},"endAt":{"type":"string"},"openAttendance":{"type":"boolean","description":"Whether someone who was not invited may view and join. With an invite list present, false makes the event private to its invitees."},"attendanceRequired":{"type":"boolean","description":"Whether invitees are expected to attend — \"Event is optional\", inverted."},"invitedUserIds":{"description":"User ids to invite individually.","type":"array","items":{"type":"number"}},"invitedGroupIds":{"description":"Group ids to invite. Members are resolved at access time, so the cohort stays live.","type":"array","items":{"type":"number"}},"joinUrl":{"type":"string","description":"Meeting link. Redacted server-side for viewers without access."}},"required":["category","coachId","title","startAt","endAt","joinUrl"]},"EditOccurrenceDto":{"type":"object","properties":{"title":{"type":"string"},"subtitle":{"type":"string"},"description":{"type":"array","items":{"$ref":"#/components/schemas/EventParagraphDto"}},"startAt":{"type":"string"},"endAt":{"type":"string"},"joinUrl":{"type":"string","description":"Meeting link. Redacted server-side for viewers without access."},"openAttendance":{"type":"boolean"},"attendanceRequired":{"type":"boolean"},"coachId":{"type":"number","description":"Facilitator. Settable on create, and now on edit too."},"coFacilitatorIds":{"description":"Full replace of the co-facilitator list. Omit to leave it alone; [] clears it.","type":"array","items":{"type":"number"}},"groupId":{"type":"number","description":"Cohort for a group_coaching event. Refused if it would leave one without a group."}}},"SetEventRecordingUrlDto":{"type":"object","properties":{"url":{"type":"object","nullable":true,"description":"Zoom recording link, or null to remove it. Redacted server-side for viewers without access."}},"required":["url"]},"SetEventInvitationsDto":{"type":"object","properties":{"userIds":{"description":"User ids to invite individually.","type":"array","items":{"type":"number"}},"groupIds":{"description":"Group ids to invite.","type":"array","items":{"type":"number"}}}},"UploadEventResourceDto":{"type":"object","properties":{"name":{"type":"string","description":"Display name. Defaults to the uploaded file's own name."},"visibleBeforeStart":{"type":"boolean","default":true,"description":"Whether members can open it before the session starts."},"visibleAfterEnd":{"type":"boolean","default":true,"description":"Whether members can still open it once the session has ended."},"scope":{"type":"string","enum":["event","series"],"default":"event","description":"`series` attaches the same file to every later occurrence of this event's series as well. Ignored on an event that is not part of one."}}},"CancelEventDto":{"type":"object","properties":{"scope":{"type":"string","enum":["event","series"],"description":"'event' cancels this occurrence only. 'series' cancels it and every later occurrence, and closes the series so none are regenerated."}}},"ScheduleCoaching1on1Dto":{"type":"object","properties":{"clientUserId":{"type":"number","description":"The client this session is for"},"coachId":{"type":"number","description":"Defaults to the authenticated coach"},"coFacilitatorIds":{"description":"Additional coaches running this session alongside `coachId`. Equal-weight for access and reminders; only the primary is slot-checked and only the primary holds the GHL appointment.","type":"array","items":{"type":"number"}},"startTime":{"type":"string"},"endTime":{"type":"string"},"title":{"type":"string"},"joinUrl":{"type":"string","description":"Meeting link typed by the coach, as on every other category. Wins over GHL's own link when both exist — a coach who took the trouble to paste one meant it."}},"required":["clientUserId","startTime","endTime"]},"CreateEventSeriesDto":{"type":"object","properties":{"category":{"type":"string","enum":["workshop","coaching_1on1","group_coaching","community"]},"groupId":{"type":"number"},"coachId":{"type":"number","description":"Primary facilitator running every occurrence."},"coFacilitatorIds":{"description":"Additional facilitators, copied onto every occurrence. Equal-weight with coachId, which stays the primary.","type":"array","items":{"type":"number"}},"recurrenceRule":{"type":"string","enum":["none","weekly","biweekly","monthly"]},"startsAt":{"type":"string","example":"2026-09-01T15:00:00.000Z","description":"When the first occurrence starts. A full instant — the time of day is kept and every occurrence repeats at it."},"endsOn":{"type":"string","example":"2026-12-01","description":"Last day the series may repeat on. A calendar day, not an instant."},"occurrenceCount":{"type":"number"},"titleTemplate":{"type":"string"},"subtitleTemplate":{"type":"string"},"descriptionTemplate":{"type":"array","items":{"$ref":"#/components/schemas/EventParagraphDto"}},"durationMinutes":{"type":"number"},"joinUrlTemplate":{"type":"string","description":"Meeting link copied onto every occurrence. Redacted server-side for viewers without access."},"attendanceRequired":{"type":"boolean","description":"Copied onto every occurrence — \"Event is optional\", inverted."},"openAttendance":{"type":"boolean","description":"Whether someone who was not invited may view and join. Copied onto every occurrence."},"invitedUserIds":{"description":"Invited individually on every occurrence this series generates.","type":"array","items":{"type":"number"}},"invitedGroupIds":{"description":"Invited as cohorts on every occurrence this series generates.","type":"array","items":{"type":"number"}}},"required":["category","coachId","recurrenceRule","startsAt","titleTemplate","durationMinutes","joinUrlTemplate"]},"EditEventSeriesDto":{"type":"object","properties":{"titleTemplate":{"type":"string"},"subtitleTemplate":{"type":"string"},"descriptionTemplate":{"type":"array","items":{"$ref":"#/components/schemas/EventParagraphDto"}},"durationMinutes":{"type":"number"},"startTimeOfDay":{"type":"string","description":"New time of day, \"HH:mm\" on a 24-hour clock, read in the series' own timezone. Moves every future, not-yet-detached occurrence to this time on its own date and re-anchors the series. Dates and the repeat rule are unchanged — this is the time of day only. An occurrence that would land in the past keeps its original time.","example":"09:30"},"joinUrlTemplate":{"type":"string","description":"Meeting link copied onto every occurrence. Redacted server-side for viewers without access."},"attendanceRequired":{"type":"boolean"},"openAttendance":{"type":"boolean","description":"Whether someone who was not invited may view and join. Copied onto every occurrence."},"invitedUserIds":{"type":"array","items":{"type":"number"}},"invitedGroupIds":{"type":"array","items":{"type":"number"}},"coachId":{"type":"number","description":"Facilitator, re-applied to every future occurrence."},"coFacilitatorIds":{"description":"Full replace of the co-facilitator list, re-applied to every future occurrence. Omit to leave it alone; [] clears it.","type":"array","items":{"type":"number"}},"groupId":{"type":"number","description":"Cohort, re-applied to every future occurrence. Refused if it would leave a group_coaching series without a group."}}},"CreateRecordingDto":{"type":"object","properties":{"eventId":{"type":"number"},"storageKey":{"type":"string"},"durationSeconds":{"type":"number"}},"required":["eventId","storageKey"]},"WeeklyHourRangeDto":{"type":"object","properties":{"dayOfWeek":{"type":"number","minimum":0,"maximum":6,"description":"0 = Sunday .. 6 = Saturday"},"startTime":{"type":"string","example":"09:00"},"endTime":{"type":"string","example":"17:00"}},"required":["dayOfWeek","startTime","endTime"]},"SetWeeklyHoursDto":{"type":"object","properties":{"hours":{"type":"array","items":{"$ref":"#/components/schemas/WeeklyHourRangeDto"}}},"required":["hours"]},"OverrideRangeDto":{"type":"object","properties":{"start":{"type":"string","example":"09:00"},"end":{"type":"string","example":"17:00"}},"required":["start","end"]},"UpsertCalendarMappingDto":{"type":"object","properties":{"category":{"type":"string","enum":["coaching_1on1","group_coaching","sales","workshop","community","reward"]},"groupId":{"type":"number"},"coachId":{"type":"number"},"ghlCalendarId":{"type":"string"}},"required":["category","ghlCalendarId"]},"ImportPastEventDto":{"type":"object","properties":{"category":{"type":"string","enum":["workshop","group_coaching","community"]},"title":{"type":"string"},"subtitle":{"type":"string"},"description":{"type":"string","description":"Plain text. A blank line starts a new paragraph."},"startAt":{"type":"string","example":"2026-08-14T17:00:00-04:00"},"endAt":{"type":"string","example":"2026-08-14T18:00:00-04:00"},"coachId":{"type":"number","description":"Primary facilitator. Must be an active coach or admin."},"coFacilitatorIds":{"type":"array","items":{"type":"number"}},"groupId":{"type":"number","description":"The cohort, for a group_coaching session."},"recordingUrl":{"type":"string","description":"Recording link (e.g. a Zoom share link)."},"joinUrl":{"type":"string","description":"The meeting link the session used, if known."},"openAttendance":{"type":"boolean"},"invitedUserIds":{"type":"array","items":{"type":"number"}},"invitedGroupIds":{"type":"array","items":{"type":"number"}}},"required":["category","title","startAt","endAt","coachId","recordingUrl"]},"ImportPastEventsDto":{"type":"object","properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/ImportPastEventDto"}}},"required":["events"]},"EditPastEventDto":{"type":"object","properties":{"category":{"type":"string","enum":["workshop","group_coaching","community"]},"title":{"type":"string"},"subtitle":{"type":"string"},"description":{"type":"string","description":"Plain text. A blank line starts a new paragraph."},"startAt":{"type":"string","example":"2026-08-14T17:00:00-04:00"},"endAt":{"type":"string","example":"2026-08-14T18:00:00-04:00"},"coachId":{"type":"number","description":"Primary facilitator. Must be an active coach or admin."},"groupId":{"type":"number","description":"The cohort, for a group_coaching session."},"recordingUrl":{"type":"string","description":"Recording link (e.g. a Zoom share link)."},"joinUrl":{"type":"string","description":"The meeting link the session used, if known."}},"required":["category","title","startAt","endAt","coachId","recordingUrl"]},"BookRewardSlotDto":{"type":"object","properties":{"slot_start":{"type":"string","example":"2026-10-20T15:00:00.000Z","description":"A `slot_start` from GET rewards/grants/:id/slots."},"coach_id":{"type":"number","example":31,"description":"The `coach_id` of the chosen slot."},"focus":{"type":"string","enum":["regain","plateau","eating_patterns","movement","other"],"description":"The main thing the user wants to focus on."},"note":{"type":"string","maxLength":1000,"description":"Anything the coach should know before the call."}},"required":["slot_start","coach_id"]},"BookRewardChatDto":{"type":"object","properties":{"date":{"type":"string","example":"2026-10-28","description":"A day from GET rewards/grants/:id/chat-days, in `timezone`."},"coach_id":{"type":"object","example":31,"description":"The chosen coach; omit (or null) for the first available coach that day."},"timezone":{"type":"string","example":"America/New_York","description":"The zone `date` is in — the one the days were listed in."}},"required":["date","timezone"]},"RewardCallDidntHappenDto":{"type":"object","properties":{"reason":{"type":"string","enum":["no_show","coach_unavailable"],"description":"`no_show`: the user didn't show; the prize counts as delivered. `coach_unavailable`: the coach couldn't make it; the prize goes back to booking."},"note":{"type":"string","maxLength":500,"description":"Kept on the grant as fulfilment_meta.note."}},"required":["reason"]},"RecordConsentDecisionDto":{"type":"object","properties":{"decision":{"type":"string","enum":["granted","declined"]}},"required":["decision"]},"ChooseGrantOptionDto":{"type":"object","properties":{"catalogue_id":{"type":"number","description":"catalogue_id of the chosen option, from GET grants/:id/options"}},"required":["catalogue_id"]},"PreviewGrantClaimDto":{"type":"object","properties":{"catalogue_id":{"type":"number","description":"A choice prize: catalogue_id of the option picked, from GET grants/:id/options"},"consent":{"type":"string","enum":["granted","declined"],"description":"The answer to a `consent_needed` step. Recorded nowhere."}}},"PickCommunityCallDto":{"type":"object","properties":{"event_id":{"type":"number","description":"The community call to join, from `GET grants/:id/calls`."}},"required":["event_id"]},"SpotlightChoiceDto":{"type":"object","properties":{"opt_in":{"type":"boolean","description":"true: share me in the weekly spotlight post. false: a private coach message instead."}}},"EquipCosmeticDto":{"type":"object","properties":{"cosmetic_key":{"type":"string","example":"avatar_frame_l5","description":"Key of an unlocked cosmetic."},"equipped":{"type":"boolean","default":true,"description":"false takes it off. Equipping one takes off any other of the same kind."}},"required":["cosmetic_key"]},"UpdateRewardCatalogueDto":{"type":"object","properties":{"unit_cost":{"type":"number"},"value_rank":{"type":"number"},"free_days":{"type":"number"},"requires_backend_billing":{"type":"boolean"},"is_active":{"type":"boolean"},"consent_scope":{"type":"string","enum":["food_logs","lucille_rag","food_preferences"],"description":"A coach needs temporary access to this to deliver the prize — asked at claim, never blocks it. Omit/null for prizes that need no data access."},"host_coach_id":{"type":"object","nullable":true,"description":"The one coach who delivers this prize (an active coach account): its calls and chat days are offered on their calendar only, and a chat prize opens with them. null for any coach."}}},"UpdateGoalCatalogueDto":{"type":"object","properties":{"points":{"type":"number"},"total_days":{"type":"number"},"is_active":{"type":"boolean"},"is_recommendable":{"type":"boolean"},"sort_order":{"type":"number"}}},"RewardOfferAudienceDto":{"type":"object","properties":{"billing":{"type":"string","enum":["free","premium","lifetime"],"description":"\"premium\" also matches lifetime."},"relationship":{"type":"string","enum":["user","member","client"]}}},"RewardOfferEligibilityDto":{"type":"object","properties":{"user_types":{"type":"array","description":"Any of these user types. Omit for \"everyone\".","items":{"type":"string","enum":["client","coach","lead","member","pc"]}},"min_days_since_signup":{"type":"number","description":"Only users whose program started at least this many days ago."},"requires_active_subscription":{"type":"boolean","description":"Only users with an active paid subscription."},"requires_never_subscribed":{"type":"boolean","description":"Only users who have never had a subscription of any status — the \"never paid us anything\" gate for the free-days prize (client Q1)."},"requires_no_prior_coach_call":{"type":"boolean","description":"Only users who have never received a real 1:1 coach call prize before — the \"lifetime call access\" split in the Level 3 waterfall (client priority #2)."},"audiences":{"description":"Who this offer is for: matches when any entry matches the user's billing (free / premium / lifetime) and relationship (user / member / client), resolved at claim. Omit for \"everyone\". Clients never get a coach_time prize unless an entry names relationship \"client\" explicitly.","type":"array","items":{"$ref":"#/components/schemas/RewardOfferAudienceDto"}}}},"UpsertRewardOfferDto":{"type":"object","properties":{"catalogue_id":{"type":"number","example":2,"description":"reward_catalogue.id"},"trigger_type":{"type":"string","enum":["level_up","goal_completion","streak_milestone","manual"]},"trigger_level":{"type":"object","example":5,"description":"For trigger_type \"level_up\": the level this fires on. Null = any level."},"trigger_days":{"type":"object","example":100,"description":"For trigger_type \"streak_milestone\": the consecutive-day count this fires on (e.g. 100, 365) — a choice offer, not auto-resolved like level_up."},"starts_at":{"type":"string"},"ends_at":{"type":"object"},"quantity_available":{"type":"object","description":"Hard cap on grants. Always set this for anything with a real cost — a free coach slot is not unlimited."},"eligibility":{"$ref":"#/components/schemas/RewardOfferEligibilityDto"},"priority":{"type":"number","default":0},"claim_window_days":{"type":"number","default":14},"is_active":{"type":"boolean","default":true}},"required":["catalogue_id","trigger_type"]},"GrantBonusRestDaysDto":{"type":"object","properties":{"count":{"type":"number","example":1,"description":"How many to add (1–3); the bank stops at the cap."},"reason":{"type":"string","example":"Streak broke on 12 Oct due to the sync bug (ticket #4821)"}}},"GiftPointsDto":{"type":"object","properties":{"amount":{"type":"number","example":500,"description":"Points per user, 1–5000."},"reason":{"type":"string","example":"Thanks for joining the 30-day challenge","description":"Shown to the user in the app when the points land."},"note":{"type":"string","example":"October webinar attendees","description":"Internal only — kept in the activity log, never shown to the user."},"request_id":{"type":"string","example":"0b6c1f5e-6a51-4c3e-9a0e-2f7f0c4d9b1a","description":"A fresh UUID per submit. Sending the same one again (a double click, a retry) never gives the points twice. Omit and every call is a new gift."}},"required":["amount","reason"]},"FulfilGrantDto":{"type":"object","properties":{"note":{"type":"string","maxLength":500,"description":"Kept on the grant as fulfilment_meta.note."}}},"SetCoachPoolDto":{"type":"object","properties":{"coach_ids":{"description":"Coach user ids that make up the pool. Replaces the pool.","type":"array","items":{"type":"number"}}}},"SetStoreCreditEnabledDto":{"type":"object","properties":{"enabled":{"type":"boolean","example":true}}},"SpotlightHandledDto":{"type":"object","properties":{"action":{"type":"string","enum":["posted","messaged"],"description":"posted for an opt-in, messaged for a decliner."}}},"UpdateSettingDto":{"type":"object","properties":{"value":{"type":"number"}},"required":["value"]},"UpdateSourceTypeConfigDto":{"type":"object","properties":{"base_points":{"type":"number"},"bonus_points":{"type":"number"},"tier":{"type":"string","enum":["gimme","stretch"],"description":"The daily featured-task mix — \"gimme\" (near-zero-effort) vs \"stretch\"."}}},"UpsertStreakMilestoneDto":{"type":"object","properties":{"days":{"type":"number","description":"Consecutive days this milestone fires at."},"points":{"type":"number"}},"required":["days","points"]},"UpdateLevelThresholdDto":{"type":"object","properties":{"points_required":{"type":"number","description":"Cumulative points required to reach this level."}},"required":["points_required"]},"UpdateRolloutModeDto":{"type":"object","properties":{"mode":{"type":"string","enum":["off","staff_only","allowlist","all"]}},"required":["mode"]},"AddRolloutAllowlistDto":{"type":"object","properties":{"user_id":{"type":"number"}},"required":["user_id"]},"ScheduleLiveAmaDto":{"type":"object","properties":{"start_at":{"type":"string","description":"When the AMA starts."},"duration_minutes":{"type":"number","example":60,"description":"How long it runs, in minutes (15–240). Default 60."},"host_coach_id":{"type":"number","example":31,"description":"The host (an active coach): Shenelle."},"join_url":{"type":"string","description":"The https link seat holders join with. Can be added later."},"title":{"type":"string","description":"Title on the calendar. Default \"Live AMA with Shenelle\"."}},"required":["start_at","host_coach_id"]},"UpdateLiveAmaDto":{"type":"object","properties":{"start_at":{"type":"string","description":"A new start. Seat holders keep their seat and are told."},"duration_minutes":{"type":"number","example":60},"host_coach_id":{"type":"number","example":31},"join_url":{"type":"object","nullable":true,"description":"The https link; null clears it, omitted leaves it."},"title":{"type":"string"},"cancelled":{"type":"boolean","example":true,"description":"Cancel it. Final. Seat holders go back to picking, and are told (the community-call cron)."}}},"SetStandingHuddleLinkDto":{"type":"object","properties":{"join_url":{"type":"object","nullable":true,"example":"https://us02web.zoom.us/j/123456789","description":"The standing https link every Huddle uses unless it has its own; null clears it."}},"required":["join_url"]},"ScheduleHuddleDto":{"type":"object","properties":{"scheduled_at":{"type":"string","description":"When the huddle happens — the invite-list snapshot fires a configured number of days before this."},"join_url":{"type":"string","description":"This Huddle's own https link. Omit it and the Huddle follows the standing link (PUT huddles/standing-link), now and whenever that changes."},"duration_minutes":{"type":"number","example":60,"description":"How long the call runs, in minutes (15–240). Default 60. (G27)"},"host_coach_id":{"type":"number","example":31,"description":"The hosting coach (an active coach). (G27)"}},"required":["scheduled_at"]},"UpdateHuddleDto":{"type":"object","properties":{"join_url":{"type":"object","nullable":true,"description":"Give this Huddle its own https link (it stops following the standing link); null leaves it with none, omitted leaves it as is. Never served to a user who is not invited."},"use_standing_link":{"type":"boolean","example":true,"description":"true puts the Huddle back on the standing link. Not with join_url."},"scheduled_at":{"type":"string","description":"Reschedule (BE-5.1). The calendar event moves with it."},"cancelled":{"type":"boolean","description":"true cancels the Huddle and its calendar event (BE-5.1). Final."},"duration_minutes":{"type":"number","example":60,"description":"How long the call runs, in minutes (15–240). Default 60. (G27)"},"host_coach_id":{"type":"object","nullable":true,"example":31,"description":"Change the hosting coach; null clears. (G27)"}}},"QaApplyScenarioDto":{"type":"object","properties":{"scenario":{"type":"string","enum":["about_to_level_up","level_up_with_pending_grant","streak_one_day_from_milestone","streak_choice_milestone_ready","consent_prize_ready","grant_expired","grant_already_claimed","goal_near_completion","huddle_invited","huddle_locked_out","comeback_due","existing_user_historical_seed"]},"level":{"type":"number","description":"Used by level-related scenarios."},"days":{"type":"number","description":"Used by streak-related scenarios."},"catalogue_key":{"type":"string","description":"Used by grant/goal-related scenarios."}},"required":["scenario"]},"QaSetPointsDto":{"type":"object","properties":{"points_total":{"type":"number","example":4990,"description":"Target points_total. Ledger and today's activity are cleared first, then re-seeded to exactly this value via a single ledger adjustment — never a direct write to the derived total."}},"required":["points_total"]},"QaSetStreakDto":{"type":"object","properties":{"current_days":{"type":"number","example":29},"best_days":{"type":"number","description":"Defaults to current_days."},"last_active_date":{"type":"string","description":"Local date (YYYY-MM-DD) the streak is \"active as of\". Defaults to yesterday in the user's own timezone, not today — so a real log today naturally advances the streak by one, rather than being silently treated as already counted. Replaces what earlier streaks earned, then earns every stone up to current_days (badges, cosmetics, prizes, Huddle seat) without milestone points."}},"required":["current_days"]},"QaSetBonusRestDaysDto":{"type":"object","properties":{"count":{"type":"number","example":1,"description":"Banked bonus rest days to set (replaces the current count)."}},"required":["count"]},"QaSetGrantDto":{"type":"object","properties":{"status":{"type":"string","enum":["unresolved_pending","resolved_pending","expired","claimed"],"description":"unresolved_pending: waiting on the waterfall/choice pick (needs trigger_level or trigger_days). resolved_pending: a specific prize already attached, ready to claim (needs catalogue_key) — claiming it against a consent-gated entry exercises the consent flow for real. expired / claimed: terminal states, for testing history and \"already claimed\" copy (both need catalogue_key)."},"trigger_level":{"type":"number","example":5},"trigger_days":{"type":"number","example":100},"catalogue_key":{"type":"string","example":"log_review","description":"reward_catalogue.key — required for every status except unresolved_pending."}},"required":["status"]},"QaSetGoalDto":{"type":"object","properties":{"catalogue_key":{"type":"string","example":"vitamins_7_day_streak"},"status":{"type":"string","enum":["active","completed"]},"progress_days":{"type":"number","description":"Defaults to one day short of the catalogue's total_days for \"active\", or the full total_days for \"completed\"."}},"required":["catalogue_key","status"]},"QaSetHuddleInviteDto":{"type":"object","properties":{"status":{"type":"string","enum":["pending_snapshot","invited","locked"]},"used_banked_pass":{"type":"boolean","description":"Mark the invite as coming from a banked pass rather than streak alone."}},"required":["status"]},"QaSeedActivityDaysDto":{"type":"object","properties":{"days":{"type":"number","default":30,"description":"How many of the last N days (including today) get a qualifying activity row."},"gap_offsets":{"description":"Day-offsets from today to deliberately leave empty (0 = today, 1 = yesterday, etc.) — for testing rest-day forgiveness or a broken run. Empty by default (an unbroken run).","type":"array","items":{"type":"number"}},"timezone":{"type":"string","description":"Defaults to UTC."}}},"QaRunHistoricalSeedDto":{"type":"object","properties":{"timezone":{"type":"string","description":"Defaults to UTC."}}},"SetRecordingVisibilityDto":{"type":"object","properties":{"hidden":{"type":"boolean","description":"True hides the recording from everyone but coaches and admins"}},"required":["hidden"]},"SearchKnowledgeDto":{"type":"object","properties":{}},"IngestKnowledgeDto":{"type":"object","properties":{}},"IngestBatchDto":{"type":"object","properties":{}},"SeedCoachingContextDto":{"type":"object","properties":{}},"ResolveUserDto":{"type":"object","properties":{}},"MatchCoachingClientDto":{"type":"object","properties":{}},"ResearchCohortQueryDto":{"type":"object","properties":{}},"UpdateFeedbackDto":{"type":"object","properties":{}},"CreateBridgeKeyDto":{"type":"object","properties":{}},"UpdateBridgeKeyDto":{"type":"object","properties":{}},"WipeSeedDto":{"type":"object","properties":{}},"LogErrorDto":{"type":"object","properties":{"errorMessage":{"type":"string","example":"Cannot read property of undefined"},"userId":{"type":"number","example":42},"errorCode":{"type":"string","example":"API_FAILURE"},"action":{"type":"string","example":"food_log.submit"},"screen":{"type":"string","example":"FoodLogScreen"},"platform":{"type":"string","example":"ios"},"appVersion":{"type":"string","example":"2.1.0"},"stackTrace":{"type":"string","example":"Error: ...\n  at foo.js:12"},"metadata":{"type":"object","example":{"device":"iPhone 14","osVersion":"17.2"}}},"required":["errorMessage"]},"SubmitFeedbackDto":{"type":"object","properties":{"type":{"type":"string","enum":["suggestion","bug_report","contact"]},"query_type":{"type":"string","enum":["billing_support","accessing_program_materials","tech_support","anything_else"],"description":"Required when type is \"contact\". Ignored for other types."},"message":{"type":"string"},"metadata":{"type":"object","description":"Optional context: screen, app_version, device"}},"required":["type","message"]}}}}