diff --git a/.dockerignore b/.dockerignore index 1901f009e..2ddf2aa37 100644 --- a/.dockerignore +++ b/.dockerignore @@ -53,11 +53,8 @@ public/sw-version.js *.har *.heapsnapshot -# Editor / agent worktrees + caches. -.claude +# Editor worktrees and caches. .worktrees -.cursor -.aider* .vscode .idea .DS_Store @@ -80,15 +77,12 @@ Dockerfile # Documentation not needed for runtime image README.md CHANGELOG.md -CLAUDE.md CONTRIBUTING.md -CONTRIBUTING-AI.md CODE_OF_CONDUCT.md SECURITY.md # LICENSE / LICENSE.md are deliberately NOT ignored: the PolyForm # Noncommercial 1.0.0 notice terms require the licence text (or its URL) # to ship with every copy of the software, so it ships inside the image. -AGENTS.md .planning docs docs/*.md diff --git a/.env.production.example b/.env.production.example index c5c87c0e6..a56dbf734 100644 --- a/.env.production.example +++ b/.env.production.example @@ -541,24 +541,6 @@ API_TOKEN_HMAC_KEY="" # FHIR_MAX_MEDICATION_ADMINISTRATIONS="5000" -# ----------------------------------------------------------------------------- -# Dashboard snapshot -- BUILD-TIME toggle (default ON) -# ----------------------------------------------------------------------------- -# The dashboard hydrates every above-the-fold tile from the single -# `GET /api/dashboard/snapshot` cell so the whole strip shares one -# completion moment. This is the default and the desired behaviour. -# -# `NEXT_PUBLIC_*` vars are inlined into the client bundle at BUILD time, -# not read at runtime -- so this has NO effect on the prebuilt GHCR -# image and is intentionally absent from the `docker-compose.yml` -# runtime `environment:` whitelist. To fall back to the legacy -# four-cell path you must rebuild the image with the var set to "false": -# -# $ NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false pnpm build # or a Dockerfile ARG -# -# NEXT_PUBLIC_DASHBOARD_SNAPSHOT="false" - - # ----------------------------------------------------------------------------- # Client-IP resolution -- optional (required behind Cloudflare) # ----------------------------------------------------------------------------- diff --git a/.github/ISSUE_TEMPLATE/translation.yml b/.github/ISSUE_TEMPLATE/translation.yml index 331815c1d..d12e9ce63 100644 --- a/.github/ISSUE_TEMPLATE/translation.yml +++ b/.github/ISSUE_TEMPLATE/translation.yml @@ -1,12 +1,12 @@ name: Translation feedback -description: Report a wording, grammar, or register problem in HealthLog's community-maintained locales (FR / ES / IT / PL / KO / NL / PT-BR). +description: Report a wording, grammar, or register problem in HealthLog's community-reviewed locales (FR / ES / IT / PL / KO / NL / PT-BR / RU). title: "[Translation] " labels: ["i18n", "translation-feedback"] body: - type: markdown attributes: value: | - HealthLog ships German and English as maintainer-curated translations. French, Spanish, Italian, Polish, Korean, Dutch, and Brazilian Portuguese ship as **AI-initial, community-maintained translations** — the prose passes the locale-integrity test (full key parity, no empty values) but has not been hand-reviewed for register or regional vocabulary. + HealthLog ships German and English as maintainer-curated translations. French, Spanish, Italian, Polish, Korean, Dutch, Brazilian Portuguese and Russian started as machine-translated first drafts and are reviewed by the community. They pass the locale-integrity test (full key parity, no empty values), but not every line has been read by a native speaker for register or regional vocabulary. This template captures concrete fixes you would like to see. Phrasing nuance, regional vocabulary (e.g. "Tú" vs "Usted"), gendered forms, and domain-specific health terms are all in scope. The maintainer applies fixes during the regular release cycle. @@ -23,6 +23,7 @@ body: - Korean (ko) - Dutch (nl) - Brazilian Portuguese (pt-BR) + - Russian (ru) - Other (specify under "Surface") validations: required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 04679d7a0..be6042671 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -28,16 +28,16 @@ ## Checklist -- [ ] Targets the `main` branch (trunk-based — see [CONTRIBUTING.md](../CONTRIBUTING.md)) +- [ ] Targets the `main` branch (trunk-based, see [CONTRIBUTING.md](../CONTRIBUTING.md)) - [ ] `pnpm typecheck` passes locally - [ ] `pnpm lint` passes locally - [ ] `pnpm test` passes locally - [ ] `pnpm format:check` passes locally - [ ] `pnpm build` passes locally -- [ ] User-facing strings go through `t("key")` with both `messages/en.json` and `messages/de.json` updated +- [ ] User-facing strings go through `t("key")`, with the key in all ten `messages/*.json` files - [ ] No secrets, personal data, or maintainer-name references in committed files - [ ] Updated `CHANGELOG.md` if user-visible -- [ ] Updated `docs/audit/` or `docs.healthlog.dev` if behavior or self-hosting docs change +- [ ] Updated `docs/` or `docs.healthlog.dev` if behaviour or self-hosting steps change ## Linked issues diff --git a/.gitignore b/.gitignore index 655bb269e..3b554dc0b 100644 --- a/.gitignore +++ b/.gitignore @@ -66,17 +66,8 @@ next-env.d.ts # COPY target always exists. /assets/geolite2/*.mmdb -# Local workspace files, never committed -/docs/superpowers/ -.claude/ -.cursor/ -.codex/ -.aider* - -# Planning scratch — all internal per-release reports, QA findings, ad-hoc -# backlog, and the iOS coordination channel. These are internal working -# notes; everything user-facing lives in the public surfaces (README, -# CHANGELOG, docs/). Kept on disk for the working session, never tracked. +# Internal working notes (per-release reports, backlog, iOS coordination). +# Everything user-facing lives in README, CHANGELOG and docs/. /.planning/ # Docs scratch — temporary spec drafts that live in the repo only diff --git a/.mcp.json b/.mcp.json deleted file mode 100644 index 29585572e..000000000 --- a/.mcp.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "mcpServers": { - "shadcn": { - "command": "npx", - "args": ["-y", "shadcn@latest", "mcp"] - } - } -} diff --git a/CHANGELOG.md b/CHANGELOG.md index b2224ad9d..4058e701d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,75 @@ ## [Unreleased] +## [1.47.5] — 2026-11-12 + +### Fixed + +- **Sheets no longer throw away what you typed.** A tap outside, a swipe + down or Escape closed a sheet and dropped its input without a word in + the medication wizard, the add sheets on Measurements, Mood and Labs, + the visit, vaccination, practitioner and check-up forms, and the edit + sheets for illnesses, cycle days, custom metrics, targets, mood tags, + symptoms and life events. They now ask first, and an untouched form + closes at once. A sheet that asks before closing slides back after a + swipe instead of staying half off the screen. +- **Hints open on a tap.** Information that only appeared on mouse hover + now opens on a tap: the measurement-pattern tip, the sleep-source + warning, score coverage, target status, range-bar values, the colour + of dashboard averages and long admin token names. +- **No more zoom on iPhone when a field gets focus.** The medical-history + notes, the import and passphrase fields, the Coach memory editor, the + managed profile's handover link, the cycle length fields and the + command search use 16 px text now. +- The "More" sheet scrolls when the phone is held sideways, the + practitioner and vaccine pickers open as wide as their field, the + remove buttons on chips are easier to hit without catching the label, + the first-run tour card sits above the iPhone home indicator, and the + Coach's New conversation button no longer covers the last message. +- Illness screens no longer show "Weight", "Pulse", "Recovery" and "Day" + in English in German, French, Spanish, Italian and Polish, and the + medication wizard's "Name" step and the dashboard layout's "Chart" + column are translated too. The Coach question on an empty metric page + is in your language on every metric page. +- The check-up list view shows the Coach and switched-off badges like + the cards. "Log day" on a recovered illness opens on the recovery day. + After switching the glucose unit, the target panels switch at once. +- Blood ketones (new in 1.47.2) can be logged from the metric catalog + and tied to a check-up, and a ketone check-up goes quiet with the + Glucose module like the glucose one. + +### Changed + +- **Insights pages load faster.** The tab bar no longer downloads the + whole overview, a workout and the ECG list on every Insights page; one + small read, `GET /api/insights/presence`, tells it which tabs have + data for the record you are looking at. The Coach line under a metric + chart is worked out once per account and language instead of once per + page, the health status card loads from cache after the first open, + and Insights > Medications asks for each medication's efficacy only + when you scroll to the summary. +- The dose history of a long-taken medication loads the window it shows + instead of every dose ever recorded. +- A background tab no longer checks for a new version every minute; it + checks once when you come back. Every page load saves one request, the + dashboard's mood chart reuses what the dashboard already loaded, and + the admin Coach page loads its quality summary once. +- The admin backups page and the cycle page no longer jump while they + load. +- Medication tabs, the health record page and the check-ups layout page + have English addresses. Old links and bookmarks still work. +- Settings, About opens the release notes instead of the full changelog + file. + +### Removed + +- The build-time switch `NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false` and the + dashboard's old fallback behind it; the dashboard always reads its one + snapshot. If you build your own image with that switch, drop it. +- The unused `GET /api/monitoring/settings` endpoint. +- One-off scripts nothing runs, code paths nothing reaches and tests + about features removed long ago, about 15,000 lines in all. + ## [1.47.4] — 2026-11-12 ### Fixed @@ -19279,9 +19348,8 @@ NOT NULL`) is the defence-in-depth backstop. - **DE+EN bilingual CHANGELOG entries (v1.4.14 + v1.4.15) normalised to English-only.** Per the English-only voice rule. -- **`CLAUDE.md` filename retired** so the filename is no longer - AI-vendor-specific; `CONTRIBUTING.md` reference updated. - `AGENTS.md` stays for multi-agent compatibility. +- **Maintainer notes renamed**; the `CONTRIBUTING.md` reference is + updated. ### Deferred to v1.4.23 @@ -20599,8 +20667,8 @@ token '<', ".json`. Questionnaire text is not in it; the new language needs a wording file per instrument instead (see Questionnaire wording above), which step 4 makes impossible to forget. -3. Translate. AI assistance is welcome as a starting point but the PR description should note whether it's an AI-initial pass or a fully human-reviewed one — that information drives the banner state. +3. Translate. A machine translation is fine as a starting point, but the PR description should say whether the pass is machine-translated or fully reviewed by someone who reads the language; that decides the banner state. 4. Add the locale to `locales` in `src/lib/i18n/config.ts` and to `locales` in `project.inlang/settings.json`, then run `pnpm typecheck`. Every per-locale table that still lacks the new locale fails to compile, and that list is the work: date and number formatting, the reply-language footers of the Coach and insights prompts, the outbound safety banks, the prose-grounding patterns, the no-key fallback copy and the questionnaire wording registry. Include those in the same PR. 5. Run `pnpm test` before submitting. The i18n guards hold the new bundle to the English keys and placeholders. @@ -185,7 +185,7 @@ If you want to introduce a locale that isn't shipped (e.g. `sv`, `cs`): ### Long-term -Fink covers browser editing today. It edits the files in this repository directly and keeps no copy of the translations anywhere else. If the community outgrows it, a self-hosted [Weblate](https://weblate.org/) instance remains the fallback. Tracking issue: `v1.6 — Weblate self-hosted on edge-01`. +Fink covers browser editing today. It edits the files in this repository directly and keeps no copy of the translations anywhere else. If the community outgrows it, a self-hosted [Weblate](https://weblate.org/) instance remains the fallback. ## Code of Conduct diff --git a/README.md b/README.md index f0954ed0d..fb74a885b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

HealthLog

- Self-hosted, privacy-first health tracking. Your vitals, your devices, your server. + Self-hosted health tracking that syncs your devices and keeps the data on your own server.

@@ -17,10 +17,10 @@

- Website · - Live Demo · - Documentation · - iOS App Store · + Website | + Live Demo | + Documentation | + iOS App Store | iOS TestFlight

@@ -36,27 +36,27 @@ Coach: a conversation about the week's recovery, grounded in the recorded data

-## Why HealthLog +## What HealthLog is -HealthLog is a self-hosted health tracking app (PWA + native iOS client) that runs from a single `docker compose up` on a NAS, homelab, or small VPS. It brings weight, blood pressure, pulse, blood glucose, body composition, sleep, mood, menstrual cycle, and medications onto one timeline, synced from the devices you already own, and keeps it in a Postgres database on hardware you control. No vendor cloud, no subscription, no usage telemetry. The few requests the server makes on its own (an update check, a password breach check, an IP geolocation lookup for logins) can each be switched off. +HealthLog is a self-hosted health tracking app (PWA + native iOS client) that runs from a single `docker compose up` on a NAS, homelab, or small VPS. It brings weight, blood pressure, pulse, blood glucose, body composition, sleep, mood, menstrual cycle, and medications onto one timeline, synced from the devices you already own, and keeps it in a Postgres database on hardware you control. It needs no vendor cloud or subscription and sends no usage telemetry. The few requests the server makes on its own (an update check, a password breach check, an IP geolocation lookup for logins) can each be switched off. -HealthLog is a personal health record-keeping and wellness tool. It is not a medical device and does not diagnose, treat, cure, or prevent any disease — it helps you log, visualise, and export your own data, and leaves clinical judgement to a clinician. +HealthLog is a personal health record-keeping and wellness tool. It is not a medical device and does not diagnose, treat, cure, or prevent any disease. It helps you log, visualise, and export your own data, and leaves clinical judgement to a clinician. -Try the [live demo](https://demo.healthlog.dev) to see a working install — sign in with `demo` / `demo123demo123`. +Try the [live demo](https://demo.healthlog.dev) to see a working install. Sign in with `demo` / `demo123demo123`. -## Highlights +## Features -- **Every vital on one timeline.** Weight, blood pressure, pulse, glucose, body composition, sleep, SpO₂, respiratory rate, mood, and cycle tracking — charted with trends, moving averages, and clinical reference ranges (ESH 2023, ADA 2024), overridable with the targets your clinician set. -- **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 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. -- **Clinician-ready output.** A doctor-report PDF rendered on your server, a read-only HL7 FHIR R4 API, and scoped, time-limited share links you revoke after the visit. -- **Private by construction.** AES-256-GCM encryption at rest for notes and other free text, documents, questionnaire answers, AI-written text and credentials, with zero-downtime key rotation. Measurement values, lab values, medication names and mood scores are stored unencrypted in the database, so the database volume and its backups need the same protection as the host. Passkey login, server-side sessions, strict CSP, and no third-party tracking anywhere. -- **Built to be lived in.** Installable PWA that keeps the open page readable when the connection drops, medication reminders over APNs, Telegram, ntfy, and Web Push, a sub-second dashboard on years of imported history, in English, German, Spanish, French, Italian, Polish, Korean, Dutch, Brazilian Portuguese and Russian end to end. +- **Vitals and trends.** Weight, blood pressure, pulse, glucose, body composition, sleep, SpO₂, respiratory rate, mood, and cycle tracking, charted with trends, moving averages, and clinical reference ranges (ESH 2023, ADA 2024) that you can override with the targets your clinician set. Health scores and correlations between your metrics are computed on your server without an AI model. +- **Questionnaires and self-checks.** Optional validated self-checks for mental wellbeing and sleep (WHO-5, PHQ-9, GAD-7 and the Sleep Condition Indicator, in their official wording, with crisis resources when an answer calls for them), grip strength, a pain score from 0 to 10, waist and body measurements, and a longevity lab panel. Each comes from one signal registry, so a new signal reaches charts, correlations, the coach, the FHIR map, and the API together. +- **Allergies, family history, labs and vaccinations.** Allergies and family history are structured records that flow into the doctor report (reactions and notes are encrypted, the substance and condition names are not). Lab values are keyed in or scanned from a printed report: OCR reads 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. +- **Documents.** An opt-in module stores letters, reports, and scans encrypted at rest. PDFs and photos preview inline, Office and text files stay download-only, and nothing a user uploads can execute in the app. One searchable timeline with type, condition, and year filters, drag-and-drop and paste-to-upload, bulk filing, a delete you can undo within 30 days, and per-file and per-account limits the operator sets. +- **Weather and environment.** An environmental module adds weather, daylight, temperature, air quality, and pollen as possible influences (winter darkness and mood, heat and blood pressure), based on a home location you set. It is on by default, and nothing leaves the host until someone sets that location. +- **Devices and integrations.** Withings, WHOOP, Oura, Polar and Strava sync over OAuth2, Garmin through a Personal API key, Google Health connects Fitbit and Pixel Watch through your own Google Cloud client, Nightscout streams CGM glucose, an Apple Health `export.zip` or a Health Connect export 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. +- **Medications.** Flexible schedules (weekly injections, weekday-only, intervals, PRN, cyclic), a configurable intake window per dose, and a dose history where every slot reads taken, late, skipped, or missed. The compliance rate is computed from the same ledger, so the percentage always matches the timeline. +- **AI features (optional).** A daily briefing and a conversational coach grounded in your own measurements and read against cited guideline reference bands: where a value sits and what that means, never a diagnosis. The coach proposes concrete if-then plans you accept or decline, and skips a proactive check-in when there is nothing new 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. +- **Doctor report and FHIR.** A doctor-report PDF rendered on your server, a read-only HL7 FHIR R4 API, and scoped, time-limited share links you revoke after the visit. +- **Encryption and privacy.** AES-256-GCM encryption at rest for notes and other free text, documents, questionnaire answers, AI-written text and credentials, with zero-downtime key rotation. Measurement values, lab values, medication names and mood scores are stored unencrypted in the database, so the database volume and its backups need the same protection as the host. Passkey login, server-side sessions, strict CSP, and no third-party tracking anywhere. +- **PWA, reminders and languages.** Installable PWA that keeps the open page readable when the connection drops, reminders over APNs, Telegram, ntfy, Web Push, email and a webhook, and a dashboard that stays fast on years of imported history. The app is available in English, German, Spanish, French, Italian, Polish, Korean, Dutch, Brazilian Portuguese and Russian. The full feature tour, integration guides, and API reference live at [docs.healthlog.dev](https://docs.healthlog.dev). @@ -71,13 +71,13 @@ echo "POSTGRES_PASSWORD=$(openssl rand -hex 32)" >> .env echo "ENCRYPTION_KEY=$(openssl rand -hex 32)" >> .env echo "API_TOKEN_HMAC_KEY=$(openssl rand -hex 32)" >> .env -# LAN / plain-HTTP host (NAS, homelab, Tailscale) — required, or login fails: +# Required on a LAN or plain-HTTP host (NAS, homelab, Tailscale), or login fails: echo "SESSION_COOKIE_SECURE=false" >> .env docker compose up -d ``` -Open **http://localhost:3000** (or `http://:3000`) — the first registered user becomes admin. The compose file pulls a pre-built multi-arch image (`amd64` + `arm64`) from [GHCR](https://github.com/MBombeck/HealthLog/pkgs/container/healthlog); no build step required. +Open **http://localhost:3000** (or `http://:3000`). The first registered user becomes admin. The compose file pulls a pre-built multi-arch image (`amd64` + `arm64`) from [GHCR](https://github.com/MBombeck/HealthLog/pkgs/container/healthlog); no build step required. Verify the running version: @@ -85,7 +85,7 @@ Verify the running version: curl -s http://localhost:3000/api/version # returns version + buildSha + builtAt ``` -`SESSION_COOKIE_SECURE=false` is the one setting plain-HTTP self-hosts must set: without it the session cookie carries the `Secure` flag, the browser drops it over HTTP, and login silently fails. Leave it unset (or `true`) only when a TLS reverse proxy serves HTTPS — and then set `NEXT_PUBLIC_APP_URL` and `APP_URL` to the public URL first (see the [self-hosting guide](https://docs.healthlog.dev/getting-started/self-host-in-10-minutes/)). +`SESSION_COOKIE_SECURE=false` is the one setting plain-HTTP self-hosts must set: without it the session cookie carries the `Secure` flag, the browser drops it over HTTP, and login silently fails. Leave it unset (or `true`) only when a TLS reverse proxy serves HTTPS, and in that case set `NEXT_PUBLIC_APP_URL` and `APP_URL` to the public URL first (see the [self-hosting guide](https://docs.healthlog.dev/getting-started/self-host-in-10-minutes/)). Persistent state lives in the database volume (`postgres-data`) and in `backups`, which holds the nightly database dumps; the app container is stateless. An install from v1.45 or earlier moves its Postgres 16 database from `pgdata` into the new volume on the first start ([details](docs/ops/postgres-major-upgrade.md)); recovering a lost server is in [disaster-recovery.md](docs/ops/disaster-recovery.md). @@ -95,7 +95,7 @@ NAS and homelab users have ready-made paths: [Unraid Community Applications](doc One container hosts both the web server and the job worker by default; split them via `HEALTHLOG_PROCESS_TYPE=web|worker` for horizontal scale. Encrypted nightly backups to any S3-compatible bucket are opt-in through five `BACKUP_*` environment variables (the admin panel shows their status), new users join by invite link or QR code, and the stack works out of the box behind Caddy, Traefik, Nginx, or [Coolify](https://coolify.io/). -HealthLog ships releases roughly weekly. Pin a tag, back up before upgrades, and skim the [CHANGELOG](CHANGELOG.md) before pulling `latest`. The operator manual — reverse proxy, migrations, encryption-key rotation, backup and restore — lives at [docs.healthlog.dev](https://docs.healthlog.dev). +HealthLog ships often. Pin a tag, back up before upgrades, and read the [release notes](https://github.com/MBombeck/HealthLog/releases) before pulling `latest`. The operator manual (reverse proxy, migrations, encryption-key rotation, backup and restore) lives at [docs.healthlog.dev](https://docs.healthlog.dev). ## Native iOS app @@ -109,18 +109,18 @@ Next.js 16 (App Router, React Server Components), TypeScript strict, PostgreSQL ## Documentation -| | | -| ---------------------- | ------------------------------------------------------------ | -| User and operator docs | [docs.healthlog.dev](https://docs.healthlog.dev) | -| Release history | [CHANGELOG.md](CHANGELOG.md) | -| API contract | [`docs/api/openapi.yaml`](docs/api/openapi.yaml) | -| Security policy | [SECURITY.md](SECURITY.md) | -| Contributing | [CONTRIBUTING.md](CONTRIBUTING.md) | -| Translating | [CONTRIBUTING.md#translations](CONTRIBUTING.md#translations) | +| | | +| ---------------------- | --------------------------------------------------------------- | +| User and operator docs | [docs.healthlog.dev](https://docs.healthlog.dev) | +| Release history | [Release notes](https://github.com/MBombeck/HealthLog/releases) | +| API contract | [`docs/api/openapi.yaml`](docs/api/openapi.yaml) | +| Security policy | [SECURITY.md](SECURITY.md) | +| Contributing | [CONTRIBUTING.md](CONTRIBUTING.md) | +| Translating | [CONTRIBUTING.md#translations](CONTRIBUTING.md#translations) | ## Status -Actively developed — new releases roughly weekly, issue reports and PRs welcome. Behaviour and schema can change between versions; migrations are forward-only. +Actively developed, with frequent releases. Issue reports and pull requests are welcome. Behaviour and schema can change between versions; migrations are forward-only. ## Thanks @@ -143,15 +143,15 @@ including anyone who found the defect without sending the fix. ## License -HealthLog is licensed under the [PolyForm Noncommercial License 1.0.0](LICENSE): free to use, self-host, and modify for noncommercial purposes. Commercial use requires a separate agreement — open an issue or reach out via [healthlog.dev](https://healthlog.dev). Releases up to and including v1.15.18 were published under AGPL-3.0 and remain available under that license. +HealthLog is licensed under the [PolyForm Noncommercial License 1.0.0](LICENSE): free to use, self-host, and modify for noncommercial purposes. Commercial use requires a separate agreement; open an issue or reach out via [healthlog.dev](https://healthlog.dev). Releases up to and including v1.15.18 were published under AGPL-3.0 and remain available under that license. ---

- healthlog.dev · - Live Demo · - Docs · - iOS App Store · - iOS TestFlight · + healthlog.dev | + Live Demo | + Docs | + iOS App Store | + iOS TestFlight | Buy Me a Coffee

diff --git a/docs/CONTRIBUTING-CLIENTS.md b/docs/CONTRIBUTING-CLIENTS.md index e2f627e8c..3f84d6276 100644 --- a/docs/CONTRIBUTING-CLIENTS.md +++ b/docs/CONTRIBUTING-CLIENTS.md @@ -1,6 +1,6 @@ # Building a native client against HealthLog -Working notes for anyone building a client app (Android, iOS, CLI, anything else) against the HealthLog server, with or without AI tooling. Everything in here is a rule we actually follow between the server and the iOS app, written down so a second client does not have to rediscover it. Pair it with `CLAUDE.md` (server-side conventions) and `docs/api/openapi.yaml` (the wire contract). +Working notes for anyone building a client app (Android, iOS, CLI, anything else) against the HealthLog server. Everything in here is a rule we actually follow between the server and the iOS app, written down so a second client does not have to rediscover it. Pair it with [`CONTRIBUTING.md`](../CONTRIBUTING.md) (server-side conventions) and `docs/api/openapi.yaml` (the wire contract). ## The contract, and where truth lives @@ -43,7 +43,7 @@ The corollary: if a resolved value you need is missing from a payload, the fix i ## Ground rules for the repo itself -The voice-and-privacy rules from `CLAUDE.md` apply to any repo under the project, including a client repo: commit messages and public text carry no tooling attribution and no assistant vocabulary, no real names of users or reporters, and no live health figures, ever. Fixtures use invented data. If an AI tool wrote part of a change, the human who commits it has read it and stands behind it; the git history knows only authors. +The privacy rules apply to any repo under the project, including a client repo: commit messages and public text carry no real names of users or reporters and no live health figures, ever. Fixtures use invented data. Whoever commits a change has read it and stands behind it. Secrets never appear in code, fixtures, or examples, not even placeholder-shaped real ones. Anything user-identifying stays out of logs on the client just as it does on the server. diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 1d4ac94e6..b5974f168 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: HealthLog API - version: 1.47.4 + version: 1.47.5 description: >- Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest. @@ -13116,6 +13116,27 @@ paths: "403": *a20 "422": *a4 "429": *a5 + /api/insights/presence: + get: + tags: + - Insights + summary: Which Insights pages have data + description: "What the Insights tab strip gates its pills on, for the record on screen: the reading count per + measurement type (after the record's module switches) and whether the record has mood entries in the last 90 + days, a tracked scheduled medication, workouts and ECG recordings. A flag whose module is off is false. Counts + and flags only; each page reads its own data. No provider is called and no AI switch refuses it. Delegable at + MANAGE level over the whole record. Cookie or Bearer auth." + responses: + "200": + description: The presence answer. + content: + application/json: + schema: + $ref: "#/components/schemas/InsightsPresenceEnvelope" + "401": *a1 + "403": *a20 + "422": *a4 + "429": *a5 /api/insights/cards: get: tags: @@ -34935,6 +34956,7 @@ components: - BLOOD_PRESSURE_SYS - PULSE - BLOOD_GLUCOSE + - BLOOD_KETONES - OXYGEN_SATURATION - BODY_TEMPERATURE - BODY_FAT @@ -68521,6 +68543,63 @@ components: additionalProperties: false description: A significant Pearson correlation (n >= 20, p < 0.05). The field holding it is null when the pairing is not significant. + InsightsPresenceEnvelope: + type: object + properties: + data: + $ref: "#/components/schemas/InsightsPresence" + error: + type: "null" + meta: + type: object + properties: + requestId: + type: string + additionalProperties: false + required: + - data + - error + additionalProperties: false + InsightsPresence: + type: object + properties: + metrics: + type: object + propertyNames: + type: string + additionalProperties: + type: object + properties: + count: + type: integer + minimum: -9007199254740991 + maximum: 9007199254740991 + required: + - count + additionalProperties: false + description: "Keyed by `MeasurementType`: the reading count of every type with at least one reading, after the record's + module switches. The same counts `GET /api/analytics?slice=summaries` reports." + hasMood: + type: boolean + description: A mood entry in the last 90 days. False while the mood module is off. + hasMedication: + type: boolean + description: An active medication with a schedule whose intake is tracked. False while the medications module is off. + hasWorkouts: + type: boolean + description: At least one workout. False while the workouts module is off. + hasEcgRecordings: + type: boolean + description: At least one ECG recording. + required: + - metrics + - hasMood + - hasMedication + - hasWorkouts + - hasEcgRecordings + additionalProperties: false + description: "Which Insights pages have something to show for the record: what the tab strip gates its pills on. Counts + and flags only, no values." InsightsCardsEnvelope: type: object properties: diff --git a/docs/integrations/fitbit.md b/docs/integrations/fitbit.md index c9eb2eae5..3d58c2c41 100644 --- a/docs/integrations/fitbit.md +++ b/docs/integrations/fitbit.md @@ -1,7 +1,9 @@ # Fitbit integration (Fitbit & Pixel) -> **Experimental.** Field mapping is still being verified against live accounts, -> so some values may be missing or change in a future update. +> **Ended.** Google turned the classic Fitbit Web API off on 30 October 2026. +> HealthLog no longer calls it; existing connections and what they imported +> stay. Fitbit and Pixel Watch data now come through +> [Google Health](google-health.md). HealthLog reads Fitbit and Pixel Watch data through the **classic Fitbit Web API** (`api.fitbit.com`). The OAuth app is a **Fitbit developer app registered @@ -16,13 +18,13 @@ Settings. The internal integration key is `fitbit`. > consent screen with `unauthorized_client — Invalid client_id`, because Fitbit > and Google Cloud are separate client registries. -> **The classic Fitbit Web API is turned off on 30 October 2026.** Google -> ended support for it on 30 September 2026 and turns it off on +> **The classic Fitbit Web API was turned off on 30 October 2026.** Google +> ended support for it on 30 September 2026 and turned it off on > **30 October 2026**, after which it "will no longer function or sync data" > ([Google's migration guide](https://developers.google.com/health/migration)). -> New app registrations at `dev.fitbit.com/apps/new` are already closed. +> New app registrations at `dev.fitbit.com/apps/new` are closed. > -> From that day HealthLog stops calling the Web API for every Fitbit +> Since that day HealthLog no longer calls the Web API for any Fitbit > connection: no hourly sync, no backfill, no token refresh, and no new > connection. Nothing is deleted. The connection and every reading it imported > stay, and the Fitbit card explains the move instead of asking for a @@ -30,10 +32,10 @@ Settings. The internal integration key is `fitbit`. > [Google Health integration](google-health.md). It reads the same devices, > but Google asks for consent once more: Fitbit tokens cannot be carried over. > -> Both integrations can run side by side while you move. Where both deliver the -> same day, the source-priority ladder keeps one of them per day (Fitbit ranks -> just above Google Health by default), so steps, sleep and the other daily -> values are not counted twice. After the turndown only Google Health delivers. +> Where a day imported from Fitbit before the turndown also comes in through +> Google Health, the source-priority ladder keeps one of them per day (Fitbit +> ranks just above Google Health by default), so steps, sleep and the other +> daily values are not counted twice. ## 1. Use an existing Fitbit developer app diff --git a/docs/self-hosting/getting-started.md b/docs/self-hosting/getting-started.md index 039ecfd43..7bd473110 100644 --- a/docs/self-hosting/getting-started.md +++ b/docs/self-hosting/getting-started.md @@ -195,26 +195,6 @@ bare Nginx — including the `X-Forwarded-For` chain shape that the rate-limiter trusts and the `TRUST_PROXY_HOPS` env var that pins how many hops to read back. -## Build-time feature toggles - -A small number of toggles are `NEXT_PUBLIC_*` variables: the framework -inlines them into the client bundle at **build time**, so the pre-built -GHCR image cannot read them at runtime. Setting them in `.env` or the -compose `environment:` block has no effect — they only take hold when -you build your own image. They are intentionally absent from the -compose runtime whitelist for that reason. - -| Variable | Default | Effect when set | -| -------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `NEXT_PUBLIC_DASHBOARD_SNAPSHOT` | on | The dashboard hydrates every above-the-fold tile from the single `GET /api/dashboard/snapshot` cell so the whole strip shares one completion moment. Build with `NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false` to fall back to the legacy four-cell path. | - -To flip one, rebuild the image with the variable exported: - -```bash -NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false pnpm build -# or pass it as a Dockerfile build ARG / Docker build --build-arg -``` - ## 7. Where to go next | Next step | File | diff --git a/docs/ui-guidelines.md b/docs/ui-guidelines.md index 5b00604e2..22b8d4827 100644 --- a/docs/ui-guidelines.md +++ b/docs/ui-guidelines.md @@ -1,104 +1,30 @@ # HealthLog UI Guidelines -> Visual & interaction patterns in the HealthLog PWA. The binding rules -> for card anatomy, spacing, text colour, tokens and primitives live in -> [`contributing/ui-standards.md`](./contributing/ui-standards.md); where -> this document disagrees with them, the standards win. If a pattern -> below doesn't fit your case, propose an addition in the same PR. - -Last updated: 2026-05-08 (v1.4 cycle) +> Interaction patterns for the HealthLog web app. Card anatomy, spacing, +> text colour, colour tokens, typography and the choice of primitive are +> owned by [`contributing/ui-standards.md`](./contributing/ui-standards.md); +> this page covers what those rules leave open. Where the two disagree, the +> standards win. If a pattern below doesn't fit your case, propose an +> addition in the same PR. --- ## 1. Foundations -### 1.1 Theme +Theme, spacing, typography, radius and colour tokens are defined in the +standards (§1 to §5 there). Two things to add: -HealthLog is a Dracula-themed app with a light counterpart (Alucard). Both -themes share the same six-color signal palette (purple, pink, green, cyan, -orange, red, yellow, lavender), exposed as CSS variables and as Tailwind -`dracula-*` color tokens. Use the semantic tokens (`--primary`, -`--destructive`, `--card`, …) for chrome and the `--dracula-*` tokens for -data viz only. - -| Surface | Token | -| ---------------------------- | ------------------ | -| App background | `--background` | -| Card / popover surface | `--card` | -| Primary action | `--primary` | -| Destructive action | `--destructive` | -| Secondary / muted background | `--secondary` | -| Border | `--border` | -| Focus ring | `--ring` | -| Chart series 1–5 | `--chart-1`…`-5` | -| Brand signal: purple | `--dracula-purple` | -| Brand signal: green (good) | `--dracula-green` | -| Brand signal: orange (warn) | `--dracula-orange` | -| Brand signal: red (alert) | `--dracula-red` | -| Brand signal: cyan (info) | `--dracula-cyan` | - -Dark mode is the default. The theme switcher in the sidebar three-dot menu -toggles `.dark` / `.light` on `:root`. Always test both modes before -shipping a component. - -### 1.2 Spacing - -Tailwind's default 4-pixel scale. Reach for these stops first: - -| Use | Token | Pixels | -| ------------------------------------ | ------------- | ------ | -| Inline gap between icon and label | `gap-1` | 4 | -| Tight stack inside a card | `gap-2` | 8 | -| Label-to-field stack (form fields) | `space-y-2` | 8 | -| Section block (heading + body) | `space-y-4` | 16 | -| Page section separator | `space-y-6` | 24 | -| Dashboard tile-row gap, hero spacing | `gap-4` | 16 | -| Page-edge padding (mobile / desktop) | `px-4 / px-6` | | - -Avoid arbitrary `space-y-5`, `gap-3.5`, etc. unless aligning to a fixed -external constraint. - -### 1.3 Typography - -The app uses the Tailwind defaults inherited via shadcn/ui. Reach for -these classes first: - -| Use | Class | -| ---------------------------------- | --------------------------------------- | -| Page title (h1) | `text-2xl font-semibold tracking-tight` | -| Section heading / section card | `text-lg font-semibold` | -| Tile / inner card title | `text-base font-semibold` | -| Body text | `text-sm` | -| Muted helper / metadata | `text-sm text-muted-foreground` | -| Microcopy / footer | `text-xs text-muted-foreground` | -| Tabular data (latency, large nums) | `font-mono tabular-nums` | - -Card titles come in exactly two sizes: `text-lg font-semibold` for a -section-level card (one per page region) and `text-base font-semibold` -for tiles and inner cards. Don't introduce a third level — a smaller -heading is a sign the content wants a list row, not a card. - -Never use `font-bold` for emphasis — bump to `font-semibold` instead and -let the design system breathe. - -### 1.4 Radius and elevation - -`--radius: 0.625rem` (10px) is the default. The Tailwind tokens -`rounded-md` (`--radius-md` ≈ 8px) and `rounded-lg` (`--radius` ≈ 10px) -cover 95% of cases. Use `rounded-2xl` only for hero cards or large -modals. - -Elevation is achieved via `shadow-xs`, `shadow-sm`, or -`shadow-[0_1px_0_rgba(255,255,255,0.04)]` for very subtle separators on -dark backgrounds. Avoid Tailwind's heavy shadows (`shadow-lg`+) in chrome. +- Dark is the default theme, and the light theme remaps the same semantic + tokens. Check every component in both before shipping it. +- Elevation stays light: `shadow-xs` or `shadow-sm` in chrome, never the + heavy `shadow-lg` and up. --- ## 2. Components -This section is **prescriptive, not exhaustive**. Each entry pairs a -Radix/shadcn primitive with the cases it's the right answer for and the -cases where another primitive should win. +This section is **prescriptive, not exhaustive**. Each entry names the +primitive for a case and the case where another one wins. ### 2.1 Buttons @@ -114,43 +40,35 @@ Every button comes from `@/components/ui/button`. Pick by purpose: | Inline link to external help docs | `link` | `sm` | | Toolbar / table row action when space is tight | `outline` | `xs` | -**Position rules.** +Where buttons sit in a card (one action row, last in the card, primary on +the right) is standards §12. Two additions: -- Forms (full-width or modal): primary on the **right**, cancel/back on - the **left**. Match shadcn dialog defaults. -- Settings sections: a single sticky `Save` button at the **right end** - of the section header _or_ per-row inline `Save`. Never both in the - same section. -- Test buttons (Telegram test, AI ping, etc.): immediately to the right - of the related Save / Apply control. Never below — keeps the eye - motion horizontal. -- Destructive actions live in their own confirm dialog with a - type-to-confirm input when the action is irreversible (delete account, - revoke API token, drop user data). +- A Test button (Telegram test, AI ping and the like) sits in the same row + as the Save or Apply control it belongs to. +- An irreversible action (delete account, revoke an API token, wipe data) + asks in its own confirm dialog, with a type-to-confirm field where the loss + is large. -**Disabled state.** A disabled button must always come with a tooltip or -helper text explaining why. Silent disabled buttons frustrate users. +**Disabled state.** A disabled button always comes with helper text that +says why. A tooltip is not enough: it never opens on a phone. ### 2.2 Inputs and forms Forms keep a controlled draft in component state and validate it with the Zod schemas from `src/lib/validations/`; the server's schema is the authority. -Wrap inputs in `