mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
feat(labs): add an AI assessment and stat strip to the biomarker detail page
Give each biomarker a detail page that matches the metric sub-pages: a numbers-first Min/Max/Median/Mean strip over the numeric readings and a data-grounded AI assessment card. The assessment is served by a new read-only endpoint GET /api/insights/biomarker-assessment?biomarkerId=&locale=, returning the same envelope as /api/insights/metric-status so InsightStatusCard consumes it unchanged. A new generator reads the marker's LabResult history and reuses the shared provider runner, the stale-while-revalidate read-only path, and the persist helper. Regeneration is gated on a fingerprint of the latest reading, so the assessment is rebuilt only when a new reading lands; an idle marker re-stamps the cached text without an LLM call. The biomarker: scope rides the existing on-demand status queue. The stat strip and the assessment card both self-gate on numeric data, so a qualitative-only or brand-new marker paints neither.
This commit is contained in:
1 parent
02d527720a
commit
0790a96ca4
10 files changed
+779
-2
No files matched your search
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Per-biomarker assessment route.
|
||||
*
|
||||
* `GET /api/insights/biomarker-assessment?biomarkerId=<id>` serves the
|
||||
* data-driven assessment for one user-scoped biomarker. The shape is
|
||||
* byte-identical to `/api/insights/metric-status` so `InsightStatusCard`
|
||||
* consumes it unchanged.
|
||||
*
|
||||
* Read-only by construction: a cache miss enqueues an out-of-band
|
||||
* generation (via `resolveReadOnlyStatusMiss` inside the generator) and
|
||||
* serves the last-good text (stale-while-revalidate), never blocking on the
|
||||
* provider. The generator regenerates ONLY when the latest reading
|
||||
* fingerprint changes, so an idle marker re-stamps cached text without an
|
||||
* LLM round-trip.
|
||||
*/
|
||||
import { NextRequest } from "next/server";
|
||||
import { z } from "zod/v4";
|
||||
import { apiSuccess, returnAllZodIssues } from "@/lib/api-response";
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { resolveServerLocale } from "@/lib/i18n/server-locale";
|
||||
import { requireAssistantSurface } from "@/lib/feature-flags";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
import { generateBiomarkerStatus } from "@/lib/insights/biomarker-status";
|
||||
import { resolveMetricStatusLocale } from "@/lib/insights/metric-status";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const querySchema = z.object({
|
||||
biomarkerId: z.string().min(1).max(64),
|
||||
});
|
||||
|
||||
export const GET = apiHandler(async (request: NextRequest) => {
|
||||
const { user } = await requireAuth();
|
||||
const m = await requireModuleEnabled(user.id, "insights");
|
||||
if (!m.enabled) return m.response;
|
||||
await requireAssistantSurface("insightStatus");
|
||||
|
||||
const parsed = querySchema.safeParse({
|
||||
biomarkerId: request.nextUrl.searchParams.get("biomarkerId"),
|
||||
});
|
||||
if (!parsed.success) {
|
||||
annotate({
|
||||
action: { name: "insights.biomarker-status.invalid" },
|
||||
meta: { issue_count: parsed.error.issues.length },
|
||||
});
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
|
||||
const localeParam = request.nextUrl.searchParams.get("locale");
|
||||
const resolved = await resolveServerLocale({
|
||||
request,
|
||||
userLocale: user.locale ?? null,
|
||||
override: localeParam,
|
||||
});
|
||||
const locale = resolveMetricStatusLocale(resolved);
|
||||
|
||||
const result = await generateBiomarkerStatus({
|
||||
biomarkerId: parsed.data.biomarkerId,
|
||||
userId: user.id,
|
||||
locale,
|
||||
force: false,
|
||||
readOnly: true,
|
||||
});
|
||||
|
||||
annotate({
|
||||
action: { name: "insights.biomarker-status" },
|
||||
meta: { biomarkerId: parsed.data.biomarkerId },
|
||||
});
|
||||
|
||||
return apiSuccess(result);
|
||||
});
|
||||
Reference in new issue
Block a user