mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 04:32:42 +00:00
Maintainer-voice sweep across `src/`. ~190 references to the maintainer by first name in code comments and JSDoc neutralised to "the maintainer", "the live tenant", "users", or "the user" depending on context. Three sed passes covering ~50 distinct patterns (`Marc reported`, `Marc wants`, `Marc kept`, `Marc rolled back`, etc.). Test fixtures (`displayName: "Marc B."`, `username: "marc"`, `<HeroStrip userName="Marc">`) kept as opaque test data per the brief. CHANGELOG normalisation. The v1.4.14 + v1.4.15 entries paired a German bullet with its English translation by historical design — the in-app copy is English-default and the user docs are English, so the mixed-language style was a holdover from the early-marathon notes. A one-off node script extracted the `_..._` English paragraph from each bilingual bullet and rewrote the entry as that text. ~44 bullets reduced; one Docs bullet that lost a leading word in the regex fixed by hand. CLAUDE.md → CONTRIBUTING-AI.md rename so the filename is no longer AI-vendor-specific. `CONTRIBUTING.md` reference updated. `AGENTS.md` stays in place for multi-agent compatibility (the conventions doc is shared with Codex / Cursor / etc.). Wave-4 state wrap. STATE.md now records C1–C5 + D + D2 as complete; v1421-backlog.md grew a "Closed in v1.4.22" header listing the 20 items the marathon landed; `.planning/phase-W4-report.md` documents the per-commit decisions, test deltas, and the 23 items deferred to v1.4.23 with reasoning. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
126 lines
13 KiB
Markdown
126 lines
13 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## Project
|
|
|
|
HealthLog — a personal health-tracking PWA (weight, blood pressure, pulse, mood, medication compliance) with Withings integration, moodLog.app sync, Dracula-themed UI, mobile-first design, offline caching, and doctor report PDF export.
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
# Development
|
|
pnpm dev # Start dev server (http://localhost:3000)
|
|
pnpm build # Production build
|
|
pnpm lint # ESLint
|
|
pnpm format # Prettier format
|
|
pnpm format:check # Prettier check
|
|
pnpm typecheck # TypeScript strict check
|
|
|
|
# Tests
|
|
pnpm test # Run all tests (vitest)
|
|
pnpm test:watch # Watch mode
|
|
|
|
# Database
|
|
pnpm db:generate # Generate Prisma client
|
|
pnpm db:migrate # Create & apply migration (dev)
|
|
pnpm db:migrate:deploy # Apply migrations (production)
|
|
pnpm db:push # Push schema without migration (prototyping)
|
|
pnpm db:studio # Prisma Studio GUI
|
|
|
|
# Docker
|
|
docker compose up -d # Start app + postgres
|
|
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 (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`.
|
|
- **Zod v4** (`zod/v4`) for all input validation (API routes and forms).
|
|
- **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.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).
|
|
- **Proxy route protection** — `src/proxy.ts` checks session cookie, enforces auth on all non-public paths. Next.js 16 uses proxy.ts (not middleware.ts).
|
|
|
|
## Key Conventions
|
|
|
|
- Default locale is **English**. All code-level strings in English. UI text via i18n `t()` with German + English translations.
|
|
- 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 HMAC-SHA256 keyed by `API_TOKEN_HMAC_KEY`).
|
|
- Rate limiting (in-memory sliding window) on auth and external-facing endpoints.
|
|
|
|
## File Layout
|
|
|
|
- `src/app/` — pages and API routes (App Router)
|
|
- `src/components/ui/` — shadcn/ui primitives
|
|
- `src/components/layout/` — shell (sidebar-nav, topbar, mobile-nav, bottom-nav)
|
|
- `src/components/medications/` — medication form, card, timeline
|
|
- `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)
|
|
- `src/lib/export.ts` — CSV/JSON export formatting (measurements, medications, mood)
|
|
- `src/lib/doctor-report-pdf.ts` — client-side PDF generation (jsPDF, European medical format)
|
|
- `src/lib/withings/` — Withings OAuth client and sync service
|
|
- `src/lib/notifications/` — dispatcher, types, senders (telegram, ntfy, web-push)
|
|
- `src/lib/jobs/` — pg-boss reminder worker (reminders, insights, data backups)
|
|
- `src/lib/analytics/` — trend calculations, compliance, correlations
|
|
- `src/lib/i18n/` — i18n context, config, locale detection
|
|
- `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 (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
|
|
- `AGENTS.md` — AI agent instructions (Codex, Cursor, etc.)
|
|
- `CHANGELOG.md` — release notes per semver tag
|
|
|
|
## Important Patterns
|
|
|
|
- Import Prisma client from `@/generated/prisma/client` (not `@/generated/prisma`)
|
|
- Prisma 7 does NOT support `url = env("...")` in schema.prisma — use `prisma.config.ts`
|
|
- Next.js 16 uses `proxy.ts` (not `middleware.ts`) — having both causes build error
|
|
- SimpleWebAuthn v13 has no `@simplewebauthn/server/script/deps` — define Transport type inline
|
|
- `zxcvbn-typescript` uses default export only (no named `zxcvbnAsync`/`zxcvbnOptions`)
|
|
- pg-boss v12: `{ PgBoss }` named import, `localConcurrency` instead of `teamSize`, handler receives `Job<T>[]` array
|
|
- **i18n**: All UI text uses `useTranslations()` hook with `t("section.key")`. Messages in `messages/en.json` (English, default) + `messages/de.json` (German, user-selectable). Keys missing in the active locale fall back to English; missing from both surfaces the raw key — the `src/lib/__tests__/i18n*.test.ts` suite guards against that. Numbers/dates go through `useFormatters()` — never hand-roll `Intl.*` with a fixed locale.
|
|
- **Notifications**: 3 channels (Telegram, ntfy, Web Push). Dispatcher at `src/lib/notifications/dispatcher.ts`. Opt-out model (all events enabled by default)
|
|
- **Gravatar**: Server-side SHA-256 hash via `src/lib/gravatar.ts`. URL returned in `/api/auth/me` response as `gravatarUrl` field
|
|
- **Sidebar**: Collapsible (localStorage-persisted). Settings mode with section shortcuts. User section at bottom with three-dot dropdown menu
|
|
- **Settings vs Admin scope rule (v1.4.16 phase B6 audit)**: `/settings/<slug>` is **per-user** — toggles, credentials, layout, target ranges that affect one account. `/admin/<slug>` is **system-wide** — defaults, feature gating, host metrics, audit log read-access. Two surfaces overlap by design: (a) notification channels — admin can disable a channel system-wide via `/api/settings/global-services`, the user sets their own credentials in `/settings/notifications`; (b) AI providers — admin sets the default Codex/OpenAI key, user can override with their own. **No user-facing toggle should appear in both surfaces.** Keys like `settings.adminAreaTitle` are dead code (planned admin-shortcut tile that never shipped) and stay in JSON marked deprecated until a hygiene PR removes them. See `docs/audit/v1416-settings-audit.md`.
|
|
- **ESLint**: Strict `react-hooks/set-state-in-effect` rule — avoid `setState` inside `useEffect`. Use lazy `useState()` initializer for localStorage reads
|
|
- **Proxy route protection**: `src/proxy.ts` checks `healthlog_session` cookie; PUBLIC_PATHS array bypasses auth. Security headers (CSP, HSTS, X-Frame-Options) applied to all responses
|
|
- **PWA safe area**: Root layout has `viewportFit: "cover"`. Bottom-nav uses `pb-[env(safe-area-inset-bottom)]` for iOS safe area
|
|
- **Doctor report PDF**: Client-side via `src/lib/doctor-report-pdf.ts` (jsPDF). Data from `POST /api/doctor-report`. European medical format, German terminology
|
|
- **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
|
|
|
|
The iOS app talks to bearer-protected routes while the web PWA uses cookie sessions; both share the same handlers (`requireAuth()` accepts either credential).
|
|
|
|
- **Bearer-token issuance**: `POST /api/auth/login` and `POST /api/auth/passkey/login-verify` return a `token` field for native clients. `isNativeClientRequest()` (`src/lib/auth/issue-token.ts`) opts in via `X-Client-Type: native` header **or** a `User-Agent` starting with `HealthLog-iOS/`. The raw `hlk_<64-hex>` value is returned exactly once; default lifetime 90 days, permissions `["*"]`.
|
|
- **Token storage**: API tokens are stored as **HMAC-SHA-256** hashes (`hashToken()` in `src/lib/auth/hmac.ts`, keyed by env `API_TOKEN_HMAC_KEY`) — not plain SHA-256. The keyed HMAC defends against precomputed rainbow tables if the DB leaks. Same mechanism is reused for external-ingest token verification.
|
|
- **Idempotency-Key**: `withIdempotency()` (`src/lib/idempotency.ts`) wraps POST/PUT/PATCH/DELETE handlers so retries with the same `Idempotency-Key` (regex `^[A-Za-z0-9_\-:.]{8,128}$`) for the same `(userId, method, path)` replay the original response with `X-Idempotent-Replay: true` for 24h. **NOT cached**: 401, 403, 408, 429, any 5xx, and any body containing `hlk_` (defence against caching a token-issuing response). Validation 4xx (422 etc.) **is** cached so a broken retry doesn't re-hit the DB. `userIdResolver` defaults to the cookie session — pass a 2nd argument for Bearer-only routes.
|
|
- **Device-registration cross-user-hijack guard**: `POST /api/devices` rejects re-registration of an existing APNs token under a different user with **409** (`Device token already registered to another account`) instead of silently transferring ownership. APNs tokens aren't a secret, so trusting wire input would let anyone who learns a token redirect another user's pushes. Audit trail: `device.register.denied` with reason `token_owned_by_other_user`.
|