mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
Mood has been a first-class module here for a long time, with entries,
structured tags, rated factors and notes. The bridge to the external moodLog
service had nothing left to add, and its Settings card was withdrawn in
a56e6fe61 on the promise of a dedicated removal. This delivers it.
Removed: the hourly `moodlog-sync` queue, cron and handler binding; the
inbound webhook and its shared-secret handling; the reverse push and its four
call sites; the pull-sync module; the per-user credential routes and the
instance-wide toggle; the entry on the `/api/integrations/status` envelope,
the `IntegrationKey` union, the cadence map and the display-name table; the
boot-time secret rotation pass; 22 message keys per locale.
Migration 0271 drops the five `users` credential columns and
`app_settings.mood_log_global`, clears the ledger row for the provider, and
deletes the retired pg-boss schedule so no cron keeps enqueuing work no
handler drains.
Mood data is untouched. `mood_entries` is not named anywhere in the migration
and cannot be reached from it: the dropped columns carry no index, constraint
or foreign key, and `mood_entries.user_id` references `users(id)`, not any of
them. Rows the bridge imported keep `source = 'MOODLOG'`, and every read path
- list filter, source badge, exports, backups - still resolves that value.
`src/lib/validations/moodlog.ts` held the native mood-entry schemas alongside
the bridge's own; it is now `validations/mood.ts` and carries only the former.
13 KiB
13 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, 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
- 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 likesettings.adminAreaTitleare dead code (planned admin-shortcut tile that never shipped) and stay in JSON marked deprecated until a hygiene PR removes them. Seedocs/audit/v1416-settings-audit.md. - 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.