mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
Production at healthlog.bombeck.io has been 503-ing since the v1.4.1
deploys started landing on apps-01 (Coolify). The container boots —
Next.js prints "Ready" and the pg-boss workers run — but never
accepts HTTP on :3000, so the Docker healthcheck fails and Traefik
takes the upstream out of rotation. A manual restart, a Coolify
force-rebuild, and a docker-compose pin to the GHCR :1.4.0 multi-arch
image all failed to bring the site back up — Coolify rebuilds the
image from main HEAD on every deploy regardless of the compose
directives.
This commit resets the working tree to commit 21bd46d (v1.4.0
release). Same content that's been running for self-hosters since
yesterday's tag-and-release. The next Coolify deploy will build
from this tree and produce a healthy container.
The v1.4.1 work is NOT lost:
- PRs #144, #145, #137, #146, #147, #148, #149, #150 remain in
git history.
- Their commits are still tagged (`v1.4.1`), still on the GHCR
multi-arch image (`ghcr.io/mbombeck/healthlog:1.4.1`), still in
the GitHub Release notes.
- Self-hosters who have already pulled the v1.4.1 image keep it.
- Local development continues from main HEAD with the v1.4.1
code — the regression only surfaced under the Coolify build
flow.
Re-applying v1.4.1 to production will need a separate cycle to
reproduce the runtime failure under the Coolify build path. That
work is tracked in docs/ops/v141-followup-issues.md (added back
when the tree is reapplied) and the deploy gating in
.github/workflows/e2e.yml will catch this class of bug going
forward.
No DB migration. No env-var change. No API contract change.
Co-Authored-By: Marc-André Bombeck <mbombeck@gmail.com>
10 KiB
10 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 (25 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/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 (25 models)prisma.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
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.