v1.4.30 PR push failed CI on first attempt because the two new MeasurementType enums weren't reflected in `docs/api/openapi.yaml`. A separate `chore(openapi): regenerate the public spec` commit was needed before the checks turned green. This hook catches the drift at commit time locally so the contributor sees the regeneration prompt before pushing. New `.githooks/pre-commit`: - Skips when no relevant files (`src/lib/openapi/`, `src/lib/validations/`, `src/app/api/`, the generator script itself) are staged. - Runs `pnpm openapi:check` quietly; exits fast on match. - On drift, runs `pnpm openapi:generate`, re-stages the regenerated `docs/api/openapi.yaml`, and continues the commit. - Skips in CI so the existing `pnpm openapi:check` job in `.github/workflows/security.yml` stays the server-side authoritative gate. `scripts/install-hooks.sh` flips `git config core.hooksPath` to `.githooks/` — idempotent, one-shot per fresh clone. AGENTS.md adds the install command to the contributor workflow section.
17 KiB
AGENTS.md
Instructions for AI coding agents (OpenAI Codex, Claude Code, Cursor, etc.) working on this repository.
Project
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.4.0 (released) → v1.4.1 in progress. v1.4.0 shipped
the UI guidelines + Skeleton/EmptyState primitives, medical-citations
consolidation, the per-route /settings/[section] split, the
status-first admin grid + aggregator endpoint, five new "Test
connection" buttons, AI insights with inline charts via allowlisted
metric tokens, off-host encrypted S3 backup, encryption-key
versioning + rotation CLI, optional HEALTHLOG_PROCESS_TYPE worker /
web split, and short-lived 24h access tokens with refresh-token
rotation for native API clients. v1.4.1 follows up with per-section
admin component extraction (the inner monolith into one file per
panel), a Postgres testcontainers integration test suite, and a
Playwright + axe-core E2E foundation. See GitHub Releases +
CHANGELOG.md for the full feature timeline.
Tech Stack
| Layer | Technology | Version | Notes |
|---|---|---|---|
| Framework | Next.js (App Router) | 16 | TypeScript strict, RSC default, "use client" only for interactivity |
| 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 |
| CSS | Tailwind | 4 | CSS-first config (@import "tailwindcss" syntax) |
| Data fetching | TanStack Query | 5 | Provider in src/components/providers.tsx |
| Validation | Zod | v4 | Import as zod/v4 (not zod) |
| Testing | Vitest | latest | Config in vitest.config.mts |
| Package manager | pnpm | latest | Not npm or yarn |
| Node | 20.x | via nvm | |
| Job queue | pg-boss | 12 | Named import { PgBoss }, see gotchas |
| Auth | SimpleWebAuthn | 13 | Passkeys primary, password fallback |
| i18n | Custom context-based | — | useTranslations() hook, messages/de.json + messages/en.json |
| jsPDF + autotable | latest | Client-side doctor report generation |
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
pnpm openapi:generate # Regenerate docs/api/openapi.yaml from Zod schemas
pnpm openapi:check # Verify the spec is in sync (CI gate)
# Git hooks (one-time per clone)
bash scripts/install-hooks.sh # Activates .githooks/, including the OpenAPI pre-commit 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
# Verification (run before completing tasks)
pnpm typecheck && pnpm lint && pnpm test && pnpm format:check && pnpm build
File Layout
src/
├── app/ # Pages and API routes (App Router)
│ ├── layout.tsx # Root layout (viewport-fit: cover for PWA)
│ ├── page.tsx # Dashboard (/) with quick entry dropdown
│ ├── globals.css # Dracula theme CSS variables
│ ├── admin/page.tsx # Admin shell — 77 LOC, mounts the per-section components from src/components/admin/
│ ├── auth/login/page.tsx # Login
│ ├── auth/register/page.tsx # Registration
│ ├── achievements/page.tsx # Gamification achievements
│ ├── bugreport/page.tsx # Bug report → GitHub issue
│ ├── insights/page.tsx # Health insights + correlations + AI (7 sections with sticky nav)
│ ├── measurements/page.tsx # Measurements CRUD table
│ ├── medications/page.tsx # Medications management
│ ├── notifications/page.tsx # Notification preferences matrix
│ ├── onboarding/page.tsx # 4-step guided onboarding
│ ├── settings/page.tsx # 308-redirects /settings → /settings/account
│ ├── settings/[section]/page.tsx # Per-section route shell (account, integrations, notifications, dashboard, ai, api, advanced, about)
│ ├── mood/page.tsx # Mood tracking
│ ├── targets/page.tsx # Target values dashboard
│ └── 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
│ ├── gamification/ # Achievements (persistent)
│ ├── integrations/moodlog/ # moodLog.app webhook + sync + status
│ └── ...
├── components/
│ ├── ui/ # shadcn/ui primitives
│ ├── layout/ # Shell: sidebar-nav, topbar, mobile-nav, bottom-nav
│ ├── medications/ # Medication form, card, timeline
│ ├── measurements/ # Measurement form, list
│ ├── mood/ # Mood form, mood list
│ ├── charts/ # Recharts wrappers, compliance charts
│ ├── insights/ # AI insights cards (status, advisor)
│ ├── gamification/ # Achievement cards, progress
│ ├── settings/ # Per-route Settings section components (one per /settings/[section])
│ ├── admin/ # Per-route Admin section components (system-status, integrations, monitoring, reminders, users, audit, danger-zone, feedback)
│ └── monitoring/ # Umami, GlitchTip bootstrap
├── lib/
│ ├── db.ts # Prisma client singleton
│ ├── crypto.ts # AES-256-GCM encryption
│ ├── gravatar.ts # Gravatar URL from email (SHA-256)
│ ├── api-response.ts # { data, error, meta } envelope helpers
│ ├── rate-limit.ts # In-memory sliding window rate limiter
│ ├── export.ts # CSV/JSON export formatting (incl. mood)
│ ├── doctor-report-pdf.ts # Client-side PDF generation (jsPDF)
│ ├── auth/session.ts # getSession() → { session, user } | null
│ ├── analytics/ # Trend calculations, compliance, correlations
│ ├── i18n/ # I18n context + config
│ ├── 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
├── generated/prisma/client/ # Generated Prisma client (DO NOT EDIT)
messages/
├── de.json # German translations (primary UI language)
└── en.json # English translations
prisma/
├── schema.prisma # Database schema (26 models)
└── migrations/ # Migration files (0001–0025; latest: refresh_tokens + user_locale_drift_fix)
prisma.config.ts # Prisma config (DB URL lives here, NOT in schema)
public/
├── sw.js # Service worker (Web Push + offline caching)
├── manifest.json # PWA manifest
└── ... # Static assets
docs.healthlog.dev # External documentation site
Key Conventions
- Default locale is English. All code-level strings in English. UI text via i18n (
t()calls) with German + English translations. - i18n: Use
useTranslations()hook →t("section.key"). Supports parameter interpolation:t("key", { count: 5 }). Messages inmessages/de.json+messages/en.json. - API response format: Always
{ data, error, meta }viaapiSuccess(data)/apiError(message, status)fromsrc/lib/api-response.ts. - Auth check pattern:
const session = await getSession(); if (!session) return apiError("Not authenticated", 401); - Timezone:
Europe/Berlinfor display, UTC in database. - Encryption: Sensitive data (Withings tokens, API keys, VAPID private keys) encrypted with AES-256-GCM via
src/lib/crypto.ts. - Dracula colors: Use CSS variables
var(--dracula-purple),var(--dracula-cyan),var(--dracula-green),var(--dracula-orange),var(--dracula-pink),var(--dracula-red),var(--dracula-yellow),var(--dracula-fg),var(--dracula-comment)for chart/graph elements. - Proxy route protection:
src/proxy.tscheckshealthlog_sessioncookie on all non-public paths. Unauthenticated page requests → redirect/auth/login. API routes keep their owngetSession()auth. Public paths defined inPUBLIC_PATHSarray. - PWA safe area: Root layout uses
viewportFit: "cover". Bottom-nav haspb-[env(safe-area-inset-bottom)]. Auth-shell usespb-[calc(5rem+env(safe-area-inset-bottom,0px))]. - PDF generation: Client-side via
src/lib/doctor-report-pdf.tsusing jsPDF. Data fetched fromPOST /api/doctor-report. European medical format with German terminology. - Data backup: pg-boss weekly job (
data-backupqueue, Sundays 03:00), stores compressed JSON inDataBackupmodel. - Achievements: Persistent in
UserAchievementtable. API at/api/gamification/achievementscomputes current state, compares with DB, persists new unlocks.unlockedAttimestamp is stable once set.
Critical Gotchas
These are hard-won lessons. Ignoring them will cause errors:
Prisma 7
- No
urlin schema.prisma — The database URL is configured inprisma.config.ts, not in thedatasourceblock. - Import path:
import { ... } from "@/generated/prisma/client"(with/clientsuffix, not@/generated/prisma). - Adapter required: PrismaClient needs
PrismaPgadapter from@prisma/adapter-pg. Seesrc/lib/db.ts. - No
earlyAccessin defineConfig — causes TypeScript errors.
Next.js 16
- Proxy, not middleware: Next.js 16 renamed middleware to proxy. The file is
src/proxy.ts(notmiddleware.ts). Having both causes a build error: "Both middleware file and proxy file detected".
Libraries
- SimpleWebAuthn v13: No
@simplewebauthn/server/script/deps— define Transport type inline. - zxcvbn-typescript: Default export only.
zxcvbnAsyncandzxcvbnOptionsdo not exist as named exports. - pg-boss v12:
{ PgBoss }named import (not default), uselocalConcurrency(notteamSize), handler receivesJob<T>[]array. - Zod v4: Import from
"zod/v4", not"zod". - jsPDF: Client-side only. Import dynamically in browser context. Used with
jspdf-autotableplugin.
Settings & Admin (per-route layout, v1.4)
- Settings: 8 routes under
/settings/[section]—account,integrations,notifications,dashboard,ai,api,advanced,about. The legacy/settings308-redirects to/settings/account. Sidebar deep-links and the<a href="/settings#anchor">patterns from 1.3 still resolve via the redirect. - Admin:
src/app/admin/page.tsxis now a 77-LOC shell that mounts the per-section components insrc/components/admin/. Status-card grid lives instatus-card-grid.tsx. The aggregator endpoint/api/admin/status-overviewreturns the six-card summaries in one batched query. - ESLint
react-hooks/set-state-in-effectis strict now (was non-blocking when the settings monolith carried inline-effect state-setters). Use lazyuseState(() => …)for localStorage reads, TanStack Query for data fetches.
Insights Page
- Large file (~1750 lines), 7 content sections with AI summaries.
- Sticky horizontal chip navigation (
InsightsSectionNavcomponent) with IntersectionObserver for active state tracking. - Section IDs:
general,bp,weight,pulse,mood,meds,bmi. - Each section has
scroll-mt-28for header offset on scroll.
Sidebar
- Collapsible (icons-only mode,
w-16). State persisted in localStorage (healthlog-sidebar-collapsed). - User section at bottom: Avatar (Gravatar if email, initials fallback) + username + three-dot MoreVertical menu (right side).
- Three-dot menu: Admin link, Notifications, Theme picker, Logout.
- ESLint enforces
react-hooks/set-state-in-effect— use lazyuseState(() => ...)for localStorage reads, NOTuseEffect+setState.
Gravatar
src/lib/gravatar.tsgenerates Gravatar URL server-side (SHA-256 hash of email,?d=404fallback).- Returned in
/api/auth/meresponse asgravatarUrlfield. Used byAvatarImagecomponent withAvatarFallbackfor initials.
Notification System
- Channels:
telegram,ntfy,web_push— stored as plainStringonNotificationChannel.type(no Prisma enum; the canonical list lives insrc/lib/notifications/types.ts). - Event types:
medication_reminder,measurement_anomaly,compliance_low,withings_sync_failed,system_alert— alsoStringonNotificationPreference.eventType. Source of truth is the discriminated union insrc/lib/notifications/types.ts. - Opt-out model: Preferences default to ON (enabled) when no
NotificationPreferencerow exists. - Dispatcher:
src/lib/notifications/dispatcher.tschecks 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.
Onboarding
- 4-step guided flow: Profile → Medications → Notifications teaser → Target values.
- Optional steps 2-4 have "Skip" buttons.
- Progress indicator bar at top.
- Located at
/onboarding, linked from proxy as public path.
Database Models (Prisma)
26 models: User, Passkey, Session, AuthChallenge, Measurement, Medication, MedicationSchedule, MedicationIntakeEvent, ReminderPhaseConfig, TelegramReminderMessage, TelegramScheduledDeletion, ApiToken, RefreshToken, WithingsConnection, MoodEntry, AppSettings, Feedback, AuditLog, NotificationChannel, NotificationPreference, PushSubscription, DataBackup, UserAchievement, RateLimit, Device, IdempotencyKey. (RefreshToken added in v1.4.0 alongside the native-client 24h access-token / refresh-token rotation flow.)
When Making Changes
- Read before modifying — understand the existing code patterns.
- Run verification after changes:
pnpm typecheck && pnpm lint && pnpm test && pnpm format:check && pnpm build - German UI text — all user-facing strings go through
t("key")with translations in bothmessages/de.jsonandmessages/en.json. - Don't over-engineer — keep changes focused. No speculative abstractions.
- Update
docs/STATUS.mdwhen completing tasks.