docs: sync README, AGENTS, CLAUDE, OpenAPI, migration guide for v1.4 (#147)

The v1.4 marathon and the v1.4.1 cycle that followed (per-section
admin extraction, Postgres testcontainers integration suite,
Playwright + axe-core E2E foundation) landed several user- and
operator-facing changes that hadn't been backfilled into the docs
shipped inside the repo. This commit closes that drift.

README.md
  - Adds Multi-tenant ready and Test connection buttons feature
    bullets.
  - Tech-stack model count 25 → 26 (RefreshToken added in v1.4.0).
  - API reference: new "Public + v1.4 additions" section listing
    /api/version, the five Test connection endpoints, the two refresh
    endpoints, and the two new admin endpoints (status-overview,
    backup/test).

AGENTS.md
  - Status block rewritten to reflect what shipped in 1.4.0 vs
    what's in flight for 1.4.1.
  - File layout: settings monolith replaced by /settings/[section]
    routing, admin/page.tsx now the 77-LOC shell, src/components/
    settings/ and src/components/admin/ added.
  - Removed the stale "Settings Page" gotcha (about non-blocking
    set-state-in-effect lint); the rule is strict now.
  - Database Models 25 → 26.

CLAUDE.md
  - Same model-count and component-folder updates as AGENTS.md.
  - Adds Multi-tenant prep and Native API client patterns under
    Important Patterns (HEALTHLOG_PROCESS_TYPE, ENCRYPTION_KEYS,
    BACKUP_S3_*, refresh-token rotation).
  - Lists tests/integration/ and e2e/ in File Layout.

docs/api/openapi.yaml
  - Adds 11 new endpoints: GET /api/version, the five test endpoints
    (Withings, moodLog, Web Push, Glitchtip, Umami), the two refresh
    endpoints, GET /api/admin/status-overview, POST /api/admin/backup/test.
  - The bulk of the diff is prettier reformatting that the existing
    file had drifted from (single → double quotes throughout YAML);
    no semantic change.

docs/migration/v1.3-to-v1.4.md
  - Corrects the now-wrong claim that v1.4 ships no migrations
    (it ships 0025_refresh_tokens + 0025_user_locale_drift_fix).
  - Adds full env-var sections for the Worker/Web split, Encryption
    key versioning, and Off-host backup target.
  - Documents URL changes (/settings#anchor → /settings/[section]).
  - Replaces the "deferred to a future release" list with a
    landed-in-v1.4 vs landed-in-v1.4.1 status table.
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-05-08 16:15:30 +02:00
1 parent fdf8dd6eb9
commit ea032c8857
5 files changed
+1719 -1105

No files matched your search

+25 -20
View File
@@ -6,16 +6,19 @@ Instructions for AI coding agents (OpenAI Codex, Claude Code, Cursor, etc.) work
**HealthLog** — a personal health-tracking web app (weight, blood pressure, pulse, mood, medication compliance) with Withings integration, moodLog.app sync, Dracula-themed UI, mobile-first PWA design.
**Status**: v1.4.0 — UI guidelines + reusable Skeleton/EmptyState
primitives, medical citations consolidated under
`src/lib/medical-citations.ts` (BP_DIA hypotension floor, ESH 2023
alignment, "WHO 8000 steps" hallucination removed, ACE body-fat bands
corrected), localised medication reminders + dashboard greeting +
Zod validations, two more N+1 queries closed, Berlin-TZ-aware weekly
buckets, single-row dashboard tile strip, public `/api/version`
endpoint, AI provider connection-test honours unsaved selection,
health-data inputs block password-manager autofill by default. See
GitHub Releases + CHANGELOG.md for the full feature timeline.
**Status**: v1.4.0 (released) → v1.4.1 in progress. v1.4.0 shipped
the UI guidelines + Skeleton/EmptyState primitives, medical-citations
consolidation, the per-route `/settings/[section]` split, the
status-first admin grid + aggregator endpoint, five new "Test
connection" buttons, AI insights with inline charts via allowlisted
metric tokens, off-host encrypted S3 backup, encryption-key
versioning + rotation CLI, optional `HEALTHLOG_PROCESS_TYPE` worker /
web split, and short-lived 24h access tokens with refresh-token
rotation for native API clients. v1.4.1 follows up with per-section
admin component extraction (the inner monolith into one file per
panel), a Postgres testcontainers integration test suite, and a
Playwright + axe-core E2E foundation. See GitHub Releases +
CHANGELOG.md for the full feature timeline.
## Tech Stack
@@ -75,7 +78,7 @@ src/
│ ├── layout.tsx # Root layout (viewport-fit: cover for PWA)
│ ├── page.tsx # Dashboard (/) with quick entry dropdown
│ ├── globals.css # Dracula theme CSS variables
│ ├── admin/page.tsx # Admin panel
│ ├── admin/page.tsx # Admin shell — 77 LOC, mounts the per-section components from src/components/admin/
│ ├── auth/login/page.tsx # Login
│ ├── auth/register/page.tsx # Registration
│ ├── achievements/page.tsx # Gamification achievements
@@ -85,7 +88,8 @@ src/
│ ├── medications/page.tsx # Medications management
│ ├── notifications/page.tsx # Notification preferences matrix
│ ├── onboarding/page.tsx # 4-step guided onboarding
│ ├── settings/page.tsx # All settings (8 top-level sections, ~3150 lines — split tracked for 1.4.0)
│ ├── settings/page.tsx # 308-redirects /settings → /settings/account
│ ├── settings/[section]/page.tsx # Per-section route shell (account, integrations, notifications, dashboard, ai, api, advanced, about)
│ ├── mood/page.tsx # Mood tracking
│ ├── targets/page.tsx # Target values dashboard
│ └── api/ # 100+ API route files (admin, auth, measurements, medications, mood, insights, integrations, ingest, dashboard, feedback, tokens, notifications, monitoring, …)
@@ -104,7 +108,8 @@ src/
│ ├── charts/ # Recharts wrappers, compliance charts
│ ├── insights/ # AI insights cards (status, advisor)
│ ├── gamification/ # Achievement cards, progress
│ ├── settings/ # Settings-page section components
│ ├── settings/ # Per-route Settings section components (one per /settings/[section])
│ ├── admin/ # Per-route Admin section components (system-status, integrations, monitoring, reminders, users, audit, danger-zone, feedback)
│ └── monitoring/ # Umami, GlitchTip bootstrap
├── lib/
│ ├── db.ts # Prisma client singleton
@@ -135,8 +140,8 @@ messages/
├── de.json # German translations (primary UI language)
└── en.json # English translations
prisma/
├── schema.prisma # Database schema (25 models)
└── migrations/ # Migration files (0001–0024; latest: oxygen_saturation)
├── schema.prisma # Database schema (26 models)
└── migrations/ # Migration files (0001–0025; latest: refresh_tokens + user_locale_drift_fix)
prisma.config.ts # Prisma config (DB URL lives here, NOT in schema)
public/
├── sw.js # Service worker (Web Push + offline caching)
@@ -183,11 +188,11 @@ These are hard-won lessons. Ignoring them will cause errors:
- **Zod v4**: Import from `"zod/v4"`, not `"zod"`.
- **jsPDF**: Client-side only. Import dynamically in browser context. Used with `jspdf-autotable` plugin.
### Settings Page
### Settings & Admin (per-route layout, v1.4)
- One large file (~3150 lines), 8 top-level sections. Sidebar switches to "settings mode" showing section shortcuts. Splitting into per-section files is tracked for 1.4.0 — until then, ESLint `react-hooks/set-state-in-effect` stays non-blocking because of the long-standing violations in this file.
- Sections scroll-to with highlight animation (`section-highlight` CSS class).
- Top-level section IDs: `section-allgemein`, `section-sicherheit`, `section-benachrichtigungen`, `section-personalization`, `section-integration`, `section-api`, `section-export`, `section-danger-zone`. Sub-anchors inside those sections include `profil`, `passwort`, `passkeys`, `telegram`, `ntfy`, `web-push`, `insights`, `dashboard-layout`, `thresholds`, `withings`, `moodlog`, `api-tokens`, `api-endpoints`.
- **Settings**: 8 routes under `/settings/[section]` — `account`, `integrations`, `notifications`, `dashboard`, `ai`, `api`, `advanced`, `about`. The legacy `/settings` 308-redirects to `/settings/account`. Sidebar deep-links and the `<a href="/settings#anchor">` patterns from 1.3 still resolve via the redirect.
- **Admin**: `src/app/admin/page.tsx` is now a 77-LOC shell that mounts the per-section components in `src/components/admin/`. Status-card grid lives in `status-card-grid.tsx`. The aggregator endpoint `/api/admin/status-overview` returns the six-card summaries in one batched query.
- ESLint `react-hooks/set-state-in-effect` is **strict** now (was non-blocking when the settings monolith carried inline-effect state-setters). Use lazy `useState(() => …)` for localStorage reads, TanStack Query for data fetches.
### Insights Page
@@ -225,7 +230,7 @@ These are hard-won lessons. Ignoring them will cause errors:
## Database Models (Prisma)
25 models: `User`, `Passkey`, `Session`, `AuthChallenge`, `Measurement`, `Medication`, `MedicationSchedule`, `MedicationIntakeEvent`, `ReminderPhaseConfig`, `TelegramReminderMessage`, `TelegramScheduledDeletion`, `ApiToken`, `WithingsConnection`, `MoodEntry`, `AppSettings`, `Feedback`, `AuditLog`, `NotificationChannel`, `NotificationPreference`, `PushSubscription`, `DataBackup`, `UserAchievement`, `RateLimit`, `Device`, `IdempotencyKey`.
26 models: `User`, `Passkey`, `Session`, `AuthChallenge`, `Measurement`, `Medication`, `MedicationSchedule`, `MedicationIntakeEvent`, `ReminderPhaseConfig`, `TelegramReminderMessage`, `TelegramScheduledDeletion`, `ApiToken`, `RefreshToken`, `WithingsConnection`, `MoodEntry`, `AppSettings`, `Feedback`, `AuditLog`, `NotificationChannel`, `NotificationPreference`, `PushSubscription`, `DataBackup`, `UserAchievement`, `RateLimit`, `Device`, `IdempotencyKey`. (`RefreshToken` added in v1.4.0 alongside the native-client 24h access-token / refresh-token rotation flow.)
## When Making Changes
+8 -2
View File
@@ -36,7 +36,7 @@ docker compose logs -f app # Tail app logs
## Architecture
- **Next.js 16** App Router with TypeScript strict. Pages are RSC by default; `"use client"` only for interactivity.
- **Prisma 7** ORM with PostgreSQL (25 models). Uses `PrismaPg` adapter from `@prisma/adapter-pg`. Client singleton at `src/lib/db.ts`. Generated client at `src/generated/prisma/client` (note the `/client` suffix). Prisma config in `prisma.config.ts` (not in schema.prisma).
- **Prisma 7** ORM with PostgreSQL (26 models). Uses `PrismaPg` adapter from `@prisma/adapter-pg`. Client singleton at `src/lib/db.ts`. Generated client at `src/generated/prisma/client` (note the `/client` suffix). Prisma config in `prisma.config.ts` (not in schema.prisma).
- **shadcn/ui** components (new-york style) in `src/components/ui/`. Add new ones via `pnpm dlx shadcn@latest add <component>`.
- **Dracula theme** via CSS variables in `globals.css`. Dark mode is default. Use `--dracula-*` tokens for chart colors.
- **TanStack Query** for client-side data fetching. Provider in `src/components/providers.tsx`.
@@ -68,6 +68,8 @@ docker compose logs -f app # Tail app logs
- `src/components/measurements/` — measurement form, list
- `src/components/mood/` — mood form, mood list
- `src/components/charts/` — Recharts wrappers
- `src/components/settings/` — per-route Settings section components (one per `/settings/[section]`)
- `src/components/admin/` — per-route Admin section components (system-status, integrations, monitoring, users, audit, danger-zone, feedback)
- `src/lib/logging/` — Wide Events structured logging (types, config, event-builder, context, sampler, transports, background)
- `src/lib/api-handler.ts` — apiHandler wrapper, requireAuth(), requireAdmin(), HttpError
- `src/lib/` — server utilities (db, crypto, auth, analytics, export, rate-limit, gravatar)
@@ -81,7 +83,9 @@ docker compose logs -f app # Tail app logs
- `src/lib/validations/` — Zod schemas shared between API + client
- `src/hooks/` — React hooks (`use-auth`)
- `messages/de.json` + `messages/en.json` — i18n translations
- `prisma/schema.prisma` — database schema (25 models)
- `prisma/schema.prisma` — database schema (26 models)
- `tests/integration/` — Postgres testcontainers integration suite (rate-limit race, idempotency replay, GDPR cascade delete, session lifecycle); `pnpm test:integration`
- `e2e/` — Playwright + axe-core suite for public smoke checks (version, auth-redirect, login, locale-switch, a11y); `pnpm e2e`
- `prisma.config.ts` — Prisma config (DB URL here, not in schema)
- `public/sw.js` — Service worker for Web Push notifications + offline caching
- `docs/` — long-form audit notes (`docs/audit/`); end-user docs live in the separate site at https://docs.healthlog.dev
@@ -107,6 +111,8 @@ docker compose logs -f app # Tail app logs
- **Achievements**: Persistent in `UserAchievement` table. Computed on API call, new unlocks written to DB with stable `unlockedAt` timestamps
- **Data backup**: pg-boss `data-backup` queue runs weekly (Sundays 03:00), stores JSON in `DataBackup` model
- **Wide Events / Structured Logging**: `apiHandler()` wraps all API routes. Use `annotate()` from `@/lib/logging/context` for business-action annotations. Use `requireAuth()` / `requireAdmin()` from `@/lib/api-handler` (auto-annotates auth). Background jobs use `withBackgroundEvent()`. External calls tracked via `getEvent()?.addExternalCall()`. No `console.log` in production code — use event annotations instead. Env vars: `LOG_LEVEL`, `LOG_SAMPLE_RATE`, `LOG_SLOW_THRESHOLD_MS`, `LOG_INCLUDE_STACK`, `LOKI_ENDPOINT`, `LOKI_USERNAME`, `LOKI_PASSWORD`
- **Multi-tenant prep (v1.4)**: `HEALTHLOG_PROCESS_TYPE=web|worker|all` (default `all`) splits HTTP and pg-boss workloads; the proxy refuses HTTP traffic with 503 + `X-HealthLog-Process-Type: worker` in worker mode. `ENCRYPTION_KEYS` is a JSON map of versioned keys (`{"v1": "...", "v2": "..."}`) plus `ENCRYPTION_ACTIVE_KEY_ID` for new writes; rotation via `pnpm tsx scripts/rotate-encryption-key.ts <id>`. `BACKUP_S3_*` env block configures off-host weekly encrypted backups (PutObject + GetObject only — retention is the bucket's lifecycle policy, never the worker).
- **Native API clients (v1.4)**: `POST /api/auth/login` and `/api/auth/passkey/login-verify` issue a 24h access token (`hlk_<64hex>`) AND a refresh token (`hlr_<64hex>`) when `X-Client-Type: native` or the User-Agent starts with `HealthLog-iOS/`. The browser flow is unchanged. Refresh-token reuse-detection revokes every refresh token for the user. The idempotency replay-cache rejects bodies containing `hlk_` OR `hlr_` so cached responses can never echo a token back.
## Headless-Client API Patterns
+76 -54
View File
@@ -63,6 +63,10 @@ Most health apps lock your data behind proprietary clouds, push subscriptions, a
**Internationalization** -- English (default) and German UI with 1500+ translation keys, guarded by a CI integrity test that fails the build on duplicate keys or drift between locales. Numbers, dates, units, and AI prompts all locale-aware via `useFormatters()`. Browser-based detection with per-user override.
**Multi-tenant ready** _(v1.4)_ — Off-host AES-GCM-encrypted weekly backups to any S3-compatible bucket (R2, B2, MinIO, AWS), encryption-key versioning + zero-downtime rotation CLI, optional `HEALTHLOG_PROCESS_TYPE=web|worker|all` so HTTP and pg-boss can scale independently, and short-lived 24h access tokens with refresh-token rotation for native API clients. The browser cookie session is unchanged.
**Test connection buttons** _(v1.4)_ — One-click probes for Withings, moodLog.app, Web Push, Glitchtip, and Umami in addition to the existing AI / Telegram / ntfy tests. Each one rate-limited, sanitised against SSRF redirects, and surfaces a localisable `errorCode` so the UI can render the failure in the user's language.
---
## Quick Start
@@ -102,7 +106,7 @@ Open **http://localhost:3000**. The first registered user becomes admin.
| ------------- | ------------------------------------------------- |
| Framework | Next.js 16 (App Router, React Server Components) |
| Language | TypeScript (strict mode) |
| Database | PostgreSQL 16 + Prisma 7 (25 models) |
| Database | PostgreSQL 16 + Prisma 7 (26 models) |
| Job Queue | pg-boss 12 (reminders, insights, backups) |
| UI | shadcn/ui, Tailwind CSS 4, Radix UI, Lucide Icons |
| Charts | Recharts 3 |
@@ -275,83 +279,101 @@ All mutations require authentication via session cookie. External ingest uses Be
<details>
<summary><strong>Auth and Integrations</strong></summary>
| Method | Endpoint | Description |
| ------ | -------------------------------- | ------------------------------------ |
| `POST` | `/api/auth/register` | Create account |
| `POST` | `/api/auth/login` | Password login |
| `POST` | `/api/auth/logout` | Destroy session |
| `GET` | `/api/auth/me` | Current user profile + Gravatar URL |
| `POST` | `/api/auth/password` | Change password |
| `PATCH`| `/api/auth/profile` | Update profile fields |
| `POST` | `/api/auth/passkey/*` | WebAuthn flows (4 sub-routes) |
| `GET` | `/api/auth/passkeys` | List enrolled passkeys |
| `GET` | `/api/auth/codex/authorize` | ChatGPT (codex) OAuth start |
| `GET` | `/api/withings/connect` | Initiate Withings OAuth |
| `POST` | `/api/withings/sync` | Trigger manual Withings sync |
| `POST` | `/api/withings/webhook` | Withings notification webhook |
| `POST` | `/api/insights/generate` | Regenerate AI insights |
| `GET` | `/api/insights/comprehensive` | Aggregated insight payload |
| `GET` | `/api/gamification/achievements` | Achievement progress |
| `GET` | `/api/health` | Docker health check |
| Method | Endpoint | Description |
| ------- | -------------------------------- | ----------------------------------- |
| `POST` | `/api/auth/register` | Create account |
| `POST` | `/api/auth/login` | Password login |
| `POST` | `/api/auth/logout` | Destroy session |
| `GET` | `/api/auth/me` | Current user profile + Gravatar URL |
| `POST` | `/api/auth/password` | Change password |
| `PATCH` | `/api/auth/profile` | Update profile fields |
| `POST` | `/api/auth/passkey/*` | WebAuthn flows (4 sub-routes) |
| `GET` | `/api/auth/passkeys` | List enrolled passkeys |
| `GET` | `/api/auth/codex/authorize` | ChatGPT (codex) OAuth start |
| `GET` | `/api/withings/connect` | Initiate Withings OAuth |
| `POST` | `/api/withings/sync` | Trigger manual Withings sync |
| `POST` | `/api/withings/webhook` | Withings notification webhook |
| `POST` | `/api/insights/generate` | Regenerate AI insights |
| `GET` | `/api/insights/comprehensive` | Aggregated insight payload |
| `GET` | `/api/gamification/achievements` | Achievement progress |
| `GET` | `/api/health` | Docker health check |
</details>
<details>
<summary><strong>Personalization (Thresholds + Dashboard)</strong></summary>
| Method | Endpoint | Description |
| -------- | ------------------------- | --------------------------------------------- |
| `GET` | `/api/user/thresholds` | Read per-user threshold overrides |
| `PUT` | `/api/user/thresholds` | Upsert thresholds (rate-limited, audit-logged)|
| `GET` | `/api/insights/targets` | Effective ranges (defaults + overrides merged)|
| `GET` | `/api/dashboard/widgets` | Read dashboard layout |
| `PUT` | `/api/dashboard/widgets` | Persist dashboard layout (show/hide/reorder) |
| `POST` | `/api/onboarding/complete`| Mark onboarding finished |
| Method | Endpoint | Description |
| ------ | -------------------------- | ---------------------------------------------- |
| `GET` | `/api/user/thresholds` | Read per-user threshold overrides |
| `PUT` | `/api/user/thresholds` | Upsert thresholds (rate-limited, audit-logged) |
| `GET` | `/api/insights/targets` | Effective ranges (defaults + overrides merged) |
| `GET` | `/api/dashboard/widgets` | Read dashboard layout |
| `PUT` | `/api/dashboard/widgets` | Persist dashboard layout (show/hide/reorder) |
| `POST` | `/api/onboarding/complete` | Mark onboarding finished |
</details>
<details>
<summary><strong>Feedback + API Tokens</strong></summary>
| Method | Endpoint | Description |
| -------- | --------------------------------------- | ------------------------------------ |
| `POST` | `/api/feedback` | Submit in-app feedback |
| `GET` | `/api/bugreport/status` | Check published GitHub issue state |
| `GET` | `/api/tokens` | List own API tokens |
| `POST` | `/api/tokens` | Mint new API token (Bearer, hashed) |
| `DELETE` | `/api/tokens/:id` | Revoke API token |
| Method | Endpoint | Description |
| -------- | ----------------------- | ----------------------------------- |
| `POST` | `/api/feedback` | Submit in-app feedback |
| `GET` | `/api/bugreport/status` | Check published GitHub issue state |
| `GET` | `/api/tokens` | List own API tokens |
| `POST` | `/api/tokens` | Mint new API token (Bearer, hashed) |
| `DELETE` | `/api/tokens/:id` | Revoke API token |
</details>
<details>
<summary><strong>Notifications</strong></summary>
| Method | Endpoint | Description |
| -------- | --------------------------------- | -------------------------------------- |
| `GET` | `/api/notifications/preferences` | Read per-channel × per-event matrix |
| `PUT` | `/api/notifications/preferences` | Update preferences |
| `GET` | `/api/notifications/vapid` | VAPID public key for Web Push |
| `POST` | `/api/notifications/web-push` | Register Web Push subscription |
| `POST` | `/api/telegram/webhook` | Telegram bot inline-button callback |
| Method | Endpoint | Description |
| ------ | -------------------------------- | ----------------------------------- |
| `GET` | `/api/notifications/preferences` | Read per-channel × per-event matrix |
| `PUT` | `/api/notifications/preferences` | Update preferences |
| `GET` | `/api/notifications/vapid` | VAPID public key for Web Push |
| `POST` | `/api/notifications/web-push` | Register Web Push subscription |
| `POST` | `/api/telegram/webhook` | Telegram bot inline-button callback |
</details>
<details>
<summary><strong>Admin (admin role required)</strong></summary>
| Method | Endpoint | Description |
| -------- | --------------------------------- | -------------------------------------- |
| `GET` | `/api/admin/status` | System + integration status |
| `GET` | `/api/admin/users` | List users |
| `POST` | `/api/admin/users/:id/reset-password` | Force password reset |
| `GET` | `/api/admin/feedback` | All feedback / bug reports |
| `POST` | `/api/admin/feedback/:id/github` | Escalate feedback to GitHub issue |
| `GET` | `/api/admin/audit-log` | Audit-log viewer |
| `GET` | `/api/admin/ai-settings` | Read shared AI provider config |
| `PUT` | `/api/admin/ai-settings` | Update shared AI provider config |
| `GET` | `/api/admin/tokens` | All issued API tokens |
| `POST` | `/api/admin/notifications/test` | Send test notification |
| `GET` | `/api/admin/data` | Data backups + counts |
| Method | Endpoint | Description |
| ------ | ------------------------------------- | ------------------------------------- |
| `GET` | `/api/admin/status` | System + integration status |
| `GET` | `/api/admin/users` | List users |
| `POST` | `/api/admin/users/:id/reset-password` | Force password reset |
| `GET` | `/api/admin/feedback` | All feedback / bug reports |
| `POST` | `/api/admin/feedback/:id/github` | Escalate feedback to GitHub issue |
| `GET` | `/api/admin/audit-log` | Audit-log viewer |
| `GET` | `/api/admin/ai-settings` | Read shared AI provider config |
| `PUT` | `/api/admin/ai-settings` | Update shared AI provider config |
| `GET` | `/api/admin/tokens` | All issued API tokens |
| `POST` | `/api/admin/notifications/test` | Send test notification |
| `GET` | `/api/admin/data` | Data backups + counts |
| `GET` | `/api/admin/status-overview` | Aggregated status for the 6-card grid |
| `POST` | `/api/admin/backup/test` | Probe S3-compatible backup target |
</details>
<details>
<summary><strong>Public + v1.4 additions</strong></summary>
| Method | Endpoint | Description |
| ------ | ---------------------------------- | ----------------------------------------------- |
| `GET` | `/api/version` | Public — version + build SHA + license, no auth |
| `POST` | `/api/integrations/withings/test` | Probe a saved Withings connection |
| `POST` | `/api/integrations/moodlog/test` | Probe moodLog.app webhook reachability |
| `POST` | `/api/notifications/web-push/test` | Send a test Web Push to the current user |
| `POST` | `/api/monitoring/glitchtip/test` | Trigger a Glitchtip ingest probe |
| `POST` | `/api/monitoring/umami/test` | Verify Umami script + website ID resolve |
| `POST` | `/api/auth/refresh` | Native client refresh-token rotation |
| `POST` | `/api/auth/refresh/revoke` | Revoke an issued refresh token |
</details>
+1492 -999
View File
File diff suppressed because it is too large. Load diff
+118 -30
View File
@@ -1,11 +1,11 @@
# Migrating from HealthLog 1.3 to 1.4
Both 1.3.x and 1.4.0 use Prisma migrations 0001–0024 and the same
PostgreSQL schema. The upgrade is **drop-in for self-hosters running
the GHCR image** — no manual migration step beyond the standard
`docker compose pull && docker compose up -d`. This guide documents
what changes for operators and integrators, in case you're extending
the API.
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
@@ -14,25 +14,83 @@ docker compose pull
docker compose up -d
```
That's it. No DB migration to apply, no env vars to add, no breaking
API change.
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
- Prisma schema unchanged from 1.3.3. Migrations 0001–0024 remain
authoritative. v1.4.0 brings no new migration files.
- **`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
- **`NEXT_PUBLIC_APP_BUILD_SHA`** *(optional, new)* — the short Git
SHA of the build, surfaced through `/api/version` so the future
About surface can render it. The `docker-publish` workflow can wire
this once the About UI lands.
- **`NEXT_PUBLIC_APP_BUILT_AT`** *(optional, new)* — ISO-8601 build
timestamp, same plumbing.
### About surface
Neither is required. Local `pnpm dev` returns `null` for both fields
and the UI falls back to "development" wording.
- **`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
@@ -41,6 +99,30 @@ and the UI falls back to "development" wording.
- **`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
@@ -111,21 +193,27 @@ n8n, and Health Connect integrations need no change.
8 000–15 000 in the effective-range resolver) now use the same
band, anchored on Saint-Maurice et al., JAMA 2020.
## What's deferred to a future release
## URL changes
The v1.3.3 ecosystem audit identified four architectural rewrites that
are intentionally outside v1.4.0:
- **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.
- Settings page split into per-section routes (`/settings/[section]`).
- Admin page redesign with status-first card grid.
- AI insights rework with severity-coloured key-findings hero and
dynamic chart inclusion via an allowlist token map.
- Multi-tenant prep (off-host backups, encryption-key versioning,
worker/web container split, native-Bearer User-Agent gating).
- Postgres-backed integration test suite (Testcontainers).
## What landed in v1.4 vs v1.4.1
These ride a future release; the v1.4 marathon closed every CRIT and
HIGH the audit identified except those four.
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