Retroactive cleanup of CHANGELOG.md and docs/audit/v1.4.X-summary.md files. Removes maintainer's real name, specific health figures tied to the live tenant, internal release-process jargon, and stray German strings in English-only release notes. Pre-release PII-grep gate added to the v1.4.20 release pipeline. No git history rewrite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
10 KiB
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.
TL;DR
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 therefresh_tokenstable 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 TEXTto backfill an environment that was kept in sync viaprisma db pushrather thanmigrate 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, defaultall) — set towebto disable the pg-boss worker (jobs queue but don't run), orworkerto refuse all HTTP traffic with503 + X-HealthLog-Process-Type: worker. Defaultallkeeps 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 withENCRYPTION_KEYindefinitely.ENCRYPTION_ACTIVE_KEY_ID(optional, new) — the key ID used for new writes. Must be a key inENCRYPTION_KEYS. Defaults to the legacy single-key behaviour if either is unset.ENCRYPTION_KEY(unchanged) — still honoured as a fallback whenENCRYPTION_KEYSis empty. No action needed for existing installs.- Rotation:
pnpm tsx scripts/rotate-encryption-key.ts <newKeyId>re-encrypts every ciphertext under the new key without downtime. Walk-through indocs/ops/encryption-key-rotation.md.
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 requestsPutObject+GetObject— neverDeleteObject. Retention is the bucket's lifecycle policy.BACKUP_S3_REGION(defaultauto).BACKUP_RETENTION_DAYS(default30) — 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 newRefreshTokenrow 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 againsthttps://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 configuredBACKUP_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 matchingApiTokenrow to revoked.
All new admin endpoints are hidden from anonymous traffic by the proxy and rate-limited.
Behaviour-only changes
POST /api/ai/testnow 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/summarylocalises thestreak.labelandgreeting.salutationfields against the user's stored locale.POST /api/insights/targetsuses 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 toautoComplete="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 explicitautoComplete(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#anchorURLs 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:
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.