mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
style: apply prettier across the tree
One-shot prettier pass so the new format:check gate starts from a clean baseline. Formatting only — no behaviour change. Generator-owned and vendored artifacts stay untouched via .prettierignore.
This commit is contained in:
1 parent
b3fea2ef59
commit
1f4ba5939d
1000 files changed
+12130
-8385
No files matched your search
@@ -32,7 +32,7 @@ Separate the two naming axes cleanly:
|
||||
|
||||
- **UI copy is always localised.** Every visible string resolves through an
|
||||
i18n `t()` key and is translated across all six locales (`de / en / es / fr /
|
||||
it / pl`). The user never sees a raw identifier.
|
||||
it / pl`). The user never sees a raw identifier.
|
||||
- **Internal identifiers are English.** Route slugs (`/insights/<slug>`), the
|
||||
tile-layout enum (`INSIGHTS_TILE_IDS`), query keys, and the internal
|
||||
slug→metric / slug→target maps all use established English terms. English is
|
||||
@@ -54,7 +54,7 @@ The migration must not break either client-visible boundary:
|
||||
navigation keep resolving.
|
||||
- **Tile-layout contract.** The layout endpoint keeps **accepting** the legacy
|
||||
German tile ids on input. The Zod enum is built from
|
||||
`ACCEPTED_INSIGHTS_TILE_IDS` (canonical English ids *plus* the legacy German
|
||||
`ACCEPTED_INSIGHTS_TILE_IDS` (canonical English ids _plus_ the legacy German
|
||||
aliases), so a `PUT` carrying old ids validates rather than tripping a 422.
|
||||
`normalizeInsightsTileId` collapses each legacy id onto its canonical English
|
||||
replacement; this normalisation runs on **both** the read path
|
||||
|
||||
@@ -13,16 +13,16 @@ row. The fold only runs when cycle tracking is enabled for the account.
|
||||
|
||||
## Day-log fields
|
||||
|
||||
| HealthKit category type | CycleDayLog field | Values |
|
||||
|---|---|---|
|
||||
| `MenstrualFlow` | `flow` | unspecified→LIGHT, light→LIGHT, medium→MEDIUM, heavy→HEAVY, none→NONE |
|
||||
| `IntermenstrualBleeding` | `intermenstrualBleeding` | presence → `true` |
|
||||
| `CervicalMucusQuality` | `cervicalMucus` | dry→DRY, sticky→STICKY, creamy→CREAMY, watery→WATERY, eggWhite→EGG_WHITE |
|
||||
| `OvulationTestResult` | `ovulationTest` | negative→NEGATIVE, lhSurge→POSITIVE_LH_SURGE, indeterminate→INDETERMINATE, estrogenSurge→ESTROGEN_SURGE |
|
||||
| `SexualActivity` | `sexualActivity` + `protectedSex` | presence → `true`; `HKMetadataKeySexualActivityProtectionUsed` → `protectedSex` |
|
||||
| `PregnancyTestResult` | `pregnancyTest` | negative→NEGATIVE, positive→POSITIVE, indeterminate→INDETERMINATE |
|
||||
| `ProgesteroneTestResult` | `progesteroneTest` | negative→NEGATIVE, positive→POSITIVE, indeterminate→INDETERMINATE |
|
||||
| `Contraceptive` | `contraceptive` (+ profile nudge) | unspecified→UNSPECIFIED, implant→IMPLANT, injection→INJECTION, iud→IUD, ring→INTRAVAGINAL_RING, oral→ORAL, patch→PATCH, emergency→EMERGENCY |
|
||||
| HealthKit category type | CycleDayLog field | Values |
|
||||
| ------------------------ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `MenstrualFlow` | `flow` | unspecified→LIGHT, light→LIGHT, medium→MEDIUM, heavy→HEAVY, none→NONE |
|
||||
| `IntermenstrualBleeding` | `intermenstrualBleeding` | presence → `true` |
|
||||
| `CervicalMucusQuality` | `cervicalMucus` | dry→DRY, sticky→STICKY, creamy→CREAMY, watery→WATERY, eggWhite→EGG_WHITE |
|
||||
| `OvulationTestResult` | `ovulationTest` | negative→NEGATIVE, lhSurge→POSITIVE_LH_SURGE, indeterminate→INDETERMINATE, estrogenSurge→ESTROGEN_SURGE |
|
||||
| `SexualActivity` | `sexualActivity` + `protectedSex` | presence → `true`; `HKMetadataKeySexualActivityProtectionUsed` → `protectedSex` |
|
||||
| `PregnancyTestResult` | `pregnancyTest` | negative→NEGATIVE, positive→POSITIVE, indeterminate→INDETERMINATE |
|
||||
| `ProgesteroneTestResult` | `progesteroneTest` | negative→NEGATIVE, positive→POSITIVE, indeterminate→INDETERMINATE |
|
||||
| `Contraceptive` | `contraceptive` (+ profile nudge) | unspecified→UNSPECIFIED, implant→IMPLANT, injection→INJECTION, iud→IUD, ring→INTRAVAGINAL_RING, oral→ORAL, patch→PATCH, emergency→EMERGENCY |
|
||||
|
||||
HealthLog's `SPOTTING` flow level has no HealthKit counterpart; it is only ever
|
||||
produced by a manual entry. HealthKit `unspecified` flow maps to `LIGHT` (a
|
||||
@@ -31,21 +31,21 @@ logged-but-unspecified bleeding day is at least light flow).
|
||||
## Symptom category types → seeded symptom keys
|
||||
|
||||
Symptom category samples link to the seeded `CycleSymptom` catalogue by key. A
|
||||
sample whose severity is *not present* (codepoint 1) does NOT create a link.
|
||||
sample whose severity is _not present_ (codepoint 1) does NOT create a link.
|
||||
|
||||
| HealthKit category type | CycleSymptom key |
|
||||
|---|---|
|
||||
| `AbdominalCramps` | `cramps` |
|
||||
| `Headache` | `headache` |
|
||||
| `Bloating` | `bloating` |
|
||||
| `Acne` | `acne` |
|
||||
| `BreastPain` | `breast_tenderness` |
|
||||
| `Fatigue` | `fatigue` |
|
||||
| `LowerBackPain` | `back_pain` |
|
||||
| `SleepChanges` | `insomnia` |
|
||||
| `MoodChanges` | `mood_swings` |
|
||||
| `AppetiteChanges` | `food_cravings` |
|
||||
| `Nausea` | `nausea` |
|
||||
| HealthKit category type | CycleSymptom key |
|
||||
| ----------------------- | ------------------- |
|
||||
| `AbdominalCramps` | `cramps` |
|
||||
| `Headache` | `headache` |
|
||||
| `Bloating` | `bloating` |
|
||||
| `Acne` | `acne` |
|
||||
| `BreastPain` | `breast_tenderness` |
|
||||
| `Fatigue` | `fatigue` |
|
||||
| `LowerBackPain` | `back_pain` |
|
||||
| `SleepChanges` | `insomnia` |
|
||||
| `MoodChanges` | `mood_swings` |
|
||||
| `AppetiteChanges` | `food_cravings` |
|
||||
| `Nausea` | `nausea` |
|
||||
|
||||
## Profile-level signals
|
||||
|
||||
@@ -64,8 +64,8 @@ the temperature-trend ovulation method directly from `Measurement`.
|
||||
|
||||
## Deferred
|
||||
|
||||
| HealthKit category type | Status |
|
||||
|---|---|
|
||||
| `Pregnancy`, `Lactation` | Deferred — no `CycleProfile` column yet (pregnancy-mode is a later release). |
|
||||
| `BleedingDuringPregnancy`, `BleedingAfterPregnancy` | Deferred — pregnancy-mode. |
|
||||
| `IrregularMenstrualCycles`, `InfrequentMenstrualCycles`, `ProlongedMenstrualPeriods`, `PersistentIntermenstrualBleeding` | Server-derived from the cycle engine, never ingested. |
|
||||
| HealthKit category type | Status |
|
||||
| ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
|
||||
| `Pregnancy`, `Lactation` | Deferred — no `CycleProfile` column yet (pregnancy-mode is a later release). |
|
||||
| `BleedingDuringPregnancy`, `BleedingAfterPregnancy` | Deferred — pregnancy-mode. |
|
||||
| `IrregularMenstrualCycles`, `InfrequentMenstrualCycles`, `ProlongedMenstrualPeriods`, `PersistentIntermenstrualBleeding` | Server-derived from the cycle engine, never ingested. |
|
||||
@@ -11,6 +11,7 @@ last_updated: 2026-05-15
|
||||
## TL;DR
|
||||
|
||||
Three submission gates that need the maintainer's hand at ASC:
|
||||
|
||||
1. Regulated-Medical-Device declaration (Guideline 1.4 + 5.1.1(ix))
|
||||
2. Privacy Nutrition Labels matching the iOS Privacy Manifest (Guideline 5.1.2)
|
||||
3. Standard metadata (name, description, screenshots, categories, age rating)
|
||||
@@ -24,9 +25,11 @@ Plus pre-submit final checks.
|
||||
**Decision**: HealthLog is **NOT a medical device** under EU MDR 2017/745 / FDA 21 CFR 880 / Apple's interpretation.
|
||||
|
||||
**Recommended wording in the declaration form**:
|
||||
|
||||
> HealthLog is a personal health-data logging and visualization app. It displays user-entered observations + integrates with Apple HealthKit, Withings, and manual entry. It does not diagnose, treat, prescribe, or recommend dosing. The AI Coach surfaces are explicit-refusal-gated (GROUND RULES 9 + 15) against any medical-device-class output. The drug-level estimation chart is display-only behind a versioned MDR-acknowledgment dialog citing EU 2017/745 + MDCG 2021-24. For medical advice, users must consult licensed clinicians.
|
||||
|
||||
**What to check**:
|
||||
|
||||
- [ ] Check "No" on the medical-device toggle
|
||||
- [ ] Add the description above to the medical-device-justification field if Apple shows one
|
||||
- [ ] Cross-reference the in-app `/privacy` page (https://<your-instance>/privacy) §7 MDR-boundary
|
||||
@@ -44,22 +47,26 @@ Plus pre-submit final checks.
|
||||
For each category below: **Linked to user, used for App Functionality + Account Management**. Optional purposes: never enable Analytics, Tracking, Advertising, or Third-Party Tracking.
|
||||
|
||||
**Health & Fitness**
|
||||
|
||||
- Health: yes (all HKQuantityType + HKCategoryType the iOS app reads — see `/privacy` page §2)
|
||||
- Fitness: yes (steps, distance, active energy, workouts)
|
||||
- Purpose: App Functionality
|
||||
- Linked to user: yes
|
||||
|
||||
**Contact Info**
|
||||
|
||||
- Email Address: yes (for account login + GDPR data-subject correspondence)
|
||||
- Purpose: App Functionality + Account Management
|
||||
- Linked to user: yes
|
||||
|
||||
**Sensitive Info**
|
||||
|
||||
- Blood pressure, glucose, medications, drug-related observations: yes
|
||||
- Purpose: App Functionality
|
||||
- Linked to user: yes
|
||||
|
||||
**User Content**
|
||||
|
||||
- Emails or Text Messages: NO (the AI Coach is in-app conversation, but iOS does not send emails)
|
||||
- Photos or Videos: NO
|
||||
- Audio Data: NO (HealthKit audio-exposure is environmental dB; not the user's voice)
|
||||
@@ -69,12 +76,14 @@ For each category below: **Linked to user, used for App Functionality + Account
|
||||
- Linked to user: yes
|
||||
|
||||
**Identifiers**
|
||||
|
||||
- User ID: yes (server-side user UUID)
|
||||
- Device ID: yes (X-Device-Id Keychain-stored UUID, APNs push token)
|
||||
- Purpose: App Functionality + Account Management
|
||||
- Linked to user: yes
|
||||
|
||||
**Other Data Types**
|
||||
|
||||
- Other Data Types: yes (general medical observations not fitting above)
|
||||
- Purpose: App Functionality
|
||||
- Linked to user: yes
|
||||
@@ -97,6 +106,7 @@ For each category below: **Linked to user, used for App Functionality + Account
|
||||
## Section 3 — Standard Metadata Checklist
|
||||
|
||||
### App Information
|
||||
|
||||
- [ ] Name: "HealthLog"
|
||||
- [ ] Subtitle: 30-char tagline (suggestion: "Personal health-data log + Coach")
|
||||
- [ ] Primary category: Health & Fitness
|
||||
@@ -105,25 +115,30 @@ For each category below: **Linked to user, used for App Functionality + Account
|
||||
- [ ] Age rating: 17+ (because Sensitive Health Info content)
|
||||
|
||||
### Description
|
||||
|
||||
- [ ] Long-form description (4000 char max): cover Insights + Coach + GLP-1 + Withings + Apple HealthKit + open-source nature + EU GDPR compliance
|
||||
- [ ] What's New (version-by-version notes): v0.3.0 = "Server-sync, comprehensive Apple HealthKit support across 19 sample types, smart Insights, AI Coach, Health Score, GLP-1 tracking with Research-Mode-gated drug-level chart, interactive charts with drill-down, push notifications, deep links, and account deletion."
|
||||
|
||||
### Screenshots
|
||||
|
||||
- [ ] 6.5" iPhone screenshots: cover Onboarding (welcome carousel + permissions), Dashboard (Health Score tile + tile-strip), Insights (sub-page with severity sort), Coach (refusal example to show MDR boundary), Medications (GLP-1 detail stack), Settings (Research Mode toggle + account deletion).
|
||||
- [ ] 5.5" iPhone screenshots: same set (Apple's legacy size).
|
||||
- [ ] iPad screenshots if supporting iPad.
|
||||
|
||||
### URLs
|
||||
|
||||
- [ ] Marketing URL: https://<your-instance> (or the marketing site once the landing repo's v1.4.25 is deployed)
|
||||
- [ ] Privacy Policy URL: **https://<your-instance>/privacy** (ships with v1.4.25.1 hotfix)
|
||||
- [ ] Support URL: https://github.com/MBombeck/HealthLog/issues
|
||||
|
||||
### App Review Information
|
||||
|
||||
- [ ] Demo account credentials: the maintainer creates a read-only demo account on `demo.healthlog.dev` and provides credentials in the demo account fields
|
||||
- [ ] Review notes: explain MDR boundary + AI Coach safety contract + Research Mode gating; reference `/privacy` §7 MDR-boundary. Apple reviewers DO read this and DO check the in-app behavior matches the claim.
|
||||
- [ ] Sign-in required: yes (provide demo credentials)
|
||||
|
||||
### Pricing + Availability
|
||||
|
||||
- [ ] Free
|
||||
- [ ] Availability: select countries (maintainer decision — recommend EU + US + Canada + Australia + Japan for first launch)
|
||||
|
||||
@@ -154,6 +169,7 @@ Run through this list:
|
||||
---
|
||||
|
||||
**Source links**:
|
||||
|
||||
- Apple Health App Guidelines: https://developer.apple.com/app-store/review/guidelines/#health-and-health-research
|
||||
- Apple App Privacy Details: https://developer.apple.com/app-store/app-privacy-details/
|
||||
- EU MDR 2017/745: https://eur-lex.europa.eu/eli/reg/2017/745/oj
|
||||
|
||||
@@ -6,13 +6,13 @@ pages as `<img>` tags. Mirrored into `healthlog-docs/src/assets/diagrams/`
|
||||
and `healthlog-landing/public/diagrams/` so each surface ships its own
|
||||
copy.
|
||||
|
||||
| File | Story it tells |
|
||||
| ---- | -------------- |
|
||||
| `01-data-flow.svg` | Sources (Withings, Apple Health export.zip, iPhone HealthKit, manual, moodLog) → ingest endpoints → Measurement + MeasurementRollup → reads (rollup probe / live SQL fallback / cache) → surfaces (dashboard, insights, Coach, doctor PDF). |
|
||||
| `02-coach-pipeline.svg` | User question → snapshot builder → prompt assembler → provider chain (ChatGPT OAuth / OpenAI / Anthropic / local / admin-shared) → response parser → cited reply with mini-charts. |
|
||||
| `03-self-hosting-topology.svg` | Internet → reverse proxy (TLS) → Next.js app (port 3000) ↔ pg-boss worker ↔ Postgres 16, plus GHCR image pull, optional Coolify autodeploy, optional S3 off-host backup. |
|
||||
| `04-source-priority.svg` | Three sources for the same metric on the same day → resolver picks one canonical row per (cumulative vs point) ladder. Losing rows stay in the audit trail. |
|
||||
| `05-security-model.svg` | Three concentric layers — auth perimeter (passkey + password fallback), session perimeter (HttpOnly cookie + Postgres sessions), encrypted core (AES-256-GCM versioned keys). Side rails: rate limiter, audit log, HMAC tokens, CSP/HSTS, SSRF guard. |
|
||||
| File | Story it tells |
|
||||
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `01-data-flow.svg` | Sources (Withings, Apple Health export.zip, iPhone HealthKit, manual, moodLog) → ingest endpoints → Measurement + MeasurementRollup → reads (rollup probe / live SQL fallback / cache) → surfaces (dashboard, insights, Coach, doctor PDF). |
|
||||
| `02-coach-pipeline.svg` | User question → snapshot builder → prompt assembler → provider chain (ChatGPT OAuth / OpenAI / Anthropic / local / admin-shared) → response parser → cited reply with mini-charts. |
|
||||
| `03-self-hosting-topology.svg` | Internet → reverse proxy (TLS) → Next.js app (port 3000) ↔ pg-boss worker ↔ Postgres 16, plus GHCR image pull, optional Coolify autodeploy, optional S3 off-host backup. |
|
||||
| `04-source-priority.svg` | Three sources for the same metric on the same day → resolver picks one canonical row per (cumulative vs point) ladder. Losing rows stay in the audit trail. |
|
||||
| `05-security-model.svg` | Three concentric layers — auth perimeter (passkey + password fallback), session perimeter (HttpOnly cookie + Postgres sessions), encrypted core (AES-256-GCM versioned keys). Side rails: rate limiter, audit log, HMAC tokens, CSP/HSTS, SSRF guard. |
|
||||
|
||||
## Editing
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ Ollama endpoint can fall back from one to the other automatically.
|
||||
|
||||
## Provider matrix
|
||||
|
||||
| Provider | Key acquisition | Default model | Endpoint | Privacy stance |
|
||||
| -------- | ---------------- | ------------- | -------- | -------------- |
|
||||
| `OPENAI` | <https://platform.openai.com/api-keys> | `gpt-4o` | `https://api.openai.com/v1` | Measurement context sent to OpenAI |
|
||||
| `ANTHROPIC` | <https://console.anthropic.com/settings/keys> | `claude-sonnet-4-6` | Anthropic SDK default | Measurement context sent to Anthropic |
|
||||
| `LOCAL` | None (your endpoint) | `local-model` | OpenAI-compatible URL you control | Stays on your network |
|
||||
| `CHATGPT_OAUTH` | Sign in with your ChatGPT account | Codex-routed | `chatgpt.com/backend-api/codex/responses` | Routed via your ChatGPT subscription |
|
||||
| Provider | Key acquisition | Default model | Endpoint | Privacy stance |
|
||||
| --------------- | --------------------------------------------- | ------------------- | ----------------------------------------- | ------------------------------------- |
|
||||
| `OPENAI` | <https://platform.openai.com/api-keys> | `gpt-4o` | `https://api.openai.com/v1` | Measurement context sent to OpenAI |
|
||||
| `ANTHROPIC` | <https://console.anthropic.com/settings/keys> | `claude-sonnet-4-6` | Anthropic SDK default | Measurement context sent to Anthropic |
|
||||
| `LOCAL` | None (your endpoint) | `local-model` | OpenAI-compatible URL you control | Stays on your network |
|
||||
| `CHATGPT_OAUTH` | Sign in with your ChatGPT account | Codex-routed | `chatgpt.com/backend-api/codex/responses` | Routed via your ChatGPT subscription |
|
||||
|
||||
The four spellings above are the canonical `User.aiProvider` enum
|
||||
values. Anything else falls through to the admin-shared OpenAI key
|
||||
|
||||
@@ -97,12 +97,12 @@ they triggered.
|
||||
The parser folds four XML element types into the existing HealthLog
|
||||
schema (CHANGELOG v1.4.34):
|
||||
|
||||
| XML element | Maps to | Notes |
|
||||
| ----------- | ------- | ----- |
|
||||
| `<Record>` | `Measurement` rows | One row per spot sample. Keyed by `HKMetadataKeyExternalUUID` when present, otherwise a deterministic `sample:<sha256-of-attributes>` fallback so re-imports stay idempotent. |
|
||||
| `<Workout>` | `Workout` rows | Activity type + duration + energy + distance. |
|
||||
| `<Correlation>` | `Measurement` rows | Compound samples like blood pressure (systolic + diastolic) explode into their per-metric children. |
|
||||
| `<ClinicalRecord>` | `Measurement` rows where the type maps | Health-record FHIR snapshots; only the metric-typed fields are ingested. |
|
||||
| XML element | Maps to | Notes |
|
||||
| ------------------ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `<Record>` | `Measurement` rows | One row per spot sample. Keyed by `HKMetadataKeyExternalUUID` when present, otherwise a deterministic `sample:<sha256-of-attributes>` fallback so re-imports stay idempotent. |
|
||||
| `<Workout>` | `Workout` rows | Activity type + duration + energy + distance. |
|
||||
| `<Correlation>` | `Measurement` rows | Compound samples like blood pressure (systolic + diastolic) explode into their per-metric children. |
|
||||
| `<ClinicalRecord>` | `Measurement` rows where the type maps | Health-record FHIR snapshots; only the metric-typed fields are ingested. |
|
||||
|
||||
The HK quantity types that map to HealthLog metric types live in
|
||||
`src/lib/measurements/apple-health-mapping.ts`. Anything outside the
|
||||
@@ -187,13 +187,13 @@ Override per-user via the Sources section of `/settings/thresholds`.
|
||||
|
||||
## Failure modes
|
||||
|
||||
| Failure | What you see | Recovery |
|
||||
| ------- | ------------ | -------- |
|
||||
| Archive is missing `apple_health_export/export.xml` | `failureReason: "Archive is missing the apple_health_export/export.xml member"` | Re-export from the iPhone; a partial AirDrop or a renamed archive can lose the canonical entry. |
|
||||
| Archive is encrypted | `failureReason: "Encrypted ZIP entries are not supported"` | Apple does not encrypt the export — this means the file was repacked by a third-party tool. Re-export from iOS directly. |
|
||||
| Unsupported compression method | `failureReason: "Unsupported ZIP compression method <n> for export.xml"` | Same recovery — re-export from iOS, which always emits method 0 (stored) or method 8 (deflate). |
|
||||
| Worker is not running | `503 Background worker is not running` from the kick-off endpoint | The worker process is down. Check `docker compose ps` for the `app-worker` container (or `HEALTHLOG_PROCESS_TYPE=all` mode on the main `app` container) and restart. |
|
||||
| Rate-limited | `429 Too many import uploads, try again later` | Wait sixty seconds; the limit is three uploads per minute per user. |
|
||||
| Failure | What you see | Recovery |
|
||||
| --------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Archive is missing `apple_health_export/export.xml` | `failureReason: "Archive is missing the apple_health_export/export.xml member"` | Re-export from the iPhone; a partial AirDrop or a renamed archive can lose the canonical entry. |
|
||||
| Archive is encrypted | `failureReason: "Encrypted ZIP entries are not supported"` | Apple does not encrypt the export — this means the file was repacked by a third-party tool. Re-export from iOS directly. |
|
||||
| Unsupported compression method | `failureReason: "Unsupported ZIP compression method <n> for export.xml"` | Same recovery — re-export from iOS, which always emits method 0 (stored) or method 8 (deflate). |
|
||||
| Worker is not running | `503 Background worker is not running` from the kick-off endpoint | The worker process is down. Check `docker compose ps` for the `app-worker` container (or `HEALTHLOG_PROCESS_TYPE=all` mode on the main `app` container) and restart. |
|
||||
| Rate-limited | `429 Too many import uploads, try again later` | Wait sixty seconds; the limit is three uploads per minute per user. |
|
||||
|
||||
The wide-event log line `import.apple-health.kickoff` (or
|
||||
`import.apple-health.kickoff.denied` on failure) captures every
|
||||
|
||||
@@ -36,8 +36,12 @@ arrays:
|
||||
|
||||
```json
|
||||
{
|
||||
"measurements": [ /* … */ ],
|
||||
"moodEntries": [ /* … */ ]
|
||||
"measurements": [
|
||||
/* … */
|
||||
],
|
||||
"moodEntries": [
|
||||
/* … */
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -48,26 +52,26 @@ skipped and counted under `skipped` in the response.
|
||||
|
||||
### Measurement entry
|
||||
|
||||
| Field | Required | Type | Notes |
|
||||
|---|---|---|---|
|
||||
| `type` | yes | enum | One of the `MeasurementType` values in the table below. |
|
||||
| `value` | yes | number | Plausibility-checked per type; out-of-range values are rejected. |
|
||||
| `unit` | yes | string | The canonical unit for the type (see the table). |
|
||||
| `measuredAt` | yes | string | ISO-8601 datetime, e.g. `2026-05-01T08:00:00.000Z`. |
|
||||
| `source` | no | string | Free-text origin label. Imported rows are tagged `IMPORT` server-side regardless. |
|
||||
| `notes` | no | string | Optional free text. |
|
||||
| `glucoseContext` | no | enum | Only for `BLOOD_GLUCOSE` (e.g. fasting / post-meal). |
|
||||
| Field | Required | Type | Notes |
|
||||
| ---------------- | -------- | ------ | --------------------------------------------------------------------------------- |
|
||||
| `type` | yes | enum | One of the `MeasurementType` values in the table below. |
|
||||
| `value` | yes | number | Plausibility-checked per type; out-of-range values are rejected. |
|
||||
| `unit` | yes | string | The canonical unit for the type (see the table). |
|
||||
| `measuredAt` | yes | string | ISO-8601 datetime, e.g. `2026-05-01T08:00:00.000Z`. |
|
||||
| `source` | no | string | Free-text origin label. Imported rows are tagged `IMPORT` server-side regardless. |
|
||||
| `notes` | no | string | Optional free text. |
|
||||
| `glucoseContext` | no | enum | Only for `BLOOD_GLUCOSE` (e.g. fasting / post-meal). |
|
||||
|
||||
### Mood entry
|
||||
|
||||
| Field | Required | Type | Notes |
|
||||
|---|---|---|---|
|
||||
| `date` | yes | string | `YYYY-MM-DD`. |
|
||||
| `mood` | yes | enum | One of `SUPER_GUT`, `GUT`, `OKAY`, `SCHLECHT`, `LAUSIG`. |
|
||||
| `score` | yes | integer | `1`–`5`. |
|
||||
| `tags` | no | string | Comma-separated tags. |
|
||||
| `loggedAt` | no | string | ISO-8601 datetime; defaults to now. |
|
||||
| `externalId` | no | string | A source-stable id (1–120 chars). When present, a re-import upserts on `(user, source, externalId)` so an edit upstream is reflected rather than skipped. |
|
||||
| Field | Required | Type | Notes |
|
||||
| ------------ | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `date` | yes | string | `YYYY-MM-DD`. |
|
||||
| `mood` | yes | enum | One of `SUPER_GUT`, `GUT`, `OKAY`, `SCHLECHT`, `LAUSIG`. |
|
||||
| `score` | yes | integer | `1`–`5`. |
|
||||
| `tags` | no | string | Comma-separated tags. |
|
||||
| `loggedAt` | no | string | ISO-8601 datetime; defaults to now. |
|
||||
| `externalId` | no | string | A source-stable id (1–120 chars). When present, a re-import upserts on `(user, source, externalId)` so an edit upstream is reflected rather than skipped. |
|
||||
|
||||
### `MeasurementType` values and canonical units
|
||||
|
||||
@@ -76,72 +80,72 @@ display per the user's preference. Event-class types record a single
|
||||
fired event (`value` is `1`) and are normally produced by a device, not
|
||||
hand-authored.
|
||||
|
||||
| Type | Canonical unit |
|
||||
|---|---|
|
||||
| `WEIGHT` | kg |
|
||||
| `BLOOD_PRESSURE_SYS` | mmHg |
|
||||
| `BLOOD_PRESSURE_DIA` | mmHg |
|
||||
| `PULSE` | bpm |
|
||||
| `RESTING_HEART_RATE` | bpm |
|
||||
| `WALKING_HEART_RATE_AVERAGE` | bpm |
|
||||
| `AVERAGE_HEART_RATE` | bpm |
|
||||
| `MAX_HEART_RATE` | bpm |
|
||||
| `CARDIO_RECOVERY` | bpm |
|
||||
| `HEART_RATE_VARIABILITY` | ms (SDNN) |
|
||||
| `HRV_RMSSD` | ms (RMSSD) |
|
||||
| `RESPIRATORY_RATE` | count/min |
|
||||
| `OXYGEN_SATURATION` | percent (0–100) |
|
||||
| `BLOOD_GLUCOSE` | mg/dL |
|
||||
| `BODY_FAT` | percent (0–100) |
|
||||
| `FAT_MASS` | kg |
|
||||
| `FAT_FREE_MASS` | kg |
|
||||
| `LEAN_BODY_MASS` | kg |
|
||||
| `MUSCLE_MASS` | kg |
|
||||
| `BONE_MASS` | kg |
|
||||
| `TOTAL_BODY_WATER` | kg |
|
||||
| `VISCERAL_FAT` | rating |
|
||||
| `BODY_MASS_INDEX` | kg/m² |
|
||||
| `BODY_TEMPERATURE` | °C (core) |
|
||||
| `SKIN_TEMPERATURE` | °C (dermal spot) |
|
||||
| `WRIST_TEMPERATURE` | °C (overnight wrist) |
|
||||
| `SLEEP_DURATION` | minutes |
|
||||
| `SLEEP_NEED` | minutes |
|
||||
| `SLEEP_PERFORMANCE` | percent (0–100) |
|
||||
| `SLEEP_EFFICIENCY` | percent (0–100) |
|
||||
| `SLEEP_CONSISTENCY` | percent (0–100) |
|
||||
| `SLEEP_DISTURBANCE_COUNT` | count |
|
||||
| `BREATHING_DISTURBANCES` | count |
|
||||
| `ACTIVITY_STEPS` | count |
|
||||
| `FLIGHTS_CLIMBED` | count |
|
||||
| `WALKING_RUNNING_DISTANCE` | metres |
|
||||
| `SIX_MINUTE_WALK_DISTANCE` | metres |
|
||||
| `WALKING_STEP_LENGTH` | metres |
|
||||
| `WALKING_SPEED` | m/s |
|
||||
| `STAIR_ASCENT_SPEED` | m/s |
|
||||
| `STAIR_DESCENT_SPEED` | m/s |
|
||||
| `WALKING_STEADINESS` | percent (0–100) |
|
||||
| `WALKING_ASYMMETRY` | percent (0–100) |
|
||||
| `WALKING_DOUBLE_SUPPORT` | percent (0–100) |
|
||||
| `ACTIVE_ENERGY_BURNED` | kcal |
|
||||
| `ENERGY_EXPENDITURE_KJ` | kJ |
|
||||
| `VO2_MAX` | mL/(kg·min) |
|
||||
| `PULSE_WAVE_VELOCITY` | m/s |
|
||||
| `VASCULAR_AGE` | years |
|
||||
| `FALL_COUNT` | count |
|
||||
| `AUDIO_EXPOSURE_ENV` | dBA |
|
||||
| `AUDIO_EXPOSURE_HEADPHONE` | dBA |
|
||||
| `TIME_IN_DAYLIGHT` | minutes |
|
||||
| `RECOVERY_SCORE` | score (0–100) |
|
||||
| `STRESS_SCORE` | score (0–100) |
|
||||
| `STRAIN_SCORE` | score (0–100) |
|
||||
| `DAY_STRAIN` | score (0–21) |
|
||||
| `WORKOUT_STRAIN` | score (0–21) |
|
||||
| `AUDIO_EXPOSURE_EVENT` | count (event) |
|
||||
| `IRREGULAR_RHYTHM_NOTIFICATION` | event |
|
||||
| `HIGH_HEART_RATE_EVENT` | event |
|
||||
| `LOW_HEART_RATE_EVENT` | event |
|
||||
| `WALKING_STEADINESS_EVENT` | event |
|
||||
| `BREATHING_DISTURBANCE_EVENT` | event |
|
||||
| Type | Canonical unit |
|
||||
| ------------------------------- | -------------------- |
|
||||
| `WEIGHT` | kg |
|
||||
| `BLOOD_PRESSURE_SYS` | mmHg |
|
||||
| `BLOOD_PRESSURE_DIA` | mmHg |
|
||||
| `PULSE` | bpm |
|
||||
| `RESTING_HEART_RATE` | bpm |
|
||||
| `WALKING_HEART_RATE_AVERAGE` | bpm |
|
||||
| `AVERAGE_HEART_RATE` | bpm |
|
||||
| `MAX_HEART_RATE` | bpm |
|
||||
| `CARDIO_RECOVERY` | bpm |
|
||||
| `HEART_RATE_VARIABILITY` | ms (SDNN) |
|
||||
| `HRV_RMSSD` | ms (RMSSD) |
|
||||
| `RESPIRATORY_RATE` | count/min |
|
||||
| `OXYGEN_SATURATION` | percent (0–100) |
|
||||
| `BLOOD_GLUCOSE` | mg/dL |
|
||||
| `BODY_FAT` | percent (0–100) |
|
||||
| `FAT_MASS` | kg |
|
||||
| `FAT_FREE_MASS` | kg |
|
||||
| `LEAN_BODY_MASS` | kg |
|
||||
| `MUSCLE_MASS` | kg |
|
||||
| `BONE_MASS` | kg |
|
||||
| `TOTAL_BODY_WATER` | kg |
|
||||
| `VISCERAL_FAT` | rating |
|
||||
| `BODY_MASS_INDEX` | kg/m² |
|
||||
| `BODY_TEMPERATURE` | °C (core) |
|
||||
| `SKIN_TEMPERATURE` | °C (dermal spot) |
|
||||
| `WRIST_TEMPERATURE` | °C (overnight wrist) |
|
||||
| `SLEEP_DURATION` | minutes |
|
||||
| `SLEEP_NEED` | minutes |
|
||||
| `SLEEP_PERFORMANCE` | percent (0–100) |
|
||||
| `SLEEP_EFFICIENCY` | percent (0–100) |
|
||||
| `SLEEP_CONSISTENCY` | percent (0–100) |
|
||||
| `SLEEP_DISTURBANCE_COUNT` | count |
|
||||
| `BREATHING_DISTURBANCES` | count |
|
||||
| `ACTIVITY_STEPS` | count |
|
||||
| `FLIGHTS_CLIMBED` | count |
|
||||
| `WALKING_RUNNING_DISTANCE` | metres |
|
||||
| `SIX_MINUTE_WALK_DISTANCE` | metres |
|
||||
| `WALKING_STEP_LENGTH` | metres |
|
||||
| `WALKING_SPEED` | m/s |
|
||||
| `STAIR_ASCENT_SPEED` | m/s |
|
||||
| `STAIR_DESCENT_SPEED` | m/s |
|
||||
| `WALKING_STEADINESS` | percent (0–100) |
|
||||
| `WALKING_ASYMMETRY` | percent (0–100) |
|
||||
| `WALKING_DOUBLE_SUPPORT` | percent (0–100) |
|
||||
| `ACTIVE_ENERGY_BURNED` | kcal |
|
||||
| `ENERGY_EXPENDITURE_KJ` | kJ |
|
||||
| `VO2_MAX` | mL/(kg·min) |
|
||||
| `PULSE_WAVE_VELOCITY` | m/s |
|
||||
| `VASCULAR_AGE` | years |
|
||||
| `FALL_COUNT` | count |
|
||||
| `AUDIO_EXPOSURE_ENV` | dBA |
|
||||
| `AUDIO_EXPOSURE_HEADPHONE` | dBA |
|
||||
| `TIME_IN_DAYLIGHT` | minutes |
|
||||
| `RECOVERY_SCORE` | score (0–100) |
|
||||
| `STRESS_SCORE` | score (0–100) |
|
||||
| `STRAIN_SCORE` | score (0–100) |
|
||||
| `DAY_STRAIN` | score (0–21) |
|
||||
| `WORKOUT_STRAIN` | score (0–21) |
|
||||
| `AUDIO_EXPOSURE_EVENT` | count (event) |
|
||||
| `IRREGULAR_RHYTHM_NOTIFICATION` | event |
|
||||
| `HIGH_HEART_RATE_EVENT` | event |
|
||||
| `LOW_HEART_RATE_EVENT` | event |
|
||||
| `WALKING_STEADINESS_EVENT` | event |
|
||||
| `BREATHING_DISTURBANCE_EVENT` | event |
|
||||
|
||||
The live, machine-readable list of writable types and their units is
|
||||
also served by `GET /api/meta/capabilities` under `ingest.quantityTypes`
|
||||
|
||||
@@ -31,6 +31,7 @@ grant scoped to credentials they control.
|
||||
and the connection dies when the first access token expires (~1 hour).
|
||||
The remaining `read:*` scopes are read-only; request all of them so the
|
||||
user grants once and every sync resource is covered.
|
||||
|
||||
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.
|
||||
|
||||
@@ -11,14 +11,14 @@ relay.
|
||||
The Withings client (`src/lib/withings/client.ts`) maps the following
|
||||
metric types into `Measurement` rows tagged `source = WITHINGS`:
|
||||
|
||||
| Withings meastype | HealthLog `MeasurementType` |
|
||||
| ----------------- | --------------------------- |
|
||||
| 1 | `WEIGHT` |
|
||||
| 5 | `FAT_FREE_MASS` |
|
||||
| 9 | `BLOOD_PRESSURE_DIA` |
|
||||
| 10 | `BLOOD_PRESSURE_SYS` |
|
||||
| 11 | `PULSE` |
|
||||
| 54, 35 | `OXYGEN_SATURATION` (ScanWatch / pulse-ox products) |
|
||||
| Withings meastype | HealthLog `MeasurementType` |
|
||||
| ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
|
||||
| 1 | `WEIGHT` |
|
||||
| 5 | `FAT_FREE_MASS` |
|
||||
| 9 | `BLOOD_PRESSURE_DIA` |
|
||||
| 10 | `BLOOD_PRESSURE_SYS` |
|
||||
| 11 | `PULSE` |
|
||||
| 54, 35 | `OXYGEN_SATURATION` (ScanWatch / pulse-ox products) |
|
||||
| Plus the body-composition family (fat ratio, muscle mass, bone mass, hydration) and sleep / heart-rate variability via the heart-list endpoint |
|
||||
|
||||
Activity data (steps, active energy, distance, floors climbed) lands
|
||||
@@ -136,7 +136,7 @@ Override per-user via the Sources section of `/settings/thresholds`.
|
||||
- **`Withings credentials not configured`** — `WITHINGS_CLIENT_ID`
|
||||
or `WITHINGS_CLIENT_SECRET` is missing from `.env`. Confirm both
|
||||
are set in the container's runtime environment (`docker compose
|
||||
exec app env | grep WITHINGS`).
|
||||
exec app env | grep WITHINGS`).
|
||||
- **`Callback URI mismatch`** — the URL HealthLog sends to Withings
|
||||
must match an entry in the developer-portal app's callback list.
|
||||
Add the public URL exactly, including the `https://` scheme and
|
||||
@@ -183,7 +183,7 @@ The Settings page **Disconnect** button:
|
||||
the database — disconnect does not delete data. Re-linking the
|
||||
same Withings account merges back onto the same metric
|
||||
timeline; the upsert path matches on `(userId, type, measuredAt,
|
||||
source)` so previously-synced rows update in place.
|
||||
source)` so previously-synced rows update in place.
|
||||
|
||||
## Admin probe
|
||||
|
||||
|
||||
@@ -39,11 +39,11 @@ export without polluting the local shell.
|
||||
Output is one row per declared variable, grouped by section. Three
|
||||
states:
|
||||
|
||||
| Marker | Meaning |
|
||||
| --------------------- | ---------------------------------------------------------------------------------- |
|
||||
| `[OK]` | Variable is set and non-empty. |
|
||||
| `[MISSING-REQUIRED]` | Variable is required for boot. The script exits 1. |
|
||||
| `[missing-optional]` | Variable is optional. The script reports it but does not fail. |
|
||||
| Marker | Meaning |
|
||||
| -------------------- | -------------------------------------------------------------- |
|
||||
| `[OK]` | Variable is set and non-empty. |
|
||||
| `[MISSING-REQUIRED]` | Variable is required for boot. The script exits 1. |
|
||||
| `[missing-optional]` | Variable is optional. The script reports it but does not fail. |
|
||||
|
||||
The grep-friendly format lets a CI pipeline do `grep -q
|
||||
'\[MISSING-REQUIRED\]'` to gate a deploy. The CI integration itself
|
||||
@@ -93,7 +93,7 @@ so changes go through a PR. To add a new variable:
|
||||
Deferred to v1.4.43:
|
||||
|
||||
- GitHub Actions workflow that runs `pnpm check-env --file
|
||||
.env.production.example` on every PR, blocking changes that
|
||||
.env.production.example` on every PR, blocking changes that
|
||||
introduce new required vars without updating the example file.
|
||||
- Coolify pre-deploy hook that runs `pnpm check-env` inside the
|
||||
target container and aborts the deploy on exit code 1.
|
||||
@@ -96,6 +96,7 @@ pnpm dlx --package pg --package tsx tsx scripts/repair-intake-anomalies.ts --fix
|
||||
```
|
||||
|
||||
`--tombstone-implausible` is refused without `--fix`.
|
||||
|
||||
- **Drifted schedule windows are reconciled** to
|
||||
`min/max(times_of_day)` so the legacy pair stops contradicting the
|
||||
canonical dose times. Overnight windows (`window_end < window_start`)
|
||||
|
||||
@@ -9,8 +9,8 @@ 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.
|
||||
- linux/arm64), so a Raspberry Pi 5 or an x86 VPS both work
|
||||
unchanged.
|
||||
- **2 GB RAM, 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.
|
||||
@@ -43,11 +43,11 @@ echo "ENCRYPTION_KEY=$(openssl rand -hex 32)" >> .env
|
||||
echo "API_TOKEN_HMAC_KEY=$(openssl rand -hex 32)" >> .env
|
||||
```
|
||||
|
||||
| Variable | Purpose | Rotation cost |
|
||||
| -------- | ------- | ------------- |
|
||||
| `POSTGRES_PASSWORD` | Bundled Postgres password — used by both the `db` service and `DATABASE_URL` | Database restart + connection-string update |
|
||||
| `ENCRYPTION_KEY` | AES-256-GCM key for at-rest secrets (Withings tokens, AI provider keys, VAPID secrets) | Coordinated; see `docs/ops/encryption-key-rotation.md` |
|
||||
| `API_TOKEN_HMAC_KEY` | HMAC-SHA256 key for hashing Bearer API tokens before storage | Invalidates every issued `hlk_*` token |
|
||||
| Variable | Purpose | Rotation cost |
|
||||
| -------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------ |
|
||||
| `POSTGRES_PASSWORD` | Bundled Postgres password — used by both the `db` service and `DATABASE_URL` | Database restart + connection-string update |
|
||||
| `ENCRYPTION_KEY` | AES-256-GCM key for at-rest secrets (Withings tokens, AI provider keys, VAPID secrets) | Coordinated; see `docs/ops/encryption-key-rotation.md` |
|
||||
| `API_TOKEN_HMAC_KEY` | HMAC-SHA256 key for hashing Bearer API tokens before storage | Invalidates every issued `hlk_*` token |
|
||||
|
||||
Then open `.env` and confirm `DATABASE_URL` includes the password you
|
||||
just generated. The default template (`postgresql://healthlog:CHANGE-ME@db:5432/healthlog`)
|
||||
@@ -131,9 +131,9 @@ 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. |
|
||||
| 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:
|
||||
|
||||
@@ -144,18 +144,18 @@ NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false pnpm build
|
||||
|
||||
## 7. Where to go next
|
||||
|
||||
| Next step | File |
|
||||
| --------- | ---- |
|
||||
| TLS + reverse-proxy configuration | `docs/self-hosting/reverse-proxy.md` |
|
||||
| Push notifications — Web Push, Telegram, ntfy, webhook, email, APNs | `docs/self-hosting/notifications.md` |
|
||||
| Web/worker process split for horizontal scale | `docs/self-hosting/scaling.md` |
|
||||
| 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` |
|
||||
| Google Health sync — Fitbit & Pixel (per-user BYO-keys) | `docs/integrations/google-health.md` |
|
||||
| Apple Health `export.zip` import | `docs/integrations/apple-health.md` |
|
||||
| AI provider setup (OpenAI / Anthropic / local / ChatGPT OAuth) | `docs/integrations/ai-providers.md` |
|
||||
| Next step | File |
|
||||
| ------------------------------------------------------------------- | ------------------------------------- |
|
||||
| TLS + reverse-proxy configuration | `docs/self-hosting/reverse-proxy.md` |
|
||||
| Push notifications — Web Push, Telegram, ntfy, webhook, email, APNs | `docs/self-hosting/notifications.md` |
|
||||
| Web/worker process split for horizontal scale | `docs/self-hosting/scaling.md` |
|
||||
| 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` |
|
||||
| Google Health sync — Fitbit & Pixel (per-user BYO-keys) | `docs/integrations/google-health.md` |
|
||||
| Apple Health `export.zip` import | `docs/integrations/apple-health.md` |
|
||||
| AI provider setup (OpenAI / Anthropic / local / ChatGPT OAuth) | `docs/integrations/ai-providers.md` |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -168,7 +168,7 @@ NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false pnpm build
|
||||
`app_settings.singleton.registrationEnabled`. The admin panel can
|
||||
flip it back if a previous owner closed it.
|
||||
- **Service unreachable on `localhost:3000`.** Check `docker compose
|
||||
ps` — the `app` healthcheck polls `/api/version` every 30 seconds
|
||||
ps` — the `app` healthcheck polls `/api/version` every 30 seconds
|
||||
and will mark the container unhealthy if Postgres is still
|
||||
starting. Tail `docker compose logs db` to confirm Postgres came up
|
||||
cleanly.
|
||||
|
||||
@@ -24,14 +24,14 @@ Apple-platform exclusives the web cannot offer.
|
||||
|
||||
## TL;DR — which channel needs what
|
||||
|
||||
| Channel | What it needs | Cost | Where it works |
|
||||
| --- | --- | --- | --- |
|
||||
| **Web Push** | A VAPID keypair (one command, or paste into the admin panel) | Free | Any modern desktop/Android browser; the installed PWA on iPhone (iOS 16.4+, after *Add to Home Screen*) |
|
||||
| **Telegram** | A bot token from @BotFather + each user's chat ID | Free | Anywhere the Telegram app runs |
|
||||
| **ntfy** | A topic on `ntfy.sh` or your own ntfy server | Free | The ntfy app (iOS/Android) or any browser |
|
||||
| **Webhook** | A URL you own (and optionally one custom header) | Free | Anywhere — you route the POST yourself |
|
||||
| **Email** | An SMTP transport you operate or rent | Free–cheap | Any mailbox |
|
||||
| **APNs** | An Apple Developer account + a `.p8` push key + your own signed iOS build | Apple Developer Program (paid) | A native iOS app you build and sign |
|
||||
| Channel | What it needs | Cost | Where it works |
|
||||
| ------------ | ------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------- |
|
||||
| **Web Push** | A VAPID keypair (one command, or paste into the admin panel) | Free | Any modern desktop/Android browser; the installed PWA on iPhone (iOS 16.4+, after _Add to Home Screen_) |
|
||||
| **Telegram** | A bot token from @BotFather + each user's chat ID | Free | Anywhere the Telegram app runs |
|
||||
| **ntfy** | A topic on `ntfy.sh` or your own ntfy server | Free | The ntfy app (iOS/Android) or any browser |
|
||||
| **Webhook** | A URL you own (and optionally one custom header) | Free | Anywhere — you route the POST yourself |
|
||||
| **Email** | An SMTP transport you operate or rent | Free–cheap | Any mailbox |
|
||||
| **APNs** | An Apple Developer account + a `.p8` push key + your own signed iOS build | Apple Developer Program (paid) | A native iOS app you build and sign |
|
||||
|
||||
Web Push is the recommended default — it needs no third-party account,
|
||||
just a keypair you generate yourself once.
|
||||
@@ -173,7 +173,7 @@ pub/sub push relay you can use hosted (`ntfy.sh`) or run yourself. You
|
||||
subscribe to a **topic** in the ntfy app (iOS/Android) or a browser, and
|
||||
HealthLog publishes to that topic.
|
||||
|
||||
1. Pick a hard-to-guess topic name (the topic *is* the access control on
|
||||
1. Pick a hard-to-guess topic name (the topic _is_ the access control on
|
||||
public `ntfy.sh` — anyone who knows the string can read it, so treat
|
||||
it like a secret).
|
||||
2. Subscribe to that topic in the ntfy app or at `https://ntfy.sh/<topic>`.
|
||||
@@ -268,14 +268,14 @@ under your own Apple identity. Everything below is that path.
|
||||
|
||||
The APNs env vars (`src/lib/notifications/senders/apns.ts`):
|
||||
|
||||
| Variable | Purpose |
|
||||
| --- | --- |
|
||||
| `APNS_KEY_ID` | The `.p8` key's Key ID |
|
||||
| `APNS_TEAM_ID` | Your Apple Developer Team ID |
|
||||
| `APNS_BUNDLE_ID` | Your app's bundle identifier |
|
||||
| `APNS_KEY_B64` | The `.p8` PEM, base64-encoded (recommended key source) |
|
||||
| `APNS_KEY` | OR the raw PEM body inline |
|
||||
| `APNS_KEY_FILE` | OR a path to the `.p8` file |
|
||||
| Variable | Purpose |
|
||||
| --------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `APNS_KEY_ID` | The `.p8` key's Key ID |
|
||||
| `APNS_TEAM_ID` | Your Apple Developer Team ID |
|
||||
| `APNS_BUNDLE_ID` | Your app's bundle identifier |
|
||||
| `APNS_KEY_B64` | The `.p8` PEM, base64-encoded (recommended key source) |
|
||||
| `APNS_KEY` | OR the raw PEM body inline |
|
||||
| `APNS_KEY_FILE` | OR a path to the `.p8` file |
|
||||
| `APNS_CRITICAL_ENTITLEMENT` | Optional — set `true` only if Apple grants your app the Critical Alerts entitlement |
|
||||
|
||||
APNs is **all-or-none**: either set the three IDs plus exactly one key
|
||||
@@ -311,14 +311,14 @@ Two gotchas worth knowing before you blame a stale token:
|
||||
|
||||
## Which channel should I pick?
|
||||
|
||||
| Your setup | Recommended channel |
|
||||
| --- | --- |
|
||||
| PWA on Android or desktop | **Web Push** — generate a VAPID keypair, subscribe, done. |
|
||||
| PWA on an iPhone | **Web Push**, but install the PWA via *Add to Home Screen* first (iOS 16.4+). Reminders, taken-clears, and the app badge all work; the lock-screen Live Activity needs a native build. |
|
||||
| You want the native iOS lock-screen Live Activity | Build and sign **your own** iOS app under your own Apple account and wire up **APNs** (paid), with Web Push as a fallback for any non-iOS device. |
|
||||
| Headless / no browser, or you live in chat | **Telegram** or **ntfy** — neither needs a browser, both are free. ntfy is the most self-host-native; Telegram adds inline reminder action buttons. |
|
||||
| You already run an automation hub | **Webhook** — POST every event to your own URL and route it however you like. |
|
||||
| You want reminders in your inbox | **Email** — set the SMTP transport once as the operator; users add their address. |
|
||||
| Your setup | Recommended channel |
|
||||
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| PWA on Android or desktop | **Web Push** — generate a VAPID keypair, subscribe, done. |
|
||||
| PWA on an iPhone | **Web Push**, but install the PWA via _Add to Home Screen_ first (iOS 16.4+). Reminders, taken-clears, and the app badge all work; the lock-screen Live Activity needs a native build. |
|
||||
| You want the native iOS lock-screen Live Activity | Build and sign **your own** iOS app under your own Apple account and wire up **APNs** (paid), with Web Push as a fallback for any non-iOS device. |
|
||||
| Headless / no browser, or you live in chat | **Telegram** or **ntfy** — neither needs a browser, both are free. ntfy is the most self-host-native; Telegram adds inline reminder action buttons. |
|
||||
| You already run an automation hub | **Webhook** — POST every event to your own URL and route it however you like. |
|
||||
| You want reminders in your inbox | **Email** — set the SMTP transport once as the operator; users add their address. |
|
||||
|
||||
Mixing channels is fine and common — e.g. Web Push on the laptop and
|
||||
ntfy on the phone. Each user configures their own set from
|
||||
|
||||
@@ -45,7 +45,7 @@ reached:
|
||||
- **Physical or legal seizure of your own host.** If the machine
|
||||
running PostgreSQL — your VPS, your home server, your NAS — is seized,
|
||||
imaged, or compelled, the data is on it. Encryption at rest protects
|
||||
against a stolen disk or a leaked backup only while the key is *not*
|
||||
against a stolen disk or a leaked backup only while the key is _not_
|
||||
also available; if the host is running, or the key is stored next to
|
||||
the data, that protection is gone.
|
||||
- **Your hosting provider.** A VPS provider can snapshot your disk and
|
||||
@@ -77,7 +77,7 @@ record, not speculation:
|
||||
California privacy statute over the sharing of menstrual-tracking
|
||||
data; the verdict and the underlying class action are public record.
|
||||
|
||||
These are cited as factual public record to explain *why* the
|
||||
These are cited as factual public record to explain _why_ the
|
||||
"who holds the data" question is the one that matters for this category.
|
||||
HealthLog makes no claim about any other product's current practices.
|
||||
|
||||
|
||||
@@ -11,11 +11,11 @@ the env-var pairing is identical across them.
|
||||
|
||||
## Pre-flight — env vars the proxy interacts with
|
||||
|
||||
| Variable | Required for | Notes |
|
||||
| -------- | ------------ | ----- |
|
||||
| `NEXT_PUBLIC_APP_URL` | Every public-facing absolute URL the UI emits | Must match the hostname the proxy serves |
|
||||
| `APP_URL` | Server-side absolute URLs from background jobs | Mirror of the above; both must match |
|
||||
| `TRUST_PROXY_HOPS` | `X-Forwarded-For` chain trust | Defaults to `1` — one trusted hop |
|
||||
| Variable | Required for | Notes |
|
||||
| --------------------- | ---------------------------------------------- | ---------------------------------------- |
|
||||
| `NEXT_PUBLIC_APP_URL` | Every public-facing absolute URL the UI emits | Must match the hostname the proxy serves |
|
||||
| `APP_URL` | Server-side absolute URLs from background jobs | Mirror of the above; both must match |
|
||||
| `TRUST_PROXY_HOPS` | `X-Forwarded-For` chain trust | Defaults to `1` — one trusted hop |
|
||||
|
||||
Set the URL pair to the public origin (`https://your-instance.example.com`)
|
||||
and restart the `app` container. The image reads both at startup; no
|
||||
@@ -28,12 +28,12 @@ chain from the **right**, counting back `TRUST_PROXY_HOPS` entries.
|
||||
This guards against a client rotating `X-Forwarded-For` per request
|
||||
to defeat IP-based rate limits.
|
||||
|
||||
| Topology | `TRUST_PROXY_HOPS` |
|
||||
| -------- | ------------------ |
|
||||
| App is internet-facing with no proxy | `0` (XFF ignored entirely) |
|
||||
| Single proxy in front (Caddy / Traefik / Nginx / Coolify) | `1` (default) |
|
||||
| Cloudflare → your proxy → app | `2` |
|
||||
| Cloudflare → Coolify-Tunnel → Coolify → app | `3` |
|
||||
| Topology | `TRUST_PROXY_HOPS` |
|
||||
| --------------------------------------------------------- | -------------------------- |
|
||||
| App is internet-facing with no proxy | `0` (XFF ignored entirely) |
|
||||
| Single proxy in front (Caddy / Traefik / Nginx / Coolify) | `1` (default) |
|
||||
| Cloudflare → your proxy → app | `2` |
|
||||
| Cloudflare → Coolify-Tunnel → Coolify → app | `3` |
|
||||
|
||||
A misconfigured count logs a one-shot warning to stderr and collapses
|
||||
every anonymous caller into one shared rate-limit bucket. Match the
|
||||
@@ -132,7 +132,7 @@ operator attention:
|
||||
|
||||
The following variables are **build-time only** — they are injected
|
||||
via `--build-arg` by the `docker-publish` workflow and baked into the
|
||||
shipped image. Setting them under Coolify's *Environment Variables*
|
||||
shipped image. Setting them under Coolify's _Environment Variables_
|
||||
panel has zero effect on the running container, and the stale value
|
||||
becomes a misleading artefact next time someone audits the deploy
|
||||
config:
|
||||
|
||||
@@ -90,7 +90,7 @@ services:
|
||||
app:
|
||||
image: ghcr.io/mbombeck/healthlog:latest
|
||||
environment:
|
||||
DATABASE_POOL_MAX: "12" # 6 containers × 12 = 72 < 100
|
||||
DATABASE_POOL_MAX: "12" # 6 containers × 12 = 72 < 100
|
||||
HEALTHLOG_PROCESS_TYPE: web
|
||||
```
|
||||
|
||||
|
||||
@@ -235,7 +235,7 @@ lists) uses the unified `<FilterBar>` rail from
|
||||
labelled `<Select>`s and date inputs.
|
||||
|
||||
- **Grammar**: `[icon] [Zeitraum] [Typ] [Quelle] [extras…]
|
||||
[Zurücksetzen] … [count]`. Canonical dimension order is date range,
|
||||
[Zurücksetzen] … [count]`. Canonical dimension order is date range,
|
||||
type, source; surface-specific dimensions append after.
|
||||
- **Pill = trigger + chip**: inactive pills show only the dimension
|
||||
name; active pills show `Name: Wert`, pick up the primary accent and
|
||||
|
||||
Reference in new issue
Block a user