True up the integration references

- ai-providers: /api/ai/test returns provider type, model, token usage
  and a sample with a reasonCode on failure; drop drifted line numbers
- apple-health: source-priority overrides moved to /settings/sources;
  cite the constant instead of line numbers
- data-import: four import paths (medication dose history added); ECG
  and metric-typed clinical records are imported; measurement
  externalId documented; CSV re-uploads dedupe on the natural key;
  mood loggedAt defaults to the start of the given date; the type
  table is a curated subset with /api/meta/capabilities authoritative
- fitbit: the source-priority ladder ships, FITBIT is ranked in every
  default ladder
- google-health: Sources is its own settings section, not nested under
  Integrations
- withings: disconnect unsubscribes the webhook and deletes stored
  tokens without revoking the Withings-side grant; the connection test
  is self-serve, not admin-only; the safety-net cron is hourly;
  refresh stale references
This commit is contained in:
Marc-André Bombeck committed 2026-08-27 12:26:08 +02:00
1 parent 123fcc4ec3
commit b2ee7f11d1
6 files changed
+53 -41

No files matched your search

+10 -10
View File
@@ -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
+3 -2
View File
@@ -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
+16 -10
View File
@@ -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:
+3 -2
View File
@@ -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
+1 -1
View File
@@ -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
+20 -16
View File
@@ -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).