mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
Add three read-only awareness cards to the Insights overview, each drawn off an existing engine and each honest about empty state — the section un-mounts rather than surfacing a husk. - Health status: surfaces what is drifting from the user's own normal by folding the personal-band deviations (coincident-deviation engine) with the dated, sustained level shifts (changepoint detector) at GET /api/insights/health-status. Awareness only, never a diagnosis. - Breathing screening: summarises the last 30 nights of the per-night sleep-breathing-disturbance index plus device-flagged events at GET /api/insights/breathing-screening. Mirrors the device's own classification and states plainly that it is a screening signal, not a diagnosis — discuss with a clinician. - Lab changes: pairs the two most-recent numeric lab panels and reports each shared analyte's signed delta and reference-band standing at GET /api/insights/labs-changes. Neutral framing only. Each reshaping helper is pure and unit-tested for its present/absent states. Register the three sections in the insights-layout universe so they merge in as default-visible, wire them into the overview and the edit-mode title map, add the query-factory keys, document the routes in the OpenAPI contract, and translate every new string across all six locales.
68 lines
2.2 KiB
TypeScript
68 lines
2.2 KiB
TypeScript
/**
|
|
* v1.25 — "what changed since your last panel" read.
|
|
*
|
|
* GET /api/insights/labs-changes
|
|
*
|
|
* Groups the user's live numeric lab results by panel date, pairs the two
|
|
* most-recent panels, and reports the per-analyte delta + reference-band
|
|
* standing (`summariseLabChanges`). Absent when there are fewer than two
|
|
* panels or no analyte is shared. Neutral framing only — a delta is not a
|
|
* diagnosis.
|
|
*
|
|
* Mirrors `/api/insights/derived`: `apiHandler`, `requireAuth`, the `insights`
|
|
* module gate, the shared analytics-read budget. `userId` is narrowed from the
|
|
* session; soft-deleted rows (`deletedAt`) are filtered out.
|
|
*/
|
|
import { apiError, apiSuccess } from "@/lib/api-response";
|
|
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
|
import { annotate } from "@/lib/logging/context";
|
|
import { checkAnalyticsReadRateLimit } from "@/lib/rate-limit";
|
|
import { requireModuleEnabled } from "@/lib/modules/gate";
|
|
import { prisma } from "@/lib/db";
|
|
import { summariseLabChanges } from "@/lib/insights/labs-changes";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export const GET = apiHandler(async () => {
|
|
const { user } = await requireAuth();
|
|
|
|
const m = await requireModuleEnabled(user.id, "insights");
|
|
if (!m.enabled) return m.response;
|
|
|
|
const rl = await checkAnalyticsReadRateLimit(user.id);
|
|
if (!rl.allowed) {
|
|
return apiError("Too many analytics requests. Please retry later.", 429);
|
|
}
|
|
|
|
const rows = await prisma.labResult.findMany({
|
|
where: { userId: user.id, deletedAt: null, value: { not: null } },
|
|
select: {
|
|
analyte: true,
|
|
unit: true,
|
|
value: true,
|
|
referenceLow: true,
|
|
referenceHigh: true,
|
|
takenAt: true,
|
|
},
|
|
orderBy: { takenAt: "desc" },
|
|
});
|
|
|
|
const summary = summariseLabChanges(
|
|
rows.map((r) => ({
|
|
analyte: r.analyte,
|
|
unit: r.unit,
|
|
value: r.value as number,
|
|
referenceLow: r.referenceLow,
|
|
referenceHigh: r.referenceHigh,
|
|
takenAt: r.takenAt,
|
|
})),
|
|
);
|
|
|
|
annotate({
|
|
action: { name: "insights.labs-changes.read" },
|
|
meta: { present: summary.present, changes: summary.changes.length },
|
|
});
|
|
|
|
return apiSuccess({ ...summary, generatedAt: new Date().toISOString() });
|
|
});
|