mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
- docs/README.md: drop the doctor-report.md and audit/ entries (neither exists), broaden the ops/ description, list the real subdirectories, and stop promising release-by-release migration notes - migration/v1.3-to-v1.4: note which v1.4 routes have since moved or gone; rotation runs via pnpm dlx tsx - ui-guidelines: six locales with guard tests, settings sections point at the registry instead of an eight-item list, /settings redirect is a 308 - apple-store-connect-checklist: drop the unsourced 40% rejection-rate figure - codex-protocol-spec: the slug chain rotated on 2026-06-02; default is gpt-5.5, the shipped constant is authoritative
240 lines
11 KiB
Markdown
240 lines
11 KiB
Markdown
# Migrating from HealthLog 1.3 to 1.4
|
||
|
||
v1.4.0 is **drop-in for self-hosters running the GHCR image** — the
|
||
two new migrations (`0025_refresh_tokens`, `0025_user_locale_drift_fix`)
|
||
both apply on container start via `prisma migrate deploy`, and every
|
||
new env var in §Environment variables below has a safe default. This
|
||
guide documents what changes for operators and integrators, in case
|
||
you're extending the API or scaling the deployment.
|
||
|
||
> Historical note: route paths on this page reflect v1.4. Several have
|
||
> since moved or gone: `/api/admin/status-overview` is now
|
||
> `/api/admin/status`, the Umami probe is
|
||
> `/api/admin/monitoring/umami-test`, and the moodLog integration was
|
||
> removed entirely.
|
||
|
||
## TL;DR
|
||
|
||
```bash
|
||
docker compose pull
|
||
docker compose up -d
|
||
```
|
||
|
||
That's it. The migrations are forward-compatible (`IF NOT EXISTS`
|
||
guards), no env vars are _required_ to be added, and no API contract
|
||
changes shape or status code.
|
||
|
||
## Database
|
||
|
||
- **`0025_refresh_tokens`** _(new)_ — adds the `refresh_tokens` table
|
||
for the native-client 24h access-token / refresh-token rotation
|
||
flow. Browser cookie sessions don't use it.
|
||
- **`0025_user_locale_drift_fix`** _(new)_ — `ADD COLUMN IF NOT
|
||
EXISTS users.locale TEXT` to backfill an environment that was kept
|
||
in sync via `prisma db push` rather than `migrate deploy`. No-op
|
||
on installations that were already current.
|
||
- Both migrations are idempotent. A rollback to 1.3.3 keeps the
|
||
schema usable; the dropped tables/columns are simply unused.
|
||
|
||
## Environment variables
|
||
|
||
### About surface
|
||
|
||
- **`NEXT_PUBLIC_APP_BUILD_SHA`** _(optional, new)_ — short Git SHA
|
||
exposed through `/api/version`, rendered on Settings → About.
|
||
- **`NEXT_PUBLIC_APP_BUILT_AT`** _(optional, new)_ — ISO-8601 build
|
||
timestamp.
|
||
|
||
Both are optional; without them, the About surface renders
|
||
"development" wording.
|
||
|
||
### Worker / web split
|
||
|
||
- **`HEALTHLOG_PROCESS_TYPE`** _(optional, default `all`)_ — set to
|
||
`web` to disable the pg-boss worker (jobs queue but don't run), or
|
||
`worker` to refuse all HTTP traffic with `503 + X-HealthLog-Process-Type:
|
||
worker`. Default `all` keeps the single-container behaviour.
|
||
Only relevant if you're scaling HTTP and the worker independently.
|
||
|
||
### Encryption-key versioning
|
||
|
||
- **`ENCRYPTION_KEYS`** _(optional, new)_ — JSON object mapping
|
||
versioned key IDs to 64-char hex keys, e.g. `{"v1": "...", "v2":
|
||
"..."}`. When present, ciphertexts are written with the
|
||
`<keyId>.<base64>` format and decrypted by looking up the key ID.
|
||
Existing 1.3-format ciphertexts (no key prefix) keep decrypting
|
||
with `ENCRYPTION_KEY` indefinitely.
|
||
- **`ENCRYPTION_ACTIVE_KEY_ID`** _(optional, new)_ — the key ID used
|
||
for new writes. Must be a key in `ENCRYPTION_KEYS`. Defaults to
|
||
the legacy single-key behaviour if either is unset.
|
||
- **`ENCRYPTION_KEY`** _(unchanged)_ — still honoured as a fallback
|
||
when `ENCRYPTION_KEYS` is empty. No action needed for existing
|
||
installs.
|
||
- Rotation: `pnpm dlx tsx scripts/rotate-encryption-key.ts <newKeyId>`
|
||
re-encrypts every ciphertext under the new key without downtime.
|
||
Walk-through in `docs/ops/encryption-key-rotation.md` (the current
|
||
playbook; the production image strips `tsx`, hence `pnpm dlx`).
|
||
|
||
### Off-host backup target _(optional)_
|
||
|
||
If unset, the existing local `DataBackup` storage continues to work.
|
||
|
||
- **`BACKUP_S3_ENDPOINT`** — S3-compatible endpoint URL. Leave blank
|
||
to disable off-host backup.
|
||
- **`BACKUP_S3_BUCKET`** — destination bucket name.
|
||
- **`BACKUP_S3_ACCESS_KEY`**, **`BACKUP_S3_SECRET_KEY`** — IAM
|
||
credentials. Worker only requests `PutObject` + `GetObject` —
|
||
never `DeleteObject`. Retention is the bucket's lifecycle policy.
|
||
- **`BACKUP_S3_REGION`** _(default `auto`)_.
|
||
- **`BACKUP_RETENTION_DAYS`** _(default `30`)_ — surfaced in the
|
||
admin UI; the lifecycle rule on your bucket is the actual enforcer.
|
||
- **`BACKUP_ENCRYPTION_KEY`** — separate 64-char hex key for the
|
||
AES-GCM envelope of each upload. **Keep offline / in a vault.**
|
||
Lose this and the off-host backups are unreadable.
|
||
|
||
### API token hashing _(unchanged but worth re-confirming)_
|
||
|
||
- **`API_TOKEN_HMAC_KEY`** — 64-char hex string. Used to HMAC
|
||
Bearer tokens before storage so a DB leak can't be reversed via
|
||
rainbow table. Same key now also keys the new `RefreshToken` row
|
||
hashes. **Do not rotate without invalidating every native client.**
|
||
|
||
## API
|
||
|
||
### New
|
||
|
||
- **`GET /api/version`** — public, static, returns
|
||
`{ version, buildSha, builtAt, license, repository, changelog, docs }`.
|
||
No authentication.
|
||
- **`POST /api/integrations/withings/test`** — admin-only probe
|
||
against the saved Withings OAuth connection. Returns `{ ok, latency_ms }`
|
||
on success.
|
||
- **`POST /api/integrations/moodlog/test`** — admin-only HEAD
|
||
request against `https://moodlog.app/api/health`. SSRF-guarded.
|
||
- **`POST /api/notifications/web-push/test`** — sends a smoke push
|
||
notification to the current user's subscribed devices.
|
||
- **`POST /api/monitoring/glitchtip/test`** — fires a benign event
|
||
at the configured DSN.
|
||
- **`POST /api/monitoring/umami/test`** — verifies the script URL +
|
||
website ID resolve and respond with the expected payload.
|
||
- **`POST /api/admin/backup/test`** — admin-only round-trip against
|
||
the configured `BACKUP_S3_*` credentials.
|
||
- **`GET /api/admin/status-overview`** — admin-only aggregator
|
||
returning the six-card summary the new admin top strip renders in
|
||
one batched query.
|
||
- **`POST /api/auth/refresh`** — native-client refresh-token
|
||
rotation. Cookie-session browsers don't use this.
|
||
- **`POST /api/auth/refresh/revoke`** — paired logout-on-device that
|
||
flips both the refresh row and the matching `ApiToken` row to
|
||
revoked.
|
||
|
||
All new admin endpoints are hidden from anonymous traffic by the
|
||
proxy and rate-limited.
|
||
|
||
### Behaviour-only changes
|
||
|
||
- **`POST /api/ai/test`** now accepts an optional JSON body that
|
||
mirrors the AI dropdown in `/settings`. Empty body keeps the
|
||
pre-1.4 behaviour (test the saved provider). Non-empty body tests
|
||
the unsaved selection without persisting plaintext keys. The
|
||
existing rate limit (5/min) and error-leak shielding stay in place.
|
||
- **`GET /api/dashboard/summary`** localises the `streak.label` and
|
||
`greeting.salutation` fields against the user's stored locale.
|
||
- **`POST /api/insights/targets`** uses a single batched query for
|
||
per-type latest readings; returns identical payloads.
|
||
|
||
No `/api/...` route changed shape, status codes, or auth rules. iOS,
|
||
n8n, and Health Connect integrations need no change.
|
||
|
||
## UI / UX
|
||
|
||
### Dashboard
|
||
|
||
- **Tile strip is always one row.** Previously the four-to-five-tile
|
||
grid wrapped to a second row when many widgets were enabled. v1.4
|
||
uses a horizontal-scroll strip — total width caps at the chart
|
||
width below, snap points keep the touch experience deliberate. To
|
||
reduce the visible tile count, open Settings → Dashboard Layout
|
||
and toggle widgets off.
|
||
|
||
### Forms
|
||
|
||
- **Password managers no longer autofill health-data inputs.**
|
||
Browsers and LastPass / 1Password were happily filling the user's
|
||
saved account password into measurement notes, AI tokens, and
|
||
similar free-text fields. The `<Input>` primitive now defaults to
|
||
`autoComplete="off"` plus the LastPass / 1Password ignore
|
||
attributes whenever the caller doesn't pass a semantic value. Auth
|
||
and profile forms (login, register, change-email) keep autofilling
|
||
normally because they pass an explicit `autoComplete` (e.g.
|
||
`"username"`, `"email"`, `"new-password"`).
|
||
|
||
### Settings
|
||
|
||
- **AI provider connection-test honours the unsaved selection.** Pick
|
||
a provider in the dropdown and click "Verbindung testen" without
|
||
saving first — the test now runs against your in-form selection
|
||
instead of the saved provider.
|
||
|
||
### Notifications
|
||
|
||
- **Telegram, ntfy, and Web Push reminders follow your locale.**
|
||
Switching the user's locale to English now produces English
|
||
reminders; the previous build always sent German strings regardless.
|
||
Telegram callback IDs stay stable (English) so the bot dispatcher
|
||
keeps matching them.
|
||
|
||
## Medical defaults
|
||
|
||
- **Diastolic blood-pressure orange band no longer reaches 60 mmHg.**
|
||
A reading of 60 mmHg now lands in red instead of yellow. The
|
||
user-override path is intact — if you set a personal threshold via
|
||
`/settings#thresholds`, it still applies and is audit-logged.
|
||
- **Body-fat classifier mirrors the ACE table** — readings below the
|
||
ACE essential floor (M < 2 / F < 10) now classify as
|
||
"Below essential" (danger), the actual essential band (M 2–5 /
|
||
F 10–13) as warning, and the realistic healthy range
|
||
(M 14–24 / F 21–31) as the green band on the dashboard target tile.
|
||
- **Step target unified at 8 000–15 000.** The two surfaces that
|
||
previously disagreed (7 000–10 000 in `getStepsRange()` versus
|
||
8 000–15 000 in the effective-range resolver) now use the same
|
||
band, anchored on Saint-Maurice et al., JAMA 2020.
|
||
|
||
## URL changes
|
||
|
||
- **Settings has moved to per-section routes.** `/settings#integrations`
|
||
→ `/settings/integrations`, etc. The legacy `/settings#anchor` URLs
|
||
still 308-redirect to the new routes; sidebar deep-links and the
|
||
`<a href="/settings#thresholds">` patterns from 1.3 keep working.
|
||
|
||
## What landed in v1.4 vs v1.4.1
|
||
|
||
The v1.3.3 ecosystem audit identified architectural rewrites tracked
|
||
through v1.4. Status as of the v1.4.1 cycle:
|
||
|
||
| Bucket | Status |
|
||
| -------------------------------------------------------------------------------------- | ------------------- |
|
||
| Settings page split into `/settings/[section]` | ✓ shipped in v1.4.0 |
|
||
| Admin status-first card grid + aggregator endpoint | ✓ shipped in v1.4.0 |
|
||
| AI insights rework — severity hero + allowlisted inline charts | ✓ shipped in v1.4.0 |
|
||
| Multi-tenant prep (off-host backups, key versioning, worker/web split, refresh tokens) | ✓ shipped in v1.4.0 |
|
||
| Per-section admin component extraction (inner monolith → one file per panel) | ✓ shipped in v1.4.1 |
|
||
| Postgres-backed integration test suite (testcontainers) | ✓ shipped in v1.4.1 |
|
||
| Playwright + axe-core E2E foundation | ✓ shipped in v1.4.1 |
|
||
|
||
## Rollback
|
||
|
||
If 1.4.0 surfaces a regression on your install, redeploy the previous
|
||
1.3.3 image:
|
||
|
||
```bash
|
||
docker compose down
|
||
docker pull ghcr.io/mbombeck/healthlog:1.3.3
|
||
sed -i 's/healthlog:1.4.0/healthlog:1.3.3/' docker-compose.yml
|
||
docker compose up -d
|
||
```
|
||
|
||
The schema is unchanged so a rollback does not require a database
|
||
restore. File a GitHub issue with the symptom — the v1.4 cycle ran
|
||
multi-pass QA per stage, but no migration is risk-free.
|