mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
Merge remote-tracking branch 'origin/release/v1.46.0' into fix/v146-security
# Conflicts: # src/lib/ai/coach/labs-snapshot.ts # src/lib/ai/coach/snapshot.ts
This commit is contained in:
commit
21aa791c3a
54 files changed
+544
-338
No files matched your search
+13
-7
@@ -17,9 +17,13 @@ notes, and machine-readable specs that ship alongside the source:
|
||||
encryption-key rotation, password reset and account recovery, env
|
||||
checks, TLS pinning, data repair. These are what an operator reaches
|
||||
for during an incident.
|
||||
- [`self-hosting/`](./self-hosting/) — horizontal-scaling notes
|
||||
(`HEALTHLOG_PROCESS_TYPE=web|worker|all`) and the deploy-pipeline
|
||||
recipe. Routine install steps stay on `docs.healthlog.dev`. Includes
|
||||
- [`self-hosting/`](./self-hosting/) — operator notes next to the code:
|
||||
[`getting-started.md`](./self-hosting/getting-started.md) (install and
|
||||
updates, the page the boot log points at), the TrueNAS, Unraid and
|
||||
Portainer installs with their catalog templates, Tailscale, the reverse
|
||||
proxy, horizontal scaling (`HEALTHLOG_PROCESS_TYPE=web|worker|all`),
|
||||
notification channels, the environment module, document sources and
|
||||
reproductive-health data. Includes
|
||||
[`self-hosting/mcp.md`](./self-hosting/mcp.md) — enabling the
|
||||
off-by-default MCP connector and pointing Claude.ai / ChatGPT /
|
||||
Claude Desktop at it, and [`self-hosting/sso.md`](./self-hosting/sso.md)
|
||||
@@ -44,9 +48,11 @@ notes, and machine-readable specs that ship alongside the source:
|
||||
- [`migration/`](./migration/) — migration notes. Currently only
|
||||
v1.3 to v1.4; later releases are covered by `CHANGELOG.md` and
|
||||
<https://docs.healthlog.dev>.
|
||||
- [`integrations/`](./integrations/) — per-integration reference
|
||||
(Withings, Fitbit, Google Health, WHOOP, Apple Health, AI providers,
|
||||
data import).
|
||||
- [`integrations/`](./integrations/) — per-integration reference:
|
||||
Withings, WHOOP, Fitbit, Google Health, Strava, Nightscout, Dawarich and
|
||||
the Garmin route; the Apple Health, Health Connect, document and data
|
||||
imports; AI providers; and Home Assistant with its blueprint. Oura and
|
||||
Polar are covered on the docs site only.
|
||||
- [`adr/`](./adr/) — architecture decision records.
|
||||
- [`security/`](./security/) — threat-model notes.
|
||||
- [`diagrams/`](./diagrams/) — architecture diagrams rendered through
|
||||
@@ -70,4 +76,4 @@ notes, and machine-readable specs that ship alongside the source:
|
||||
|
||||
If you're a self-hoster looking to spin up an instance, start at
|
||||
<https://docs.healthlog.dev> or jump to the [Quick Start in the project
|
||||
README](../README.md#quick-start).
|
||||
README](../README.md#quick-start-docker-compose).
|
||||
+34
-35
@@ -82145,7 +82145,7 @@ components:
|
||||
- type: "null"
|
||||
syncHealth:
|
||||
deprecated: true
|
||||
description: "Deprecated: removed in v1.44, read `health`. Present on the GET read; omitted from the PATCH echo."
|
||||
description: "Deprecated, still served: read `health`. Present on the GET read; omitted from the PATCH echo."
|
||||
$ref: "#/components/schemas/SyncHealth"
|
||||
metricFreshness:
|
||||
description: Per-metric-type freshness for the `APPLE_HEALTH` source, with the per-type arrival facts. Present on the
|
||||
@@ -82823,7 +82823,7 @@ components:
|
||||
type: boolean
|
||||
state:
|
||||
deprecated: true
|
||||
description: "Deprecated: removed in v1.44, read `health`."
|
||||
description: "Deprecated, still served: read `health`."
|
||||
type: string
|
||||
enum:
|
||||
- connected
|
||||
@@ -82846,14 +82846,14 @@ components:
|
||||
- type: "null"
|
||||
lastError:
|
||||
deprecated: true
|
||||
description: "Deprecated: removed in v1.44, read `health`. Last recorded failure message, decrypted from the ledger.
|
||||
Null when the last attempt succeeded or none has run."
|
||||
description: "Deprecated, still served: read `health`. Last recorded failure message, decrypted from the ledger. Null
|
||||
when the last attempt succeeded or none has run."
|
||||
type:
|
||||
- string
|
||||
- "null"
|
||||
syncHealth:
|
||||
deprecated: true
|
||||
description: "Deprecated: removed in v1.44, read `health`."
|
||||
description: "Deprecated, still served: read `health`."
|
||||
$ref: "#/components/schemas/SyncHealth"
|
||||
health:
|
||||
description: The unified connection state. Present whenever the connection exists; null in the rare case it resolved to
|
||||
@@ -83042,9 +83042,9 @@ components:
|
||||
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
|
||||
tokenExpired:
|
||||
deprecated: true
|
||||
description: "Deprecated: removed in v1.44, read `health`. The stored access token's expiry is in the past. Read
|
||||
straight off the row — unlike the Withings status read, this endpoint does not refresh; the sync path
|
||||
refreshes lazily instead."
|
||||
description: "Deprecated, still served: read `health`. The stored access token's expiry is in the past. Read straight
|
||||
off the row — unlike the Withings status read, this endpoint does not refresh; the sync path refreshes
|
||||
lazily instead."
|
||||
type: boolean
|
||||
tokenExpiresAt:
|
||||
type: string
|
||||
@@ -83133,8 +83133,7 @@ components:
|
||||
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
|
||||
tokenExpired:
|
||||
deprecated: true
|
||||
description: "Deprecated: removed in v1.44, read `health`. True after a refresh attempt that did not produce a live
|
||||
token."
|
||||
description: "Deprecated, still served: read `health`. True after a refresh attempt that did not produce a live token."
|
||||
type: boolean
|
||||
tokenRefreshFailed:
|
||||
description: The read tried to refresh an expired token and could not — either the per-user credentials are gone or
|
||||
@@ -83154,8 +83153,8 @@ components:
|
||||
- "null"
|
||||
hasActivityScope:
|
||||
deprecated: true
|
||||
description: "Deprecated: removed in v1.44, read `health`. Pre-computed convenience flag the reconnect banner reads. A
|
||||
null `scope` counts as missing activity."
|
||||
description: "Deprecated, still served: read `health`. Pre-computed convenience flag the reconnect banner reads. A null
|
||||
`scope` counts as missing activity."
|
||||
type: boolean
|
||||
health:
|
||||
description: The unified connection state. Present whenever the connection exists; null in the rare case it resolved to
|
||||
@@ -83464,8 +83463,8 @@ components:
|
||||
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
|
||||
tokenExpired:
|
||||
deprecated: true
|
||||
description: "Deprecated: removed in v1.44, read `health`. Read straight off the row — this endpoint does not refresh;
|
||||
the sync path refreshes lazily."
|
||||
description: "Deprecated, still served: read `health`. Read straight off the row — this endpoint does not refresh; the
|
||||
sync path refreshes lazily."
|
||||
type: boolean
|
||||
tokenExpiresAt:
|
||||
type: string
|
||||
@@ -83635,8 +83634,8 @@ components:
|
||||
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
|
||||
tokenExpired:
|
||||
deprecated: true
|
||||
description: "Deprecated: removed in v1.44, read `health`. Read straight off the row — this endpoint does not refresh;
|
||||
the sync path refreshes lazily."
|
||||
description: "Deprecated, still served: read `health`. Read straight off the row — this endpoint does not refresh; the
|
||||
sync path refreshes lazily."
|
||||
type: boolean
|
||||
tokenExpiresAt:
|
||||
type: string
|
||||
@@ -83686,7 +83685,7 @@ components:
|
||||
when every type was read to its start. Kept after the backfill completes; cleared on reconnect.
|
||||
needsReauth:
|
||||
deprecated: true
|
||||
description: "Deprecated: removed in v1.44, read `health`. The stored refresh token was rejected with `invalid_grant` —
|
||||
description: "Deprecated, still served: read `health`. The stored refresh token was rejected with `invalid_grant` —
|
||||
Google expires it after 7 days while the OAuth client is in Testing publishing mode, and a user-revoked
|
||||
grant lands here too. Distinct from `parked` and from `disconnected`: the connection still exists, it just
|
||||
cannot mint a token until the user reconnects. Only Google Health has this field."
|
||||
@@ -83890,7 +83889,7 @@ components:
|
||||
description: The user has stored their own BYO client id and secret.
|
||||
state:
|
||||
deprecated: true
|
||||
description: "Deprecated: removed in v1.44, read `health`."
|
||||
description: "Deprecated, still served: read `health`."
|
||||
type: string
|
||||
enum:
|
||||
- connected
|
||||
@@ -83913,13 +83912,13 @@ components:
|
||||
- type: "null"
|
||||
lastError:
|
||||
deprecated: true
|
||||
description: "Deprecated: removed in v1.44, read `health`."
|
||||
description: "Deprecated, still served: read `health`."
|
||||
type:
|
||||
- string
|
||||
- "null"
|
||||
syncHealth:
|
||||
deprecated: true
|
||||
description: "Deprecated: removed in v1.44, read `health`."
|
||||
description: "Deprecated, still served: read `health`."
|
||||
$ref: "#/components/schemas/SyncHealth"
|
||||
health:
|
||||
description: The unified connection state. Present whenever the connection exists; null in the rare case it resolved to
|
||||
@@ -84314,7 +84313,7 @@ components:
|
||||
- disconnected
|
||||
- parked
|
||||
- unknown
|
||||
description: "Deprecated: removed in v1.44, read `health`."
|
||||
description: "Deprecated, still served: read `health`."
|
||||
deprecated: true
|
||||
lastSuccessAt:
|
||||
anyOf:
|
||||
@@ -84330,7 +84329,7 @@ components:
|
||||
- type: "null"
|
||||
lastError:
|
||||
deprecated: true
|
||||
description: "Deprecated: removed in v1.44, read `health`."
|
||||
description: "Deprecated, still served: read `health`."
|
||||
type:
|
||||
- string
|
||||
- "null"
|
||||
@@ -84357,11 +84356,11 @@ components:
|
||||
additionalProperties: false
|
||||
- type: "null"
|
||||
deprecated: true
|
||||
description: "Deprecated: removed in v1.44, read `health`. Per-kind consecutive-failure counters, or null when the row
|
||||
has never written a bucket payload. Compare against the top-level `threshold` to know how close the provider
|
||||
is to the operator alert."
|
||||
description: "Deprecated, still served: read `health`. Per-kind consecutive-failure counters, or null when the row has
|
||||
never written a bucket payload. Compare against the top-level `threshold` to know how close the provider is
|
||||
to the operator alert."
|
||||
syncHealth:
|
||||
description: "Deprecated: removed in v1.44, read `health`."
|
||||
description: "Deprecated, still served: read `health`."
|
||||
deprecated: true
|
||||
$ref: "#/components/schemas/SyncHealth"
|
||||
metricFreshness:
|
||||
@@ -84398,7 +84397,7 @@ components:
|
||||
- type: "null"
|
||||
tokenExpired:
|
||||
deprecated: true
|
||||
description: "Deprecated: removed in v1.44, read `health`. Null — not false — when there is no connection to judge."
|
||||
description: "Deprecated, still served: read `health`. Null — not false — when there is no connection to judge."
|
||||
type:
|
||||
- boolean
|
||||
- "null"
|
||||
@@ -84441,14 +84440,14 @@ components:
|
||||
- "null"
|
||||
hasActivityScope:
|
||||
deprecated: true
|
||||
description: "Deprecated: removed in v1.44, read `health`. Withings only."
|
||||
description: "Deprecated, still served: read `health`. Withings only."
|
||||
type: boolean
|
||||
planUnavailable:
|
||||
deprecated: true
|
||||
description: "Deprecated: removed in v1.44, read `health`. Withings only. The readings this connection's Withings API
|
||||
plan withholds (status 277 since the 2026-10-12 plan split), deduplicated and in display order. Empty when
|
||||
the plan refuses nothing. The sync skips these and keeps importing everything else; the list refreshes when
|
||||
the weekly re-probe asks again."
|
||||
description: "Deprecated, still served: read `health`. Withings only. The readings this connection's Withings API plan
|
||||
withholds (status 277 since the 2026-10-12 plan split), deduplicated and in display order. Empty when the
|
||||
plan refuses nothing. The sync skips these and keeps importing everything else; the list refreshes when the
|
||||
weekly re-probe asks again."
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
@@ -84477,7 +84476,7 @@ components:
|
||||
- weight
|
||||
needsReauth:
|
||||
deprecated: true
|
||||
description: "Deprecated: removed in v1.44, read `health`. Google Health only."
|
||||
description: "Deprecated, still served: read `health`. Google Health only."
|
||||
type: boolean
|
||||
available:
|
||||
description: Oura / Polar / Strava only.
|
||||
@@ -84487,7 +84486,7 @@ components:
|
||||
type: boolean
|
||||
scopeReconnect:
|
||||
deprecated: true
|
||||
description: "Deprecated: removed in v1.44, read `health`. Oura only. Collections the stored grant does not cover: Oura
|
||||
description: "Deprecated, still served: read `health`. Oura only. Collections the stored grant does not cover: Oura
|
||||
refused them for a missing scope (named in its 401, or inferred when sibling collections succeeded with the
|
||||
same token), or the scopes Oura reported at connect leave theirs out. Not a failure and not counted toward
|
||||
the reconnect threshold; one reconnect asks for the missing scopes. Empty when the grant covers everything
|
||||
@@ -84543,7 +84542,7 @@ components:
|
||||
description: The unified connection state, null when the provider is not connected (and for Fitbit once
|
||||
`retirement.ended`). It supersedes `state`, `lastError`, `consecutiveFailuresByKind`, `syncHealth`,
|
||||
`tokenExpired`, `needsReauth`, `hasActivityScope`, `planUnavailable` and `scopeReconnect` on this entry;
|
||||
those nine are deprecated and removed in v1.44.
|
||||
those nine are deprecated and still served for clients that predate `health`.
|
||||
required:
|
||||
- integration
|
||||
- state
|
||||
|
||||
@@ -295,7 +295,7 @@ Concrete consequences:
|
||||
RMSSD HRV has its own ladder, **WHOOP ≻ OURA ≻ POLAR ≻ APPLE_HEALTH**,
|
||||
separate from the SDNN one.
|
||||
|
||||
Override per-user via Settings → Sources (`/settings/sources`).
|
||||
Override per-user via Settings → Source priority (`/settings/sources`).
|
||||
|
||||
## Failure modes
|
||||
|
||||
|
||||
@@ -1,82 +1,7 @@
|
||||
# Dawarich integration
|
||||
|
||||
HealthLog can read daily movement context from a [Dawarich](https://dawarich.app)
|
||||
instance: how far you moved, how much of it by car or by train, how many places
|
||||
you visited, how long you were away from home, and whether a day was spent in
|
||||
another country. It is part of the Environment module and runs only while that
|
||||
module is on.
|
||||
|
||||
HealthLog never stores a coordinate, a track, a place name or a visit. It reads
|
||||
the day summaries and keeps one row of numbers per day. The place list is read
|
||||
live when you pick your home and is not saved.
|
||||
|
||||
## What is read
|
||||
|
||||
Once a night (03:20 Europe/Berlin) HealthLog fetches the last seven days, ending
|
||||
yesterday. Connecting fetches the last 30 days. Each request carries your API
|
||||
key as `Authorization: Bearer <key>`.
|
||||
|
||||
| Dawarich endpoint | What HealthLog keeps |
|
||||
| ----------------------- | -------------------------------------------------------------------------------------- |
|
||||
| `GET /api/v1/timeline` | total distance, distance by car (`driving`), distance by train, places visited |
|
||||
| `GET /api/v1/visits` | minutes spent at your home place, to work out time away from home |
|
||||
| `GET /api/v1/residency` | the country of each day, sealed at rest, and whether it differs from your home country |
|
||||
| `GET /api/v1/places` | nothing; the list is shown once so you can pick your home |
|
||||
| `GET /api/v1/users/me` | nothing; used to check the key |
|
||||
|
||||
Time away from home is the day's length minus the time a visit at your home
|
||||
place covers. Without a home place, or on a day without a single visit, it is
|
||||
left empty rather than shown as zero. Dawarich leaves out a mode that moved
|
||||
less than half a kilometre; HealthLog then reads that mode as zero for the day.
|
||||
|
||||
Residency needs API access on a hosted Dawarich plan; a self-hosted instance
|
||||
has it. Without it there are no travel days, and everything else still works.
|
||||
Dawarich keys residency by UTC date, so on a day that crosses a border late in
|
||||
the evening the country can be the next morning's.
|
||||
|
||||
## Connect
|
||||
|
||||
1. In Dawarich, open your account settings and copy the API key.
|
||||
2. In HealthLog, turn on the Environment module, then open
|
||||
**Settings → Integrations → Dawarich**.
|
||||
3. Enter the address of the instance and the key, and connect.
|
||||
4. Pick your home place from the list, and set your home country if you like.
|
||||
Without one, the country you spent most days in over the last year counts.
|
||||
|
||||
Run Dawarich and HealthLog in the same time zone. Dawarich groups the
|
||||
timeline by its own zone, HealthLog by yours; if they differ, a day can come
|
||||
back without its distances.
|
||||
|
||||
## A Dawarich instance on your own network
|
||||
|
||||
A public instance works without any server setting. An instance on a private
|
||||
network (a LAN address, a `.lan` name, a container on the same host) is refused
|
||||
unless the operator lists its exact origin:
|
||||
|
||||
```env
|
||||
DAWARICH_PRIVATE_ORIGINS="http://dawarich.lan:3000"
|
||||
```
|
||||
|
||||
The grammar is the one `NIGHTSCOUT_PRIVATE_ORIGINS` uses
|
||||
([Nightscout](./nightscout.md)): one `scheme://host[:port]` per entry, comma
|
||||
separated, no path, no wildcard and no range. Listing a host does not open its
|
||||
subdomains or another port. The unspecified address, link-local and the
|
||||
cloud-metadata range can never be listed. Restart HealthLog after changing it.
|
||||
The variable is on the `environment:` whitelist in `docker-compose.yml`.
|
||||
|
||||
## Where the values show up
|
||||
|
||||
- The day view has an **On the move** section with the day's figures.
|
||||
- Time away from home and the distance travelled join the correlation scan as
|
||||
two more factors.
|
||||
- Runs of at least two travel days that no trip covers yet are offered as
|
||||
trips under **Settings → Environment → Location periods**. Using one fills
|
||||
in the dates; you pick the place.
|
||||
- The Coach and the MCP `get_day` tool see the figures and whether it was a
|
||||
travel day, never the country.
|
||||
|
||||
## Disconnect
|
||||
|
||||
Disconnecting stops the nightly fetch and keeps the stored days. **Disconnect
|
||||
and delete days** removes them as well. The days are part of the backup; the
|
||||
address and key are not, so a restored account connects again.
|
||||
The Dawarich guide lives on the docs site:
|
||||
<https://docs.healthlog.dev/guides/connect-dawarich/>. It covers what is read
|
||||
and what is never stored, connecting an instance, allowing one on your own
|
||||
network (`DAWARICH_PRIVATE_ORIGINS`), where the values show up, and
|
||||
disconnecting.
|
||||
@@ -103,7 +103,7 @@ 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 —
|
||||
the source priority ladder in **Settings → Sources** decides which one wins
|
||||
the source priority ladder in **Settings → Source priority** decides which one wins
|
||||
per metric. FITBIT is ranked in every default metric ladder; override
|
||||
per metric on that page.
|
||||
|
||||
|
||||
@@ -305,7 +305,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 → Sources** decides which one wins
|
||||
priority ladder in **Settings → Source priority** decides which one wins
|
||||
per metric.
|
||||
|
||||
## Disconnect
|
||||
|
||||
@@ -1,74 +1,6 @@
|
||||
# Strava integration
|
||||
|
||||
HealthLog reads your Strava activities and folds them into the same workout
|
||||
history as every other source. Strava is a **workout source only** — its API is
|
||||
activity-centric and exposes no sleep, recovery, body-composition or glucose
|
||||
data, so connecting it adds runs, rides, swims and the rest to your workout
|
||||
surface and nothing else.
|
||||
|
||||
The OAuth app is brought **per user**: each self-hoster registers their own
|
||||
Strava API application and pastes the client id/secret into Settings. Strava
|
||||
caps every newly-created app at "single-player mode" (an athlete capacity of 1)
|
||||
until you request a capacity increase — perfectly fine for a personal
|
||||
self-host, and the reason a single shared app cannot serve many operators.
|
||||
|
||||
## What you get — and what you don't
|
||||
|
||||
**Covered** (one `Workout` row per activity): sport type, start and end time,
|
||||
moving duration, distance, elevation gain, average and maximum heart rate (when
|
||||
the activity recorded it), and calories. Power, cadence, the "trainer"/"commute"
|
||||
flags and the activity name are kept on the row's metadata for provenance but
|
||||
are **not** surfaced as training analytics — HealthLog treats Strava as another
|
||||
workout source, not a training-load platform.
|
||||
|
||||
**Not covered**: Strava exposes no daily metrics (steps, sleep, resting heart
|
||||
rate, weight, …). Those come from your wearable's own integration or from Apple
|
||||
Health / Google Health Connect.
|
||||
|
||||
## Duplicate activities are collapsed automatically
|
||||
|
||||
If you record a run on an Apple Watch **and** it also lands in Strava, the same
|
||||
run arrives twice — once via Apple Health, once via Strava. HealthLog keeps one
|
||||
canonical row per logical workout using your **source-priority ladder**: by
|
||||
default the device-native capture (Apple Watch, WHOOP, Withings) wins over the
|
||||
Strava copy, because a Strava activity is often a re-upload with lower-fidelity
|
||||
or missing heart-rate data. A run you record **only** on Strava (phone GPS)
|
||||
survives untouched. You can promote Strava above the wearables in
|
||||
**Settings → Sources** if you prefer the Strava copy.
|
||||
|
||||
## 1. Register a Strava API application
|
||||
|
||||
1. Sign in at <https://www.strava.com/settings/api>.
|
||||
2. Create an application. For **Authorization Callback Domain** enter the host
|
||||
of your HealthLog instance (the domain only, no scheme or path).
|
||||
3. Copy the **Client ID** and **Client Secret**.
|
||||
|
||||
## 2. Paste the credentials into HealthLog
|
||||
|
||||
1. Open **Settings → Integrations** and find the **Strava** card.
|
||||
2. Paste the Client ID and Client Secret into the **API Credentials** fields and
|
||||
save. (An operator can instead configure a shared app via the
|
||||
`STRAVA_CLIENT_ID` / `STRAVA_CLIENT_SECRET` environment variables; per-user
|
||||
credentials always take precedence.)
|
||||
3. Click **Connect Strava** and approve the access request. HealthLog requests
|
||||
the `activity:read_all` scope so it can read every activity, including those
|
||||
you marked "only me".
|
||||
|
||||
## 3. First sync
|
||||
|
||||
The first history import runs in the background shortly after you connect;
|
||||
afterwards an hourly poll pulls new activities. Strava rotates its refresh token
|
||||
on every refresh, and HealthLog persists the rotated token each time, so the
|
||||
connection stays healthy without you re-authorising.
|
||||
|
||||
## Disconnecting
|
||||
|
||||
Disconnecting revokes the grant at Strava and clears the stored tokens.
|
||||
Activities already imported stay in your history; reconnecting resumes the sync.
|
||||
|
||||
## Related
|
||||
|
||||
- Wearable that syncs to Strava but not directly to HealthLog? See the device's
|
||||
own page, or ingest via [Apple Health](./apple-health.md) /
|
||||
[Google Health Connect](./google-health.md).
|
||||
- **Garmin owners:** Garmin has no direct connector — see [garmin.md](./garmin.md).
|
||||
The Strava guide lives on the docs site:
|
||||
<https://docs.healthlog.dev/guides/connect-strava/>. It covers what Strava
|
||||
adds (workouts only), how duplicate activities from a wearable are collapsed,
|
||||
registering the Strava API application, the first sync and disconnecting.
|
||||
@@ -88,7 +88,7 @@ or right away after reconnecting Withings once.
|
||||
needed — `user.metrics` covers weight / BP / heart-list / sleep /
|
||||
SpO₂ / body comp / temperature / VO₂ max; `user.activity` is the
|
||||
workout-equivalent stream for steps / energy / distance / floors.
|
||||
See `WITHINGS_OAUTH_SCOPE` in `src/lib/withings/client.ts`.
|
||||
See `WITHINGS_OAUTH_SCOPE` in `src/lib/withings/oauth.ts`.
|
||||
4. Save the application. Withings will issue a **Client ID** and a
|
||||
**Client Secret** — keep the secret out of any chat log; it grants
|
||||
read access to every linked account's full health history.
|
||||
@@ -145,7 +145,7 @@ callback. The portal only owns the OAuth callback URL.
|
||||
## 4. Link from the Settings page
|
||||
|
||||
With the credentials saved in Settings, open
|
||||
`/settings/integrations/withings` and click **Connect Withings**. The flow:
|
||||
`/settings/integrations#withings` and click **Connect Withings**. The flow:
|
||||
|
||||
1. The Connect button redirects to Withings' `oauth2/authorize`.
|
||||
2. The user signs in on Withings and grants the requested scopes.
|
||||
@@ -181,7 +181,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 Settings → Sources (`/settings/sources`).
|
||||
Override per-user via Settings → Source priority (`/settings/sources`).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -199,7 +199,7 @@ Override per-user via Settings → Sources (`/settings/sources`).
|
||||
### The webhook stops firing
|
||||
|
||||
Webhook subscriptions can lapse after a token revoke or a portal-
|
||||
side cleanup. Re-subscribe by opening `/settings/integrations/withings`
|
||||
side cleanup. Re-subscribe by opening `/settings/integrations#withings`
|
||||
and clicking **Disconnect** then **Connect Withings** again — the
|
||||
callback exchange re-runs `Notify.subscribe()` with a fresh callback
|
||||
URL.
|
||||
|
||||
+5
-2
@@ -270,10 +270,13 @@ always confirm the served build after a deploy:
|
||||
```sh
|
||||
# Asserts prod + demo serve 1.9.0, prints each target's buildSha,
|
||||
# exits non-zero on a mismatch or timeout after a bounded retry loop.
|
||||
pnpm dlx tsx scripts/assert-deploy.ts 1.9.0
|
||||
HEALTHLOG_PROD_URL=https://healthlog.example.com \
|
||||
pnpm dlx tsx scripts/assert-deploy.ts 1.9.0
|
||||
```
|
||||
|
||||
Pass `--only=prod` / `--only=demo` to check a single target, or
|
||||
`HEALTHLOG_PROD_URL` is the base URL of your instance; it has no default and
|
||||
is required unless you pass `--only=demo`. Pass `--only=prod` /
|
||||
`--only=demo` to check a single target, or
|
||||
`--attempts=N --interval=ms` to widen the retry window while a slow
|
||||
pull settles.
|
||||
|
||||
|
||||
@@ -96,8 +96,9 @@ maintenance refuses to start while one is.
|
||||
|
||||
Have at least twice the largest size free on the database volume.
|
||||
|
||||
- **Take a backup** (Settings, Data, Backups), as before any operation on the
|
||||
whole table.
|
||||
- **Take a backup**: a database dump, as in
|
||||
[Updates](../self-hosting/getting-started.md#updates), as before any
|
||||
operation on the whole table.
|
||||
|
||||
## Starting it
|
||||
|
||||
|
||||
@@ -214,11 +214,15 @@ as something to log in as.
|
||||
or delete the profile under a fresh second factor. Deleting the profile
|
||||
deletes everything recorded for it.
|
||||
- A guardian may keep that record's settings: modules, units, thresholds,
|
||||
its language and timezone, and its notification preferences. Connected
|
||||
services are visible as a status only. Connect, callback, sync,
|
||||
credential, disconnect, resume and test are not guardian surfaces in
|
||||
this release, and their deep links land on the shared-record unavailable
|
||||
page rather than on a raw refusal.
|
||||
its language and timezone, and its notification preferences. A guardian
|
||||
can also connect and disconnect the record's Withings, WHOOP, Oura, Polar,
|
||||
Strava and Google Health from its integrations page; the data lands in the
|
||||
profile's record, and a profile without its own provider app uses the
|
||||
guardian's. Fitbit (no new connections), Nightscout and Dawarich cannot be
|
||||
connected for a profile. Sync, the connection test and provider
|
||||
credentials are not guardian surfaces: once connected, the record syncs on
|
||||
its own schedule, and their deep links land on the shared-record
|
||||
unavailable page rather than on a raw refusal.
|
||||
- **Its reminders go to its guardians.** A record with no login has no
|
||||
channels of its own, so medication reminders, measurement reminders,
|
||||
safety-floor alerts and low-stock alerts fan out to every active
|
||||
|
||||
@@ -86,10 +86,11 @@ backfill button in settings fetches both for the range it is given.
|
||||
Since v1.42 the home, each dated location period and each stored day keep
|
||||
their coarse location sealed with the instance's encryption key (one value
|
||||
per row, `{ lat, lon, label }`, under the label
|
||||
`healthlog/environment-location/v1`). The boot-time encryption backfill seals
|
||||
rows written by older releases and clears their readable columns; the
|
||||
readable columns are dropped in a later release once the backfill reports
|
||||
zero rows everywhere. Key rotation (`scripts/rotate-encryption-key.ts`, or the
|
||||
`healthlog/environment-location/v1`). The boot-time encryption backfill sealed
|
||||
rows written by older releases and cleared their readable columns, and
|
||||
migration `0409` (v1.43) dropped those columns; it refuses to run while a row
|
||||
still holds a readable location
|
||||
([migration-0409-plaintext-location.md](../ops/migration-0409-plaintext-location.md)). Key rotation (`scripts/rotate-encryption-key.ts`, or the
|
||||
in-app rotation) covers all three.
|
||||
|
||||
What the person sees is unchanged: the settings page and
|
||||
|
||||
@@ -9,8 +9,7 @@ already installed, longer if a TLS-fronted public hostname is in scope
|
||||
|
||||
- **Docker 24+ with Compose v2.** The bundled `docker-compose.yml`
|
||||
pulls a multi-arch image from GitHub Container Registry (linux/amd64
|
||||
- linux/arm64), so a Raspberry Pi 5 or an x86 VPS both work
|
||||
unchanged.
|
||||
and linux/arm64), so a Raspberry Pi 5 or an x86 VPS both work unchanged.
|
||||
- **3 GB RAM (4 GB recommended), 10 GB free disk.** The Postgres data
|
||||
volume grows with measurement history; the Apple Health import worker
|
||||
can briefly hold a 1.5 GB upload in `/tmp` while it parses. The app
|
||||
@@ -23,11 +22,12 @@ already installed, longer if a TLS-fronted public hostname is in scope
|
||||
below the supported floor. On a busy instance, raise it.
|
||||
- **Outbound HTTPS** to `ghcr.io` (image pulls) plus whichever
|
||||
integration endpoints you plan to enable (Withings, OpenAI, etc.).
|
||||
The opt-in environmental-context module additionally reaches
|
||||
`archive-api.open-meteo.com` and `geocoding-api.open-meteo.com` for
|
||||
weather and daylight; it stays off until you
|
||||
enable it and set a home location, and every outbound call goes
|
||||
through the SSRF-guarded egress wrapper.
|
||||
The environmental-context module additionally reaches
|
||||
`archive-api.open-meteo.com`, `air-quality-api.open-meteo.com` and
|
||||
`geocoding-api.open-meteo.com` for weather, air quality and the city
|
||||
search. It is on by default, but nothing leaves the host until someone
|
||||
sets a home location, and every outbound call goes through the
|
||||
SSRF-guarded egress wrapper.
|
||||
Three requests happen by default without anything connected: the
|
||||
HaveIBeenPwned range query when a password is set (only the first five
|
||||
characters of the password's SHA-1 are sent), a daily check of the
|
||||
|
||||
@@ -19,7 +19,7 @@ on top of it, see [building skills](../api/mcp-skills.md).
|
||||
|
||||
> **Read this before enabling writes.** A remote, write-capable connector
|
||||
> reached over OAuth is a powerful combination. Read the
|
||||
> [trust model](#trust-model-read-this-before-enabling-writes) at the end
|
||||
> [trust model](#trust-model--read-this-before-enabling-writes) at the end
|
||||
> of this page first.
|
||||
|
||||
## Prerequisites
|
||||
@@ -43,7 +43,7 @@ LAN-only origin works for Claude Desktop over stdio).
|
||||
The switch lives with every other module: **Settings → Modules**, the
|
||||
**MCP** row. Turn it on and the `/mcp` endpoint goes live for your account.
|
||||
|
||||
**Settings → MCP** is where you use it. The page has three parts:
|
||||
**Settings → MCP connector** is where you use it. The page has three parts:
|
||||
|
||||
- **State** — whether the module is on, with a link to its switch in
|
||||
Modules, and your endpoint URL (`<origin>/mcp`). Turning the module off
|
||||
@@ -55,7 +55,7 @@ The switch lives with every other module: **Settings → Modules**, the
|
||||
|
||||
## 2. Mint a connector token
|
||||
|
||||
Still in **Settings → MCP → Tokens**:
|
||||
Still in **Settings → MCP connector → Tokens**:
|
||||
|
||||
1. Choose the scope with the **write** toggle:
|
||||
- **off** → a read-only token (`health:read`). This is the default and
|
||||
@@ -106,7 +106,7 @@ refused (`403`) as a DNS-rebinding defence.
|
||||
client-id-metadata-document clients with dynamic registration as a
|
||||
fallback.
|
||||
3. Approve the consent screen. The connection then appears under
|
||||
**Settings → MCP → Connections**, where you can revoke it.
|
||||
**Settings → MCP connector → Connections**, where you can revoke it.
|
||||
|
||||
No token to paste — OAuth mints and rotates a short-lived `health:read`
|
||||
access token (60 minutes, with a refresh token) behind the scenes.
|
||||
|
||||
@@ -95,7 +95,7 @@ different things depending on which button you press.
|
||||
two actions below, the row stays. A start that was folded into a later
|
||||
cycle stays for as long as that cycle does, because deleting the later
|
||||
cycle gives it back.
|
||||
- **Settings → Delete All Data** removes the rows outright, tombstones
|
||||
- **Settings → Advanced → Delete all data** removes the rows outright, tombstones
|
||||
included. Everything the account owns goes except what you sign in
|
||||
with; the confirmation dialog lists it.
|
||||
- **Deleting the account** removes the account row, and the database
|
||||
|
||||
@@ -49,9 +49,10 @@ HealthLog behind a reverse proxy with its own domain, change App URL to that
|
||||
|
||||
Settings the form does not show go under Additional Environment Variables, for
|
||||
example `UPDATE_CHECK_DISABLED=1`. The full list is in
|
||||
[environment.md](environment.md). To use your own GeoLite2 databases, add the
|
||||
directory under Additional Storage and point `GEOLITE2_DIR` at its mount path
|
||||
([geolite2.md](geolite2.md)).
|
||||
[`.env.production.example`](../../.env.production.example) and on the
|
||||
[environment variables reference](https://docs.healthlog.dev/reference/environment-variables/).
|
||||
To use your own GeoLite2 databases, add the directory under Additional Storage
|
||||
and point `GEOLITE2_DIR` at its mount path ([geolite2.md](geolite2.md)).
|
||||
|
||||
## Plain HTTP on the LAN
|
||||
|
||||
@@ -206,15 +207,15 @@ before you touch it.
|
||||
Until the catalog app is listed, install HealthLog with Apps, Discover Apps,
|
||||
the menu next to Custom App, Install via YAML, using these values:
|
||||
|
||||
| Setting | Value |
|
||||
| ------------ | ---------------------------------------------------------------------------------------- |
|
||||
| Image | `ghcr.io/mbombeck/healthlog`, tag `X.Y.Z` (amd64 and arm64) |
|
||||
| Port | `3000` (HTTP) |
|
||||
| User | runs as uid `1001` by default; no extra capabilities, `no-new-privileges` is fine |
|
||||
| Health check | `GET /api/health`, `200` when ready |
|
||||
| Start period | at least 60 s, with more retries than the image's 5 × 30 s: migrations run at every boot |
|
||||
| Database | PostgreSQL 18 (16 works too), no extensions, in its own container with its own dataset |
|
||||
| Volumes | none for the app; the database gets its own dataset |
|
||||
| Setting | Value |
|
||||
| ------------ | -------------------------------------------------------------------------------------- |
|
||||
| Image | `ghcr.io/mbombeck/healthlog`, tag `X.Y.Z` (amd64 and arm64) |
|
||||
| Port | `3000` (HTTP) |
|
||||
| User | runs as uid `1001` by default; no extra capabilities, `no-new-privileges` is fine |
|
||||
| Health check | `GET /api/health`, `200` when ready |
|
||||
| Start period | 10 min, like the image: migrations run at every boot and wait out a held lock |
|
||||
| Database | PostgreSQL 18 (16 works too), no extensions, in its own container with its own dataset |
|
||||
| Volumes | none for the app; the database gets its own dataset |
|
||||
|
||||
| Variable | Required | Notes |
|
||||
| ----------------------- | -------- | ---------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -116,8 +116,8 @@ PostgreSQL container you installed in Step 1. Back that one up.
|
||||
|
||||
`SESSION_COOKIE_SECURE=false` is required for plain-HTTP LAN installs
|
||||
(`http://tower:3000`). Left at its default, the session cookie carries the
|
||||
`Secure` flag, the browser refuses to send it over HTTP, and login appears to
|
||||
"do nothing" — the page reloads to the login screen with no error.
|
||||
`Secure` flag, which the browser would drop over HTTP. The login page says so
|
||||
before anything is sent: "Signing in cannot complete on this page".
|
||||
|
||||
Set `SESSION_COOKIE_SECURE=true` (or leave it unset) only when a TLS reverse
|
||||
proxy serves HealthLog over HTTPS. In that case also point `NEXT_PUBLIC_APP_URL`
|
||||
|
||||
@@ -311,7 +311,7 @@ Tiles at the top of the dashboard live in a **single row**. Width caps
|
||||
at the chart's width directly below. **Never** wrap to a second row.
|
||||
|
||||
If the user enables more tiles than fit, the dashboard surfaces a banner
|
||||
inviting them to deactivate or reorder via Settings → Dashboard. The
|
||||
inviting them to deactivate or reorder via Settings → Arrangement. The
|
||||
layout editor in Settings shows a live preview of the constraint and
|
||||
flags overflow before save.
|
||||
|
||||
|
||||
Reference in new issue
Block a user