mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
Three readers grouped the same rows twice and joined the second grouping back to the first: the live buckets joined each hourly-mean day's hour means to the per-source days, the all-time extremes joined the day weights the same way, and the doctor report's dense buckets joined each bucket's latest reading and hour means to the bucket aggregates. On a measurements table without planner statistics (a new install, or an import under the autoanalyze threshold) the planner estimated one row on each side and ran every one of those joins as a nested loop that re-ran the inner grouping for each outer row. On ten years of readings over six types (51,000 rows, no statistics) the dense buckets and the every-type live buckets ran into the 60 s statement timeout and the all-time extremes took 11 s. With statistics all three answered in 0.1 to 0.2 s. Each is now one chain of folds over one scan. The live buckets fold the readings per local hour and the hours per local day, carrying the day of hours beside the counts. The all-time extremes take the day weights from the same windowed rows they count, the shape the pre-fold remainder already has. The dense buckets carry each bucket's latest reading and each hour's count and mean on the rows as window values, and one grouping folds them all. Same fixture without statistics: 0.2 s, 0.1 s and 0.1 s, the same as with them. A parity suite runs the joined statements as the reference against the readers across ladders, zones with whole, half-hour and negative offsets, grains, overlapping and off-ladder sources, glucose contexts and device types, tombstones and both DST changes; sums and means agree to floating-point summation order, everything else exactly. The unanalysed-table guard now runs the three readers under EXPLAIN ANALYZE: the joins measured 200 to 3,000 row visits per row stored, the folds about 10. The dense-bucket loader drops under the 200-line function cap, so its baseline entry goes.
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.