9b59390224 fix: i18n completeness + UX polish (closes #73, #74) (#75)
* fix(i18n,ux): package A — missing keys, chart labels, query invalidation, admin error UX

Addresses bugs surfaced in issues #73 and #74 plus drift found during audit.

- i18n: add 21 missing translation keys (targets.* namespace was only
  partially populated after the English-first migration; common.reset and
  settings.configured were referenced but absent), plus common helpers
  (copy, copyDetails, retry, reportIssue) and admin.settingsSaveError.
- charts: rename points7Label/30Label/90Label from cryptic "7P/30P/90P"
  to "7 pts / 30 pts / 90 pts" in EN and "7 Pkt / 30 Pkt / 90 Pkt" in DE.
- queries: add src/lib/query-keys.ts as a single source of truth with
  measurement/mood/medication-dependent key sets. Wire them into
  measurement-form/list, mood-form/list, medication-form/card,
  intake-history-list, intake-timeline, medications page, and Withings
  sync in settings. Fixes dashboard not auto-refreshing after adding a
  measurement (old code only invalidated ["measurements"], not
  ["analytics"] / ["insights"] / ["insights","targets"]).
- admin: useUpdateSettings + updateUser mutations now surface real error
  messages via sonner toasts and invoke getApiErrorMessage() so saves
  fail loudly instead of silently. Success also gets a toast.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(i18n): package B — locale-aware formatters throughout

Removes the hard-coded de-DE locale that lingered from the German-first era
and makes every number, date, and time locale-aware. English stays the
default per user preference; German remains selectable.

New:
- src/lib/format-locale.ts exposes makeFormatters(locale) + a cached
  Europe/Berlin timezone. Covers number, integer, percent, date, dateShort,
  dateTime, time, dateWithWeekday, monthShort. Also exports
  parseLocaleFromAcceptLanguage() for server code.
- useFormatters() hook in @/lib/i18n/context resolves from the active
  locale, memoized per render.

Migrated (all Intl.NumberFormat("de-DE") / toLocaleString("de-DE") /
Intl.DateTimeFormat("de-DE") call sites):
- src/app/page.tsx (dashboard)
- src/app/insights/page.tsx
- src/app/admin/page.tsx
- src/app/settings/page.tsx (Withings last-sync, doctor-report caller)
- src/components/charts/trend-card.tsx (+ i18n "7d"/"30d" short labels)
- src/components/charts/health-chart.tsx
- src/components/medications/medication-card.tsx
- src/components/medications/medication-form.tsx
- src/components/measurements/measurement-list.tsx
- src/components/mood/mood-list.tsx
- src/components/insights/insight-status-card.tsx (also removed 5 German
  hard-coded strings with TODO: i18n markers — now translated)
- src/components/insights/insight-advisor-card.tsx (same — 10+ strings)

Server: reminder-check / reminder-worker / umami-test switched from
"de-DE" to neutral en-GB/en-US where the string is machine-compared rather
than user-facing. Bug-report GitHub issue titles use ISO-8601 UTC now —
the repo is English-speaking.

Next.js metadata:
- OpenGraph locale from "de_DE" → "en_US" with "de_DE" as
  alternateLocale.
- Root layout now reads healthlog-locale cookie (falls back to
  Accept-Language) and sets <html lang> server-side, avoiding an
  EN-flashes-then-DE hydration gap and fixing SEO/a11y.
- I18nProvider mirrors locale to a 1-year SameSite=Lax cookie so the next
  SSR pass is correct.

Doctor-report PDF:
- generateDoctorReportPDF(data, { t, locale }) now accepts injected
  translator + locale. All section titles, column headers, BMI / BP
  classifications, mood labels, and the footer disclaimer are
  i18n-driven. File name slug switches between gesundheitsbericht / health-report.
- 40+ new doctorReport.* keys in EN + DE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ux): package C — typed passkey errors, bug-report gating, error boundary

Addresses issue #73 points 1, 5, 6 and lays groundwork for copy-paste
error reporting requested during review.

Passkey registration / login (#73.1):
- src/lib/passkey-errors.ts maps DOMException.name + SimpleWebAuthn code
  to specific i18n keys instead of swallowing every failure as
  "cancelled". Distinguishes NotAllowed/InvalidState/NotSupported/
  Security/Abort errors and falls back to a diagnostic message.
- settings passkey + login passkey flows both use describePasskeyError
  now, so origin mismatches and previous-registration collisions show
  actionable copy.
- 5 new passkey.* i18n keys in EN + DE.

Bug report (#73.5):
- New GET /api/bugreport/status reports {configured, hasToken, hasRepo,
  isAdmin}.
- /bugreport page now queries status first. When not configured it shows
  a notice (different copy for admins vs. regular users) with a direct
  link to the admin panel. Form is hidden, not silently broken.
- When POST /api/bugreport can't create issues it returns 503 instead
  of 500 with the same user-facing message.
- 4 new bugreport.* i18n keys.

Global error UX:
- src/components/error-details.tsx — reusable panel with retry + copy
  diagnostics (message, digest, stack (trimmed), URL, user-agent,
  request-id, locale, timestamp) + report-issue link. Clipboard fallback
  prompt covers insecure-context cases.
- src/app/error.tsx uses it.
- src/app/global-error.tsx is provider-free (no i18n dependency) since
  it replaces the root layout on catastrophic failures — hardcoded
  English + Dracula palette so it still looks like HealthLog.
- 2 new errorBoundary.* keys and 4 common.* keys (copy, copyDetails,
  retry, reportIssue).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(query-keys): collapse repeated invalidation blocks into invalidateKeys helper

Eleven mutation handlers repeated `Promise.all(keys.map(k => invalidate({queryKey: k})))`
verbatim. Adds `invalidateKeys(qc, keys)` to the existing query-keys factory and
adopts it everywhere, including replacing the local `invalidateMedicationRelated`
helper that medication-form.tsx had grown. Also fixes the import-sandwich that
helper introduced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: small simplifications in i18n/UX changes

- passkey-errors: drop the typeof-name dance now that error.name is already string after the instanceof Error narrow
- doctor-report-pdf: replace nested gender ternary with a small lookup map; clarify unitFor's null-as-translation-marker convention
- dashboard page: route the analytics queries through the new queryKeys factory so future invalidation stays in one place
- health-chart: add missing fmt dependency to trendInfo useMemo (introduced when formatSigned was switched to the locale-aware formatter)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ai): capture upstream response body in OpenAI errors

A production insights burst at 12:19 UTC failed with "OpenAI request
failed (500)". The 500 was the upstream OpenAI HTTP status (confirmed
via logs), but we had no way to tell from logs whether it was an
OpenAI incident, a model-not-found, or a response_format rejection
because the response body was thrown away.

Now the error message includes the first 500 chars of the response body
and structured fields (httpStatus, upstream, model) that Wide Event
logging can attach. No behavior change for the happy path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(review): address code-review + security findings

Review round produced five real issues — fix them all, plus three nits
that were cheap enough to bundle.

Must-fix:
- security: error-details.tsx + global-error.tsx were copying
  window.location.href into the clipboard, which on OAuth callback
  routes (/api/withings/*, password reset) would leak code/state
  tokens into GitHub issues. Strip to pathname + searchParamCount.
- fe: query-keys.invalidateKeys now uses Promise.allSettled, so a
  transient invalidate failure doesn't abort the chain and no
  unhandled rejection leaks from void invalidateKeys(...) call sites.
- fe: resolveInitialLocale in app/layout.tsx wraps cookies()/headers()
  in try/catch — a dynamic-server error in the root layout now
  degrades to the "en" default instead of crashing into global-error.
- be: reminder-worker + reminder-check parseTimeToMinutes normalize
  "24:00" → "00:00" (some ICU builds emit that for midnight under
  en-GB). Prevents a 1440-minute comparison against schedule windows.
- security: openai-client error body excerpt redacts sk-* / Bearer
  tokens before logging, and moves the excerpt off Error.message into
  a structured field (bodyExcerpt) so log transports can filter it.
- i18n: /api/withings/connect German error message → English.
- i18n: /insights/page.tsx hardcoded "Fehler beim Laden" → t(...)

Hardening:
- /api/bugreport/status collapses to {configured, isAdmin} and adds
  a 30/min rate limit per user. Client interface updated.
- i18n/context.tsx: I18nProvider's initializer now reads the cookie
  (matches SSR) before falling back to localStorage or browser
  detection — eliminates the locale-flash hydration class.
- format.ts legacy helpers also read cookie first, matching SSR.

Tests:
- new src/lib/__tests__/format-locale.test.ts — 9 cases covering
  makeFormatters, parseLocaleFromAcceptLanguage, resolveIntlLocale
- new passkey-errors.test.ts — 8 cases covering every DOMException
  name and SimpleWebAuthn code we map
- new query-keys.test.ts — bundles + invalidateKeys allSettled
  semantics (incl. partial-failure test)

118 → 142 tests. All pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Marc-André Bombeck <marc@bombeck.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:15:43 +02:00
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00
2026-03-10 20:14:33 +01:00

HealthLog Logo

HealthLog

Your health data belongs to you. Track it on your own terms.

License: AGPL-3.0 TypeScript Next.js 16 Self-Hosted PRs Welcome

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, and steps with interactive trend charts, moving averages, and traffic-light ranges based on ESC/ESH 2018 guidelines.

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.

AI-Powered Insights -- Bring your own OpenAI API key. Personalized analysis across blood pressure, weight, pulse, mood, BMI, and medication adherence. Cached daily to minimize API usage.

Doctor Report PDF Export -- Generate professional medical reports client-side. European format with vital sign summaries, BP/BMI classification, compliance rates, and optional AI analysis.

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 -- 30+ persistent achievements across intake streaks, compliance milestones, and healthy metric streaks.

Internationalization -- Full German and English UI with 1000+ translation keys. Browser-based locale detection with per-user override.


Quick Start

git clone https://github.com/MBombeck/HealthLog.git
cd HealthLog
cp .env.example .env

Generate the required secrets:

openssl rand -hex 32  # SESSION_SECRET
openssl rand -hex 32  # ENCRYPTION_KEY
openssl rand -hex 32  # API_TOKEN_HMAC_KEY

Add them to .env, then:

docker compose up -d

Open http://localhost:3000. The first registered user becomes admin.


Tech Stack

Layer Technology
Framework Next.js 16 (App Router, React Server Components)
Language TypeScript (strict mode)
Database PostgreSQL 16 + Prisma 7 (22 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)
PDF 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
DATABASE_URL PostgreSQL connection string
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 (~60 route files)
│   ├── admin/              # Admin panel
│   ├── medications/        # Medication management
│   ├── measurements/       # Health metric entry
│   ├── insights/           # AI-powered analytics
│   ├── achievements/       # Gamification page
│   ├── onboarding/         # 4-step guided setup
│   └── settings/           # User preferences
├── 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
├── lib/
│   ├── auth/               # Session, audit, passkey logic
│   ├── notifications/      # Dispatcher + channel senders
│   ├── jobs/               # pg-boss worker
│   ├── analytics/          # Trend calculations, compliance
│   ├── withings/           # OAuth client, sync service
│   ├── i18n/               # Translations context & config
│   ├── validations/        # Shared Zod schemas
│   ├── crypto.ts           # AES-256-GCM encrypt/decrypt
│   ├── 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, meta } shape
  • Encrypted secrets -- Withings tokens, API keys, VAPID keys, notification credentials
  • Timezone-aware -- Europe/Berlin for display, UTC in database
  • Route protection -- proxy.ts 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
POST /api/auth/passkey/* WebAuthn flows
GET /api/withings/connect Initiate Withings OAuth
POST /api/insights/generate Regenerate AI insights
GET /api/gamification/achievements Achievement progress
GET /api/health Docker health check

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 typecheck must pass
  • Tests: pnpm test
  • UI language: German. Code, comments, and commits: English.

License

HealthLog is licensed under the GNU Affero General Public License v3.0.


healthlog.dev · Live Demo · Docs

Languages
TypeScript 98.8%
JavaScript 0.8%
Shell 0.1%
PLpgSQL 0.1%