Files
HealthLog/docs/api/openapi.yaml
T
Marc-André BombeckandMarc-André Bombeck 21bd46d60f chore(release): v1.4.0 — CHANGELOG, version bump, migration guide (#132)
* chore(release): prepare v1.4.0 — CHANGELOG, version bump, migration guide

Bumps `package.json` 1.3.3 → 1.4.0 and the OpenAPI spec to match.
Adds a comprehensive user-facing CHANGELOG entry for v1.4.0 covering
the foundation, medical safety, i18n, performance, chart-math,
dashboard one-row, /api/version, AI insights memory + hero card,
onboarding redesign, and the chart-math fixes that landed across the
v1.4 marathon. Bundles the migration guide
(`docs/migration/v1.3-to-v1.4.md`) covering env vars, behaviour-only
API changes, UI/UX deltas, the medical-default updates, and
rollback steps. Updates AGENTS.md status block.

Tests: 440/440 green. Typecheck clean.

Note: a few additional v1.4 buckets (admin redesign A4, settings split
A2, multi-tenant prep G, test coverage lift H, A8 test buttons) are
landing in parallel branches and may merge after this. The CHANGELOG
will be amended on the release branch as those PRs land.

Co-Authored-By: Marc-André Bombeck <noreply@anthropic.com>

* chore(release): amend v1.4.0 changelog with everything from the finish-marathon

Adds the user-facing bullets for the settings split, the five
"Test connection" buttons, AI inline charts, off-host backup,
encryption-key rotation, worker/web split, refresh-token rotation,
admin status grid, and the operational hardening fixes (tzdata,
healthcheck, idempotency hlr_, paired access-token revoke,
users.locale migration backfill).

Replaces the "tracked for v1.4.x or later" deferred block — the
items it listed have shipped or moved to v1.4.1.

---------

Co-authored-by: Marc-André Bombeck <noreply@anthropic.com>
2026-05-08 15:10:38 +02:00

4976 lines
165 KiB
YAML

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_<hex>' }
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' }
# ─── 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_<hex>'
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 <token>`.
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' }