Files
HealthLog/docs/api/openapi.yaml
T
Marc-André BombeckandMarc-André Bombeck 643e6ec3a0 release: v1.4.2 — resurrect v1.4.1 work + bug-fix triple (#152)
* Revert "revert: roll source tree back to v1.4.0 to restore production"

This reverts commit e444a90b10.

* fix(v1.4.2): summaries.WEIGHT + medications array guards + healthcheck IPv4

Three production bugs surfaced after the v1.4.0 deploy stabilized:

1. `TypeError: undefined is not an object (evaluating 'E?.summaries.WEIGHT')`
   on /dashboard and /insights for users without weight readings —
   the optional chain stopped at `data` when it needed to also cover
   `data.summaries`. Fixed: `data?.summaries?.WEIGHT` (one level deeper).

2. `TypeError: y?.filter is not a function` on /medications — the
   medications query value was non-array (likely a stale
   service-worker response from an older API shape). Replaced with
   `Array.isArray(medications) ? medications : []` so the page is
   immune to any future shape drift.

3. Healthcheck-induced 503 outage (already in 1.4.1 — included here
   so 1.4.2 is the single coherent release). The docker-compose
   healthcheck used `localhost` which busybox-wget resolves to IPv6
   ::1; Next.js standalone listens on IPv4 0.0.0.0 only. Pinned
   to 127.0.0.1.

This release also includes everything from v1.4.1 (which never
published a clean GHCR tag): the per-section admin extraction, the
testcontainers integration suite, the Playwright + axe-core E2E
foundation, the moodLog SSRF fix, the central error-redaction in
WideEventBuilder + Glitchtip, the medical-citation drift fixes
(ESH 2023, Saint-Maurice attribution, steps source), and the docs
sync.

Co-Authored-By: Marc-André Bombeck <mbombeck@gmail.com>
2026-05-08 17:46:31 +02:00

5469 lines
176 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" }
/api/admin/status-overview:
get:
tags: [Admin]
security:
- sessionCookie: []
summary: "Internal: aggregator for the 6-card status grid (v1.4)"
description: |
Returns the precomputed status payload the admin top strip
renders — users / integrations / monitoring / backups /
maintenance / audit-log summaries with severity already
resolved server-side. Single batched query, no N+1.
operationId: adminStatusOverview
responses:
"200":
{
description: OK,
content:
{
application/json:
{ schema: { $ref: "#/components/schemas/ApiEnvelope" } },
},
}
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/api/admin/backup/test:
post:
tags: [Admin]
security:
- sessionCookie: []
summary: "Internal: probe the configured S3-compatible backup target (v1.4)"
description: |
Round-trips a small object against the bucket using the
configured `BACKUP_S3_*` credentials. Returns latency and the
sanitised endpoint host on success; never echoes the access key.
operationId: adminBackupTest
responses:
"200":
{
description: OK,
content:
{
application/json:
{ schema: { $ref: "#/components/schemas/ApiEnvelope" } },
},
}
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/Unprocessable" }
# ─── Public + v1.4 additions ─────────────────────────────────────────
/api/version:
get:
tags: [Public]
summary: "Public: running version + build SHA + license"
description: |
Public, unauthenticated. Returns
`{ version, buildSha, builtAt, license, repository, changelog, docs }`.
Used by the docker container healthcheck and the Settings →
About surface's "Check for updates" button.
operationId: getVersion
responses:
"200":
{
description: OK,
content:
{
application/json:
{ schema: { $ref: "#/components/schemas/ApiEnvelope" } },
},
}
/api/integrations/withings/test:
post:
tags: [Withings]
security:
- sessionCookie: []
summary: "Probe the saved Withings OAuth connection (v1.4)"
operationId: withingsTest
responses:
"200":
{
description: OK,
content:
{
application/json:
{ schema: { $ref: "#/components/schemas/ApiEnvelope" } },
},
}
"401": { $ref: "#/components/responses/Unauthorized" }
"422": { $ref: "#/components/responses/Unprocessable" }
"429": { $ref: "#/components/responses/RateLimited" }
/api/integrations/moodlog/test:
post:
tags: [Moodlog]
security:
- sessionCookie: []
summary: "Probe moodLog.app reachability (v1.4)"
description: |
SSRF-guarded HEAD against the configured moodLog endpoint.
Returns `{ ok, latency_ms, errorCode? }` so the UI can
localise the failure message.
operationId: moodlogTest
responses:
"200":
{
description: OK,
content:
{
application/json:
{ schema: { $ref: "#/components/schemas/ApiEnvelope" } },
},
}
"401": { $ref: "#/components/responses/Unauthorized" }
"429": { $ref: "#/components/responses/RateLimited" }
/api/notifications/web-push/test:
post:
tags: [Notifications]
security:
- sessionCookie: []
summary: "Send a test Web Push to the current user (v1.4)"
operationId: webPushTest
responses:
"200":
{
description: OK,
content:
{
application/json:
{ schema: { $ref: "#/components/schemas/ApiEnvelope" } },
},
}
"401": { $ref: "#/components/responses/Unauthorized" }
"422": { $ref: "#/components/responses/Unprocessable" }
"429": { $ref: "#/components/responses/RateLimited" }
/api/monitoring/glitchtip/test:
post:
tags: [Monitoring]
security:
- sessionCookie: []
summary: "Probe the Glitchtip ingest endpoint (v1.4)"
operationId: glitchtipTest
responses:
"200":
{
description: OK,
content:
{
application/json:
{ schema: { $ref: "#/components/schemas/ApiEnvelope" } },
},
}
"401": { $ref: "#/components/responses/Unauthorized" }
"422": { $ref: "#/components/responses/Unprocessable" }
"429": { $ref: "#/components/responses/RateLimited" }
/api/monitoring/umami/test:
post:
tags: [Monitoring]
security:
- sessionCookie: []
summary: "Verify the Umami script + website ID resolve (v1.4)"
operationId: umamiTest
responses:
"200":
{
description: OK,
content:
{
application/json:
{ schema: { $ref: "#/components/schemas/ApiEnvelope" } },
},
}
"401": { $ref: "#/components/responses/Unauthorized" }
"422": { $ref: "#/components/responses/Unprocessable" }
"429": { $ref: "#/components/responses/RateLimited" }
/api/auth/refresh:
post:
tags: [Auth]
summary: "Native client: refresh-token rotation (v1.4)"
description: |
Exchanges a refresh token (`hlr_<64hex>`) for a fresh 24h
access token and a new refresh token. Cookie-session browsers
do not use this. Reuse-detection: presenting the same refresh
token a second time revokes every refresh token for the user.
operationId: authRefresh
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [refreshToken]
properties:
refreshToken: { type: string, pattern: "^hlr_[A-Fa-f0-9]{64}$" }
revoke:
type: boolean
default: false
description: "When true, revokes the refresh row + paired access token instead of rotating."
responses:
"200":
{
description: OK,
content:
{
application/json:
{ schema: { $ref: "#/components/schemas/ApiEnvelope" } },
},
}
"401": { $ref: "#/components/responses/Unauthorized" }
"422": { $ref: "#/components/responses/Unprocessable" }
"429": { $ref: "#/components/responses/RateLimited" }
/api/auth/refresh/revoke:
post:
tags: [Auth]
summary: "Native client: revoke a refresh token + paired access token (v1.4)"
operationId: authRefreshRevoke
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [refreshToken]
properties:
refreshToken: { type: string, pattern: "^hlr_[A-Fa-f0-9]{64}$" }
responses:
"200":
{
description: OK,
content:
{
application/json:
{ schema: { $ref: "#/components/schemas/ApiEnvelope" } },
},
}
"401": { $ref: "#/components/responses/Unauthorized" }
"422": { $ref: "#/components/responses/Unprocessable" }
# ─── iOS adapter endpoints (v1.3) ────────────────────────────────────
/api/dashboard/summary:
get:
tags: [Dashboard]
summary: "iOS: aggregated dashboard summary"
description: |
Single-call aggregate for the iOS DashboardSummary view. Combines
greeting, current/longest activity-day streak, today's medication
compliance, optional highlight insight, and per-metric latest +
7-day sparkline + trend.
operationId: getDashboardSummary
responses:
"200":
description: Dashboard summary
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/ApiEnvelope"
- type: object
properties:
data: { $ref: "#/components/schemas/DashboardSummary" }
"401": { $ref: "#/components/responses/Unauthorized" }
/api/measurements/series:
get:
tags: [Measurements]
summary: "iOS: time-series + summary stats for a metric kind"
operationId: getMeasurementSeries
parameters:
- in: query
name: kind
required: true
schema: { $ref: "#/components/schemas/MetricKind" }
- in: query
name: days
schema:
type: integer
minimum: 1
maximum: 365
default: 30
responses:
"200":
description: Series payload
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/ApiEnvelope"
- type: object
properties:
data: { $ref: "#/components/schemas/MeasurementSeries" }
"401": { $ref: "#/components/responses/Unauthorized" }
"422": { $ref: "#/components/responses/Unprocessable" }
/api/integrations/healthkit:
get:
tags: [Integrations]
summary: "iOS: HealthKit per-metric direction config"
operationId: getHealthKitConfig
responses:
"200":
description: HealthKit config
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/ApiEnvelope"
- type: object
properties:
data: { $ref: "#/components/schemas/HealthKitConfig" }
"401": { $ref: "#/components/responses/Unauthorized" }
patch:
tags: [Integrations]
summary: "iOS: update HealthKit per-metric direction config"
operationId: updateHealthKitConfig
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/HealthKitConfigPatch" }
responses:
"200":
description: Updated config
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/ApiEnvelope"
- type: object
properties:
data: { $ref: "#/components/schemas/HealthKitConfig" }
"401": { $ref: "#/components/responses/Unauthorized" }
"422": { $ref: "#/components/responses/Unprocessable" }
/api/user/profile:
get:
tags: [Auth]
summary: "iOS: flattened user profile"
operationId: getUserProfile
responses:
"200":
description: User profile
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/ApiEnvelope"
- type: object
properties:
data: { $ref: "#/components/schemas/IosUserProfile" }
"401": { $ref: "#/components/responses/Unauthorized" }
patch:
tags: [Auth]
summary: "iOS: update profile fields"
operationId: updateUserProfile
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/IosUserProfilePatch" }
responses:
"200":
description: Updated profile
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/ApiEnvelope"
- type: object
properties:
data: { $ref: "#/components/schemas/IosUserProfile" }
"401": { $ref: "#/components/responses/Unauthorized" }
"409": { $ref: "#/components/responses/Conflict" }
"422": { $ref: "#/components/responses/Unprocessable" }
/api/devices:
post:
tags: [Notifications]
summary: "iOS: register an APNs device token"
operationId: registerDevice
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/DeviceRegisterInput" }
responses:
"201":
description: Device registered (or token transferred)
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/ApiEnvelope"
- type: object
properties:
data:
type: object
properties:
id: { type: string }
"401": { $ref: "#/components/responses/Unauthorized" }
"422": { $ref: "#/components/responses/Unprocessable" }
/api/medications/intake:
get:
tags: [Medications]
summary: "iOS: today list or compliance buckets"
operationId: listIntakeAggregator
parameters:
- in: query
name: scope
required: true
schema: { type: string, enum: [today, compliance] }
- in: query
name: days
schema:
type: integer
minimum: 1
maximum: 365
default: 30
responses:
"200":
description: Aggregator payload
content:
application/json:
schema: { $ref: "#/components/schemas/ApiEnvelope" }
"401": { $ref: "#/components/responses/Unauthorized" }
"422": { $ref: "#/components/responses/Unprocessable" }
post:
tags: [Medications]
summary: "iOS: update an intake event by id (taken/skipped/snoozed)"
operationId: updateIntakeAggregator
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/IntakeUpdateInput" }
responses:
"200":
description: Updated intake event
content:
application/json:
schema: { $ref: "#/components/schemas/ApiEnvelope" }
"401": { $ref: "#/components/responses/Unauthorized" }
"404": { $ref: "#/components/responses/NotFound" }
"422": { $ref: "#/components/responses/Unprocessable" }
/api/insights/cards:
get:
tags: [Insights]
summary: "iOS: rule-based insight cards"
operationId: getInsightCards
responses:
"200":
description: Insight cards
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/ApiEnvelope"
- type: object
properties:
data:
type: array
items: { $ref: "#/components/schemas/InsightCard" }
"401": { $ref: "#/components/responses/Unauthorized" }
/api/insights/correlations:
get:
tags: [Insights]
summary: "iOS: cross-metric correlations (placeholder)"
operationId: getInsightCorrelations
description: |
Returns an empty array until the correlations engine is wired up
(Phase 7). Audited via `insights.correlations.empty`.
responses:
"200":
description: Correlations
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/ApiEnvelope"
- type: object
properties:
data:
type: array
items:
{ $ref: "#/components/schemas/InsightCorrelation" }
"401": { $ref: "#/components/responses/Unauthorized" }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: "hlk_<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" }