* feat(crypto): G2 — encryption-key versioning + rotation CLI
Adds backwards-compatible support for ENCRYPTION_KEYS (JSON map of
{ "id": "<key>" }) plus ENCRYPTION_ACTIVE_KEY_ID. Ciphertext format is now
"<keyId>.<base64payload>"; legacy bare-base64 ciphertext still decrypts
under the synthetic "v1" id derived from the existing ENCRYPTION_KEY.
scripts/rotate-encryption-key.ts re-encrypts every encrypted column on
User / WithingsConnection / AppSettings with the active key. Idempotent —
rows already on the active id are skipped.
Why: enables zero-downtime rotation when a key is suspected leaked,
without forcing every encrypted row through a single big-bang rewrite.
Co-Authored-By: Marc-André Bombeck <noreply@anthropic.com>
* feat(auth): G4 — native-Bearer UA gating + rotating refresh tokens
Web callers (UA contains 'Mozilla/') keep the legacy 90d Bearer when they
explicitly opt in via X-Client-Type: native. Native callers (HealthLog-iOS,
HealthLog-iPad, n8n, Health-Connect, unrecognised UAs) now receive a 24h
access token plus a 60d rotating refresh token in the login response.
New /api/auth/refresh endpoint atomically validates + rotates the refresh
token (one-time-use), revokes the previously paired access token, and
audit-logs every refresh + revoke. Reuse of an already-consumed refresh
token revokes the entire family for the user/device — defence against a
stolen refresh token.
Migration 0025_refresh_tokens adds the table; the only DDL is CREATE
TABLE so it is forward-compatible. Down migration documented at
docs/ops/migrations/0025-down.sql.
Why: the V3 audit (still-V2-A) flagged that 90d tokens are too long for
native callers when refresh-token rotation is now table-stakes for mobile
apps. Web flows are unchanged so existing browser sessions keep working.
Co-Authored-By: Marc-André Bombeck <noreply@anthropic.com>
* feat(infra): G3 — web/worker container split via HEALTHLOG_PROCESS_TYPE
Adds a tiny startup gate (src/lib/process-type.ts) so the same Docker image
can run as web-only, worker-only, or 'all' (default — single-container,
backwards compatible). assertSubsystemEnabled() refuses to boot the
reminder worker when HEALTHLOG_PROCESS_TYPE=web, preventing accidental
double-cron when an operator splits the deployment.
docker-compose.yml ships the optional app-worker service block commented
out so 'docker compose up' keeps working out of the box; uncommenting it
+ flipping app to HEALTHLOG_PROCESS_TYPE=web is the only change to scale
HTTP horizontally. Healthchecks are isolated per service so neither
startup deadlocks the other; both wait on db: service_healthy.
docs/self-hosting/scaling.md walks through when + how to split.
Co-Authored-By: Marc-André Bombeck <noreply@anthropic.com>
* feat(jobs): G1 — daily off-host encrypted backup to S3-compatible storage
New pg-boss queue 'data-backup-offhost' runs at 02:30 Europe/Berlin, dumps
each user's measurements + medications + intake events + mood entries,
encrypts the JSON under a SEPARATE key (BACKUP_ENCRYPTION_KEY, isolated
from the application ENCRYPTION_KEY), and uploads to any S3-compatible
target via @aws-sdk/client-s3.
Wire format documented in docs/ops/backup-restore.md:
magic "HLBK" || version 0x01 || iv(12) || authTag(16) || ciphertext
The worker prunes objects older than BACKUP_RETENTION_DAYS (default 30);
operators are encouraged to also configure a bucket-level lifecycle rule.
Admin endpoint POST /api/admin/backup/test issues a 1-byte PUT+GET
roundtrip and returns endpoint + bucket + per-call latency. Credentials
are never returned.
scripts/restore-backup.ts downloads + decrypts a single object so
operators can verify the chain end-to-end without standing up a separate
restore harness.
Why: GDPR Art. 32 + V3 audit "data durability" — without an off-host copy
the only line of defence against a Postgres volume loss is the in-DB
DataBackup model on the same host. Separate-key isolation means a leak of
the application encryption key does not also expose every user's archive.
Co-Authored-By: Marc-André Bombeck <noreply@anthropic.com>
* fix(multi-tenant): address review findings — rotation coverage, hard decrypt errors, web/worker gates
Multi-agent review on PR #135 caught several CRITICAL + HIGH issues that
would have shipped silent data corruption or weakened the threat model:
Encryption + rotation
- scripts/rotate-encryption-key.ts: stop selecting `bugReportTokenEncrypted`
(column doesn't exist in the schema; the script crashed on the AppSettings
step). Add the three encrypted columns it was missing —
NotificationChannel.config, PushSubscription.p256dh, PushSubscription.auth
— so rotating to a new key actually re-encrypts every encrypted row.
Without this, dropping the old key id from ENCRYPTION_KEYS would leave
notification channels and web-push subscriptions permanently
undecryptable.
- src/lib/crypto.ts: refuse to silently fall back to the active key when a
legacy bare-base64 row is decrypted but no v1 entry is configured. Throws
a clear "Restore ENCRYPTION_KEY (or add a v1 entry to ENCRYPTION_KEYS)
and run scripts/rotate-encryption-key.ts before removing it" so an
operator who pulled the v1 too early notices immediately instead of
hitting opaque GCM tag errors.
- docs/ops/encryption-key-rotation.md: full step-by-step + rollback note +
troubleshooting section. Spells out that rotation is a forward-only
operation and what to do if the rollback path is needed.
Web / worker split (G3)
- src/proxy.ts: refuse HTTP traffic with a 503 +
X-HealthLog-Process-Type: worker header when HEALTHLOG_PROCESS_TYPE=worker.
The worker container (which still runs `node server.js` for now) no
longer accepts requests, so a misrouted client gets a clear hint instead
of a partially-served response.
- src/instrumentation.ts: short-circuit before importing the reminder
worker when HEALTHLOG_PROCESS_TYPE=web — avoids the noisy
"[instrumentation] CRITICAL" stderr line we emitted on every web boot.
- /api/version added to PUBLIC_PATHS so the compose healthcheck doesn't
silently 302-redirect through the auth gate (the prior wget treated the
login page as a 200 success and would never notice an unhealthy backend).
Refresh-token rotation (G4)
- src/lib/auth/refresh-token.ts: reuse-detection now revokes ALL active
refresh tokens for the user, not only those scoped to the same deviceId.
An attacker who rotated without an X-Device-Id header would otherwise
keep their family alive on the no-device branch. Defence-in-depth.
- revokeRefreshToken() also revokes the paired access token so a leaked
access token cannot outlive the refresh-token sibling that the caller
just killed (logout-on-device).
Off-host backup (G1)
- src/lib/jobs/offhost-backup.ts: drop the worker-side DeleteObject path.
Retention is the bucket's lifecycle-policy job; the worker keeps a
PutObject + GetObject grant only. A compromised worker can no longer
wipe the backup history. Per-user errors are now surfaced as Wide
Event warnings + a structured failures[] array on the report so an
operator can tell which user failed and why.
- scripts/restore-backup.ts: schema-validate the decrypted payload before
writing to disk, plus an optional --user-id=<id> flag that refuses to
proceed unless the dump's userId matches. Prevents silent
cross-tenant restores when an operator points the script at the wrong
S3 key.
Idempotency
- src/lib/idempotency.ts: the leak-guard already blocked `hlk_`
(access-token prefix); also block `hlr_` so a future caller wrapping
the new /api/auth/refresh endpoint with withIdempotency() can't
accidentally cache a freshly-issued refresh token.
Migration / schema
- prisma/migrations/0025_refresh_tokens/migration.sql: wrap in a single
transaction; switch to CREATE { TABLE | INDEX } IF NOT EXISTS so a
partial-deploy retry is a no-op instead of "relation already exists";
guard the FK ADD with information_schema lookup for the same reason.
- Schema replaces the single-column `@@index([deviceId])` with a
composite `@@index([userId, deviceId])` (every lookup also filters on
userId; the old index would never have been used). Comment on
accessTokenHash corrected to HMAC-SHA-256.
Container / ops
- Dockerfile: install tzdata + ENV TZ=Europe/Berlin so the daily
pg-boss schedule (`30 2 * * *` Europe/Berlin) actually fires at 02:30
Berlin instead of silently falling back to UTC on the alpine base.
- docker-compose.yml: tighten the healthcheck to `wget --spider` (matches
the Dockerfile HEALTHCHECK style, doesn't pipe the body), pass through
BACKUP_RETENTION_DAYS so a custom retention env reaches the container.
- .env.example: document every new env var added in this PR
(HEALTHLOG_PROCESS_TYPE, ENCRYPTION_KEYS, ENCRYPTION_ACTIVE_KEY_ID,
BACKUP_S3_*, BACKUP_ENCRYPTION_KEY, BACKUP_RETENTION_DAYS) with
comments and the all-or-none semantics for the backup block.
491 tests still green, typecheck clean.
* chore: retrigger CI
---------
Co-authored-by: Marc-André Bombeck <noreply@anthropic.com>
HealthLog
Your health data belongs to you. Track it on your own terms.
A self-hosted, privacy-first health tracking PWA.
Weight, blood pressure, mood, medications, AI insights -- all under your control.
Website · Live Demo · Documentation
Why HealthLog?
Most health apps lock your data behind proprietary clouds, push subscriptions, and sell your metrics to advertisers. HealthLog takes a different approach: your data stays on your server, encrypted at rest, accessible only to you.
Key Features
Health Metrics -- Track weight, blood pressure, pulse, body fat, sleep, steps, blood glucose (fasting/postprandial/random/bedtime, mg/dL ↔ mmol/L), total body water, bone mass, and pulse oximetry (SpO₂) with interactive trend charts, moving averages, and traffic-light ranges based on ESC/ESH 2018, ADA 2024, and consensus pulse-oximeter guidance (NICE NG115). Body-composition + SpO₂ metrics sync automatically from Withings Body+ scales and ScanWatch devices.
Custom Thresholds -- Override the computed default ranges per metric with the targets your clinician set. Audit-logged. Doctor Report PDF prints both your target and the standard reference.
Customizable Dashboard -- Show, hide, and drag-to-reorder every widget. Per-user layout with reset-to-defaults.
Mood Logging -- 5-point scale with tags, notes, and trend analytics. Syncs automatically from moodLog.app via webhook.
Medication Compliance -- Flexible scheduling with time windows, recurrence patterns, intake logging (take/skip/snooze), and compliance heatmaps. External API for iOS Shortcuts integration.
Withings Integration -- OAuth2 device sync for scales, blood pressure monitors, and activity trackers with automatic deduplication.
Multi-Provider AI Insights -- Pick OpenAI, Anthropic Claude, or any local OpenAI-compatible endpoint (Ollama, LM Studio, vLLM). BYOK or admin-shared key. Cached daily. Local endpoints keep all data on your network.
Doctor Report PDF Export -- Generate professional medical reports client-side. Locale-aware (English/German), with vital sign summaries, BP/BMI/glucose classification, compliance rates, custom-threshold badges, and optional AI analysis.
Built-in Feedback -- Send bug reports and feature requests from inside the app. Stored in your HealthLog database — no GitHub config required. Optional GitHub escalation for admins.
PWA with Offline Support -- Installable on iOS and Android. Service worker with intelligent caching strategies for reliable offline access.
Multi-Channel Notifications -- Telegram (with inline action buttons), ntfy (self-hostable), and Web Push. Medication reminders with late/missed escalation.
Gamification -- 38+ persistent achievements across intake streaks, compliance milestones, and healthy metric streaks.
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.
Quick Start
Plan ~5 minutes for a working install. The bundled docker-compose.yml pulls a pre-built multi-arch image (linux/amd64 + linux/arm64) from GitHub Container Registry; no build step required for self-hosters. Contributors who want to test local changes can docker compose up --build.
git clone https://github.com/MBombeck/HealthLog.git
cd HealthLog
cp .env.example .env
Generate the four required secrets and paste them into .env:
echo "POSTGRES_PASSWORD=$(openssl rand -base64 24)" >> .env
echo "SESSION_SECRET=$(openssl rand -hex 32)" >> .env
echo "ENCRYPTION_KEY=$(openssl rand -hex 32)" >> .env
echo "API_TOKEN_HMAC_KEY=$(openssl rand -hex 32)" >> .env
Then bring the stack up:
docker compose up -d
Open http://localhost:3000. The first registered user becomes admin.
Behind a reverse proxy (Caddy / Traefik / Nginx) for TLS, set
NEXT_PUBLIC_APP_URLandAPP_URLto your public URL in.envbefore starting. See Self-Hosting → Reverse Proxy for examples.
Tech Stack
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router, React Server Components) |
| Language | TypeScript (strict mode) |
| Database | PostgreSQL 16 + Prisma 7 (25 models) |
| Job Queue | pg-boss 12 (reminders, insights, backups) |
| UI | shadcn/ui, Tailwind CSS 4, Radix UI, Lucide Icons |
| Charts | Recharts 3 |
| Data Fetching | TanStack Query 5 |
| Forms | React Hook Form 7 + Zod 4 |
| Auth | SimpleWebAuthn 13, Argon2id |
| Notifications | Telegram Bot API, ntfy, Web Push (VAPID) |
| jsPDF (client-side generation) | |
| Testing | Vitest 4 |
| Deployment | Docker (multi-stage Alpine) |
Security and Privacy
HealthLog is designed for people who take data ownership seriously.
- Self-hosted -- Your data never leaves your server. No telemetry, no third-party tracking.
- AES-256-GCM encryption -- All stored secrets (OAuth tokens, API keys, VAPID keys) are encrypted at rest.
- Passkey authentication -- WebAuthn as primary auth with password fallback (Argon2id + zxcvbn strength validation).
- Server-side sessions -- PostgreSQL-backed with 30-day sliding expiry, HttpOnly/SameSite=Strict cookies.
- Security headers -- CSP with nonces, HSTS, X-Frame-Options DENY, Permissions-Policy, Referrer-Policy.
- Rate limiting -- Sliding window on auth and API endpoints.
- HMAC-SHA256 API tokens -- Bearer tokens are hashed before storage.
- Audit logging -- All sensitive operations tracked with IP addresses.
Environment Variables
Required
| Variable | Description |
|---|---|
POSTGRES_PASSWORD |
Password for the bundled Postgres service (Docker Compose) |
DATABASE_URL |
PostgreSQL connection string (uses POSTGRES_PASSWORD above) |
SESSION_SECRET |
64-char hex string for session signing |
ENCRYPTION_KEY |
64-char hex string for AES-256-GCM |
API_TOKEN_HMAC_KEY |
64-char hex string for API token hashing |
Optional
| Variable | Description |
|---|---|
NEXT_PUBLIC_APP_URL |
Public-facing URL (default: http://localhost:3000) |
WITHINGS_CLIENT_ID |
Withings OAuth2 client ID |
WITHINGS_CLIENT_SECRET |
Withings OAuth2 client secret |
WITHINGS_REDIRECT_URI |
OAuth callback URL |
WITHINGS_WEBHOOK_SECRET |
Webhook URL hardening secret |
TELEGRAM_WEBHOOK_SECRET |
Telegram bot webhook secret |
Telegram bot token, ntfy settings, Web Push VAPID keys, Umami, and GlitchTip URLs are configured in the Admin Panel and stored encrypted in the database.
Architecture
src/
├── app/ # Next.js App Router pages & API routes
│ ├── api/ # REST API endpoints (100+ route files)
│ ├── admin/ # Admin panel
│ ├── auth/ # Login, register, passkey enrolment
│ ├── medications/ # Medication management
│ ├── measurements/ # Health metric entry
│ ├── mood/ # Mood log
│ ├── insights/ # AI-powered analytics
│ ├── charts/ # Long-form charts
│ ├── achievements/ # Gamification page
│ ├── targets/ # Custom thresholds dashboard
│ ├── notifications/ # Notification preferences matrix
│ ├── bugreport/ # Built-in feedback / bug report
│ ├── onboarding/ # 4-step guided setup
│ └── settings/ # User preferences (8 top-level sections)
├── components/
│ ├── ui/ # shadcn/ui primitives
│ ├── layout/ # Shell (sidebar, topbar, bottom nav)
│ ├── medications/ # Medication cards, forms, timeline
│ ├── measurements/ # Measurement form, list
│ ├── mood/ # Mood form, mood list
│ ├── charts/ # Recharts wrappers
│ ├── insights/ # AI insight status / advisor cards
│ ├── gamification/ # Achievement cards, progress
│ ├── monitoring/ # Umami / GlitchTip bootstrap
│ └── settings/ # Settings-page section components
├── lib/
│ ├── auth/ # Session, audit, passkey logic
│ ├── notifications/ # Dispatcher + channel senders
│ ├── jobs/ # pg-boss worker (reminders, insights, backups)
│ ├── analytics/ # Trend calculations, compliance, correlations
│ ├── ai/ # Multi-provider client (OpenAI, Anthropic, local)
│ ├── insights/ # Insight pipeline + medical prompts
│ ├── gamification/ # Achievement definitions
│ ├── feedback/ # Built-in feedback + GitHub escalation
│ ├── moodlog/ # moodLog.app webhook + sync
│ ├── monitoring/ # Umami / GlitchTip server-side hooks
│ ├── withings/ # OAuth client, sync service
│ ├── logging/ # Wide Events: builder, context, transports
│ ├── i18n/ # Translations context & config
│ ├── validations/ # Shared Zod schemas
│ ├── api-handler.ts # apiHandler wrapper, requireAuth/requireAdmin
│ ├── api-response.ts # { data, error } envelope helpers
│ ├── crypto.ts # AES-256-GCM encrypt/decrypt
│ ├── rate-limit.ts # Sliding-window rate limiter
│ ├── db.ts # Prisma singleton
│ └── doctor-report-pdf.ts # Client-side PDF generation
├── hooks/ # React hooks
└── generated/prisma/ # Generated Prisma client
Key Patterns
- RSC by default --
"use client"only for interactive components - API envelope -- All responses follow
{ data, error }shape viaapiSuccess()/apiError()insrc/lib/api-response.ts - apiHandler wrapper -- Every API route wraps its handler in
apiHandler()(src/lib/api-handler.ts) for consistent error handling, Wide-Event structured logging, andx-request-idpropagation - Encrypted secrets -- Withings tokens, API keys, VAPID keys, notification credentials
- Timezone-aware --
Europe/Berlinfor display, UTC in database - Route protection --
proxy.ts(Next.js 16's renamed middleware) checks session cookie, redirects unauthenticated requests - Client-side PDF -- Doctor reports generated in browser via jsPDF
API Reference
All mutations require authentication via session cookie. External ingest uses Bearer tokens.
Health Data
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/measurements |
List measurements (paginated, filterable) |
POST |
/api/measurements |
Create measurement |
DELETE |
/api/measurements/:id |
Delete measurement |
GET |
/api/analytics |
Trend summaries (7d/30d) |
GET |
/api/export |
Export as CSV or JSON |
POST |
/api/import |
Import from JSON |
POST |
/api/doctor-report |
Aggregated data for PDF |
Mood
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/mood-entries |
List mood entries |
POST |
/api/mood-entries |
Create mood entry |
DELETE |
/api/mood-entries/:id |
Delete mood entry |
GET |
/api/mood/analytics |
Mood trend analytics |
POST |
/api/integrations/moodlog/webhook |
moodLog.app webhook |
Medications
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/medications |
List all medications |
POST |
/api/medications |
Create medication |
PUT |
/api/medications/:id |
Update medication |
DELETE |
/api/medications/:id |
Delete medication |
POST |
/api/medications/:id/intake |
Log intake event |
GET |
/api/medications/:id/compliance |
Compliance stats |
POST |
/api/ingest/medication |
External intake (Bearer) |
Auth and Integrations
| 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 |
Personalization (Thresholds + Dashboard)
| 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 |
Feedback + API Tokens
| 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 |
Notifications
| 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 |
Admin (admin role required)
| 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 |
Integrations
| Integration | Setup | Purpose |
|---|---|---|
| Withings | Env vars | Auto-sync weight, BP, and activity |
| Telegram | Admin Panel | Medication reminders with inline buttons |
| ntfy | User Settings | Self-hosted push notifications |
| Web Push | Admin Panel | Browser-native VAPID notifications |
| OpenAI | User Settings | AI health insights (BYOK) |
| moodLog.app | User Settings | Mood tracking sync |
| Umami | Admin Panel | Privacy-friendly analytics |
| GlitchTip | Admin Panel | Sentry-compatible error tracking |
Local Development
# Prerequisites: Node.js 20+, pnpm, PostgreSQL
cp .env.example .env
pnpm install
pnpm db:generate
pnpm db:migrate
pnpm dev
Scripts
pnpm dev # Development server
pnpm build # Production build
pnpm lint # ESLint
pnpm typecheck # TypeScript strict check
pnpm test # Vitest
pnpm format # Prettier
pnpm db:generate # Generate Prisma client
pnpm db:migrate # Create & apply migration
pnpm db:migrate:deploy # Apply migrations (production)
pnpm db:studio # Prisma Studio GUI
Deployment
The included docker-compose.yml runs the app and PostgreSQL. The entrypoint automatically waits for the database, runs pending migrations, and starts the server.
The app listens on port 3000. Place it behind Nginx, Caddy, or Traefik for TLS termination. Works out of the box with Coolify.
Contributing
Contributions are welcome. See CONTRIBUTING.md for guidelines.
- Code style:
pnpm format && pnpm lint - Type safety:
pnpm typecheckmust pass - Tests:
pnpm test - UI language: English by default, German selectable per user. Code, comments, and commits: English.
License
HealthLog is licensed under the GNU Affero General Public License v3.0.