diff --git a/README.md b/README.md index 65c2c13f3..edf748bbb 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Try the [live demo](https://demo.healthlog.dev) to see a working install — sig - **Clinical signals, opt-in and additive.** Validated self-checks for mental wellbeing and sleep (WHO-5, PHQ-9, GAD-7, Sleep Condition Indicator — official questionnaire wording, calm crisis-resource handling), grip strength, a 0–10 pain score, waist and body measurements, and a longevity lab panel — each added through one signal registry, so a new signal wires into charts, correlations, the coach, the FHIR map, and the API at once. - **Records, structured.** Allergies and family history as structured records that flow into the doctor report (reactions and notes are encrypted, the substance and condition names are not), plus lab values you key in or scan from a printed report — OCR extracts the stated values, units, and dates for you to review and confirm before anything is saved. HealthLog transcribes what a report says; it never interprets or diagnoses from it. A vaccination record keeps a lifetime immunization log, doses grouped under the disease they protect against, and prints in the doctor report. - **A document vault for the paper trail.** An opt-in module stores every letter, report, and scan encrypted at rest — PDFs and photos preview inline, Office and text files stay download-only, and nothing user-uploaded can ever execute in the app. One searchable timeline with type, condition, and year filters, drag-and-drop and paste-to-upload, bulk filing, undo-able delete with a 30-day grace, and per-file/per-account limits the operator tunes. -- **Context that explains the numbers.** An opt-in environmental module folds weather, daylight, and temperature in as influence signals (winter darkness against mood, heat against blood pressure) using a home location you set, so a pattern has somewhere to point. +- **Context that explains the numbers.** An environmental module folds weather, daylight, temperature, air quality, and pollen in as influence signals (winter darkness against mood, heat against blood pressure) using a home location you set, so a pattern has somewhere to point. It is on by default, and nothing leaves the host until someone sets that location. - **Your devices, one server.** Withings, WHOOP, Oura and Polar sync over OAuth2, Google Health connects Fitbit and Pixel Watch through your own Google Cloud client, Nightscout streams CGM glucose, an Apple Health `export.zip` folds your full history in, and the native iOS app streams HealthKit live. A per-metric source priority decides which reading is canonical when wearables overlap. - **Medication tracking that tells the truth.** Flexible schedules (weekly injections, weekday-only, intervals, PRN, cyclic), a configurable intake window per dose, and a traceable dose history where every slot reads taken, late, skipped, or missed — the same ledger the compliance rate is computed from, so the percentage can never disagree with the timeline. - **AI insights you own.** Daily briefing, health scores, correlations, and a conversational coach grounded in your own measurements and interpreted against cited guideline reference bands — where a value sits and what that means in plain words, never a diagnosis. The coach proposes concrete if-then plans you accept or decline, and suppresses proactive check-ins when there's nothing genuine to say. Bring your own OpenAI or Anthropic key, or point at a local endpoint (Ollama, LM Studio, vLLM) so AI requests stay on your network. diff --git a/docs/README.md b/docs/README.md index 7b99ebc09..225e5a1f8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 . -- [`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 or jump to the [Quick Start in the project -README](../README.md#quick-start). +README](../README.md#quick-start-docker-compose). diff --git a/docs/integrations/apple-health.md b/docs/integrations/apple-health.md index dc7a3d615..33f72b545 100644 --- a/docs/integrations/apple-health.md +++ b/docs/integrations/apple-health.md @@ -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 diff --git a/docs/integrations/dawarich.md b/docs/integrations/dawarich.md index 7e19680f9..20540d2e7 100644 --- a/docs/integrations/dawarich.md +++ b/docs/integrations/dawarich.md @@ -70,7 +70,7 @@ The variable is on the `environment:` whitelist in `docker-compose.yml`. - 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 + trips under **Settings → Location & weather → 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. diff --git a/docs/integrations/fitbit.md b/docs/integrations/fitbit.md index 4c5684c21..c9eb2eae5 100644 --- a/docs/integrations/fitbit.md +++ b/docs/integrations/fitbit.md @@ -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. diff --git a/docs/integrations/google-health.md b/docs/integrations/google-health.md index 069d09e83..3176a827d 100644 --- a/docs/integrations/google-health.md +++ b/docs/integrations/google-health.md @@ -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 diff --git a/docs/integrations/strava.md b/docs/integrations/strava.md index da6a7ed3a..5607894fb 100644 --- a/docs/integrations/strava.md +++ b/docs/integrations/strava.md @@ -34,7 +34,7 @@ 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. +**Settings → Source priority** if you prefer the Strava copy. ## 1. Register a Strava API application diff --git a/docs/integrations/withings.md b/docs/integrations/withings.md index 99ddc858a..a211978c9 100644 --- a/docs/integrations/withings.md +++ b/docs/integrations/withings.md @@ -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. diff --git a/docs/ops/deploy.md b/docs/ops/deploy.md index 0a1f8ff7c..b6cfee395 100644 --- a/docs/ops/deploy.md +++ b/docs/ops/deploy.md @@ -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. diff --git a/docs/ops/measurement-maintenance.md b/docs/ops/measurement-maintenance.md index f7ca054ab..c43caa52d 100644 --- a/docs/ops/measurement-maintenance.md +++ b/docs/ops/measurement-maintenance.md @@ -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 diff --git a/docs/self-hosting/account-sharing.md b/docs/self-hosting/account-sharing.md index 7cc76a1de..9e75646fa 100644 --- a/docs/self-hosting/account-sharing.md +++ b/docs/self-hosting/account-sharing.md @@ -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 diff --git a/docs/self-hosting/environment.md b/docs/self-hosting/environment.md index 51dbb6d3d..095b63fd7 100644 --- a/docs/self-hosting/environment.md +++ b/docs/self-hosting/environment.md @@ -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 diff --git a/docs/self-hosting/getting-started.md b/docs/self-hosting/getting-started.md index f4fb5d208..c791df566 100644 --- a/docs/self-hosting/getting-started.md +++ b/docs/self-hosting/getting-started.md @@ -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 diff --git a/docs/self-hosting/mcp.md b/docs/self-hosting/mcp.md index a54b2d1c5..9f4334175 100644 --- a/docs/self-hosting/mcp.md +++ b/docs/self-hosting/mcp.md @@ -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 (`/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. diff --git a/docs/self-hosting/reproductive-health-data.md b/docs/self-hosting/reproductive-health-data.md index 128dbc6d3..314b65aa2 100644 --- a/docs/self-hosting/reproductive-health-data.md +++ b/docs/self-hosting/reproductive-health-data.md @@ -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 diff --git a/docs/self-hosting/truenas.md b/docs/self-hosting/truenas.md index d312e4fb7..f8a1b6328 100644 --- a/docs/self-hosting/truenas.md +++ b/docs/self-hosting/truenas.md @@ -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 | | ----------------------- | -------- | ---------------------------------------------------------------------------------------- | diff --git a/docs/self-hosting/unraid.md b/docs/self-hosting/unraid.md index 87c0e91df..e0d9564b6 100644 --- a/docs/self-hosting/unraid.md +++ b/docs/self-hosting/unraid.md @@ -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` diff --git a/docs/ui-guidelines.md b/docs/ui-guidelines.md index 093a0ef35..5b00604e2 100644 --- a/docs/ui-guidelines.md +++ b/docs/ui-guidelines.md @@ -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.