The engine ended the open cycle's phase window at the predicted next start, so the day after a missed forecast carried no phase at all and the verdict resolver read that as today falling outside every known cycle window. An ordinary three-days-late period therefore reported INSUFFICIENT_DATA: no cycle day, no phase, an empty ring and the words insufficient data, while the reminder cron was pushing a period-confirm prompt for the same day. OVERDUE was only reachable when the engine itself forecast a cycle longer than the grace window. The open cycle now runs to the predicted start or through today, whichever is later, and stops the moment the next start is logged. The count is bounded on the far side where it already was: past the typical length plus its grace window the verdict degrades to OVERDUE and says how late the period is. The extension pins the open cycle's ovulation anchor to the predicted length. Left to the span, an unconfirmed ovulation is back-calculated from the window's own length, so a window that grows with the delay would walk the estimate and every phase boundary drawn from it. - buildPhaseCycles, phaseForDate and buildPhaseDayMap take the user's today; the insights route and the coach snapshot pass it - verdict round-trip covers the predicted start day, three days past it, past the grace window, and the case where a new start closes the run
src/app/
The Next.js App Router tree: the page surfaces a logged-in user sees and the REST API under api/. Server components are the default; "use client" is added only where a hook, state, or browser API needs it. Routing is file-based — a page.tsx is a page, a route.ts is an API endpoint.
API routes (api/)
Every endpoint wraps its handler in apiHandler (@/lib/api-handler), parses any body with Zod safeParse, narrows userId from the session or Bearer token (never from the body), and returns the { data, error, meta? } envelope. requireAdmin() is cookie-only, so api/admin/ is unreachable by Bearer token. Notable groups:
auth/— login, register, password, passkey (WebAuthn), refresh, Codex device-OAuth.measurements/,mood-entries/,medications/,sleep/— the core health-data CRUD + batch/bulk ingest.ingest/— external Bearer-token ingest surfaces.insights/,analytics/,dashboard/— AI insights, the Coach SSE stream, trend summaries, the first-paint dashboard snapshot.withings/,whoop/,fitbit/,integrations/,sync/— OAuth connect/sync/webhook surfaces and the offline delta feed.fhir/,share-links/,doctor-report/,export/,import/— clinician-facing FHIR R4 read API, shareable records, report + data export.notifications/,devices/,send/,telegram/— channel registration, device tokens, dispatch, webhook intake.admin/— operator panel APIs (cookie-only).health/,version/,meta/,internal/— health check, build-version probe, runtime metadata, worker-internal routes.
Page surfaces
measurements/, medications/, mood/, insights/, achievements/, notifications/, settings/, onboarding/, admin/, plus the public about/, privacy/, bugreport/, the share-record route c/, and the root dashboard (page.tsx). Shared shell + chrome lives in layout.tsx; error.tsx / global-error.tsx / not-found.tsx / loading.tsx cover the framework boundaries. layout.tsx holds the one nonce-bound dangerouslySetInnerHTML (theme bootstrap).
Conventions
See ../../CLAUDE.md. The request-flow walk in .planning/codebase/arch.md annotates each layer with file:line citations. Related dirs: handlers compose ../lib/; pages render ../components/.