diff --git a/README.md b/README.md index 04239df32..d342f07b8 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@

Self-hosted health tracker. Weight, blood pressure, glucose, mood, medications.
- Withings and Apple Health sync, multi-provider AI Insights you own, doctor-report PDF. + Withings, WHOOP, and Apple Health sync, multi-provider AI Insights you own, doctor-report PDF.

@@ -36,7 +36,7 @@ ## What it is -HealthLog is a self-hosted personal health tracker that runs from a single `docker compose up`. It covers the metrics most people actually log -- weight, blood pressure, pulse, body composition, blood glucose, sleep, mood, and medication compliance -- and brings them together in one dashboard with reference ranges from ESH 2023, ADA 2024, and NICE NG115. Withings devices sync automatically; an `export.zip` import folds your full Apple Health history into the same timeline; a native SwiftUI iOS client (public-beta via [TestFlight](https://testflight.apple.com/join/bucuTBpa)) streams HealthKit live; multi-provider AI Insights (BYOK or local) explain what the numbers mean; a doctor-report PDF generates client-side. EN/DE end-to-end. AGPL-3.0. +HealthLog is a self-hosted personal health tracker that runs from a single `docker compose up`. It covers the metrics most people actually log -- weight, blood pressure, pulse, body composition, blood glucose, sleep, mood, and medication compliance -- and brings them together in one dashboard with reference ranges from ESH 2023, ADA 2024, and NICE NG115. Withings and WHOOP devices sync automatically over OAuth2; an `export.zip` import folds your full Apple Health history into the same timeline; a native SwiftUI iOS client (public-beta via [TestFlight](https://testflight.apple.com/join/bucuTBpa)) streams HealthKit live; multi-provider AI Insights (BYOK or local) explain what the numbers mean; a doctor-report PDF generates client-side. EN/DE end-to-end. AGPL-3.0. > **Status**: active. New releases roughly weekly -- see [CHANGELOG](CHANGELOG.md). Current line: v1.10 — a derived-metrics tier (fitness-age, vascular-age delta, HRV balance, sleep score, readiness and recovery scores, an early-strain flag), each computed from your own measurements and citing its inputs, on top of the v1.7 health-record export (PDF + FHIR R4), flexible medication schedules, and full HealthKit metric coverage. The native iOS client is public-beta via [TestFlight](https://testflight.apple.com/join/bucuTBpa). @@ -98,7 +98,7 @@ HealthLog is the right fit when you want your wearable and clinical numbers in o **Apple Health import** -- Drop your iOS `export.zip` on the import page. A streaming parser handles multi-gigabyte archives (Zip64), folds every ``, ``, ``, and `` into the same timeline as your other metrics, and stays idempotent on re-upload. Per-type ingestion stats plus a live status endpoint so you can watch the progress on a long historical drain. -**AI Coach + Insights** -- A conversational Coach grounded in your own data, a daily briefing, a weekly report, and a Health Score tile on the dashboard. Pick OpenAI, Anthropic Claude, ChatGPT via Codex device-OAuth (no API key needed), or any OpenAI-compatible local endpoint (Ollama, LM Studio, vLLM). BYOK or admin-shared. Feed the Coach a chosen set of data clusters (cardiovascular, body composition, activity, workouts, sleep, mood, glucose, medication, mobility, environment) with a soft budget cap that degrades the lowest-signal clusters first. Every claim links back to the measurements that produced it. Local endpoints keep all data on your network. +**AI Coach + Insights** -- A conversational Coach grounded in your own data, a daily briefing, a weekly report, and a Health Score tile on the dashboard. Pick OpenAI, Anthropic Claude, ChatGPT via Codex device-OAuth (no API key needed), or any OpenAI-compatible local endpoint (Ollama, LM Studio, vLLM). BYOK or admin-shared. Feed the Coach a chosen set of data clusters (cardiovascular, body composition, activity, workouts, sleep, mood, glucose, medication, mobility, environment) with a soft budget cap that degrades the lowest-signal clusters first. Every claim links back to the measurements that produced it. The Coach now reads your history longitudinally — a narrative of how a metric has moved and a per-metric trajectory framing — so an answer reflects the direction of travel rather than only the latest reading. Local endpoints keep all data on your network. **Derived wellness metrics** -- A transparent metrics tier computed from your own measurements: a fitness-age band from VO₂max, a vascular-age delta, an HRV-balance band, a sleep score, a daily readiness and a stored recovery score, plus a coincident-deviation early-strain flag. Each one re-frames or blends signals you already recorded against age/sex norms or your personal baseline, cites the inputs and the published method behind it, and returns "not enough data" rather than a fabricated value when its minimum inputs are missing. The recovery score persists as a daily 0–100 series the dashboard, charts, and native client read without recomputing. These are descriptive wellness framings — not clinical or training-grade assessments. @@ -106,6 +106,8 @@ HealthLog is the right fit when you want your wearable and clinical numbers in o **Health-record export (PDF + FHIR R4)** -- `POST /api/export/health-record` produces a selectable export: an enriched clinical PDF, a machine-readable HL7 FHIR R4 document bundle (LOINC-coded observations, a BP panel, medication statements, a diagnostic report), or both packaged as one zip. The selection chooses date range and per-domain sections; both formats read the same aggregator so they describe identical numbers, and the optional AI summary is an explicit opt-in section marked as not clinically validated. Optional patient identity (name, insurer, insurance number) on Account feeds the report cover and the FHIR `Patient`. +**Clinician-grade FHIR API + shareable record** -- A read-only HL7 FHIR R4 REST API (`GET /api/fhir/{metadata,Patient,Observation,MedicationStatement,MedicationAdministration,$everything}`) lets a clinician system pull your record in a standard shape, gated behind a dedicated `fhir:read` token scope. Alongside it, a shareable clinician record: create a scoped, time-limited share link, hand a clinician the read-only `/c/` view, and revoke it when the visit is over. The two are not yet wired together — a share link does not expose the FHIR API (`capabilities.share.fhirApi=false`) — but each stands on its own. + **Built-in Feedback** -- Send bug reports and feature requests from inside the app. Stored in your HealthLog database — no GitHub config required. Optional GitHub escalation for admins. **PWA with Offline Support** -- Installable on iOS and Android. Service worker with intelligent caching strategies for reliable offline access. A paginated, opaque-cursor sync delta feed (`GET /api/sync/changes`) with measurement tombstones lets native and offline clients reconcile against the server incrementally. @@ -213,6 +215,8 @@ HealthLog is designed for people who take data ownership seriously. | `WITHINGS_CLIENT_SECRET` | Withings OAuth2 client secret | | `WITHINGS_REDIRECT_URI` | OAuth callback URL | | `WITHINGS_WEBHOOK_SECRET` | Webhook URL hardening secret | +| `WHOOP_REDIRECT_URI` | WHOOP OAuth callback URL (client id/secret set in Settings) | +| `WHOOP_WEBHOOK_SECRET` | HMAC secret for WHOOP webhook signature verification | | `TELEGRAM_WEBHOOK_SECRET` | Telegram bot webhook secret | Telegram bot token, ntfy settings, Web Push VAPID keys, Umami, and GlitchTip URLs are configured in the **Admin Panel** and stored encrypted in the database. @@ -437,6 +441,24 @@ All mutations require authentication via session cookie. External ingest uses Be +

+FHIR + clinician sharing (v1.11) + +Read-only HL7 FHIR R4 REST API, gated behind the `fhir:read` token scope: + +| Method | Endpoint | Description | +| ------ | -------------------------------------- | ---------------------------------------------------- | +| `GET` | `/api/fhir/metadata` | FHIR CapabilityStatement | +| `GET` | `/api/fhir/Patient` | Patient resource for the token's user | +| `GET` | `/api/fhir/Observation` | Vitals + labs as LOINC-coded observations | +| `GET` | `/api/fhir/MedicationStatement` | Medication regimen | +| `GET` | `/api/fhir/MedicationAdministration` | Logged intake events | +| `GET` | `/api/fhir/$everything` | Bundle of every resource above | + +Clinician share-link lifecycle — create a scoped, time-limited link, hand a clinician the read-only `/c/` view, and revoke it after the visit. Share links do not expose the FHIR API (`capabilities.share.fhirApi=false`). + +
+ --- ## Integrations @@ -444,6 +466,7 @@ All mutations require authentication via session cookie. External ingest uses Be | Integration | Setup | Purpose | | --------------- | ------------- | ---------------------------------------- | | **Withings** | Env vars | Auto-sync weight, BP, and activity | +| **WHOOP** | User Settings | OAuth2 sync of recovery, strain, and sleep (BYO-keys) | | **Telegram** | Admin Panel | Medication reminders with inline buttons | | **ntfy** | User Settings | Self-hosted push notifications | | **Web Push** | Admin Panel | Browser-native VAPID notifications | @@ -508,6 +531,8 @@ For a single-process default the same container hosts both the web and worker (` | **v1.8 – v1.10** (current) | Insights redesign with selectable time ranges (v1.8 – v1.9), then a derived-metrics tier (v1.10): fitness-age, vascular-age delta, HRV balance, sleep score, readiness and recovery scores, and a coincident-deviation early-strain flag — each computed from your own measurements, each citing its inputs and degrading to "not enough data" rather than fabricating a value. | | **v2.x** (planned) | Multi-tenant hardening, expanded device passthrough (Garmin / Polar), opt-in cross-user aggregate research mode (off by default; never enabled without explicit consent). | +Two known limitations carry forward from v1.11: dedup across two sources of the same standard vital is deferred (both rows persist, source priority decides display), and the coach's durable conversation-summary / remembered-facts layer is deferred — each answer reasons from your data afresh rather than from a running memory. + The detailed changelog lives in [`CHANGELOG.md`](CHANGELOG.md). --- diff --git a/docs/integrations/whoop.md b/docs/integrations/whoop.md new file mode 100644 index 000000000..10a87f629 --- /dev/null +++ b/docs/integrations/whoop.md @@ -0,0 +1,71 @@ +# WHOOP integration + +HealthLog connects to WHOOP via OAuth2 plus HMAC-signed webhooks for +near-real-time sync of recovery, strain, sleep, and the underlying +heart-rate / HRV signals. Unlike Withings, WHOOP credentials are +brought per user: each user (or the operator on their behalf) +registers their own WHOOP developer app and pastes the client +id/secret into Settings. + +## Why bring your own keys + +WHOOP caps the number of users a single developer app may authorise. +Per-user BYO-keys sidestep that per-app cap: every user authorises +against their own WHOOP app, so a shared HealthLog instance never +runs into one app's user ceiling. It also keeps each user's WHOOP +grant scoped to credentials they control. + +## 1. Register a WHOOP developer app + +1. Sign in at and create a new app. +2. **Redirect URI:** `https://your-instance.example.com/api/whoop/callback` + (matches `WHOOP_REDIRECT_URI`, below). Add one entry per + environment you connect from. +3. Request the scopes for recovery, cycle/strain, sleep, and the + workout/heart-rate reads the sync uses. +4. Save the app. WHOOP issues a **Client ID** and **Client Secret** — + keep the secret out of any chat log; it grants read access to the + linked account's full WHOOP history. + +## 2. Wire the credentials in Settings → Integrations + +WHOOP credentials are stored per user, not in `.env`. Sign in as the +target user, open **Settings → Integrations → WHOOP**, and paste the +client ID and secret. They are encrypted AES-256-GCM at rest. + +## 3. Connect + +Click **Connect WHOOP**. The flow: + +1. The Connect button redirects to WHOOP's OAuth authorize endpoint. +2. The user signs in on WHOOP and grants the requested scopes. +3. WHOOP redirects back to `/api/whoop/callback` with an auth code. +4. HealthLog exchanges the code for an access + refresh token, stores + them encrypted, registers the webhook, and triggers an initial + sync of recent history. +5. Subsequent syncs run on every webhook delivery plus a safety-net + cron pull. + +## Optional instance env vars + +Two server-level env vars tune the OAuth callback and webhook +verification. They are optional — the per-user client id/secret in +Settings drive the connection itself. + +```env +WHOOP_REDIRECT_URI="https://your-instance.example.com/api/whoop/callback" +WHOOP_WEBHOOK_SECRET="$(openssl rand -hex 32)" +``` + +- `WHOOP_REDIRECT_URI` — the OAuth callback URL handed to WHOOP. Must + match a redirect URI registered in the WHOOP developer app. +- `WHOOP_WEBHOOK_SECRET` — the secret HealthLog verifies the WHOOP + webhook HMAC signature against. WHOOP signs each webhook body; the + handler validates the signature before processing. Generate a fresh + random value and restart the `app` container after setting it. + +## Disconnect + +The Settings page **Disconnect** button revokes the stored tokens and +removes the connection. Existing measurements synced from WHOOP stay +in the database — disconnect does not delete data. diff --git a/docs/ops/deploy.md b/docs/ops/deploy.md index e22a1d7f7..822748a72 100644 --- a/docs/ops/deploy.md +++ b/docs/ops/deploy.md @@ -30,6 +30,20 @@ step 2 is redundant in normal Coolify flow. It's listed explicitly for the manual case so an operator can verify the migration result before flipping traffic. +### v1.11.0 notes + +Migrations `0110`–`0114` are additive (new tables / columns for the +WHOOP provider hub, the longitudinal coach, and the clinician FHIR +record). They require no special steps — `prisma migrate deploy` runs +them in order on boot like any other. + +WHOOP introduces two optional instance env vars, `WHOOP_REDIRECT_URI` +and `WHOOP_WEBHOOK_SECRET` (HMAC secret for WHOOP webhook signature +verification). Per-user WHOOP client id/secret live in Settings, not +in the environment. See `docs/integrations/whoop.md`. Add the two vars +to the compose `environment:` whitelist if you set them — vars not on +the whitelist never reach the container. + ### What goes wrong if you reverse it If the new app container starts before migrations run, the legacy diff --git a/docs/self-hosting/getting-started.md b/docs/self-hosting/getting-started.md index 8fc7244de..e3d02362e 100644 --- a/docs/self-hosting/getting-started.md +++ b/docs/self-hosting/getting-started.md @@ -151,6 +151,7 @@ NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false pnpm build | Off-host encrypted backups to S3/R2/B2 | `docs/ops/backup-restore.md` | | Encryption-key rotation procedure | `docs/ops/encryption-key-rotation.md` | | Withings device sync | `docs/integrations/withings.md` | +| WHOOP device sync (per-user BYO-keys) | `docs/integrations/whoop.md` | | Apple Health `export.zip` import | `docs/integrations/apple-health.md` | | AI provider setup (OpenAI / Anthropic / local / ChatGPT OAuth) | `docs/integrations/ai-providers.md` |