mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
Publish the nineteen routes the drift check could not see
`openapi:check` compares the registry against the committed YAML, so a route that was never registered produced no drift and no failure. These nineteen had shipped without a contract: the health probe, the dashboard analytics aggregate, the profile summary, personal records, the badge grid, the whole environmental-context module, both onboarding writes, the Apple Health import pair, the external medication ingest, the self-context adopt, the AI-provider update and the AI connection probe. Each entry is read off the route rather than inferred from a neighbour, so it carries what a client cannot guess: that `/api/health` answers a bare object and not the envelope, that `/api/analytics` records a score row and mints pattern ids on a GET, that `/api/ai/test` reports a provider failure as 200 with `ok: false` because a 5xx comes back as somebody's HTML error page, that the ingest route replays with 200 and creates with 201, and that a home location re-stamps its effective-from instant on every write. Three route-local request schemas move to `src/lib/validations/` so the published shape and the runtime parse are one object rather than two copies — a route module may only export handlers, which is why they could not be imported where they were. The AI-provider PATCH has no schema to move: it hand-rolls its parse field by field, so its entry says so and spells out that a wrongly-typed key is skipped in silence. The generated YAML is deliberately not in this commit.
This commit is contained in:
1 parent
1c2e6c5c80
commit
49da15feaa
15 files changed
+2097
-68
No files matched your search
@@ -1,5 +1,4 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { z } from "zod/v4";
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
resolveProviderForTest,
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
reserveBudget,
|
||||
resolveDailyCap,
|
||||
} from "@/lib/ai/coach/budget";
|
||||
import { aiTestOverrideSchema } from "@/lib/validations/ai-provider";
|
||||
|
||||
/**
|
||||
* Output ceiling of the probe below. Mirrors the `maxTokens` on the completion
|
||||
@@ -39,32 +39,6 @@ const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const overrideSchema = z
|
||||
.object({
|
||||
provider: z
|
||||
.enum([
|
||||
"OPENAI",
|
||||
"ANTHROPIC",
|
||||
"LOCAL",
|
||||
"OPENAI_COMPATIBLE",
|
||||
"CHATGPT_OAUTH",
|
||||
])
|
||||
.optional()
|
||||
.nullable(),
|
||||
model: z.string().min(1).max(120).optional().nullable(),
|
||||
baseUrl: z.string().url().max(2048).optional().nullable(),
|
||||
anthropicKey: z.string().min(1).max(500).optional().nullable(),
|
||||
localKey: z.string().min(1).max(500).optional().nullable(),
|
||||
openaiKey: z.string().min(1).max(500).optional().nullable(),
|
||||
// v1.33.1 (#470) — the OpenAI-compatible gateway's own fields, kept
|
||||
// separate from `baseUrl` / `openaiKey` so testing an unsaved gateway
|
||||
// config can never reach the pinned OpenAI arm and vice versa.
|
||||
compatBaseUrl: z.string().url().max(2048).optional().nullable(),
|
||||
compatKey: z.string().min(1).max(500).optional().nullable(),
|
||||
compatModel: z.string().min(1).max(120).optional().nullable(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const { user } = await requireAuth();
|
||||
annotate({ action: { name: "ai.test" } });
|
||||
@@ -93,7 +67,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
maxBytes: 64 * 1024,
|
||||
});
|
||||
if (error) return error;
|
||||
const parsed = overrideSchema.safeParse(data);
|
||||
const parsed = aiTestOverrideSchema.safeParse(data);
|
||||
if (!parsed.success) {
|
||||
return apiError("Invalid override payload", 422);
|
||||
}
|
||||
|
||||
@@ -5,17 +5,7 @@ import { apiSuccess, apiError, safeJson } from "@/lib/api-response";
|
||||
import { setOnboardingPendingCookie } from "@/lib/auth/session";
|
||||
import { NextRequest } from "next/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
const onboardingSchema = z.object({
|
||||
displayName: z.string().trim().min(1).max(50).optional(),
|
||||
heightCm: z.number().min(50).max(300).optional(),
|
||||
dateOfBirth: z.string().optional(),
|
||||
// Same three values the profile schema stores. A narrower enum here
|
||||
// rejected the whole onboarding submission over a field the account
|
||||
// is allowed to hold, and left the value unrecordable until the person
|
||||
// found the setting again afterwards.
|
||||
gender: z.enum(["MALE", "FEMALE", "OTHER"]).optional(),
|
||||
});
|
||||
import { onboardingCompleteSchema } from "@/lib/validations/onboarding";
|
||||
|
||||
/**
|
||||
* Complete the onboarding flow. Saves optional profile data and marks
|
||||
@@ -30,7 +20,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
});
|
||||
|
||||
if (jsonError) return jsonError;
|
||||
const result = z.safeParse(onboardingSchema, body);
|
||||
const result = z.safeParse(onboardingCompleteSchema, body);
|
||||
if (!result.success) {
|
||||
return apiError("Invalid input", 422, {
|
||||
errorCode: "onboarding.complete.invalid",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import {
|
||||
@@ -14,10 +13,8 @@ import { prisma, toJson } from "@/lib/db";
|
||||
import { Prisma } from "@/generated/prisma/client";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import {
|
||||
ONBOARDING_GOAL_SLUGS,
|
||||
buildGoalSeededDashboardLayout,
|
||||
} from "@/lib/onboarding/goals";
|
||||
import { buildGoalSeededDashboardLayout } from "@/lib/onboarding/goals";
|
||||
import { onboardingStepSchema } from "@/lib/validations/onboarding";
|
||||
|
||||
/**
|
||||
* v1.4.25 W14b — POST /api/onboarding/step.
|
||||
@@ -45,19 +42,13 @@ import {
|
||||
* but tight enough to defang a stuck retry loop.
|
||||
*/
|
||||
|
||||
const stepBodySchema = z.object({
|
||||
step: z.number().int().min(1).max(4),
|
||||
// v1.17.1 — optional goal slugs from the GoalsChipPicker (step 2
|
||||
// submit). Validated against the closed slug enum so an unknown slug
|
||||
// 422s; deduped + capped at the full set size. Persisted to
|
||||
// `User.onboardingGoals` (field-by-field, no mass assignment) and on
|
||||
// completion (step 4) seeds the dashboard layout when the user never
|
||||
// customized it. Omitted leaves the stored goals untouched.
|
||||
goals: z
|
||||
.array(z.enum(ONBOARDING_GOAL_SLUGS))
|
||||
.max(ONBOARDING_GOAL_SLUGS.length)
|
||||
.optional(),
|
||||
});
|
||||
// v1.17.1 — the optional goal slugs from the GoalsChipPicker (step 2 submit)
|
||||
// ride the same body. Validated against the closed slug enum so an unknown
|
||||
// slug 422s; persisted to `User.onboardingGoals` field-by-field (no mass
|
||||
// assignment) and, on completion (step 4), used to seed the dashboard layout
|
||||
// when the user never customized it. Omitted leaves the stored goals
|
||||
// untouched. The schema itself lives in `@/lib/validations/onboarding` so the
|
||||
// OpenAPI registry reads the same object this parse does.
|
||||
|
||||
export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const { user } = await requireAuth();
|
||||
@@ -83,7 +74,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = stepBodySchema.safeParse(body);
|
||||
const parsed = onboardingStepSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
annotate({
|
||||
action: { name: "onboarding.step" },
|
||||
|
||||
Reference in new issue
Block a user