Files
HealthLog/docs/migration/v1.3-to-v1.4.md
T
Marc-André BombeckandClaude Opus 4.7 a1cf9bcb37 docs: scrub PII + internal jargon + German leaks from user-facing artifacts
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>
2026-05-10 15:14:28 +02:00

10 KiB
Raw Blame History

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 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 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.

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:

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.