openapi: 3.1.0 info: title: HealthLog API version: 1.4.0 description: | REST API for HealthLog — a personal health-tracking PWA covering weight, blood pressure, pulse, mood, medication compliance, blood glucose, body composition (total body water + bone mass), pulse oximetry (SpO2), and sleep. The same API powers the Next.js web client and the native iOS client. ## Authentication All non-public endpoints accept either a session cookie (`healthlog_session`, obtained via password login or passkey verification) or a long-lived Bearer token (`Authorization: Bearer hlk_...`) issued via `/api/tokens`. The two schemes are interchangeable on user endpoints — clients pick whichever suits them (cookies for the web app, Bearer for the iOS client and machine-to-machine integrations). Admin endpoints (`/api/admin/*`) accept only the session cookie; Bearer tokens never elevate to admin. The API uses an envelope response shape: `{ data, error }`. On success, `data` carries the payload and `error` is `null`. On failure, `data` is `null` and `error` is a human-readable message. ## Conventions - All timestamps are ISO 8601 (`date-time`) in UTC. - All numeric IDs are CUIDs (opaque strings). - Pagination uses `limit` + `offset` query parameters; responses include a `meta: { total, limit, offset }` block. - Rate limiting is applied per-IP or per-user on auth, ingest, export, AI, and bug-report endpoints. Exceeding the budget yields a `429`. ## Admin endpoints Routes under `/api/admin/*` are tagged `Admin` and are intended for the internal admin UI only. iOS clients should not consume them. contact: name: HealthLog url: https://healthlog.bombeck.io license: name: MIT url: https://github.com/MBombeck/healthlog/blob/main/LICENSE servers: - url: https://healthlog.bombeck.io description: Production - url: http://localhost:3000 description: Local development tags: - name: Auth description: Registration, login (password + passkey/WebAuthn), logout, session info, profile, password change. - name: Measurements description: CRUD for measurements (weight, BP, pulse, body fat, glucose, sleep, steps). - name: Medications description: Medication management, schedules, intake events, compliance, reminder phase config. - name: Mood description: Mood entries (manual + moodLog imports) and mood analytics. - name: Dashboard description: User-customizable dashboard widget layout. - name: Insights description: AI-generated insights, classifications (BMI, BP, mood, etc.), per-metric status, target ranges. - name: Achievements description: Gamification achievements (streaks, milestones). - name: DoctorReport description: Aggregated health data for client-side PDF export. - name: Integrations description: Withings, moodLog, Codex (ChatGPT) OAuth integrations. - name: User description: User-level configuration (AI provider, threshold overrides). - name: Tokens description: User-issued API tokens for external ingest. - name: Notifications description: Notification channels and preferences (Telegram, ntfy, Web Push). - name: Onboarding description: First-run profile setup. - name: Health description: Service health probe. - name: Settings description: Per-user settings (account deletion, data wipe, integrations, notifications). - name: Analytics description: Aggregated analytics summaries. - name: Export description: User data export (CSV/JSON) and re-import. - name: AuditLog description: User-facing audit log of own activity. - name: Feedback description: User feedback submission and history. - name: BugReport description: User bug reports (creates a GitHub issue when configured). - name: Monitoring description: Public monitoring/error reporting endpoints (CSP, Glitchtip, Umami proxy). - name: Webhooks description: Inbound webhooks (Withings, moodLog, Telegram). - name: Ingest description: External ingest endpoints authenticated via Bearer token. - name: Admin description: Internal admin-only endpoints. Not consumed by the iOS client. security: - bearerAuth: [] - sessionCookie: [] paths: # ─── Auth ──────────────────────────────────────────────────────── /api/auth/register: post: tags: [Auth] summary: Register a new user (password) operationId: registerUser security: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/RegisterInput" } responses: "201": description: Created content: application/json: schema: $ref: "#/components/schemas/RegisterResponseEnvelope" "403": { $ref: "#/components/responses/Forbidden" } "409": { $ref: "#/components/responses/Conflict" } "422": { $ref: "#/components/responses/Unprocessable" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/ServerError" } /api/auth/login: post: tags: [Auth] summary: Login with email/username + password operationId: loginUser security: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/LoginInput" } responses: "200": description: Authenticated content: application/json: schema: { $ref: "#/components/schemas/LoginResponseEnvelope" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/ServerError" } /api/auth/logout: post: tags: [Auth] summary: Destroy current session operationId: logoutUser responses: "200": description: Logged out content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: loggedOut: { type: boolean } "500": { $ref: "#/components/responses/ServerError" } /api/auth/me: get: tags: [Auth] summary: Get current authenticated user operationId: getCurrentUser responses: "200": description: Current user content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/CurrentUser" } "401": { $ref: "#/components/responses/Unauthorized" } "500": { $ref: "#/components/responses/ServerError" } /api/auth/profile: put: tags: [Auth] summary: Update profile (email, height, DOB, gender) operationId: updateProfile requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ProfileInput" } responses: "200": description: Updated profile content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/User" } "401": { $ref: "#/components/responses/Unauthorized" } "409": { $ref: "#/components/responses/Conflict" } "422": { $ref: "#/components/responses/Unprocessable" } "500": { $ref: "#/components/responses/ServerError" } /api/auth/password: post: tags: [Auth] summary: Change password (invalidates other sessions) operationId: changePassword requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ChangePasswordInput" } responses: "200": description: Password changed content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: changed: { type: boolean } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/ServerError" } /api/auth/registration-status: get: tags: [Auth] summary: Whether public registration is enabled operationId: getRegistrationStatus security: [] responses: "200": description: Registration status content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: registrationEnabled: { type: boolean } # ─── Passkeys (WebAuthn) ─────────────────────────────────────── /api/auth/passkey/register-options: post: tags: [Auth] summary: Begin passkey registration — get WebAuthn options operationId: getPasskeyRegisterOptions responses: "200": description: WebAuthn registration options content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/PasskeyRegisterOptions" } "401": { $ref: "#/components/responses/Unauthorized" } /api/auth/passkey/register-verify: post: tags: [Auth] summary: Complete passkey registration — verify attestation operationId: verifyPasskeyRegistration requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/PasskeyVerifyRegistrationInput" } responses: "200": description: Passkey registered content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: verified: { type: boolean } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } /api/auth/passkey/login-options: post: tags: [Auth] summary: Begin passkey login — get WebAuthn options operationId: getPasskeyLoginOptions security: [] responses: "200": description: WebAuthn authentication options content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/PasskeyLoginOptions" } "429": { $ref: "#/components/responses/RateLimited" } /api/auth/passkey/login-verify: post: tags: [Auth] summary: Complete passkey login — verify assertion operationId: verifyPasskeyLogin security: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/PasskeyVerifyLoginInput" } responses: "200": description: Authenticated via passkey content: application/json: schema: { $ref: "#/components/schemas/LoginResponseEnvelope" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Unprocessable" } "429": { $ref: "#/components/responses/RateLimited" } /api/auth/passkeys: get: tags: [Auth] summary: List the user's passkeys operationId: listPasskeys responses: "200": description: List of passkeys content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: array items: { $ref: "#/components/schemas/PasskeySummary" } "401": { $ref: "#/components/responses/Unauthorized" } /api/auth/passkeys/{id}: delete: tags: [Auth] summary: Delete a passkey operationId: deletePasskey parameters: - $ref: "#/components/parameters/IdPathParam" responses: "200": description: Deleted content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: success: { type: boolean } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } # ─── Codex (ChatGPT) OAuth ──────────────────────────────────── /api/auth/codex/authorize: get: tags: [Integrations] summary: Begin ChatGPT OAuth flow (redirects to ChatGPT) operationId: codexAuthorize responses: "302": description: Redirect to ChatGPT auth URL "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } /api/auth/codex/callback: get: tags: [Integrations] summary: ChatGPT OAuth callback — exchange code for tokens operationId: codexCallback parameters: - in: query name: code schema: { type: string } - in: query name: state schema: { type: string } - in: query name: error schema: { type: string } responses: "302": description: Redirect back to /settings with status "401": { $ref: "#/components/responses/Unauthorized" } /api/auth/codex/disconnect: delete: tags: [Integrations] summary: Disconnect ChatGPT integration operationId: codexDisconnect responses: "200": description: Disconnected content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: disconnected: { type: boolean } "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } # ─── Measurements ──────────────────────────────────────────────── /api/measurements: get: tags: [Measurements] summary: List measurements (paginated, filterable) operationId: listMeasurements parameters: - in: query name: type schema: { $ref: "#/components/schemas/MeasurementType" } - in: query name: from schema: { type: string, format: date-time } - in: query name: to schema: { type: string, format: date-time } - $ref: "#/components/parameters/LimitParam" - $ref: "#/components/parameters/OffsetParam" - in: query name: sortBy schema: type: string enum: [type, value, measuredAt, source] default: measuredAt - in: query name: sortDir schema: type: string enum: [asc, desc] default: desc responses: "200": description: List of measurements content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: measurements: type: array items: { $ref: "#/components/schemas/Measurement" } meta: { $ref: "#/components/schemas/PageMeta" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } "500": { $ref: "#/components/responses/ServerError" } post: tags: [Measurements] summary: Create a measurement (single or batch) operationId: createMeasurement requestBody: required: true content: application/json: schema: oneOf: - $ref: "#/components/schemas/CreateMeasurementInput" - type: array items: { $ref: "#/components/schemas/CreateMeasurementInput" } minItems: 1 maxItems: 5 responses: "201": description: Created content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: oneOf: - $ref: "#/components/schemas/Measurement" - type: array items: { $ref: "#/components/schemas/Measurement" } "401": { $ref: "#/components/responses/Unauthorized" } "409": { $ref: "#/components/responses/Conflict" } "422": { $ref: "#/components/responses/Unprocessable" } "500": { $ref: "#/components/responses/ServerError" } /api/measurements/{id}: parameters: - $ref: "#/components/parameters/IdPathParam" get: tags: [Measurements] summary: Get a single measurement operationId: getMeasurement responses: "200": description: Measurement content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/Measurement" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } put: tags: [Measurements] summary: Update a measurement operationId: updateMeasurement requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/UpdateMeasurementInput" } responses: "200": description: Updated content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/Measurement" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Unprocessable" } delete: tags: [Measurements] summary: Delete a measurement operationId: deleteMeasurement responses: "200": description: Deleted content: application/json: schema: { $ref: "#/components/schemas/DeletedEnvelope" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } # ─── Medications ───────────────────────────────────────────────── /api/medications: get: tags: [Medications] summary: List medications (with today's intake summary) operationId: listMedications responses: "200": description: List content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: array items: { $ref: "#/components/schemas/MedicationWithSchedules", } "401": { $ref: "#/components/responses/Unauthorized" } post: tags: [Medications] summary: Create a medication with schedules operationId: createMedication requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateMedicationInput" } responses: "201": description: Created content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/MedicationWithSchedules" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } /api/medications/{id}: parameters: - $ref: "#/components/parameters/IdPathParam" get: tags: [Medications] summary: Get a single medication operationId: getMedication responses: "200": description: Medication content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/MedicationWithSchedules" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } put: tags: [Medications] summary: Update a medication operationId: updateMedication requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/UpdateMedicationInput" } responses: "200": description: Updated content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/MedicationWithSchedules" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Unprocessable" } delete: tags: [Medications] summary: Delete a medication (revokes its API tokens) operationId: deleteMedication responses: "200": description: Deleted content: application/json: schema: { $ref: "#/components/schemas/DeletedEnvelope" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } /api/medications/{id}/intake: parameters: - $ref: "#/components/parameters/IdPathParam" get: tags: [Medications] summary: List intake events for a medication operationId: listMedicationIntakes parameters: - $ref: "#/components/parameters/LimitParam" - $ref: "#/components/parameters/OffsetParam" - in: query name: sortBy schema: type: string enum: [scheduledFor, takenAt, source, createdAt] default: scheduledFor - in: query name: sortDir schema: { type: string, enum: [asc, desc], default: desc } responses: "200": description: Events content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: events: type: array items: { $ref: "#/components/schemas/MedicationIntakeEvent", } meta: { $ref: "#/components/schemas/PageMeta" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Unprocessable" } post: tags: [Medications] summary: Log a medication intake or skip operationId: createMedicationIntake requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateIntakeInput" } responses: "200": description: Idempotent return of existing event content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/MedicationIntakeEvent" } "201": description: Created content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/MedicationIntakeEvent" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Unprocessable" } /api/medications/{id}/intake/{eventId}: parameters: - $ref: "#/components/parameters/IdPathParam" - in: path name: eventId required: true schema: { type: string } put: tags: [Medications] summary: Update an intake event operationId: updateMedicationIntake requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/UpdateIntakeInput" } responses: "200": description: Updated content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/MedicationIntakeEvent" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Unprocessable" } delete: tags: [Medications] summary: Delete an intake event operationId: deleteMedicationIntake responses: "200": description: Deleted content: application/json: schema: { $ref: "#/components/schemas/DeletedEnvelope" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } /api/medications/{id}/intake/import: parameters: - $ref: "#/components/parameters/IdPathParam" post: tags: [Medications] summary: Bulk-import historical intakes for one medication operationId: importMedicationIntakes requestBody: required: true content: application/json: schema: oneOf: - type: array items: { $ref: "#/components/schemas/IntakeImportEntry" } minItems: 1 maxItems: 1000 - type: object description: Object whose first array-valued property is treated as the entries. additionalProperties: true responses: "201": description: Import result content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/IntakeImportResult" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Unprocessable" } /api/medications/{id}/intake/purge: parameters: - $ref: "#/components/parameters/IdPathParam" delete: tags: [Medications] summary: Delete ALL intake events for a medication operationId: purgeMedicationIntakes responses: "200": description: Purged content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: purged: { type: boolean } count: { type: integer } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } /api/medications/{id}/compliance: parameters: - $ref: "#/components/parameters/IdPathParam" get: tags: [Medications] summary: Compliance summary for a medication (7d, 30d, 90 daily) operationId: getMedicationCompliance responses: "200": description: Compliance content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/MedicationComplianceResponse", } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } /api/medications/{id}/phase-config: parameters: - $ref: "#/components/parameters/IdPathParam" get: tags: [Medications] summary: Get reminder-phase config (or defaults) operationId: getPhaseConfig responses: "200": description: Phase config content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/ReminderPhaseConfig" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } put: tags: [Medications] summary: Update reminder-phase config operationId: updatePhaseConfig requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ReminderPhaseConfig" } responses: "200": description: Updated content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/ReminderPhaseConfig" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } delete: tags: [Medications] summary: Reset reminder-phase config to defaults operationId: resetPhaseConfig responses: "200": description: Reset content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: reset: { type: boolean } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } /api/medications/{id}/api-endpoint: parameters: - $ref: "#/components/parameters/IdPathParam" get: tags: [Medications] summary: Whether an external API token is enabled for this medication operationId: getMedicationApiEndpoint responses: "200": description: Status content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: enabled: { type: boolean } activeTokenCount: { type: integer } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } put: tags: [Medications] summary: Enable / disable a medication-scoped API token operationId: setMedicationApiEndpoint requestBody: required: true content: application/json: schema: type: object properties: enabled: { type: boolean } required: [enabled] responses: "200": description: Existing token reused or revocation result content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/MedicationApiEndpointResponse", } "201": description: New medication-scoped token created content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/MedicationApiEndpointResponse", } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Unprocessable" } /api/medications/intake-summary: get: tags: [Medications] summary: Daily intake summary (for streak chart) operationId: getIntakeSummary parameters: - in: query name: from schema: { type: string, format: date-time } - in: query name: to schema: { type: string, format: date-time } responses: "200": description: Summary content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/IntakeSummaryResponse" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } # ─── Mood ───────────────────────────────────────────────────── /api/mood-entries: get: tags: [Mood] summary: List mood entries (paginated, filterable) operationId: listMoodEntries parameters: - in: query name: mood schema: { $ref: "#/components/schemas/MoodLevel" } - in: query name: from description: "YYYY-MM-DD lower bound (inclusive)." schema: { type: string } - in: query name: to description: "YYYY-MM-DD upper bound (inclusive)." schema: { type: string } - $ref: "#/components/parameters/LimitParam" - $ref: "#/components/parameters/OffsetParam" - in: query name: sortBy schema: { type: string, default: moodLoggedAt } - in: query name: sortDir schema: { type: string, enum: [asc, desc], default: desc } responses: "200": description: Mood entries content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: entries: type: array items: { $ref: "#/components/schemas/MoodEntry" } meta: { $ref: "#/components/schemas/PageMeta" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } post: tags: [Mood] summary: Create a mood entry operationId: createMoodEntry requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateMoodEntryInput" } responses: "201": description: Created content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/MoodEntry" } "401": { $ref: "#/components/responses/Unauthorized" } "409": { $ref: "#/components/responses/Conflict" } "422": { $ref: "#/components/responses/Unprocessable" } /api/mood-entries/{id}: parameters: - $ref: "#/components/parameters/IdPathParam" get: tags: [Mood] summary: Get a mood entry operationId: getMoodEntry responses: "200": description: Mood entry content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/MoodEntry" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } put: tags: [Mood] summary: Update a mood entry operationId: updateMoodEntry requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/UpdateMoodEntryInput" } responses: "200": description: Updated content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/MoodEntry" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Unprocessable" } delete: tags: [Mood] summary: Delete a mood entry operationId: deleteMoodEntry responses: "200": description: Deleted content: application/json: schema: { $ref: "#/components/schemas/DeletedEnvelope" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } /api/mood/analytics: get: tags: [Mood] summary: Mood analytics (daily averages + summary) operationId: getMoodAnalytics responses: "200": description: Analytics content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/MoodAnalytics" } "401": { $ref: "#/components/responses/Unauthorized" } # ─── Dashboard ──────────────────────────────────────────────── /api/dashboard/widgets: get: tags: [Dashboard] summary: Get the resolved dashboard widget layout operationId: getDashboardWidgets responses: "200": description: Layout content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/DashboardLayout" } "401": { $ref: "#/components/responses/Unauthorized" } put: tags: [Dashboard] summary: Replace the dashboard widget layout operationId: updateDashboardWidgets requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/DashboardLayout" } responses: "200": description: Updated content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/DashboardLayout" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } delete: tags: [Dashboard] summary: Reset dashboard layout to defaults operationId: resetDashboardWidgets responses: "200": description: Reset content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/DashboardLayout" } "401": { $ref: "#/components/responses/Unauthorized" } # ─── Insights ──────────────────────────────────────────────── /api/insights/comprehensive: get: tags: [Insights] summary: Comprehensive 90-day analytics bundle operationId: getComprehensiveInsights responses: "200": description: Insights bundle content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/ComprehensiveInsights" } "401": { $ref: "#/components/responses/Unauthorized" } /api/insights/generate: post: tags: [Insights] summary: Generate AI-narrative insights (cached for 24h) operationId: generateInsights requestBody: required: false content: application/json: schema: type: object properties: force: { type: boolean, description: "Bypass 24h cache" } responses: "200": description: Insights content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/InsightGenerateResult" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } "429": { $ref: "#/components/responses/RateLimited" } "502": description: Upstream AI provider failure content: application/json: schema: { $ref: "#/components/schemas/ApiError" } /api/insights/settings: get: tags: [Insights] summary: Get insights settings (privacy mode, provider status) operationId: getInsightsSettings responses: "200": description: Settings content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/InsightsSettings" } "401": { $ref: "#/components/responses/Unauthorized" } put: tags: [Insights] summary: Update insights settings (privacy mode) operationId: updateInsightsSettings requestBody: required: true content: application/json: schema: type: object properties: privacyMode: { type: string, enum: [aggregated, raw] } responses: "200": description: Updated content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: updated: { type: boolean } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } /api/insights/targets: get: tags: [Insights] summary: Per-metric target ranges + classification operationId: getInsightTargets responses: "200": description: Targets content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/InsightTargetsResponse" } "401": { $ref: "#/components/responses/Unauthorized" } /api/insights/blood-pressure-status: get: tags: [Insights] summary: BP-specific narrative status operationId: getBloodPressureStatus parameters: - $ref: "#/components/parameters/LocaleParam" responses: "200": description: BP status content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/InsightStatusBundle" } "401": { $ref: "#/components/responses/Unauthorized" } /api/insights/bmi-status: get: tags: [Insights] summary: BMI narrative status operationId: getBmiStatus parameters: - $ref: "#/components/parameters/LocaleParam" responses: "200": description: BMI status content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/InsightStatusBundle" } "401": { $ref: "#/components/responses/Unauthorized" } /api/insights/general-status: get: tags: [Insights] summary: General health narrative status operationId: getGeneralStatus parameters: - $ref: "#/components/parameters/LocaleParam" responses: "200": description: Status content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/InsightStatusBundle" } "401": { $ref: "#/components/responses/Unauthorized" } /api/insights/medication-compliance-status: get: tags: [Insights] summary: Medication compliance narrative status operationId: getMedicationComplianceStatus parameters: - $ref: "#/components/parameters/LocaleParam" responses: "200": description: Status content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/InsightStatusBundle" } "401": { $ref: "#/components/responses/Unauthorized" } /api/insights/mood-status: get: tags: [Insights] summary: Mood narrative status operationId: getMoodStatus parameters: - $ref: "#/components/parameters/LocaleParam" responses: "200": description: Status content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/InsightStatusBundle" } "401": { $ref: "#/components/responses/Unauthorized" } /api/insights/pulse-status: get: tags: [Insights] summary: Pulse narrative status operationId: getPulseStatus parameters: - $ref: "#/components/parameters/LocaleParam" responses: "200": description: Status content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/InsightStatusBundle" } "401": { $ref: "#/components/responses/Unauthorized" } /api/insights/weight-status: get: tags: [Insights] summary: Weight narrative status operationId: getWeightStatus parameters: - $ref: "#/components/parameters/LocaleParam" responses: "200": description: Status content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/InsightStatusBundle" } "401": { $ref: "#/components/responses/Unauthorized" } # ─── Achievements ───────────────────────────────────────────── /api/gamification/achievements: get: tags: [Achievements] summary: Get achievements progress + unlocks operationId: getAchievements responses: "200": description: Achievements content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/AchievementsResult" } "401": { $ref: "#/components/responses/Unauthorized" } # ─── DoctorReport ───────────────────────────────────────────── /api/doctor-report: post: tags: [DoctorReport] summary: Aggregated health data for doctor-report PDF operationId: getDoctorReport requestBody: required: false content: application/json: schema: type: object properties: days: type: integer minimum: 1 maximum: 365 default: 90 responses: "200": description: Report data content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/DoctorReportResponse" } "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } # ─── Onboarding ─────────────────────────────────────────────── /api/onboarding/complete: post: tags: [Onboarding] summary: Mark onboarding complete and save initial profile operationId: completeOnboarding requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/OnboardingInput" } responses: "200": description: Completed content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: completed: { type: boolean } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } # ─── Health probe ───────────────────────────────────────────── /api/health: get: tags: [Health] summary: Service liveness probe operationId: healthCheck security: [] responses: "200": description: Healthy content: application/json: schema: { $ref: "#/components/schemas/HealthStatus" } "503": description: Degraded content: application/json: schema: { $ref: "#/components/schemas/HealthStatus" } # ─── Tokens ────────────────────────────────────────────────── /api/tokens: get: tags: [Tokens] summary: List the user's API tokens (metadata only — secrets never returned) operationId: listTokens responses: "200": description: Tokens content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: array items: { $ref: "#/components/schemas/ApiTokenSummary" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } post: tags: [Tokens] summary: Create an API token (token returned ONCE) operationId: createToken requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateTokenInput" } responses: "201": description: Created (token visible once) content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: token: { type: string, example: "hlk_" } name: { type: string } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "422": { $ref: "#/components/responses/Unprocessable" } /api/tokens/{id}: parameters: - $ref: "#/components/parameters/IdPathParam" delete: tags: [Tokens] summary: Revoke an API token operationId: revokeToken responses: "200": description: Revoked content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: revoked: { type: boolean } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } # ─── Ingest (external) ──────────────────────────────────────── /api/ingest/medication: post: tags: [Ingest] summary: External medication intake ingest (Bearer token, idempotent) operationId: ingestMedicationIntake security: - bearerAuth: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ExternalIntakeInput" } responses: "200": description: Existing event returned (idempotent replay) content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/MedicationIntakeEvent" } "201": description: Created content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/MedicationIntakeEvent" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Unprocessable" } "429": { $ref: "#/components/responses/RateLimited" } # ─── Notifications ──────────────────────────────────────────── /api/notifications/preferences: get: tags: [Notifications] summary: List notification channels + preferences operationId: getNotificationPreferences responses: "200": description: Preferences content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/NotificationPreferencesResponse", } "401": { $ref: "#/components/responses/Unauthorized" } put: tags: [Notifications] summary: Toggle a single channel/event preference operationId: updateNotificationPreference requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/UpdateNotificationPreferenceInput" } responses: "200": description: Updated content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/NotificationPreference" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Unprocessable" } /api/notifications/vapid: get: tags: [Notifications] summary: Get VAPID public key (Web Push) operationId: getVapidKey security: [] responses: "200": description: VAPID key content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: publicKey: { type: string } "503": description: Not configured content: application/json: schema: { $ref: "#/components/schemas/ApiError" } /api/notifications/web-push: post: tags: [Notifications] summary: Subscribe a Web Push endpoint operationId: subscribeWebPush requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/WebPushSubscribeInput" } responses: "200": description: Subscribed content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: subscribed: { type: boolean } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } delete: tags: [Notifications] summary: Unsubscribe a Web Push endpoint operationId: unsubscribeWebPush requestBody: required: true content: application/json: schema: type: object properties: endpoint: { type: string, format: uri } required: [endpoint] responses: "200": description: Unsubscribed content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: unsubscribed: { type: boolean } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } # ─── User configuration ─────────────────────────────────────── /api/user/ai-provider: get: tags: [User] summary: Get user-level AI provider config operationId: getUserAiProvider responses: "200": description: Config content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/UserAiProvider" } "401": { $ref: "#/components/responses/Unauthorized" } patch: tags: [User] summary: Update user-level AI provider config operationId: updateUserAiProvider requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/UpdateUserAiProviderInput" } responses: "200": description: Updated content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: updated: { type: boolean } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } /api/user/thresholds: get: tags: [User] summary: Get effective + override threshold ranges operationId: getThresholds responses: "200": description: Thresholds content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/ThresholdsResponse" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } put: tags: [User] summary: Set per-metric threshold overrides (partial) operationId: updateThresholds requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ThresholdOverrides" } responses: "200": description: Updated content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: overrides: { $ref: "#/components/schemas/ThresholdOverrides" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } delete: tags: [User] summary: Reset threshold overrides (all or one metric) operationId: resetThresholds parameters: - in: query name: metric schema: { type: string } description: "If supplied, only this metric is reset; otherwise all overrides are cleared." responses: "200": description: Reset content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: overrides: { $ref: "#/components/schemas/ThresholdOverrides" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } # ─── Analytics + AI test ────────────────────────────────────── /api/analytics: get: tags: [Analytics] summary: Per-type measurement summaries + BMI + BP-in-target operationId: getAnalytics responses: "200": description: Analytics content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/AnalyticsResponse" } "401": { $ref: "#/components/responses/Unauthorized" } /api/ai/test: post: tags: [User] summary: Test the configured AI provider operationId: testAiProvider responses: "200": description: AI test result content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/AiTestResult" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } "429": { $ref: "#/components/responses/RateLimited" } "502": description: Upstream AI provider failure content: application/json: schema: { $ref: "#/components/schemas/ApiError" } # ─── Export / Import ────────────────────────────────────────── /api/export: get: tags: [Export] summary: Export user data (CSV or JSON) operationId: exportData parameters: - in: query name: format schema: { type: string, enum: [csv, json], default: json } - in: query name: type schema: type: string enum: [measurements, medications, intake, mood, all] default: all responses: "200": description: Export payload content: application/json: schema: type: object properties: data: type: object additionalProperties: true text/csv: schema: { type: string } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } "429": { $ref: "#/components/responses/RateLimited" } /api/import: post: tags: [Export] summary: Import previously exported data (JSON) operationId: importData requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ImportInput" } responses: "200": description: Import stats content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/ImportResult" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } # ─── Audit log + Feedback + Bug report ──────────────────────── /api/audit-log: get: tags: [AuditLog] summary: Personal audit log entries operationId: getAuditLog parameters: - $ref: "#/components/parameters/LimitParam" - $ref: "#/components/parameters/OffsetParam" responses: "200": description: Entries content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/AuditLogResponse" } "401": { $ref: "#/components/responses/Unauthorized" } /api/feedback: get: tags: [Feedback] summary: List the user's submitted feedback operationId: listFeedback parameters: - $ref: "#/components/parameters/LimitParam" - $ref: "#/components/parameters/OffsetParam" responses: "200": description: Feedback content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: items: type: array items: { $ref: "#/components/schemas/FeedbackSummary" } meta: { $ref: "#/components/schemas/PageMeta" } "401": { $ref: "#/components/responses/Unauthorized" } post: tags: [Feedback] summary: Submit feedback (with optional screenshot) operationId: submitFeedback requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateFeedbackInput" } responses: "201": description: Created content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/FeedbackSummary" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } "429": { $ref: "#/components/responses/RateLimited" } /api/bugreport: post: tags: [BugReport] summary: File a bug report (creates a GitHub issue when configured) operationId: submitBugReport requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/BugReportInput" } responses: "200": description: Issue created content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: issueNumber: { type: integer } issueUrl: { type: string, format: uri } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } "429": { $ref: "#/components/responses/RateLimited" } "500": { $ref: "#/components/responses/ServerError" } "503": description: Bug report not configured content: application/json: schema: { $ref: "#/components/schemas/ApiError" } /api/bugreport/status: get: tags: [BugReport] summary: Whether bug-report submission is configured operationId: getBugReportStatus responses: "200": description: Status content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: configured: { type: boolean } isAdmin: { type: boolean } "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } # ─── Settings (per-user) ────────────────────────────────────── /api/settings/account: delete: tags: [Settings] summary: Delete the user's account (and all data) operationId: deleteAccount responses: "200": description: Account deleted content: application/json: schema: { $ref: "#/components/schemas/DeletedEnvelope" } "401": { $ref: "#/components/responses/Unauthorized" } /api/settings/data: delete: tags: [Settings] summary: Wipe the user's data (keep account) operationId: wipeData responses: "200": description: Data wiped content: application/json: schema: { $ref: "#/components/schemas/DeletedEnvelope" } "401": { $ref: "#/components/responses/Unauthorized" } /api/settings/global-services: get: tags: [Settings] summary: Global service availability flags (telegram, ntfy, web-push, …) operationId: getGlobalServices responses: "200": description: Flags content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/GlobalServicesFlags" } "401": { $ref: "#/components/responses/Unauthorized" } /api/settings/moodlog: put: tags: [Settings] summary: Configure moodLog integration operationId: updateMoodLogSettings requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/MoodLogSettingsInput" } responses: "200": description: Updated content: application/json: schema: { $ref: "#/components/schemas/UpdatedEnvelope" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } delete: tags: [Settings] summary: Disconnect moodLog integration operationId: deleteMoodLogSettings responses: "200": description: Removed content: application/json: schema: { $ref: "#/components/schemas/DeletedEnvelope" } "401": { $ref: "#/components/responses/Unauthorized" } /api/settings/ntfy: get: tags: [Settings] summary: Get ntfy channel settings operationId: getNtfySettings responses: "200": description: Config content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/NtfySettings" } "401": { $ref: "#/components/responses/Unauthorized" } put: tags: [Settings] summary: Update ntfy channel settings operationId: updateNtfySettings requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/NtfySettingsInput" } responses: "200": description: Updated content: application/json: schema: { $ref: "#/components/schemas/UpdatedEnvelope" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } /api/settings/ntfy/test: post: tags: [Settings] summary: Send a test ntfy notification operationId: testNtfy responses: "200": description: Test result content: application/json: schema: { $ref: "#/components/schemas/ApiEnvelope" } "401": { $ref: "#/components/responses/Unauthorized" } /api/settings/reminder-thresholds: get: tags: [Settings] summary: Get reminder lateness thresholds (admin-defined defaults) operationId: getReminderThresholds responses: "200": description: Thresholds content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/ReminderThresholds" } "401": { $ref: "#/components/responses/Unauthorized" } /api/settings/telegram: get: tags: [Settings] summary: Get Telegram channel settings operationId: getTelegramSettings responses: "200": description: Config content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/TelegramSettings" } "401": { $ref: "#/components/responses/Unauthorized" } put: tags: [Settings] summary: Update Telegram channel settings operationId: updateTelegramSettings requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/TelegramSettingsInput" } responses: "200": description: Updated content: application/json: schema: { $ref: "#/components/schemas/UpdatedEnvelope" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } /api/settings/telegram/test: post: tags: [Settings] summary: Send a test Telegram message operationId: testTelegram responses: "200": description: Test result content: application/json: schema: { $ref: "#/components/schemas/ApiEnvelope" } "401": { $ref: "#/components/responses/Unauthorized" } # ─── Withings integration ──────────────────────────────────── /api/withings/connect: get: tags: [Integrations] summary: Begin Withings OAuth (302 redirect) operationId: withingsConnect responses: "302": description: Redirect to Withings "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } /api/withings/callback: get: tags: [Integrations] summary: Withings OAuth callback (302 to /settings) operationId: withingsCallback parameters: - in: query name: code schema: { type: string } - in: query name: state schema: { type: string } responses: "302": description: Redirect back to /settings with status /api/withings/disconnect: post: tags: [Integrations] summary: Disconnect Withings operationId: withingsDisconnect responses: "200": description: Disconnected content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: disconnected: { type: boolean } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } /api/withings/credentials: get: tags: [Integrations] summary: Whether Withings client credentials are stored operationId: getWithingsCredentialsStatus responses: "200": description: Status content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: hasCredentials: { type: boolean } "401": { $ref: "#/components/responses/Unauthorized" } put: tags: [Integrations] summary: Save Withings client ID + secret (encrypted) operationId: saveWithingsCredentials requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/WithingsCredentialsInput" } responses: "200": description: Saved content: application/json: schema: { $ref: "#/components/schemas/UpdatedEnvelope" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } delete: tags: [Integrations] summary: Delete Withings credentials (and disconnect) operationId: deleteWithingsCredentials responses: "200": description: Deleted content: application/json: schema: { $ref: "#/components/schemas/DeletedEnvelope" } "401": { $ref: "#/components/responses/Unauthorized" } /api/withings/status: get: tags: [Integrations] summary: Withings connection status operationId: withingsStatus responses: "200": description: Status content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/WithingsStatus" } "401": { $ref: "#/components/responses/Unauthorized" } /api/withings/sync: post: tags: [Integrations] summary: Trigger a Withings sync (incremental or full) operationId: withingsSync requestBody: required: false content: application/json: schema: type: object properties: fullSync: { type: boolean, default: false } responses: "200": description: Sync result content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: imported: { type: integer } fullSync: { type: boolean } "401": { $ref: "#/components/responses/Unauthorized" } /api/withings/webhook: post: tags: [Webhooks] summary: Withings webhook (notification of new measurements) operationId: withingsWebhook security: [] parameters: - in: query name: secret schema: { type: string } description: "Shared secret for webhook authentication." responses: "200": description: Acknowledged content: application/json: schema: type: object properties: status: { type: string } "401": description: Unauthorized "429": description: Rate limited get: tags: [Webhooks] summary: Withings webhook URL verification operationId: withingsWebhookVerifyGet security: [] parameters: - in: query name: secret schema: { type: string } responses: "200": description: OK content: application/json: schema: type: object properties: status: { type: string } "401": description: Unauthorized head: tags: [Webhooks] summary: Withings webhook URL verification (HEAD) operationId: withingsWebhookVerifyHead security: [] parameters: - in: query name: secret schema: { type: string } responses: "200": description: OK "401": description: Unauthorized # ─── moodLog integration ───────────────────────────────────── /api/integrations/moodlog/status: get: tags: [Integrations] summary: moodLog connection status operationId: moodLogStatus responses: "200": description: Status content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/MoodLogStatus" } "401": { $ref: "#/components/responses/Unauthorized" } /api/integrations/moodlog/sync: post: tags: [Integrations] summary: Trigger a moodLog sync operationId: moodLogSync requestBody: required: false content: application/json: schema: type: object properties: fullSync: { type: boolean, default: false } responses: "200": description: Sync result content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: imported: { type: integer } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/integrations/moodlog/webhook: post: tags: [Webhooks] summary: Inbound webhook from moodLog operationId: moodLogWebhook security: [] parameters: - in: header name: X-Webhook-Secret required: true schema: { type: string } requestBody: required: true content: application/json: schema: type: object additionalProperties: true responses: "200": description: Acknowledged "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } /api/telegram/webhook: post: tags: [Webhooks] summary: Telegram inbound webhook operationId: telegramWebhook security: [] requestBody: required: true content: application/json: schema: type: object additionalProperties: true responses: "200": description: Acknowledged get: tags: [Webhooks] summary: Telegram webhook health probe operationId: telegramWebhookGet security: [] responses: "200": description: OK # ─── Public monitoring / proxies ───────────────────────────── /api/monitoring/csp-report: post: tags: [Monitoring] summary: Browser CSP violation report sink operationId: cspReport security: [] requestBody: required: true content: application/csp-report: schema: type: object additionalProperties: true application/json: schema: type: object additionalProperties: true responses: "204": description: Accepted (no body) /api/monitoring/glitchtip: post: tags: [Monitoring] summary: Forward errors to Glitchtip operationId: glitchtipReport security: [] requestBody: required: true content: application/json: schema: type: object additionalProperties: true responses: "204": description: Accepted /api/monitoring/settings: get: tags: [Monitoring] summary: Public monitoring settings (umami / glitchtip flags) operationId: getMonitoringSettings security: [] responses: "200": description: Public settings content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/PublicMonitoringSettings", } /api/monitoring/umami-script: get: tags: [Monitoring] summary: Server-side proxy for the Umami tracker script operationId: getUmamiScript security: [] responses: "200": description: Tracker script content: application/javascript: schema: { type: string } /api/send: post: tags: [Monitoring] summary: Server-side proxy for Umami /api/send operationId: umamiProxySend security: [] requestBody: required: true content: application/json: schema: type: object additionalProperties: true responses: "200": description: Forwarded "204": description: No-op when umami is disabled "413": description: Payload too large # ─── Admin (internal) ──────────────────────────────────────── /api/admin/ai-settings: get: tags: [Admin] security: - sessionCookie: [] summary: "Internal: get admin-managed AI fallback settings" operationId: adminGetAiSettings responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } put: tags: [Admin] security: - sessionCookie: [] summary: "Internal: update admin-managed AI fallback settings" operationId: adminUpdateAiSettings requestBody: required: true content: application/json: schema: { type: object, additionalProperties: true } responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "422": { $ref: "#/components/responses/Unprocessable" } /api/admin/audit-log: get: tags: [Admin] security: - sessionCookie: [] summary: "Internal: cross-user audit log" operationId: adminGetAuditLog parameters: - $ref: "#/components/parameters/LimitParam" - $ref: "#/components/parameters/OffsetParam" responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/admin/data: delete: tags: [Admin] security: - sessionCookie: [] summary: "Internal: wipe per-user or all data" operationId: adminWipeData responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/admin/feedback: get: tags: [Admin] security: - sessionCookie: [] summary: "Internal: list all feedback" operationId: adminListFeedback parameters: - $ref: "#/components/parameters/LimitParam" - $ref: "#/components/parameters/OffsetParam" responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/admin/feedback/{id}: parameters: - $ref: "#/components/parameters/IdPathParam" patch: tags: [Admin] security: - sessionCookie: [] summary: "Internal: update feedback status / admin note" operationId: adminUpdateFeedback requestBody: required: true content: application/json: schema: { type: object, additionalProperties: true } responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } delete: tags: [Admin] security: - sessionCookie: [] summary: "Internal: delete feedback" operationId: adminDeleteFeedback responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } /api/admin/feedback/{id}/github: parameters: - $ref: "#/components/parameters/IdPathParam" post: tags: [Admin] security: - sessionCookie: [] summary: "Internal: escalate feedback to a GitHub issue" operationId: adminEscalateFeedback responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } /api/admin/monitoring/glitchtip-test: post: tags: [Admin] security: - sessionCookie: [] summary: "Internal: send Glitchtip test event" operationId: adminGlitchtipTest responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/admin/monitoring/umami-test: post: tags: [Admin] security: - sessionCookie: [] summary: "Internal: send Umami test ping" operationId: adminUmamiTest responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/admin/notifications/reminder-check: post: tags: [Admin] security: - sessionCookie: [] summary: "Internal: trigger medication reminder evaluation" operationId: adminTriggerReminderCheck responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/admin/notifications/test: post: tags: [Admin] security: - sessionCookie: [] summary: "Internal: send a test notification" operationId: adminTestNotification responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/admin/settings: get: tags: [Admin] security: - sessionCookie: [] summary: "Internal: get app-wide settings" operationId: adminGetSettings responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } put: tags: [Admin] security: - sessionCookie: [] summary: "Internal: update app-wide settings" operationId: adminUpdateSettings requestBody: required: true content: application/json: schema: { type: object, additionalProperties: true } responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "422": { $ref: "#/components/responses/Unprocessable" } /api/admin/status: get: tags: [Admin] security: - sessionCookie: [] summary: "Internal: deep system status" operationId: adminStatus responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/admin/tokens: get: tags: [Admin] security: - sessionCookie: [] summary: "Internal: list all API tokens (cross-user)" operationId: adminListTokens responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/admin/users: get: tags: [Admin] security: - sessionCookie: [] summary: "Internal: list all users" operationId: adminListUsers responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/admin/users/{id}: parameters: - $ref: "#/components/parameters/IdPathParam" put: tags: [Admin] security: - sessionCookie: [] summary: "Internal: update a user (username/email/role)" operationId: adminUpdateUser requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/AdminUpdateUserInput" } responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Unprocessable" } /api/admin/users/{id}/reset-password: parameters: - $ref: "#/components/parameters/IdPathParam" post: tags: [Admin] security: - sessionCookie: [] summary: "Internal: reset a user's password" operationId: adminResetUserPassword responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } /api/admin/status-overview: get: tags: [Admin] security: - sessionCookie: [] summary: "Internal: aggregator for the 6-card status grid (v1.4)" description: | Returns the precomputed status payload the admin top strip renders — users / integrations / monitoring / backups / maintenance / audit-log summaries with severity already resolved server-side. Single batched query, no N+1. operationId: adminStatusOverview responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/admin/backup/test: post: tags: [Admin] security: - sessionCookie: [] summary: "Internal: probe the configured S3-compatible backup target (v1.4)" description: | Round-trips a small object against the bucket using the configured `BACKUP_S3_*` credentials. Returns latency and the sanitised endpoint host on success; never echoes the access key. operationId: adminBackupTest responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "422": { $ref: "#/components/responses/Unprocessable" } # ─── Public + v1.4 additions ───────────────────────────────────────── /api/version: get: tags: [Public] summary: "Public: running version + build SHA + license" description: | Public, unauthenticated. Returns `{ version, buildSha, builtAt, license, repository, changelog, docs }`. Used by the docker container healthcheck and the Settings → About surface's "Check for updates" button. operationId: getVersion responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } /api/integrations/withings/test: post: tags: [Withings] security: - sessionCookie: [] summary: "Probe the saved Withings OAuth connection (v1.4)" operationId: withingsTest responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } "429": { $ref: "#/components/responses/RateLimited" } /api/integrations/moodlog/test: post: tags: [Moodlog] security: - sessionCookie: [] summary: "Probe moodLog.app reachability (v1.4)" description: | SSRF-guarded HEAD against the configured moodLog endpoint. Returns `{ ok, latency_ms, errorCode? }` so the UI can localise the failure message. operationId: moodlogTest responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } /api/notifications/web-push/test: post: tags: [Notifications] security: - sessionCookie: [] summary: "Send a test Web Push to the current user (v1.4)" operationId: webPushTest responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } "429": { $ref: "#/components/responses/RateLimited" } /api/monitoring/glitchtip/test: post: tags: [Monitoring] security: - sessionCookie: [] summary: "Probe the Glitchtip ingest endpoint (v1.4)" operationId: glitchtipTest responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } "429": { $ref: "#/components/responses/RateLimited" } /api/monitoring/umami/test: post: tags: [Monitoring] security: - sessionCookie: [] summary: "Verify the Umami script + website ID resolve (v1.4)" operationId: umamiTest responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } "429": { $ref: "#/components/responses/RateLimited" } /api/auth/refresh: post: tags: [Auth] summary: "Native client: refresh-token rotation (v1.4)" description: | Exchanges a refresh token (`hlr_<64hex>`) for a fresh 24h access token and a new refresh token. Cookie-session browsers do not use this. Reuse-detection: presenting the same refresh token a second time revokes every refresh token for the user. operationId: authRefresh requestBody: required: true content: application/json: schema: type: object required: [refreshToken] properties: refreshToken: { type: string, pattern: "^hlr_[A-Fa-f0-9]{64}$" } revoke: type: boolean default: false description: "When true, revokes the refresh row + paired access token instead of rotating." responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } "429": { $ref: "#/components/responses/RateLimited" } /api/auth/refresh/revoke: post: tags: [Auth] summary: "Native client: revoke a refresh token + paired access token (v1.4)" operationId: authRefreshRevoke requestBody: required: true content: application/json: schema: type: object required: [refreshToken] properties: refreshToken: { type: string, pattern: "^hlr_[A-Fa-f0-9]{64}$" } responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ApiEnvelope" } }, }, } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } # ─── iOS adapter endpoints (v1.3) ──────────────────────────────────── /api/dashboard/summary: get: tags: [Dashboard] summary: "iOS: aggregated dashboard summary" description: | Single-call aggregate for the iOS DashboardSummary view. Combines greeting, current/longest activity-day streak, today's medication compliance, optional highlight insight, and per-metric latest + 7-day sparkline + trend. operationId: getDashboardSummary responses: "200": description: Dashboard summary content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/DashboardSummary" } "401": { $ref: "#/components/responses/Unauthorized" } /api/measurements/series: get: tags: [Measurements] summary: "iOS: time-series + summary stats for a metric kind" operationId: getMeasurementSeries parameters: - in: query name: kind required: true schema: { $ref: "#/components/schemas/MetricKind" } - in: query name: days schema: type: integer minimum: 1 maximum: 365 default: 30 responses: "200": description: Series payload content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/MeasurementSeries" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } /api/integrations/healthkit: get: tags: [Integrations] summary: "iOS: HealthKit per-metric direction config" operationId: getHealthKitConfig responses: "200": description: HealthKit config content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/HealthKitConfig" } "401": { $ref: "#/components/responses/Unauthorized" } patch: tags: [Integrations] summary: "iOS: update HealthKit per-metric direction config" operationId: updateHealthKitConfig requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/HealthKitConfigPatch" } responses: "200": description: Updated config content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/HealthKitConfig" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } /api/user/profile: get: tags: [Auth] summary: "iOS: flattened user profile" operationId: getUserProfile responses: "200": description: User profile content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/IosUserProfile" } "401": { $ref: "#/components/responses/Unauthorized" } patch: tags: [Auth] summary: "iOS: update profile fields" operationId: updateUserProfile requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/IosUserProfilePatch" } responses: "200": description: Updated profile content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: { $ref: "#/components/schemas/IosUserProfile" } "401": { $ref: "#/components/responses/Unauthorized" } "409": { $ref: "#/components/responses/Conflict" } "422": { $ref: "#/components/responses/Unprocessable" } /api/devices: post: tags: [Notifications] summary: "iOS: register an APNs device token" operationId: registerDevice requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/DeviceRegisterInput" } responses: "201": description: Device registered (or token transferred) content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: id: { type: string } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } /api/medications/intake: get: tags: [Medications] summary: "iOS: today list or compliance buckets" operationId: listIntakeAggregator parameters: - in: query name: scope required: true schema: { type: string, enum: [today, compliance] } - in: query name: days schema: type: integer minimum: 1 maximum: 365 default: 30 responses: "200": description: Aggregator payload content: application/json: schema: { $ref: "#/components/schemas/ApiEnvelope" } "401": { $ref: "#/components/responses/Unauthorized" } "422": { $ref: "#/components/responses/Unprocessable" } post: tags: [Medications] summary: "iOS: update an intake event by id (taken/skipped/snoozed)" operationId: updateIntakeAggregator requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/IntakeUpdateInput" } responses: "200": description: Updated intake event content: application/json: schema: { $ref: "#/components/schemas/ApiEnvelope" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } "422": { $ref: "#/components/responses/Unprocessable" } /api/insights/cards: get: tags: [Insights] summary: "iOS: rule-based insight cards" operationId: getInsightCards responses: "200": description: Insight cards content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: array items: { $ref: "#/components/schemas/InsightCard" } "401": { $ref: "#/components/responses/Unauthorized" } /api/insights/correlations: get: tags: [Insights] summary: "iOS: cross-metric correlations (placeholder)" operationId: getInsightCorrelations description: | Returns an empty array until the correlations engine is wired up (Phase 7). Audited via `insights.correlations.empty`. responses: "200": description: Correlations content: application/json: schema: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: array items: { $ref: "#/components/schemas/InsightCorrelation" } "401": { $ref: "#/components/responses/Unauthorized" } components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: "hlk_" description: | Long-lived API token issued via `POST /api/tokens`. Accepted on all user endpoints (interchangeable with the session cookie) and required on external ingest endpoints (e.g. `/api/ingest/medication`). Tokens are prefixed `hlk_` and stored as HMAC-SHA-256 hashes server-side (keyed with `API_TOKEN_HMAC_KEY` so a DB dump alone does not allow precomputed-rainbow lookups). Bearer tokens never grant admin access — admin routes require the session cookie. Permission scopes are enforced from the token's `permissions` array; a token MUST declare either the explicit required permission for the route or the wildcard `"*"`. sessionCookie: type: apiKey in: cookie name: healthlog_session description: | HttpOnly session cookie set by `/api/auth/login` and the passkey login flow. Authenticates the web app and (optionally) the iOS client when used in a web view. parameters: IdPathParam: in: path name: id required: true schema: type: string description: "CUID identifier." LimitParam: in: query name: limit schema: type: integer minimum: 1 maximum: 500 default: 100 OffsetParam: in: query name: offset schema: type: integer minimum: 0 default: 0 LocaleParam: in: query name: locale schema: type: string enum: [de, en] description: "Override the resolved locale for narrative text." responses: BadRequest: description: Bad request content: application/json: schema: { $ref: "#/components/schemas/ApiError" } Unauthorized: description: Authentication required content: application/json: schema: { $ref: "#/components/schemas/ApiError" } Forbidden: description: Forbidden content: application/json: schema: { $ref: "#/components/schemas/ApiError" } NotFound: description: Not found content: application/json: schema: { $ref: "#/components/schemas/ApiError" } Conflict: description: Conflict (e.g. duplicate) content: application/json: schema: { $ref: "#/components/schemas/ApiError" } Unprocessable: description: Validation failure content: application/json: schema: { $ref: "#/components/schemas/ApiError" } RateLimited: description: Rate limit exceeded content: application/json: schema: { $ref: "#/components/schemas/ApiError" } ServerError: description: Internal server error content: application/json: schema: { $ref: "#/components/schemas/ApiError" } schemas: # ─── Envelope / errors ───────────────────────────────────── ApiEnvelope: type: object description: Standard envelope. On success `error` is `null`; on failure `data` is `null` and `error` is a string. properties: data: description: Response payload (typed per-endpoint). error: type: [string, "null"] required: [data, error] ApiError: type: object properties: data: type: "null" error: type: string required: [data, error] DeletedEnvelope: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: deleted: { type: boolean } UpdatedEnvelope: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: updated: { type: boolean } PageMeta: type: object properties: total: { type: integer } limit: { type: integer } offset: { type: integer } required: [total, limit, offset] # ─── Enums ───────────────────────────────────────────────── MeasurementType: type: string enum: - WEIGHT - BLOOD_PRESSURE_SYS - BLOOD_PRESSURE_DIA - PULSE - BODY_FAT - SLEEP_DURATION - ACTIVITY_STEPS - BLOOD_GLUCOSE - TOTAL_BODY_WATER - BONE_MASS - OXYGEN_SATURATION MeasurementSource: type: string enum: [MANUAL, WITHINGS, IMPORT] GlucoseContext: type: string enum: [FASTING, POSTPRANDIAL, RANDOM, BEDTIME] IntakeSource: type: string enum: [WEB, API, REMINDER, IMPORT] ReminderPhase: type: string enum: [GREEN, YELLOW, ORANGE, RED] PhaseMode: type: string enum: [MINUTES, PERCENT] UserRole: type: string enum: [ADMIN, USER] Gender: type: string enum: [MALE, FEMALE] MoodLevel: type: string enum: [SUPER_GUT, GUT, OKAY, SCHLECHT, LAUSIG] MedicationCategory: type: string enum: - BLOOD_PRESSURE - VITAMIN - SUPPLEMENT - PAIN_RELIEF - ALLERGY - DIGESTIVE - THYROID - HORMONE - SKIN - SLEEP_AID - OTHER NotificationChannelType: type: string enum: [TELEGRAM, NTFY, WEB_PUSH] AiProviderType: type: string enum: [OPENAI, ANTHROPIC, LOCAL, CHATGPT_OAUTH] InsightSeverity: type: string enum: [normal, mild, moderate, severe, info] Trend: type: [string, "null"] enum: [up, down, stable, null] # ─── Auth DTOs ───────────────────────────────────────────── RegisterInput: type: object properties: email: { type: string, format: email } username: type: string minLength: 3 maxLength: 30 pattern: "^[a-zA-Z0-9_-]+$" password: type: string minLength: 12 required: [email, username, password] LoginInput: type: object properties: email: type: string description: Email or username. password: { type: string } required: [email, password] LoginResponseEnvelope: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: user: type: object properties: id: { type: string } username: { type: string } required: [id, username] token: type: string description: | Long-lived `hlk_*` Bearer token. Only returned when the request carries `X-Client-Type: native` or a User-Agent starting with `HealthLog-iOS`. Use it for subsequent requests as `Authorization: Bearer `. tokenExpiresAt: type: string format: date-time description: ISO timestamp at which the auto-issued token expires. RegisterResponseEnvelope: allOf: - $ref: "#/components/schemas/ApiEnvelope" - type: object properties: data: type: object properties: user: type: object properties: id: { type: string } username: { type: string } email: { type: [string, "null"] } ProfileInput: type: object properties: email: { type: [string, "null"], format: email } heightCm: type: [number, "null"] minimum: 50 maximum: 300 dateOfBirth: type: [string, "null"] format: date gender: oneOf: - $ref: "#/components/schemas/Gender" - type: "null" ChangePasswordInput: type: object properties: currentPassword: { type: string } newPassword: { type: string, minLength: 12 } confirmPassword: { type: string } required: [currentPassword, newPassword, confirmPassword] User: type: object description: Public-facing user shape. properties: id: { type: string } username: { type: string } email: { type: [string, "null"], format: email } role: { $ref: "#/components/schemas/UserRole" } heightCm: { type: [number, "null"] } dateOfBirth: { type: [string, "null"], format: date-time } gender: oneOf: - $ref: "#/components/schemas/Gender" - type: "null" timezone: { type: string } required: [id, username, role, timezone] CurrentUser: allOf: - $ref: "#/components/schemas/User" - type: object properties: onboardingCompletedAt: type: [string, "null"] format: date-time gravatarUrl: type: [string, "null"] glucoseUnit: type: [string, "null"] enum: ["mg/dL", "mmol/L", null] # ─── Passkey DTOs ────────────────────────────────────────── PasskeyRegisterOptions: type: object description: SimpleWebAuthn `PublicKeyCredentialCreationOptionsJSON` plus a server-issued challenge id. properties: options: type: object additionalProperties: true description: WebAuthn registration options. challengeId: { type: string } required: [options, challengeId] PasskeyLoginOptions: type: object properties: options: type: object additionalProperties: true description: WebAuthn authentication options. challengeId: { type: string } required: [options, challengeId] PasskeyVerifyRegistrationInput: type: object properties: challengeId: { type: string } credential: type: object additionalProperties: true description: "`RegistrationResponseJSON` from `@simplewebauthn/browser`." required: [challengeId, credential] PasskeyVerifyLoginInput: type: object properties: challengeId: { type: string } credential: type: object additionalProperties: true description: "`AuthenticationResponseJSON` from `@simplewebauthn/browser`." required: [challengeId, credential] PasskeySummary: type: object properties: id: { type: string } name: { type: string } credentialDeviceType: { type: string } credentialBackedUp: { type: boolean } createdAt: { type: string, format: date-time } required: [id, name, credentialDeviceType, credentialBackedUp, createdAt] # ─── Measurement DTOs ────────────────────────────────────── Measurement: type: object properties: id: { type: string } userId: { type: string } type: { $ref: "#/components/schemas/MeasurementType" } value: { type: number } unit: { type: string } source: { $ref: "#/components/schemas/MeasurementSource" } measuredAt: { type: string, format: date-time } notes: { type: [string, "null"], maxLength: 25 } externalId: { type: [string, "null"] } glucoseContext: oneOf: - $ref: "#/components/schemas/GlucoseContext" - type: "null" createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } required: [ id, userId, type, value, unit, source, measuredAt, createdAt, updatedAt, ] CreateMeasurementInput: type: object properties: type: { $ref: "#/components/schemas/MeasurementType" } value: { type: number } measuredAt: { type: string, format: date-time } notes: { type: string, maxLength: 25 } source: allOf: [{ $ref: "#/components/schemas/MeasurementSource" }] default: MANUAL glucoseContext: $ref: "#/components/schemas/GlucoseContext" required: [type, value, measuredAt] UpdateMeasurementInput: type: object properties: value: { type: number, minimum: 0, maximum: 500000 } measuredAt: { type: string, format: date-time } notes: { type: [string, "null"], maxLength: 25 } # ─── Medication DTOs ─────────────────────────────────────── MedicationSchedule: type: object properties: id: { type: string } medicationId: { type: string } windowStart: type: string pattern: '^([01]\d|2[0-3]):([0-5]\d)$' description: HH:mm windowEnd: type: string pattern: '^([01]\d|2[0-3]):([0-5]\d)$' label: { type: [string, "null"] } dose: { type: [string, "null"], description: "Per-schedule dose override" } daysOfWeek: type: [string, "null"] description: "Serialized recurrence (CSV of 0..6, optional intervalWeeks suffix)." required: [id, medicationId, windowStart, windowEnd] Medication: type: object properties: id: { type: string } userId: { type: string } name: { type: string } dose: { type: string } active: { type: boolean } notificationsEnabled: { type: boolean } pausedAt: { type: [string, "null"], format: date-time } snoozedUntil: { type: [string, "null"], format: date-time } createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } required: [ id, userId, name, dose, active, notificationsEnabled, createdAt, updatedAt, ] MedicationWithSchedules: allOf: - $ref: "#/components/schemas/Medication" - type: object properties: schedules: type: array items: { $ref: "#/components/schemas/MedicationSchedule" } category: { $ref: "#/components/schemas/MedicationCategory" } lastTakenAt: type: [string, "null"] format: date-time todayEventCount: type: integer MedicationScheduleInput: type: object properties: windowStart: { type: string, pattern: '^([01]\d|2[0-3]):([0-5]\d)$' } windowEnd: { type: string, pattern: '^([01]\d|2[0-3]):([0-5]\d)$' } label: { type: string, maxLength: 50 } dose: { type: string, maxLength: 50 } daysOfWeek: type: array items: { type: integer, minimum: 0, maximum: 6 } intervalWeeks: type: integer minimum: 1 maximum: 4 required: [windowStart, windowEnd] CreateMedicationInput: type: object properties: name: { type: string, minLength: 1, maxLength: 100 } dose: { type: string, minLength: 1, maxLength: 50 } category: { $ref: "#/components/schemas/MedicationCategory" } schedules: type: array minItems: 1 items: { $ref: "#/components/schemas/MedicationScheduleInput" } required: [name, dose, schedules] UpdateMedicationInput: type: object properties: name: { type: string, minLength: 1, maxLength: 100 } dose: { type: string, minLength: 1, maxLength: 50 } category: { $ref: "#/components/schemas/MedicationCategory" } active: { type: boolean } notificationsEnabled: { type: boolean } schedules: type: array items: { $ref: "#/components/schemas/MedicationScheduleInput" } MedicationIntakeEvent: type: object properties: id: { type: string } userId: { type: string } medicationId: { type: string } scheduledFor: { type: string, format: date-time } takenAt: { type: [string, "null"], format: date-time } skipped: { type: boolean } source: { $ref: "#/components/schemas/IntakeSource" } idempotencyKey: { type: [string, "null"] } createdAt: { type: string, format: date-time } required: [id, userId, medicationId, scheduledFor, skipped, source, createdAt] CreateIntakeInput: type: object properties: scheduledFor: { type: string, format: date-time } takenAt: { type: string, format: date-time } skipped: { type: boolean, default: false } idempotencyKey: { type: string, maxLength: 128 } UpdateIntakeInput: type: object properties: takenAt: { type: [string, "null"], format: date-time } skipped: { type: boolean } scheduledFor: { type: string, format: date-time } IntakeImportEntry: type: object properties: datum: type: string pattern: '^\d{4}-\d{2}-\d{2}$' description: YYYY-MM-DD uhrzeit: type: string pattern: '^\d{2}:\d{2}:\d{2}$' description: HH:mm:ss zaehler: oneOf: - { type: integer } - { type: string } required: [datum, uhrzeit] IntakeImportResult: type: object properties: imported: { type: integer } skippedDuplicates: { type: integer } skippedInvalid: { type: integer } ExternalIntakeInput: type: object properties: medicationName: { type: string, maxLength: 200 } takenAt: { type: string, format: date-time } idempotencyKey: { type: string, maxLength: 128 } required: [medicationName, idempotencyKey] DailyComplianceEntry: type: object properties: expected: { type: integer } taken: { type: integer } skipped: { type: integer } onTime: { type: integer } late: { type: integer } veryLate: { type: integer } ComplianceSummary: type: object properties: rate: { type: number, description: "Fraction 0..1" } taken: { type: integer } skipped: { type: integer } missed: { type: integer } streak: { type: integer } MedicationComplianceResponse: type: object properties: compliance7: { $ref: "#/components/schemas/ComplianceSummary" } compliance30: { $ref: "#/components/schemas/ComplianceSummary" } dailyCompliance: type: object additionalProperties: { $ref: "#/components/schemas/DailyComplianceEntry" } ReminderPhaseConfig: type: object properties: greenValue: { type: integer, default: 60 } greenMode: { allOf: [{ $ref: "#/components/schemas/PhaseMode" }], default: MINUTES, } yellowValue: { type: integer, default: 30 } yellowMode: { allOf: [{ $ref: "#/components/schemas/PhaseMode" }], default: MINUTES, } orangeValue: { type: integer, default: 0 } orangeMode: { allOf: [{ $ref: "#/components/schemas/PhaseMode" }], default: MINUTES, } redValue: { type: integer, default: 240 } redMode: { allOf: [{ $ref: "#/components/schemas/PhaseMode" }], default: MINUTES, } MedicationApiEndpointResponse: type: object properties: enabled: { type: boolean } activeTokenCount: { type: integer } token: type: [string, "null"] description: "Plaintext token, only returned once on creation." created: { type: boolean } revokedTokenCount: { type: integer } IntakeSummaryPoint: type: object properties: date: type: string description: YYYY-MM-DD (Berlin local). total: { type: integer } medications: type: object additionalProperties: { type: integer } required: [date, total] IntakeSummaryResponse: type: object properties: points: type: array items: { $ref: "#/components/schemas/IntakeSummaryPoint" } medications: type: array items: { type: string } # ─── Mood DTOs ───────────────────────────────────────────── MoodEntry: type: object properties: id: { type: string } userId: { type: string } date: type: string description: YYYY-MM-DD mood: { $ref: "#/components/schemas/MoodLevel" } score: type: integer minimum: 1 maximum: 5 tags: type: array items: { type: string } source: { type: string, default: MANUAL } moodLoggedAt: { type: string, format: date-time } syncedAt: { type: string, format: date-time } createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } required: [id, userId, date, mood, score, source, moodLoggedAt] CreateMoodEntryInput: type: object properties: mood: { $ref: "#/components/schemas/MoodLevel" } moodLoggedAt: { type: string, format: date-time } tags: type: array items: { type: string } source: { type: string } required: [mood, moodLoggedAt] UpdateMoodEntryInput: type: object properties: mood: { $ref: "#/components/schemas/MoodLevel" } moodLoggedAt: { type: string, format: date-time } tags: type: array items: { type: string } MoodAnalytics: type: object properties: entries: type: array items: type: object properties: date: { type: string } score: { type: number } samples: { type: integer } summary: type: object additionalProperties: true # ─── Dashboard DTOs ──────────────────────────────────────── DashboardWidget: type: object properties: id: type: string enum: [ weight, bp, pulse, bodyFat, mood, medications, sleep, steps, glucose, bpInTarget, ] visible: { type: boolean } order: { type: integer, minimum: 0, maximum: 99 } required: [id, visible, order] DashboardLayout: type: object properties: version: { type: integer, enum: [1] } widgets: type: array minItems: 1 maxItems: 20 items: { $ref: "#/components/schemas/DashboardWidget" } required: [version, widgets] # ─── Insights DTOs ───────────────────────────────────────── Classification: type: object properties: category: { type: string } color: { type: string } severity: { $ref: "#/components/schemas/InsightSeverity" } SummaryStats: type: object properties: latest: { type: [number, "null"] } avg7: { type: [number, "null"] } avg30: { type: [number, "null"] } min: { type: [number, "null"] } max: { type: [number, "null"] } count: { type: integer } slope30: type: [object, "null"] properties: slope: { type: number } r2: { type: number } anomalyCount: { type: integer } CorrelationResult: type: [object, "null"] properties: r: { type: number } strength: { type: string } n: { type: integer } Alert: type: object properties: severity: { $ref: "#/components/schemas/InsightSeverity" } message: { type: string } type: { type: string } BpTargets: type: [object, "null"] properties: sysLow: { type: integer } sysHigh: { type: integer } diaLow: { type: integer } diaHigh: { type: integer } ComprehensiveInsights: type: object properties: summaries: type: object additionalProperties: { $ref: "#/components/schemas/SummaryStats" } bmi: { type: [number, "null"] } bmiClassification: oneOf: - $ref: "#/components/schemas/Classification" - type: "null" bpClassification: oneOf: - $ref: "#/components/schemas/Classification" - type: "null" bpPctInTarget: { type: [integer, "null"] } bpTargets: { $ref: "#/components/schemas/BpTargets" } weightBpCorrelation: { $ref: "#/components/schemas/CorrelationResult" } scatterData: type: array items: type: object properties: weight: { type: number } sysBP: { type: number } bpMedicationCorrelation: type: [object, "null"] properties: r: { type: number } strength: { type: string } n: { type: integer } medicationCount: { type: integer } bpMedicationScatterData: type: array items: type: object properties: continuityPct: { type: integer } sysBP: { type: number } moodSummary: oneOf: - $ref: "#/components/schemas/SummaryStats" - type: "null" moodBpCorrelation: { $ref: "#/components/schemas/CorrelationResult" } moodBpScatterData: type: array items: type: object properties: mood: { type: number } sysBP: { type: number } moodWeightCorrelation: { $ref: "#/components/schemas/CorrelationResult" } moodWeightScatterData: type: array items: type: object properties: mood: { type: number } weight: { type: number } moodPulseCorrelation: { $ref: "#/components/schemas/CorrelationResult" } moodPulseScatterData: type: array items: type: object properties: mood: { type: number } pulse: { type: number } medications: type: array items: type: object properties: id: { type: string } name: { type: string } dose: { type: string } category: { $ref: "#/components/schemas/MedicationCategory" } compliance7: { type: number } compliance30: { type: number } streak: { type: integer } taken7: { type: integer } skipped7: { type: integer } missed7: { type: integer } alerts: type: array items: { $ref: "#/components/schemas/Alert" } hasProvider: { type: boolean } dataSpanDays: { type: integer } totalMeasurements: { type: integer } InsightTargetItem: type: object properties: type: { type: string } label: { type: string } current: { type: [number, "null"] } average30: { type: [number, "null"] } trend: { $ref: "#/components/schemas/Trend" } unit: { type: string } range: type: [object, "null"] properties: min: { type: number } max: { type: number } classification: oneOf: - $ref: "#/components/schemas/Classification" - type: "null" source: { type: string } details: type: object properties: medications: type: array items: type: object properties: name: { type: string } compliance7: { type: number } compliance30: { type: number } InsightTargetsResponse: type: object properties: targets: type: array items: { $ref: "#/components/schemas/InsightTargetItem" } bpDiastolic: type: object properties: current: { type: [number, "null"] } average30: { type: [number, "null"] } range: type: [object, "null"] properties: min: { type: number } max: { type: number } profile: type: object properties: heightCm: { type: [number, "null"] } age: { type: [integer, "null"] } gender: oneOf: - $ref: "#/components/schemas/Gender" - type: "null" glucoseUnit: { type: string } InsightStatusBundle: type: object description: Narrative status payload (shape varies per status route). additionalProperties: true InsightGenerateResult: type: object properties: insights: type: object additionalProperties: true cached: { type: boolean } cachedAt: { type: string, format: date-time } InsightsSettings: type: object properties: codexStatus: { type: string, enum: [connected, disconnected] } codexConnectedAt: { type: [string, "null"], format: date-time } hasAdminKey: { type: boolean } privacyMode: { type: string, enum: [aggregated, raw] } lastInsightAt: { type: [string, "null"], format: date-time } # ─── Achievements ────────────────────────────────────────── Achievement: type: object properties: id: { type: string } title: { type: string } description: { type: string } target: { type: integer } progress: { type: integer } unlocked: { type: boolean } completedAt: type: [string, "null"] format: date-time AchievementsResult: type: object properties: achievements: type: array items: { $ref: "#/components/schemas/Achievement" } unlockedCount: { type: integer } totalCount: { type: integer } # ─── Doctor report ───────────────────────────────────────── DoctorReportResponse: type: object properties: period: type: object properties: days: { type: integer } since: { type: string, format: date-time } patient: type: object properties: username: { type: [string, "null"] } dateOfBirth: { type: [string, "null"], format: date-time } gender: oneOf: - $ref: "#/components/schemas/Gender" - type: "null" heightCm: { type: [number, "null"] } measurements: type: object additionalProperties: type: array items: type: object properties: value: { type: number } measuredAt: { type: string, format: date-time } stats: type: object additionalProperties: type: object properties: avg: { type: number } min: { type: number } max: { type: number } count: { type: integer } latest: { type: number } glucoseStats: type: object additionalProperties: type: object properties: avg: { type: number } min: { type: number } max: { type: number } count: { type: integer } latest: { type: number } glucoseRanges: type: object additionalProperties: type: object properties: min: { type: number } max: { type: number } glucoseUnit: { type: string } bmi: { type: [number, "null"] } compliance: type: object additionalProperties: type: object properties: total: { type: integer } taken: { type: integer } skipped: { type: integer } missed: { type: integer } medications: type: array items: type: object properties: name: { type: string } dose: { type: string } schedules: type: array items: type: object properties: windowStart: { type: string } windowEnd: { type: string } label: { type: [string, "null"] } mood: type: [object, "null"] properties: avg: { type: number } min: { type: number } max: { type: number } count: { type: integer } distribution: type: object additionalProperties: { type: integer } # ─── Onboarding ──────────────────────────────────────────── OnboardingInput: type: object properties: heightCm: { type: number, minimum: 50, maximum: 300 } dateOfBirth: { type: string, format: date } gender: { $ref: "#/components/schemas/Gender" } # ─── Health probe ────────────────────────────────────────── HealthStatus: type: object properties: status: { type: string, enum: [ok, degraded] } timestamp: { type: string, format: date-time } database: { type: string } worker: { type: string } workerLastHeartbeat: { type: string, format: date-time } # ─── Tokens ──────────────────────────────────────────────── ApiTokenSummary: type: object properties: id: { type: string } name: { type: string } permissions: type: array items: { type: string } lastUsedAt: { type: [string, "null"], format: date-time } expiresAt: { type: [string, "null"], format: date-time } createdAt: { type: string, format: date-time } revoked: { type: boolean } CreateTokenInput: type: object properties: name: { type: string, minLength: 1, maxLength: 100 } expiresInDays: { type: integer, minimum: 1, maximum: 365 } required: [name] # ─── Notifications ───────────────────────────────────────── NotificationChannel: type: object properties: id: { type: string } type: { $ref: "#/components/schemas/NotificationChannelType" } label: { type: string } enabled: { type: boolean } globallyEnabled: { type: boolean } NotificationPreference: type: object properties: channelId: { type: string } eventType: { type: string } enabled: { type: boolean } NotificationPreferencesResponse: type: object properties: channels: type: array items: { $ref: "#/components/schemas/NotificationChannel" } preferences: type: array items: { $ref: "#/components/schemas/NotificationPreference" } eventTypes: type: array items: { type: string } UpdateNotificationPreferenceInput: type: object properties: channelId: { type: string } eventType: { type: string } enabled: { type: boolean } required: [channelId, eventType, enabled] WebPushSubscribeInput: type: object properties: endpoint: { type: string, format: uri } keys: type: object properties: p256dh: { type: string } auth: { type: string } required: [p256dh, auth] required: [endpoint, keys] # ─── User config ─────────────────────────────────────────── UserAiProvider: type: object properties: provider: oneOf: - $ref: "#/components/schemas/AiProviderType" - type: "null" model: { type: [string, "null"] } baseUrl: { type: [string, "null"] } hasAnthropicKey: { type: boolean } anthropicKeyPreview: { type: [string, "null"] } hasLocalKey: { type: boolean } UpdateUserAiProviderInput: type: object properties: provider: oneOf: - $ref: "#/components/schemas/AiProviderType" - type: "null" model: { type: [string, "null"] } baseUrl: { type: [string, "null"] } anthropicKey: { type: [string, "null"] } localKey: { type: [string, "null"] } ThresholdRange: type: object properties: greenMin: { type: number } greenMax: { type: number } orangeMin: { type: number } orangeMax: { type: number } redMin: { type: number } redMax: { type: number } EffectiveRange: type: object properties: range: oneOf: - $ref: "#/components/schemas/ThresholdRange" - type: "null" isOverride: { type: boolean } defaultRange: oneOf: - $ref: "#/components/schemas/ThresholdRange" - type: "null" ThresholdOverrides: type: object description: "Map of metric name → `{ min, max }` override." additionalProperties: type: object properties: min: { type: number } max: { type: number } ThresholdsResponse: type: object properties: effective: type: object additionalProperties: { $ref: "#/components/schemas/EffectiveRange" } overrides: { $ref: "#/components/schemas/ThresholdOverrides" } # ─── Analytics ───────────────────────────────────────────── AnalyticsResponse: type: object properties: summaries: type: object additionalProperties: { $ref: "#/components/schemas/SummaryStats" } bmi: { type: [number, "null"] } bpInTargetPct: { type: [integer, "null"] } glucoseByContext: type: object additionalProperties: { $ref: "#/components/schemas/SummaryStats" } AiTestResult: type: object properties: ok: { type: boolean } providerType: { $ref: "#/components/schemas/AiProviderType" } model: { type: string } tokensUsed: type: object properties: input: { type: integer } output: { type: integer } sample: { type: string } # ─── Export / Import ─────────────────────────────────────── ImportInput: type: object properties: measurements: type: array maxItems: 10000 items: type: object properties: type: { $ref: "#/components/schemas/MeasurementType" } value: { type: number } unit: { type: string } measuredAt: { type: string, format: date-time } source: { type: string } notes: { type: string } required: [type, value, unit, measuredAt] moodEntries: type: array maxItems: 10000 items: type: object properties: date: { type: string } mood: { $ref: "#/components/schemas/MoodLevel" } score: { type: integer, minimum: 1, maximum: 5 } tags: { type: string } loggedAt: { type: string, format: date-time } required: [date, mood, score] ImportResult: type: object properties: measurements: { type: integer } moodEntries: { type: integer } skipped: { type: integer } # ─── Audit log ───────────────────────────────────────────── AuditLogEntry: type: object properties: id: { type: string } action: { type: string } ipAddress: { type: [string, "null"] } location: { type: [string, "null"] } details: { type: [string, "null"], description: "JSON string" } createdAt: { type: string, format: date-time } AuditLogResponse: type: object properties: entries: type: array items: { $ref: "#/components/schemas/AuditLogEntry" } meta: { $ref: "#/components/schemas/PageMeta" } # ─── Feedback ────────────────────────────────────────────── FeedbackCategory: type: string enum: [BUG, FEATURE_REQUEST, QUESTION, OTHER] FeedbackStatus: type: string enum: [OPEN, ACKNOWLEDGED, RESOLVED, ARCHIVED] FeedbackSummary: type: object properties: id: { type: string } category: { $ref: "#/components/schemas/FeedbackCategory" } subject: { type: string } status: { $ref: "#/components/schemas/FeedbackStatus" } adminNote: { type: [string, "null"] } gitHubIssueUrl: { type: [string, "null"], format: uri } createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } CreateFeedbackInput: type: object properties: category: { $ref: "#/components/schemas/FeedbackCategory" } subject: { type: string } description: { type: string } screenshot: type: string description: "Base64-encoded PNG/JPG (≤5 MB)." metadata: type: object additionalProperties: true required: [category, subject, description] BugReportInput: type: object properties: description: { type: string, minLength: 10, maxLength: 5000 } screenshot: type: string description: "`data:image/(png|jpeg|webp|gif);base64,...` data URL." required: [description] # ─── Settings ────────────────────────────────────────────── GlobalServicesFlags: type: object properties: telegram: { type: boolean } ntfy: { type: boolean } webPush: { type: boolean } api: { type: boolean } moodLog: { type: boolean } MoodLogSettingsInput: type: object properties: url: { type: [string, "null"], format: uri } apiKey: { type: [string, "null"] } enabled: { type: boolean } webhookSecret: { type: [string, "null"] } NtfySettings: type: object properties: configured: { type: boolean } enabled: { type: boolean } topic: { type: [string, "null"] } serverUrl: { type: [string, "null"] } NtfySettingsInput: type: object properties: topic: { type: [string, "null"] } serverUrl: { type: [string, "null"], format: uri } enabled: { type: boolean } accessToken: { type: [string, "null"] } TelegramSettings: type: object properties: configured: { type: boolean } enabled: { type: boolean } chatId: { type: [string, "null"] } TelegramSettingsInput: type: object properties: botToken: { type: [string, "null"] } chatId: { type: [string, "null"] } enabled: { type: boolean } ReminderThresholds: type: object properties: reminderLateMinutes: { type: integer } reminderMissedMinutes: { type: integer } # ─── Withings + moodLog status ───────────────────────────── WithingsCredentialsInput: type: object properties: clientId: { type: string } clientSecret: { type: string } required: [clientId, clientSecret] WithingsStatus: type: object properties: connected: { type: boolean } configured: { type: boolean } lastSyncedAt: { type: [string, "null"], format: date-time } connectedAt: { type: string, format: date-time } tokenExpired: { type: boolean } tokenRefreshFailed: { type: boolean } tokenExpiresAt: { type: string, format: date-time } MoodLogStatus: type: object properties: configured: { type: boolean } enabled: { type: boolean } lastSyncedAt: { type: [string, "null"], format: date-time } entryCount: { type: integer } webhookSecret: { type: [string, "null"] } PublicMonitoringSettings: type: object properties: umamiEnabled: { type: boolean } umamiScriptUrl: { type: [string, "null"] } umamiWebsiteId: { type: [string, "null"] } glitchtipEnabled: { type: boolean } glitchtipDsn: { type: [string, "null"] } glitchtipEnvironment: { type: [string, "null"] } # ─── iOS adapter DTOs (v1.3) ────────────────────────────── # Used by /api/dashboard/summary (`MetricCard.kind`) and the # /api/measurements/series adapter. Extended in v1.3.3 to cover the # body-composition + SpO2 measurements that the server already emits. # iOS clients SHOULD decode unknown values defensively; we still # publish the canonical list here so generated codecs stay accurate. MetricKind: type: string enum: - weight - bloodPressure - pulse - bodyFat - glucose - sleep - steps - totalBodyWater - boneMass - oxygenSaturation InsightSeverityIos: type: string enum: [alert, caution, info, good] DashboardSummaryGreeting: type: object properties: salutation: { type: string } date: { type: string, format: date-time } required: [salutation, date] DashboardSummaryStreak: type: object properties: currentDays: { type: integer } longest: { type: integer } label: { type: string } required: [currentDays, longest, label] DashboardSummaryCompliance: type: object properties: scheduledToday: { type: integer } takenToday: { type: integer } required: [scheduledToday, takenToday] DashboardMetricCard: type: object properties: id: { type: string } kind: { $ref: "#/components/schemas/MetricKind" } title: { type: string } latestValue: { type: [number, "null"] } secondaryValue: { type: [number, "null"] } unit: { type: string } trend: type: string enum: [up, down, flat, unknown] sparkline: type: array items: { type: number } updatedAt: { type: [string, "null"], format: date-time } required: [id, kind, title, unit, trend, sparkline] DashboardSummary: type: object properties: greeting: { $ref: "#/components/schemas/DashboardSummaryGreeting" } streak: { $ref: "#/components/schemas/DashboardSummaryStreak" } compliance: { $ref: "#/components/schemas/DashboardSummaryCompliance" } highlightInsight: oneOf: - { type: "null" } - { $ref: "#/components/schemas/InsightCard" } metrics: type: array items: { $ref: "#/components/schemas/DashboardMetricCard" } lastUpdated: { type: string, format: date-time } required: [greeting, streak, compliance, metrics, lastUpdated] MeasurementSeriesPoint: type: object properties: id: { type: string } at: { type: string, format: date-time } value: { type: number } secondary: { type: [number, "null"] } required: [id, at, value] MeasurementSeriesStats: type: object properties: mean: { type: number } min: { type: number } max: { type: number } stdDev: { type: number } count: { type: integer } required: [mean, min, max, stdDev, count] MeasurementSeries: type: object properties: kind: { $ref: "#/components/schemas/MetricKind" } points: type: array items: { $ref: "#/components/schemas/MeasurementSeriesPoint" } stats: { $ref: "#/components/schemas/MeasurementSeriesStats" } required: [kind, points, stats] HealthKitDirection: type: string enum: [bidirectional, readOnly, writeOnly, disabled] HealthKitEntry: type: object properties: id: { type: string } kind: { type: string } direction: { $ref: "#/components/schemas/HealthKitDirection" } enabled: { type: boolean } required: [id, kind, direction, enabled] HealthKitConfig: type: object properties: entries: type: array items: { $ref: "#/components/schemas/HealthKitEntry" } lastSyncedAt: { type: [string, "null"], format: date-time } required: [entries] HealthKitConfigPatch: type: object properties: entries: type: array maxItems: 50 items: type: object properties: id: { type: string } kind: { type: string } direction: { $ref: "#/components/schemas/HealthKitDirection" } enabled: { type: boolean } required: [id, direction] required: [entries] IosUserProfile: type: object properties: username: { type: string } displayName: { type: [string, "null"] } email: { type: [string, "null"], format: email } dateOfBirth: { type: [string, "null"], format: date-time } gender: { $ref: "#/components/schemas/Gender" } heightCm: { type: [number, "null"] } locale: { type: [string, "null"] } timezone: { type: string } required: [username, timezone] IosUserProfilePatch: type: object properties: displayName: { type: [string, "null"], minLength: 1, maxLength: 80 } email: { type: [string, "null"], format: email } dateOfBirth: { type: [string, "null"] } gender: oneOf: - { type: "null" } - { $ref: "#/components/schemas/Gender" } heightCm: { type: [number, "null"], minimum: 50, maximum: 300 } locale: type: [string, "null"] enum: [de, en, null] timezone: { type: string, minLength: 1, maxLength: 64 } DeviceRegisterInput: type: object properties: token: { type: string, minLength: 8, maxLength: 512 } bundleId: { type: string, minLength: 1, maxLength: 128 } locale: { type: string } appVersion: { type: string } model: { type: string } required: [token, bundleId] IntakeUpdateInput: type: object properties: intakeId: { type: string } status: type: string enum: [taken, skipped, snoozed] takenAt: { type: string, format: date-time } snoozedUntil: { type: string, format: date-time } required: [intakeId, status] InsightRecommendation: type: object properties: id: { type: string } label: { type: string } actionURL: { type: [string, "null"] } required: [id, label] InsightCard: type: object properties: id: { type: string } title: { type: string } summary: { type: string } body: { type: [string, "null"] } severity: { $ref: "#/components/schemas/InsightSeverityIos" } recommendations: type: array items: { $ref: "#/components/schemas/InsightRecommendation" } generatedAt: { type: string, format: date-time } provider: { type: string } required: [id, title, summary, severity, recommendations, generatedAt, provider] InsightCorrelation: type: object properties: id: { type: string } leftMetric: { type: string } rightMetric: { type: string } coefficient: { type: number } sampleSize: { type: integer } required: [id, leftMetric, rightMetric, coefficient, sampleSize] # ─── Admin DTOs ──────────────────────────────────────────── AdminUpdateUserInput: type: object properties: username: { type: string, minLength: 3, maxLength: 30 } email: { type: [string, "null"], format: email } role: { $ref: "#/components/schemas/UserRole" }