Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f20ad79
feat(db): add ClinicianShareLink model (migration 0110)
MBombeck Jun 3, 2026
747a625
refactor(fhir): extract per-resource emitters into resources module
MBombeck Jun 3, 2026
6db3bef
feat(measurements): add WHOOP source, native score types, and source-…
MBombeck Jun 3, 2026
fbd45f3
feat(ai): durable provider-health ledger and credential-expired surfa…
MBombeck Jun 3, 2026
86dea3e
chore(api): regenerate OpenAPI for the WHOOP measurement types
MBombeck Jun 3, 2026
999f2b6
feat(insights): period-narrative context assembler
MBombeck Jun 3, 2026
2258cbb
feat(whoop): WHOOP v2 API client, response classifier, credential res…
MBombeck Jun 3, 2026
6c3b8be
feat(share-links): owner clinician share-link lifecycle API
MBombeck Jun 3, 2026
57b8fd8
feat(fhir): read-only FHIR R4 REST search routes
MBombeck Jun 3, 2026
ceabf8a
chore(api): regenerate OpenAPI for the FHIR and share-link routes
MBombeck Jun 3, 2026
9830ae8
feat(share-links): public share-token resolver
MBombeck Jun 3, 2026
6aa3c75
feat(share-links): public clinician view at /c/[token]
MBombeck Jun 3, 2026
337f024
feat(whoop): pg-boss sync workers + self-converging backfill
MBombeck Jun 3, 2026
5917bba
feat(insights): period-narrative summary on the overview
MBombeck Jun 3, 2026
f4629a1
feat(share-links): scrub the share token from logs and harden the /c …
MBombeck Jun 3, 2026
be94e43
feat(share-links): owner share-link management in Settings
MBombeck Jun 3, 2026
dd10711
feat(whoop): OAuth connect/callback + HMAC-signed webhook + CSP gating
MBombeck Jun 3, 2026
0be484c
feat(insights): TRAJECTORY forecasting derived metric with a widening…
MBombeck Jun 3, 2026
4afe1dc
chore(api): regenerate OpenAPI for the trajectory derived metric
MBombeck Jun 3, 2026
8e00e16
fix(settings): add the sharing section icon to the placeholder map
MBombeck Jun 3, 2026
01f3374
fix(insights): make TRAJECTORY_TYPES a distinct array, not an alias
MBombeck Jun 3, 2026
93694ca
fix(idempotency): scrub hls_ share-link tokens from cached bodies
MBombeck Jun 4, 2026
d4686ea
feat(capabilities): advertise the FHIR REST face and share-link surface
MBombeck Jun 4, 2026
008160d
feat(whoop): settings surface — status, disconnect, credentials, sync…
MBombeck Jun 4, 2026
401511d
test(whoop): cover WHOOP workout cross-source resolution in the read-…
MBombeck Jun 4, 2026
e1cdbf3
chore(api): regenerate OpenAPI for the capabilities FHIR/share descri…
MBombeck Jun 4, 2026
d99e1f0
feat(coach): fold a rolling-profile memory block into the snapshot
MBombeck Jun 4, 2026
462c6e8
feat(coach): wire short-horizon trajectory into the Coach snapshot
MBombeck Jun 4, 2026
7246aa2
feat(coach): add encrypted conversation-summary columns (migration 0114)
MBombeck Jun 4, 2026
cd3982a
fix(integrations): surface the WHOOP dual-source vitals caveat on the…
MBombeck Jun 4, 2026
47ba474
fix(capabilities): stop advertising a share-scoped FHIR API that is n…
MBombeck Jun 4, 2026
153026e
fix(clinician): localise measurement-type labels in the shared record…
MBombeck Jun 4, 2026
7036ff3
fix(insights): make the narrative period toggle accessible
MBombeck Jun 4, 2026
1b9f914
fix(crypto): cover the WHOOP encrypted columns in key rotation
MBombeck Jun 4, 2026
df5f1d1
chore(release): v1.11.0 — WHOOP provider hub, longitudinal coach, cli…
MBombeck Jun 4, 2026
65c7012
docs: document the v1.11.0 features
MBombeck Jun 4, 2026
37cec93
test: account for the WHOOP card + narrative route in inventory tests
MBombeck Jun 4, 2026
98693cd
test(e2e): expect three integration pills on mobile after WHOOP card
MBombeck Jun 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .env.production.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ API_TOKEN_HMAC_KEY="<openssl rand -hex 32>"
# WITHINGS_WEBHOOK_SECRET=""


# -----------------------------------------------------------------------------
# WHOOP -- optional integration
# -----------------------------------------------------------------------------
# The WHOOP app client id/secret are per-user BYO-keys: each user registers
# their own WHOOP developer app and pastes the client id/secret into Settings
# (stored encrypted in the DB). There is therefore no WHOOP_CLIENT_ID/SECRET.
#
# WHOOP_WEBHOOK_SECRET is the instance-level secret carried as the trailing
# path segment of the webhook URL AND used to verify the WHOOP HMAC body
# signature. WHOOP_REDIRECT_URI overrides the derived
# `${NEXT_PUBLIC_APP_URL}/api/whoop/callback` when the public URL differs.
# WHOOP_WEBHOOK_SECRET="<openssl rand -hex 32>"
# WHOOP_REDIRECT_URI=""


# -----------------------------------------------------------------------------
# APNs -- iOS push (optional, all-or-none)
# -----------------------------------------------------------------------------
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## [1.11.0] — 2026-06-04 — WHOOP, a longitudinal coach, and a clinician-grade record

A multi-feature milestone across three fronts: a second connected provider, deeper Insights, and a shareable clinical record.

### Added

- **WHOOP integration.** Connect a WHOOP account with your own developer keys to bring its recovery, day- and workout-strain, sleep-performance, HRV (RMSSD), and energy readings into HealthLog alongside Apple Health and Withings — synced on a schedule and via signed webhooks, each value kept distinct by its source so it never overwrites a reading from another device.
- **A longitudinal Insights coach.** A weekly/monthly narrative summarises what changed over the period and the likely contributing factors (descriptive, never causal); the Coach now carries a rolling profile of your recent baselines and trends; and a short-horizon trajectory projection shows where a metric is heading with an honest, widening confidence band — shown only when there is enough history to mean something.
- **A clinician-grade health record.** A read-only FHIR REST API (`GET /api/fhir/*`) serves your data as standard FHIR R4 resources, and you can mint a scoped, time-limited, revocable share link that opens a clean read-only clinician view — no account needed. Wellness figures stay fenced off from the clinical ones with a plain "not a clinical assessment" note.
- **More resilient AI generation.** A durable provider-health record skips a known-bad credential instead of failing every run and surfaces an expired credential proactively, with a local model as a guaranteed fallback.

### Known limitations (planned follow-ups)

- When two sources supply the same standard vital (e.g. WHOOP and an Apple Watch both report resting heart rate), both readings may currently appear for a day until a source-aware aggregation update resolves them to your preferred source. WHOOP's own scores are unaffected; a note explains this on the WHOOP card.
- The coach reflects your rolling health profile and trajectory; full conversation-summary memory and saved personal facts are a later refinement.

## [1.10.4] — 2026-06-03 — Strain honesty + six-minute-walk caveat

### Changed
Expand Down
31 changes: 28 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

<p align="center">
Self-hosted health tracker. Weight, blood pressure, glucose, mood, medications.<br/>
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.
</p>

<p align="center">
Expand All @@ -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).

Expand Down Expand Up @@ -98,14 +98,16 @@ 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 `<Record>`, `<Workout>`, `<Correlation>`, and `<ClinicalRecord>` 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.

**Doctor Report PDF Export** -- Generate professional medical reports client-side. Locale-aware (English/German), with vital sign summaries, BP/BMI/glucose classification, compliance rates, custom-threshold badges, and optional AI analysis.

**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/<token>` 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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -437,13 +441,32 @@ All mutations require authentication via session cookie. External ingest uses Be

</details>

<details>
<summary><strong>FHIR + clinician sharing (v1.11)</strong></summary>

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/<token>` view, and revoke it after the visit. Share links do not expose the FHIR API (`capabilities.share.fhirApi=false`).

</details>

---

## Integrations

| 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 |
Expand Down Expand Up @@ -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).

---
Expand Down
10 changes: 10 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ services:
# an operator override in .env actually reaches the container (the
# `environment:` block is a whitelist).
FHIR_MAX_MEDICATION_ADMINISTRATIONS: "${FHIR_MAX_MEDICATION_ADMINISTRATIONS:-}"
# WHOOP integration (optional). The app client id/secret are per-user
# BYO-keys stored encrypted in the DB, so there is no
# WHOOP_CLIENT_ID/SECRET env. WHOOP_WEBHOOK_SECRET is the instance-level
# path-segment + HMAC-verification secret for the webhook;
# WHOOP_REDIRECT_URI overrides the derived
# `${NEXT_PUBLIC_APP_URL}/api/whoop/callback` when the public URL
# differs. Listed here so an operator override in .env reaches the
# container (the `environment:` block is a whitelist).
WHOOP_WEBHOOK_SECRET: "${WHOOP_WEBHOOK_SECRET:-}"
WHOOP_REDIRECT_URI: "${WHOOP_REDIRECT_URI:-}"
depends_on:
db:
condition: service_healthy
Expand Down
Loading
Loading