mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
* Revert "revert: roll source tree back to v1.4.0 to restore production"
This reverts commit e444a90b10.
* fix(v1.4.2): summaries.WEIGHT + medications array guards + healthcheck IPv4
Three production bugs surfaced after the v1.4.0 deploy stabilized:
1. `TypeError: undefined is not an object (evaluating 'E?.summaries.WEIGHT')`
on /dashboard and /insights for users without weight readings —
the optional chain stopped at `data` when it needed to also cover
`data.summaries`. Fixed: `data?.summaries?.WEIGHT` (one level deeper).
2. `TypeError: y?.filter is not a function` on /medications — the
medications query value was non-array (likely a stale
service-worker response from an older API shape). Replaced with
`Array.isArray(medications) ? medications : []` so the page is
immune to any future shape drift.
3. Healthcheck-induced 503 outage (already in 1.4.1 — included here
so 1.4.2 is the single coherent release). The docker-compose
healthcheck used `localhost` which busybox-wget resolves to IPv6
::1; Next.js standalone listens on IPv4 0.0.0.0 only. Pinned
to 127.0.0.1.
This release also includes everything from v1.4.1 (which never
published a clean GHCR tag): the per-section admin extraction, the
testcontainers integration suite, the Playwright + axe-core E2E
foundation, the moodLog SSRF fix, the central error-redaction in
WideEventBuilder + Glitchtip, the medical-citation drift fixes
(ESH 2023, Saint-Maurice attribution, steps source), and the docs
sync.
Co-Authored-By: Marc-André Bombeck <mbombeck@gmail.com>
12 KiB
12 KiB
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
# 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
PrismaPgadapter from@prisma/adapter-pg. Client singleton atsrc/lib/db.ts. Generated client atsrc/generated/prisma/client(note the/clientsuffix). Prisma config inprisma.config.ts(not in schema.prisma). - shadcn/ui components (new-york style) in
src/components/ui/. Add new ones viapnpm 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 insrc/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 viaWideEventBuilder+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.tschecks 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/Berlinfor 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 primitivessrc/components/layout/— shell (sidebar-nav, topbar, mobile-nav, bottom-nav)src/components/medications/— medication form, card, timelinesrc/components/measurements/— measurement form, listsrc/components/mood/— mood form, mood listsrc/components/charts/— Recharts wrapperssrc/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(), HttpErrorsrc/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 servicesrc/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, correlationssrc/lib/i18n/— i18n context, config, locale detectionsrc/lib/validations/— Zod schemas shared between API + clientsrc/hooks/— React hooks (use-auth)messages/de.json+messages/en.json— i18n translationsprisma/schema.prisma— database schema (26 models)tests/integration/— Postgres testcontainers integration suite (rate-limit race, idempotency replay, GDPR cascade delete, session lifecycle);pnpm test:integratione2e/— Playwright + axe-core suite for public smoke checks (version, auth-redirect, login, locale-switch, a11y);pnpm e2eprisma.config.ts— Prisma config (DB URL here, not in schema)public/sw.js— Service worker for Web Push notifications + offline cachingdocs/— long-form audit notes (docs/audit/); end-user docs live in the separate site at https://docs.healthlog.devAGENTS.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 — useprisma.config.ts - Next.js 16 uses
proxy.ts(notmiddleware.ts) — having both causes build error - SimpleWebAuthn v13 has no
@simplewebauthn/server/script/deps— define Transport type inline zxcvbn-typescriptuses default export only (no namedzxcvbnAsync/zxcvbnOptions)- pg-boss v12:
{ PgBoss }named import,localConcurrencyinstead ofteamSize, handler receivesJob<T>[]array - i18n: All UI text uses
useTranslations()hook witht("section.key"). Messages inmessages/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 — thesrc/lib/__tests__/i18n*.test.tssuite guards against that. Numbers/dates go throughuseFormatters()— never hand-rollIntl.*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/meresponse asgravatarUrlfield - Sidebar: Collapsible (localStorage-persisted). Settings mode with section shortcuts. User section at bottom with three-dot dropdown menu
- ESLint: Strict
react-hooks/set-state-in-effectrule — avoidsetStateinsideuseEffect. Use lazyuseState()initializer for localStorage reads - Proxy route protection:
src/proxy.tscheckshealthlog_sessioncookie; 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 usespb-[env(safe-area-inset-bottom)]for iOS safe area - Doctor report PDF: Client-side via
src/lib/doctor-report-pdf.ts(jsPDF). Data fromPOST /api/doctor-report. European medical format, German terminology - Achievements: Persistent in
UserAchievementtable. Computed on API call, new unlocks written to DB with stableunlockedAttimestamps - Data backup: pg-boss
data-backupqueue runs weekly (Sundays 03:00), stores JSON inDataBackupmodel - Wide Events / Structured Logging:
apiHandler()wraps all API routes. Useannotate()from@/lib/logging/contextfor business-action annotations. UserequireAuth()/requireAdmin()from@/lib/api-handler(auto-annotates auth). Background jobs usewithBackgroundEvent(). External calls tracked viagetEvent()?.addExternalCall(). Noconsole.login 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(defaultall) splits HTTP and pg-boss workloads; the proxy refuses HTTP traffic with 503 +X-HealthLog-Process-Type: workerin worker mode.ENCRYPTION_KEYSis a JSON map of versioned keys ({"v1": "...", "v2": "..."}) plusENCRYPTION_ACTIVE_KEY_IDfor new writes; rotation viapnpm 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/loginand/api/auth/passkey/login-verifyissue a 24h access token (hlk_<64hex>) AND a refresh token (hlr_<64hex>) whenX-Client-Type: nativeor the User-Agent starts withHealthLog-iOS/. The browser flow is unchanged. Refresh-token reuse-detection revokes every refresh token for the user. The idempotency replay-cache rejects bodies containinghlk_ORhlr_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/loginandPOST /api/auth/passkey/login-verifyreturn atokenfield for native clients.isNativeClientRequest()(src/lib/auth/issue-token.ts) opts in viaX-Client-Type: nativeheader or aUser-Agentstarting withHealthLog-iOS/. The rawhlk_<64-hex>value is returned exactly once; default lifetime 90 days, permissions["*"]. - Token storage: API tokens are stored as HMAC-SHA-256 hashes (
hashToken()insrc/lib/auth/hmac.ts, keyed by envAPI_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 sameIdempotency-Key(regex^[A-Za-z0-9_\-:.]{8,128}$) for the same(userId, method, path)replay the original response withX-Idempotent-Replay: truefor 24h. NOT cached: 401, 403, 408, 429, any 5xx, and any body containinghlk_(defence against caching a token-issuing response). Validation 4xx (422 etc.) is cached so a broken retry doesn't re-hit the DB.userIdResolverdefaults to the cookie session — pass a 2nd argument for Bearer-only routes. - Device-registration cross-user-hijack guard:
POST /api/devicesrejects 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.deniedwith reasontoken_owned_by_other_user.