diff --git a/AGENTS.md b/AGENTS.md index 9ecb744f4..e52cc5064 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,14 +6,14 @@ 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.2.0 (Custom Thresholds, Blood Glucose, Dashboard Customization, Built-in Feedback, Multi-Provider AI). See GitHub Issues for open tasks. +**Status**: v1.3.2 — Body composition (Total Body Water + Bone Mass), SSRF-hardened outbound fetches, GHCR multi-arch images (`linux/amd64` + `linux/arm64`) with SLSA provenance + SBOM, pg-boss graceful SIGTERM drain, blocking TypeScript CI, locale-integrity test guard. See GitHub Releases + CHANGELOG.md for the full feature timeline (v1.0 → v1.3). ## Tech Stack | Layer | Technology | Version | Notes | | --------------- | -------------------- | ------- | ----------------------------------------------------------------------- | | Framework | Next.js (App Router) | 16 | TypeScript strict, RSC default, `"use client"` only for interactivity | -| ORM | Prisma | 7.4 | Uses `PrismaPg` adapter, **not** `url` in schema — see gotchas below | +| ORM | Prisma | 7.8 | Uses `PrismaPg` adapter, **not** `url` in schema — see gotchas below | | Database | PostgreSQL | 16 | Docker Compose service, port 5432, user `healthlog` | | UI | shadcn/ui (new-york) | latest | Components in `src/components/ui/` | | Theme | Dracula | — | CSS variables in `globals.css`, dark mode default, `--dracula-*` tokens | @@ -76,10 +76,10 @@ src/ │ ├── medications/page.tsx # Medications management │ ├── notifications/page.tsx # Notification preferences matrix │ ├── onboarding/page.tsx # 4-step guided onboarding -│ ├── settings/page.tsx # All settings (13 sections, ~2100 lines) +│ ├── settings/page.tsx # All settings (8 top-level sections, ~3150 lines — split tracked for 1.4.0) │ ├── mood/page.tsx # Mood tracking │ ├── targets/page.tsx # Target values dashboard -│ └── api/ # ~60 API route files (see docs/api.md) +│ └── api/ # 100+ API route files (admin, auth, measurements, medications, mood, insights, integrations, ingest, dashboard, feedback, tokens, notifications, monitoring, …) │ ├── mood-entries/ # Mood CRUD │ ├── import/ # JSON data import │ ├── doctor-report/ # Doctor report data aggregation @@ -93,7 +93,9 @@ src/ │ ├── measurements/ # Measurement form, list │ ├── mood/ # Mood form, mood list │ ├── charts/ # Recharts wrappers, compliance charts -│ ├── insights/ # AI insights card +│ ├── insights/ # AI insights cards (status, advisor) +│ ├── gamification/ # Achievement cards, progress +│ ├── settings/ # Settings-page section components │ └── monitoring/ # Umami, GlitchTip bootstrap ├── lib/ │ ├── db.ts # Prisma client singleton @@ -109,6 +111,13 @@ src/ │ ├── notifications/ # Dispatcher, types, senders (telegram, ntfy, web-push) │ ├── jobs/ # pg-boss worker (reminders, insights, backups) │ ├── validations/ # Zod schemas (shared between API + client) +│ ├── ai/ # Multi-provider client (OpenAI, Anthropic, local OpenAI-compat) +│ ├── feedback/ # In-app feedback + optional GitHub escalation +│ ├── gamification/ # Achievement definitions + progress +│ ├── insights/ # Insight pipeline + prompts +│ ├── logging/ # Wide Events: builder, context, sampler, transports, background +│ ├── moodlog/ # moodLog.app webhook + sync +│ ├── monitoring/ # Umami / GlitchTip server-side hooks │ └── withings/ # Withings OAuth client + sync service ├── hooks/ │ └── use-auth.ts # useAuth(), useLogout() hooks @@ -118,7 +127,7 @@ messages/ └── en.json # English translations prisma/ ├── schema.prisma # Database schema (23 models) -└── migrations/ # Migration files (0001–0018) +└── migrations/ # Migration files (0001–0022; latest: body_composition_metrics) prisma.config.ts # Prisma config (DB URL lives here, NOT in schema) public/ ├── sw.js # Service worker (Web Push + offline caching) @@ -167,15 +176,15 @@ These are hard-won lessons. Ignoring them will cause errors: ### Settings Page -- One large file (~2100 lines), 13 sections. Sidebar switches to "settings mode" showing section shortcuts. +- 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). -- Section IDs: `profil`, `sprache`, `sicherheit`, `telegram`, `ntfy`, `web-push`, `insights`, `withings`, `api`, `export`, `protokoll`, `daten`. +- 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`. ### Insights Page -- Large file (~1800+ lines), 7 content sections with AI summaries. +- Large file (~1750 lines), 7 content sections with AI summaries. - Sticky horizontal chip navigation (`InsightsSectionNav` component) with IntersectionObserver for active state tracking. -- Section IDs: `general`, `bp`, `weight`, `pulse`, `mood`, `medications`, `bmi`. +- Section IDs: `general`, `bp`, `weight`, `pulse`, `mood`, `meds`, `bmi`. - Each section has `scroll-mt-28` for header offset on scroll. ### Sidebar @@ -192,8 +201,8 @@ These are hard-won lessons. Ignoring them will cause errors: ### Notification System -- **Channels**: `TELEGRAM`, `NTFY`, `WEB_PUSH` (DB: `NotificationChannel`). -- **Event types**: `MEDICATION_REMINDER`, `MEASUREMENT_ANOMALY`, `COMPLIANCE_LOW`, `WITHINGS_SYNC_FAILED`, `SYSTEM_ALERT`. +- **Channels**: `telegram`, `ntfy`, `web_push` — stored as plain `String` on `NotificationChannel.type` (no Prisma enum; the canonical list lives in `src/lib/notifications/types.ts`). +- **Event types**: `medication_reminder`, `measurement_anomaly`, `compliance_low`, `withings_sync_failed`, `system_alert` — also `String` on `NotificationPreference.eventType`. Source of truth is the discriminated union in `src/lib/notifications/types.ts`. - **Opt-out model**: Preferences default to ON (enabled) when no `NotificationPreference` row exists. - **Dispatcher**: `src/lib/notifications/dispatcher.ts` checks channel enabled + preference per event type. - **Telegram**: Inline buttons (Take/Skip/Snooze 1h/Snooze 3h), pre-end window reminders (30 min before), message auto-cleanup after 24h. diff --git a/CLAUDE.md b/CLAUDE.md index d241aac51..c181711de 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,7 +44,7 @@ docker compose logs -f app # Tail app logs - **API response envelope**: `{ data, error, meta }` via helpers in `src/lib/api-response.ts`. - **apiHandler** wraps ALL API route handlers (`src/lib/api-handler.ts`) — error handling, Wide Event logging, x-request-id propagation. - **Structured Logging** (Wide Events) in `src/lib/logging/` — one JSON event per request/operation via `WideEventBuilder` + `AsyncLocalStorage`. Tail sampling, stdout JSON + optional Loki Push. -- **Vitest** for unit testing. Config in `vitest.config.ts`. +- **Vitest** for unit testing. Config in `vitest.config.mts`. - **pg-boss** (PostgreSQL-native) for job queue: medication reminders, insight caching, weekly data backups. - **jsPDF** + jspdf-autotable for client-side doctor report PDF generation (`src/lib/doctor-report-pdf.ts`). - **PWA offline** via service worker caching strategies in `public/sw.js` (cache-first for static, network-first for pages, network-only for API). @@ -56,7 +56,7 @@ docker compose logs -f app # Tail app logs - Timezone: `Europe/Berlin` for display, UTC in database. - Sensitive data (Withings tokens, API keys, VAPID private keys) encrypted with AES-256-GCM (`src/lib/crypto.ts`) before DB storage. - Passkeys are primary auth (SimpleWebAuthn v13). Sessions stored server-side in PostgreSQL. -- All API mutations require authentication. External ingest uses Bearer token (hashed with SHA-256). +- All API mutations require authentication. External ingest uses Bearer token (hashed with HMAC-SHA256 keyed by `API_TOKEN_HMAC_KEY`). - Rate limiting (in-memory sliding window) on auth and external-facing endpoints. ## File Layout @@ -84,9 +84,9 @@ docker compose logs -f app # Tail app logs - `prisma/schema.prisma` — database schema (23 models) - `prisma.config.ts` — Prisma config (DB URL here, not in schema) - `public/sw.js` — Service worker for Web Push notifications + offline caching -- `docs/` — architecture, security, assumptions, ops, API docs, ADRs -- `docs/STATUS.md` — Current project status + open tasks +- `docs/` — long-form audit notes (`docs/audit/`); end-user docs live in the separate site at https://docs.healthlog.dev - `AGENTS.md` — AI agent instructions (Codex, Cursor, etc.) +- `CHANGELOG.md` — release notes per semver tag ## Important Patterns diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 942e7a34a..e7e5126d0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Contributions are welcome. This guide covers how to set up the project and submi ## Prerequisites -- Node.js 22+ +- Node.js 20+ - pnpm - PostgreSQL 16+ (or Docker) - Git @@ -20,13 +20,21 @@ cp .env.example .env Edit `.env` and configure at minimum: ``` -DATABASE_URL="postgresql://healthlog:healthlog@localhost:5432/healthlog" +POSTGRES_PASSWORD= +DATABASE_URL="postgresql://healthlog:${POSTGRES_PASSWORD}@db:5432/healthlog" SESSION_SECRET=<64-char hex> ENCRYPTION_KEY=<64-char hex> API_TOKEN_HMAC_KEY=<64-char hex> ``` -Generate secrets with `openssl rand -hex 32`. +Generate the four secrets: + +```bash +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 +``` ### With Docker (database only) @@ -47,23 +55,28 @@ docker compose up -d ## Development Workflow ```bash -pnpm dev # Start dev server (http://localhost:3000) -pnpm lint # ESLint -pnpm format # Prettier -pnpm typecheck # TypeScript strict mode -pnpm test # Vitest +pnpm dev # Start dev server (http://localhost:3000) +pnpm lint # ESLint +pnpm format:check # Prettier (check) +pnpm typecheck # TypeScript strict mode +pnpm test # Vitest +pnpm build # Next.js production build ``` -All four checks must pass before submitting a PR. +All five checks must pass before submitting a PR. The full one-liner that mirrors CI: + +```bash +pnpm typecheck && pnpm lint && pnpm test && pnpm format:check && pnpm build +``` ## Code Conventions -- **TypeScript strict mode** -- no `any` types, no type assertions unless absolutely necessary -- **UI text in German**, code/comments/docs in English -- **API responses** follow `{ data, error, meta }` envelope pattern -- **Zod v4** for input validation (API routes and forms) -- **shadcn/ui** components (new-york style) for UI primitives -- **Dracula theme** -- dark mode only, use `--dracula-*` CSS tokens +- **TypeScript strict mode** -- avoid `any`. Where it's truly unavoidable (e.g. variadic Next.js handler signatures in `src/lib/api-handler.ts`), document the reason inline. +- **English-default UI** -- the default locale is English. German is user-selectable. Code, comments, commit messages, and docs are English. All user-facing strings go through `t("key")` with translations in both `messages/en.json` and `messages/de.json`. The `i18n-locale-integrity.test.ts` suite blocks merges if keys drift between locales or duplicate at any nesting depth. +- **API responses** follow the `{ data, error }` envelope via `apiSuccess()` / `apiError()` in `src/lib/api-response.ts`. Wrap every route handler in `apiHandler()` (`src/lib/api-handler.ts`) for error handling, structured Wide-Event logging, and `x-request-id` propagation. +- **Zod v4** (`import { ... } from "zod/v4"`) for input validation in API routes and forms. +- **shadcn/ui** components (new-york style) for UI primitives. +- **Dracula theme** -- dark mode is the default; a `light`/`system` toggle is exposed in the topbar. Chart and accent colors use the `--dracula-*` CSS tokens so both themes stay coherent. See `CLAUDE.md` for detailed architecture and patterns. @@ -82,7 +95,7 @@ Always include the migration file in your PR. 1. Fork the repository 2. Create a feature branch (`git checkout -b feature/your-feature`) 3. Make your changes -4. Ensure all checks pass (`pnpm lint && pnpm typecheck && pnpm test`) +4. Ensure all checks pass (`pnpm typecheck && pnpm lint && pnpm test && pnpm format:check && pnpm build`) 5. Submit a PR with a clear description of the changes ## Security diff --git a/README.md b/README.md index 9c5e6092f..68bf817d9 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,9 @@ Most health apps lock your data behind proprietary clouds, push subscriptions, a **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. +**Gamification** -- 38+ persistent achievements across intake streaks, compliance milestones, and healthy metric streaks. -**Internationalization** -- English (default) and German UI with 1000+ translation keys. Numbers, dates, units, and AI prompts all locale-aware via `useFormatters()`. Browser-based detection with per-user override. +**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. --- @@ -163,30 +163,50 @@ Telegram bot token, ntfy settings, Web Push VAPID keys, Umami, and GlitchTip URL ``` src/ ├── app/ # Next.js App Router pages & API routes -│ ├── api/ # REST API endpoints (~60 route files) +│ ├── 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 +│ └── 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 +│ ├── 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 -│ ├── analytics/ # Trend calculations, compliance +│ ├── 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 @@ -196,10 +216,11 @@ src/ ### Key Patterns - **RSC by default** -- `"use client"` only for interactive components -- **API envelope** -- All responses follow `{ data, error, meta }` shape +- **API envelope** -- All responses follow `{ data, error }` shape via `apiSuccess()` / `apiError()` in `src/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, and `x-request-id` propagation - **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 +- **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 --- @@ -254,17 +275,83 @@ All mutations require authentication via session cookie. External ingest uses Be
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 | +| 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 |
diff --git a/SECURITY.md b/SECURITY.md index fadaf9b98..787a2c731 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -70,6 +70,6 @@ HealthLog Docker images are built and published from this repository's CI: - **Provenance attestation**: each image carries a [SLSA build provenance](https://slsa.dev/spec/v1.0/provenance) statement linking it back to the GitHub Actions run, the commit SHA, and the workflow definition - **SBOM**: each image includes a [Software Bill of Materials](https://docs.docker.com/build/metadata/attestations/sbom/) you can inspect with `docker buildx imagetools inspect ghcr.io/mbombeck/healthlog:latest --format '{{ json .SBOM }}'` -To pin to a specific version in production, replace `:latest` with the released tag in your `docker-compose.yml`, e.g. `ghcr.io/mbombeck/healthlog:1.2.0`. Pinning is recommended for self-hosters who want explicit control over upgrades. +To pin to a specific version in production, replace `:latest` with the released tag in your `docker-compose.yml`, e.g. `ghcr.io/mbombeck/healthlog:1.3.2`. Pinning is recommended for self-hosters who want explicit control over upgrades. If you discover a tampered or unexpected image, please report it as a vulnerability via the email above.