diff --git a/docs/integrations/ai-providers.md b/docs/integrations/ai-providers.md index 80cc2abc3..af14d614d 100644 --- a/docs/integrations/ai-providers.md +++ b/docs/integrations/ai-providers.md @@ -45,8 +45,8 @@ fallback for users who skip the setup. ## The fallback chain `User.aiProviderChain` is a JSON array of providers tried in priority -order. The default is encoded in -`src/lib/ai/provider-chain.ts:60-66`: +order. The default is encoded as `PROVIDER_CHAIN_DEFAULT` in +`src/lib/ai/provider-chain.ts`: 1. `codex` — ChatGPT OAuth (covered by your ChatGPT Plus / Pro subscription; no per-token cost on top). @@ -79,7 +79,7 @@ the default rather than 500-ing. 4. In HealthLog, open `/settings/ai`. Set provider to **OpenAI**, paste the key, optionally override the model. -The default model is `gpt-4o` (`src/lib/ai/provider.ts:80`) — a +The default model is `gpt-4o` (see `src/lib/ai/provider.ts`) — a full-size model that keeps the headline Insights assessments grounded and well-reasoned out of the box. Override to a lighter model such as `gpt-4o-mini` if you want to trade some reasoning headroom for a lower @@ -234,7 +234,7 @@ UI setup: `connected`. 5. Token refresh runs automatically; the client triggers a refresh on 401 and persists the new tokens transparently - (`src/lib/ai/provider.ts:122-217`). + (`src/lib/ai/provider.ts`). Disconnect cleanly from `/settings/ai` — `POST /api/auth/codex/disconnect` clears the encrypted token columns. The wire-level protocol is @@ -410,12 +410,12 @@ All three variables must also be listed in the compose ## Connection test `POST /api/ai/test` runs a one-shot probe against the resolved -provider and returns the latency in milliseconds plus the model the -provider routed to. The Settings UI surfaces a **Test connection** -button next to each provider section that fires this endpoint. A -failed probe surfaces a localisable `errorCode` so the UI renders -the failure in the user's language rather than the raw upstream -error. +provider and returns the resolved provider type, the model, the token +usage, and a short response sample. The Settings UI surfaces a +**Test connection** button next to each provider section that fires +this endpoint. A failed probe carries a categorised `reasonCode` so +the UI renders the failure in the user's language rather than the raw +upstream error. ## Troubleshooting diff --git a/docs/integrations/apple-health.md b/docs/integrations/apple-health.md index bc21e02ae..456dd96c3 100644 --- a/docs/integrations/apple-health.md +++ b/docs/integrations/apple-health.md @@ -265,7 +265,8 @@ IMPORT** for cumulative metrics (steps, active energy, distance, flights, sleep, HRV, resting HR) and **WITHINGS ≻ APPLE_HEALTH ≻ MANUAL** for point measurements where the Withings device is the primary sensor (weight, BP, body fat, body temperature, SpO₂, VO₂ -max). The defaults live in `src/lib/validations/source-priority.ts:205-220`. +max). The defaults live in `DEFAULT_SOURCE_PRIORITY` in +`src/lib/validations/source-priority.ts`. Concrete consequences: @@ -283,7 +284,7 @@ Concrete consequences: source tag — the lowest rank — is reserved for legacy CSV/JSON imports that pre-date the Apple Health passthrough. -Override per-user via the Sources section of `/settings/thresholds`. +Override per-user via Settings → Sources (`/settings/sources`). ## Failure modes diff --git a/docs/integrations/data-import.md b/docs/integrations/data-import.md index bd4d13097..45540e71c 100644 --- a/docs/integrations/data-import.md +++ b/docs/integrations/data-import.md @@ -1,6 +1,6 @@ # Data import -HealthLog has three ways to bring data in, all reachable from +HealthLog has four ways to bring data in, all reachable from **Settings → Export & Import**: 1. An **Apple Health `export.zip`** upload — the full archive from the @@ -12,8 +12,11 @@ HealthLog has three ways to bring data in, all reachable from from another tracker. This page documents that format. 3. A **CSV import** — one measurement per row, for a spreadsheet or a meter / sensor export. Documented in section 3 below. +4. A **medication dose-history import** — its own card in the same + panel, posting to `POST /api/medications/intake/dose-history-import` + with its own `?dryRun=1` preview. -All three paths skip rows that already exist, so re-running an import is +All paths skip rows that already exist, so re-running an import is safe — it merges rather than duplicating. Every import reports what it actually wrote. A file where nothing was @@ -33,9 +36,10 @@ the browser — and a background job parses it while a progress indicator polls for the imported / skipped counts. Re-uploading the same archive resolves to the same job by content hash, -so it merges instead of creating duplicates. Clinical records -(electrocardiograms, lab documents) are intentionally skipped. The full -type mapping lives in [`apple-health.md`](./apple-health.md). +so it merges instead of creating duplicates. ECG CSVs and metric-typed +clinical records are imported too; see +[`apple-health.md`](./apple-health.md) for the details and the full +type mapping. ## 2. Generic JSON import @@ -65,6 +69,7 @@ skipped and counted under `skipped` in the response. | `source` | no | string | Free-text origin label. Imported rows are tagged `IMPORT` server-side regardless. | | `notes` | no | string | Optional free text. | | `glucoseContext` | no | enum | Only for `BLOOD_GLUCOSE`. See [Glucose context](#glucose-context). | +| `externalId` | no | string | A source-stable id (1–120 chars). When present, a re-import upserts on `(user, type, source, externalId)` so an edit upstream is reflected rather than skipped. | ### Glucose context @@ -95,7 +100,7 @@ moment you know it. | `mood` | yes | enum | One of `SUPER_GUT`, `GUT`, `OKAY`, `SCHLECHT`, `LAUSIG`. | | `score` | yes | integer | `1`–`5`. | | `tags` | no | string | Comma-separated tags. | -| `loggedAt` | no | string | ISO-8601 datetime; defaults to now. | +| `loggedAt` | no | string | ISO-8601 datetime; defaults to the start of the given `date` in the default timezone. | | `externalId` | no | string | A source-stable id (1–120 chars). When present, a re-import upserts on `(user, source, externalId)` so an edit upstream is reflected rather than skipped. | ### `MeasurementType` values and canonical units @@ -172,9 +177,10 @@ hand-authored. | `WALKING_STEADINESS_EVENT` | event | | `BREATHING_DISTURBANCE_EVENT` | event | -The live, machine-readable list of writable types and their units is -also served by `GET /api/meta/capabilities` under `ingest.quantityTypes` -— the authoritative source if this table ever lags a release. +The table is a curated subset and lags the enum by a few +device-derived types. The live, machine-readable list of writable +types and their units is served by `GET /api/meta/capabilities` under +`ingest.quantityTypes` and is the authoritative source. ### Worked example @@ -242,7 +248,7 @@ matching is case-insensitive. | `measuredAt` | yes | ISO-8601 **with an explicit offset** (`Z` or `±HH:MM`, and `±HHMM` is accepted). No offset, no import. | | `glucoseContext` | no | Only for `BLOOD_GLUCOSE`; blank means no context. See [Glucose context](#glucose-context). | | `notes` | no | Free text, up to 200 characters. | -| `externalId` | no | A source-stable id. With it, a re-upload updates the same row; without it, a re-upload creates a new one. | +| `externalId` | no | A source-stable id. With it, a re-upload updates the same row even when the value changed; without it, an identical re-upload is skipped via the natural key `(user, type, measuredAt, source)`. | A sensor export therefore needs no glucose column at all: diff --git a/docs/integrations/fitbit.md b/docs/integrations/fitbit.md index 66f0e0c51..7e4310c39 100644 --- a/docs/integrations/fitbit.md +++ b/docs/integrations/fitbit.md @@ -93,8 +93,9 @@ The Fitbit client id/secret are per-user (Settings), not env vars. If Fitbit and another source (Withings, WHOOP, Apple Health) both supply the same vital — resting heart rate, blood oxygen, body temperature or respiratory rate — -you may see both values until a future update settles on a single preferred -source. +the source priority ladder in **Settings → Sources** decides which one wins +per metric. FITBIT is ranked in every default metric ladder; override +per metric on that page. ## Disconnect diff --git a/docs/integrations/google-health.md b/docs/integrations/google-health.md index 410034c24..ca623a9d0 100644 --- a/docs/integrations/google-health.md +++ b/docs/integrations/google-health.md @@ -134,7 +134,7 @@ triggers the verification + CASA requirements above.) If Google Health and another source (Withings, WHOOP, Apple Health) both supply the same vital — resting heart rate, blood oxygen, respiratory rate — the source -priority ladder in **Settings → Integrations → Sources** decides which one wins +priority ladder in **Settings → Sources** decides which one wins per metric. ## Disconnect diff --git a/docs/integrations/withings.md b/docs/integrations/withings.md index 3d08b370a..58253d76d 100644 --- a/docs/integrations/withings.md +++ b/docs/integrations/withings.md @@ -73,7 +73,7 @@ only when the OAuth callback must differ from that default. When HealthLog calls Withings `Notify.subscribe()` for a newly-linked account, it hands Withings the callback URL constructed by -`getWithingsWebhookCallbackUrl()` (`src/lib/withings/sync.ts:38-48`). +`getWithingsWebhookCallbackUrl()` (`src/lib/withings/sync.ts`). The shape is: ``` @@ -107,8 +107,9 @@ With the credentials saved in Settings, open 5. The settings page now shows **Connected** with the timestamp of the last successful sync. -Subsequent syncs run automatically on every webhook delivery plus a -safety-net cron pull every few hours. +Subsequent syncs run automatically on every webhook delivery plus an +hourly safety-net cron pull (measurements at :00, sleep at :15, ECG +at :41). ## Source-priority interaction @@ -117,7 +118,7 @@ for point measurements (weight, BP, pulse, body fat, body temperature, SpO₂, VO₂ max) and **APPLE_HEALTH ≻ WITHINGS ≻ MANUAL** for cumulative metrics (steps, active energy, distance, flights, sleep, HRV, resting HR). The defaults live in -`src/lib/validations/source-priority.ts:205-220`. +`DEFAULT_SOURCE_PRIORITY` in `src/lib/validations/source-priority.ts`. Concrete consequences if you run Withings alongside an Apple Health import: @@ -130,7 +131,7 @@ import: when Apple Health is also linked, so the Apple Health stream wins for the cumulative axis. The Withings rows stay in the DB. -Override per-user via the Sources section of `/settings/thresholds`. +Override per-user via Settings → Sources (`/settings/sources`). ## Troubleshooting @@ -169,7 +170,7 @@ fixes the mismatch by updating the URL Withings holds. Withings revokes refresh tokens on password change, app un-authorise, or after extended inactivity. The `WithingsConnection` row flips to `error_reauth` status and the scheduled sync no-ops until the user -re-runs the OAuth flow (`src/lib/withings/sync.ts:155-161`). +re-runs the OAuth flow (`src/lib/withings/sync.ts`). Click **Connect Withings** again from the settings page; the callback exchanges fresh tokens and the next sync runs through the last 30 days of history to catch up. @@ -179,20 +180,23 @@ last 30 days of history to catch up. The Settings page **Disconnect** button: 1. Calls `POST /api/withings/disconnect`. -2. Server-side: revokes the access token via Withings' - `Notify.revoke`, deletes the `WithingsConnection` row, and audits - the action. +2. Server-side: unsubscribes the webhook categories (best-effort + Withings `Notify` revoke per category), deletes the + `WithingsConnection` row with its stored encrypted tokens, and + audits the action. The Withings-side grant itself persists until + the user un-authorises HealthLog in their Withings account. 3. Existing `Measurement` rows tagged `source = WITHINGS` stay in the database — disconnect does not delete data. Re-linking the same Withings account merges back onto the same metric timeline; the upsert path matches on `(userId, type, measuredAt, source)` so previously-synced rows update in place. -## Admin probe +## Connection test -`POST /api/integrations/withings/test` (admin-only) runs a -read-only round-trip against the user's stored credentials and -returns the latency in milliseconds. Use it from the admin panel -when a user reports stale data — a green test plus a stale -`lastSyncedAt` usually means the webhook URL Withings holds is out -of sync (re-subscribe per the steps above). +`POST /api/integrations/withings/test` runs a read-only round-trip +against the calling user's own stored credentials and returns the +latency in milliseconds (rate-limited to 5/min per user). The +Withings settings card surfaces it as the connection test. Use it +when data looks stale — a green test plus a stale `lastSyncedAt` +usually means the webhook URL Withings holds is out of sync +(re-subscribe per the steps above).