mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
feat(insights): intraday pulse shape and elevated-at-rest tension window
Add an on-demand intraday layer to the pulse insight: 10-minute mean heart rate for the viewed day, folded from raw samples via the read-swap pattern (never persisted as 10-minute rollups for all history). Draw the personal resting baseline and softly shade any sustained elevated-at-rest stretch. The tension signal is not raw heart rate — it is heart rate held above the personal resting baseline while movement is low (the elevated-at-rest window), so a workout or a walk never reads as tension. Detection is deliberately conservative: baseline maturity, per-bucket sample density, a mandatory step-based low-movement gate, and a minimum sustained duration must all hold before anything is reported. Silence over speculation. Surface the same signal as a calm, non-diagnostic tension_window item in the daily digest — at most once per day, honest-absent otherwise, gated on the insights module and yielding the rail to time-sensitive actions. Cautious copy across all six locales; framed as "possible tension", never a clinical stress verdict.
This commit is contained in:
1 parent
1427125b7e
commit
a33527da57
17 files changed
+1290
-6
No files matched your search
@@ -112,6 +112,12 @@ const NOT_COACH_OWNED_ROUTES: ReadonlyArray<string> = [
|
||||
"src/app/api/insights/health-status/route.ts",
|
||||
"src/app/api/insights/breathing-screening/route.ts",
|
||||
"src/app/api/insights/labs-changes/route.ts",
|
||||
// S11 — the intraday pulse / tension read. Deterministic compute over the
|
||||
// day's raw PULSE / steps / workouts (10-minute mean shape + the cautious
|
||||
// elevated-at-rest window); it carries no assistant prose and gates on the
|
||||
// `insights` module, not the Coach surface. Same posture as health-status /
|
||||
// labs-changes directly above — disabling Coach must not wedge this read.
|
||||
"src/app/api/insights/pulse/intraday/route.ts",
|
||||
// v1.29.x (S7) — the FENCED multi-document coach chat + its attach/detach
|
||||
// mutations. These live under /insights/chat for URL grouping but are
|
||||
// DOCUMENT-surface routes: they gate on the `inboundDocuments` module
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* GET /api/insights/pulse/intraday?date=YYYY-MM-DD
|
||||
*
|
||||
* The on-demand read for the intraday pulse / tension layer (S11). Returns one
|
||||
* local day's 10-minute mean heart-rate shape plus, when every confidence gate
|
||||
* holds, a single cautious elevated-at-rest ("tension") window. Computed from
|
||||
* raw samples via the read-swap pattern — NOT persisted as 10-min rollups for
|
||||
* all history.
|
||||
*
|
||||
* Cookie OR Bearer auth via `requireAuth()`; `userId` is narrowed from the
|
||||
* resolved session — never a body field. Gated on the `insights` module. The
|
||||
* `date` defaults to the user's local today; anything but a `YYYY-MM-DD`
|
||||
* literal is a 422 via `returnAllZodIssues`.
|
||||
*/
|
||||
import { z } from "zod";
|
||||
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { apiSuccess, returnAllZodIssues } from "@/lib/api-response";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
import { NO_STORE_BUT_BFCACHE } from "@/lib/http/cache-headers";
|
||||
import { resolveUserTimezone } from "@/lib/tz/resolver";
|
||||
import { userDayKey } from "@/lib/tz/format";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { loadIntradayPulse } from "@/lib/analytics/intraday-pulse-io";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const querySchema = z.object({
|
||||
date: z
|
||||
.string()
|
||||
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const GET = apiHandler(async (req: Request) => {
|
||||
const { user } = await requireAuth();
|
||||
const m = await requireModuleEnabled(user.id, "insights");
|
||||
if (!m.enabled) return m.response;
|
||||
|
||||
const url = new URL(req.url);
|
||||
const parsed = querySchema.safeParse({
|
||||
date: url.searchParams.get("date") ?? undefined,
|
||||
});
|
||||
if (!parsed.success) return returnAllZodIssues(parsed.error);
|
||||
|
||||
const timezone = await resolveUserTimezone(user.id);
|
||||
const dateKey = parsed.data.date ?? userDayKey(new Date(), timezone);
|
||||
|
||||
const result = await loadIntradayPulse(user.id, timezone, dateKey);
|
||||
|
||||
annotate({
|
||||
action: { name: "insights.pulse.intraday" },
|
||||
meta: {
|
||||
buckets: result.series.length,
|
||||
baseline_source: result.baselineSource,
|
||||
tension: result.tension !== null,
|
||||
},
|
||||
});
|
||||
|
||||
const response = apiSuccess(result);
|
||||
response.headers.set("Cache-Control", NO_STORE_BUT_BFCACHE);
|
||||
return response;
|
||||
});
|
||||
Reference in new issue
Block a user