mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
HRV lives under two measurement types and they are not the same measure: Apple Health / Fitbit / Google Health write SDNN as `HEART_RATE_VARIABILITY`, while Oura / Polar / WHOOP — and a bridge pushing a watch that reports it that way — write nightly RMSSD as `HRV_RMSSD`. Every HRV surface unions the two: `/insights/hrv` through `fallbackMeasurementType`, the Coach through `COACH_SOURCE_MEASUREMENT_TYPES.hrv`, the MCP reads through `withHrvFallback`. `sub-page-metric.ts` even says listing both is what keeps "the tab, pill, and dashboard tile lit whichever source the user has". The dashboard tile did not. It read SDNN alone, so an RMSSD-only account got a Settings → Dashboard toggle that turned on and rendered nothing, with no explanation, while the insights page charted the same readings — which is why it reads as a broken layout rather than a missing series. `pickHrvSummary` picks the series with the same rule `withHrvFallback` uses: the primary wins whenever it has any rows, the fallback only when it has none. Never a merge — the two share `ms` but measure different things, so blending them would invent a number no device reported. The caller gets the winning type back, because the freshness caption has to read the series on show and the label has to name the measure when it is not SDNN (`· RMSSD`, as the insights chart title already does). Second, less visible half: `HRV_RMSSD` is recovery-owned in `METRIC_STATUS_MODULE_OWNERS`, but the dashboard snapshot gates off `SUMMARY_TYPE_MODULE`, a different map, and that one did not list it. The omission was inert while the tile read SDNN alone; adding the fallback without it would hand a recovery-off account a tile built from recovery-owned rows — the exact hazard the owner map documents. The entry goes in beside the four sibling recovery types, so the gate stays server side and the client keeps no competing copy.
src/
Application source. HealthLog is a Next.js 16 App Router project (output: "standalone", React 19, TypeScript strict). The path alias @/* resolves to this directory. Everything a request touches lives under here; the layout below follows the request flow rather than alphabetical order.
Request flow
client → src/proxy.ts → src/app/<route>/route.ts → src/lib/api-handler.ts → handler body → src/lib/api-response.ts → client
proxy.ts— the HTTP edge (Next.js 16's renamedmiddleware). Public-path allowlist, demo-mode mutation block, server-side onboarding redirect, worker-only refusal,x-request-id+ CSP nonce generation, every security header.app/— App Router pages (RSC by default) and API routes. Every API route wraps its handler inapiHandler. Seeapp/README.md.lib/— the server/runtime toolkit:apiHandler, auth, crypto, rollups, AI, notifications, pg-boss jobs, integrations, rate limiting. Seelib/README.md.components/— React components (shadcn/ui new-york, kebab-case files). Seecomponents/README.md.
Other directories
hooks/— shared React hooks (use-prefix, kebab-case).types/— shared TypeScript declarations.generated/prisma/— the generated Prisma client (do not edit; regenerated byprisma generate). Imported as@/generated/prisma.instrumentation.ts— Next.js startup hook; boots the pg-boss worker registry and boot-time backfill queues.__tests__/— cross-cutting unit tests; most tests live in__tests__/folders next to the code they cover.
Conventions
See ../CLAUDE.md for the full set. The load-bearing ones: RSC by default ("use client" only when a hook/state/browser API needs it); every API route wraps apiHandler and parses its body with Zod safeParse; userId is always narrowed from session or Bearer, never accepted as a body field; TanStack Query keys come only from src/lib/query-keys/; outbound HTTP goes through src/lib/safe-fetch.ts.