Merge branch 'feat/v146-server-insights' into feat/v146-server

This commit is contained in:
Marc-André Bombeck committed 2026-10-23 15:44:47 +02:00
commit af15be4708
81 files changed
+18200 -1821

No files matched your search

@@ -41,7 +41,7 @@ const ADMITTED: Record<string, { jobs: string[]; why: string }> = {
jobs: ["daily_briefing"],
why: "The daily briefing's generation and its phrasing re-roll; the JSON and grounding repairs do not reason.",
},
"app/api/insights/generate/route.ts": {
"lib/insights/briefing-request/completion.ts": {
jobs: ["daily_briefing"],
why: "A briefing asked for by hand reasons like the scheduled one; its JSON and grounding repairs do not.",
},
@@ -177,7 +177,7 @@ const TS_READERS: Record<string, Entry> = {
"app/api/insights/cards/route.ts": {
exempt: "pulse is summarised from day means (readDayMeanSeries)",
},
"app/api/insights/comprehensive/route.ts": {
"lib/insights/comprehensive-overview/build.ts": {
exempt: "summarises the mood series only",
},
};
+23 -8
View File
@@ -76,7 +76,7 @@ import type { ShareScope } from "@/lib/sharing/scope";
import { ADMITTED_MUTATING_HANDLERS } from "../../tests/fixtures/v137/sharing-matrix";
import { API_HANDLER, guardModuleOf } from "./helpers/auth-guards";
import { readSourceText } from "./helpers/source-files";
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
const SRC = join(process.cwd(), "src");
@@ -3316,15 +3316,30 @@ describe("(g) the MANAGE route set is frozen", () => {
// and refreshes no token. Credential policy belongs inside every resolver
// and presence helper so a delegate or managed system job cannot select a
// personal BYOK key or custom base URL first.
// The route hands its body to the overview modules, so the leg reads the
// route and every one of them: the probe is called among them, and no
// resolver is called by any.
const comprehensiveModules = [
"app/api/insights/comprehensive/route.ts",
...walkSourceFiles(join(SRC, "lib/insights/comprehensive-overview"), {
floor: 5,
}).map((f) => `lib/insights/comprehensive-overview/${f}`),
];
expect(read(comprehensiveModules[0])).toContain(
'"@/lib/insights/comprehensive-overview/overview"',
);
expect(
callsSymbol(
"app/api/insights/comprehensive/route.ts",
"probeProviderPresence",
),
comprehensiveModules.some((m) => callsSymbol(m, "probeProviderPresence")),
).toBe(true);
expect(
callsSymbol("app/api/insights/comprehensive/route.ts", "resolveProvider"),
).toBe(false);
for (const m of comprehensiveModules) {
for (const name of [
"resolveProvider",
"resolveProviderChain",
"probeProviderChain",
]) {
expect(callsSymbol(m, name), `${m} calls ${name}`).toBe(false);
}
}
for (const name of [
"resolveProvider",
"resolveProviderChain",
@@ -506,7 +506,7 @@ const READER_UNIT_PRODUCERS = [
"lib/ai/prompts/weight.ts",
"lib/insights/features-units.ts",
"lib/insights/comprehensive-generate.ts",
"app/api/insights/generate/route.ts",
"lib/insights/briefing-request/features.ts",
"lib/insights/weight-status.ts",
"lib/insights/metric-status.ts",
"lib/insights/narrative/period-narrative.ts",
+11 -700
View File
@@ -1,59 +1,20 @@
import { prisma } from "@/lib/db";
import { probeProviderPresence } from "@/lib/ai/provider";
import { aiCapabilityToServe } from "@/lib/ai/capabilities/gate";
import { apiError, apiSuccess } from "@/lib/api-response";
import type { DataPoint, DataSummary } from "@/lib/analytics/trends";
import { summarize } from "@/lib/analytics/trends";
import { getBpTargets } from "@/lib/analytics/bp-targets";
import { computeBpInTargetPct } from "@/lib/analytics/bp-in-target";
import {
classifyBMI,
classifyBP,
generateAlerts,
} from "@/lib/analytics/classifications";
import {
significantPearsonCorrelation,
type PairedPoint,
} from "@/lib/analytics/correlations";
import {
buildComplianceMedicationContext,
COURSES_COMPLIANCE_SELECT,
calculateCompliance,
expectsDoses,
lastNonSkippedTakenAt,
SCHEDULE_COMPLIANCE_SELECT,
} from "@/lib/analytics/compliance";
import { resolveMedicationCategories } from "@/lib/medication-category";
import { resolveRestingPulseSeries } from "@/lib/analytics/resting-pulse";
import { readRestingPulseProxy } from "@/lib/analytics/resting-pulse-read";
import {
apiHandler,
requireRecordAuth,
type AuthContext,
} from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { checkAnalyticsReadRateLimit } from "@/lib/rate-limit";
import {
cachedSwrWithMeta,
caches,
type ServerCache,
} from "@/lib/cache/server-cache";
import { buildComprehensiveAggregate } from "@/lib/insights/comprehensive-aggregator";
import {
ensureUserMoodRollupsFresh,
readMoodDayRollups,
} from "@/lib/rollups/mood-rollups";
import { userDayKey, DEFAULT_TIMEZONE } from "@/lib/tz/format";
import { TRACKED_INTAKE_WHERE } from "@/lib/medications/intake-tracking";
import { dayKeyAsUtcMidnight, dateOnlyKey } from "@/lib/tz/date-only";
import { readComprehensiveOverview } from "@/lib/insights/comprehensive-overview/overview";
export const dynamic = "force-dynamic";
/**
* GET /api/insights/comprehensive — the Insights overview's computed read.
* The body is built in `src/lib/insights/comprehensive-overview/`.
*/
export const GET = apiHandler(async () => {
// v1.37.0 — MANAGE-level read: computed over the whole record, with no
// provider anywhere on the path.
// MANAGE-level read: computed over the whole record, with no provider
// anywhere on the path.
const { user } = await requireRecordAuth("manage", "record");
// v1.15.20 — shared analytics-read budget (generous; caps runaway loops).
// Shared analytics-read budget (generous; caps runaway loops).
const rl = await checkAnalyticsReadRateLimit(user.id);
if (!rl.allowed) {
return apiError("Too many analytics requests. Please retry later.", 429);
@@ -66,37 +27,7 @@ export const GET = apiHandler(async () => {
// provider up, still gets the overview. The `ai` block beside the body says
// whether the briefing and status notes that decorate the overview can be
// shown, resolved per request and never cached with the body.
// v1.4.35 — read-through the analytics cache keyed on
// (userId, "comprehensive"). The /insights page mount routinely
// fans out to this endpoint alongside the Coach drawer + the
// recommendations grid; the 60s TTL converts every duplicate
// mount within the window to a Map lookup. Invalidation is handled
// by `invalidateUserMeasurements`, which marks every key under the
// `${userId}|` prefix stale (see `lib/cache/invalidate.ts`).
//
// v1.16.7 — stale-while-revalidate. This is the single heaviest
// SQL-side aggregation on the /insights mount; serving the prior
// body instantly while ONE background recompute refreshes it keeps
// the page interactive across the bucket's 10-minute stale window
// (a measurement sync used to bust the entry into a blocking cold
// rebuild on the very next mount, all day). The truly-cold first
// read of the day still computes inline — there is no prior body to
// serve — but it no longer recurs per visit.
//
// The `revalidating` flag marks a stale-served body honestly so the
// client can poll (bounded) until the background rebuild lands —
// mirroring the `/api/insights/generate` GET contract. It rides on
// the response, never inside the cached body, so a warmed entry
// doesn't freeze the flag.
const { value: body, outcome } = await cachedSwrWithMeta(
caches.analytics as ServerCache<
Awaited<ReturnType<typeof buildComprehensiveResponse>>
>,
`${user.id}|comprehensive`,
() => buildComprehensiveResponse(user),
annotate,
);
const { body, revalidating } = await readComprehensiveOverview(user);
const [briefing, statusText] = await Promise.all([
aiCapabilityToServe(user.id, "briefing"),
@@ -105,627 +36,7 @@ export const GET = apiHandler(async () => {
return apiSuccess({
...body,
revalidating: outcome === "stale",
revalidating,
ai: { briefing, statusText },
});
});
type AuthedUser = AuthContext["user"];
/**
* v1.4.35 — comprehensive insights response body, lifted out of the
* route handler so `cached()` can wrap it. Replaces the previous
* 100k-row-in-JS aggregation with SQL-side aggregates served by
* `buildComprehensiveAggregate`. The medication-compliance block keeps
* its bounded Prisma reads (medications + intake events) — the directive
* called those out as already on the safe path. The byte-shape of the
* returned envelope is unchanged from the legacy route.
*/
export async function buildComprehensiveResponse(user: AuthedUser) {
const userId = user.id;
const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
// v1.30.3 — the one shared per-user tz for every day-bucketing
// pass in this route (resting-pulse proxy, medication-continuity, BP
// pairing). Was previously re-derived ad hoc (or hardcoded UTC) at each
// call site.
const userTz = user.timezone || DEFAULT_TIMEZONE;
// Fetch user profile (height + DOB drive BMI + BP targets).
const dbUser = await prisma.user.findUnique({
where: { id: userId },
select: {
heightCm: true,
dateOfBirth: true,
},
});
// ── Aggregate-first reads ─────────────────────────────────
// SQL-side rollup replaces the 100k-row findMany on the legacy
// route. Returns per-type DataSummary identical to the JS
// `summarize()` semantics over the 90-day window, plus the bounded
// raw rows the BP target adherence pairing needs, plus daily-mean
// buckets for the correlation pairings.
const aggregate = await buildComprehensiveAggregate(userId);
// v1.4.40 — swap the 90-day unbounded `prisma.moodEntry.findMany`
// for the persistent mood-rollup DAY tier (audit Critical Finding
// #2). The two consumers downstream — `dailyMoodEntries` for the
// mood × metric correlations and `moodSummary` for the dashboard
// mood block — read at a per-day resolution, which is precisely the
// shape the rollup writer emits.
//
// Coverage-fallback: when the user has mood entries but zero rollup
// rows yet (legacy account before the boot-time backfill has caught
// up), fall back to a bounded 90-day raw walk once and fire the
// warm-up so the next request lands on the rollup tier. Same posture
// as `/api/mood/analytics`.
//
// Since v1.32.12 the mood rollup writer mints `bucketStart` from the
// canonical local `MoodEntry.date` label at UTC midnight, so
// `.toISOString().slice(0, 10)` recovers the user's LOCAL calendar day —
// byte-identical to the live fallback's `entry.date`. Both mood paths
// therefore emit local-day keys. The mood × metric pairing partners
// below (sys BP, weight, resting pulse) are consequently derived in the
// user's tz (v1.32.17) so both join sides share ONE local-day key space.
// The measurement-rollup tier itself still buckets UTC (`dailyByType`)
// and migrates to local in a documented follow-up release; the two
// internally-consistent UTC joins (weight × BP, BP-med continuity) are
// left on it deliberately (see their call sites below).
void ensureUserMoodRollupsFresh(userId);
const moodRollupDayRows = await readMoodDayRollups(userId, ninetyDaysAgo);
let dailyMoodEntries: Array<{ day: string; value: number }>;
let moodDataPoints: DataPoint[];
let moodEntryCount: number;
if (moodRollupDayRows.length > 0) {
dailyMoodEntries = moodRollupDayRows.map((r) => ({
day: dateOnlyKey(r.bucketStart),
value: Math.round(r.mean * 100) / 100,
}));
moodDataPoints = moodRollupDayRows.map((r) => ({
date: r.bucketStart,
value: r.mean,
}));
moodEntryCount = moodRollupDayRows.reduce((s, r) => s + r.count, 0);
} else {
// Coverage-fallback: legacy account with mood entries but no
// rollup coverage yet. Bounded by the 90-day window so the walk is
// capped even when the rollup miss happens.
const moodEntries = await prisma.moodEntry.findMany({
// v1.7.0 sync — exclude tombstoned rows.
where: { userId, deletedAt: null, moodLoggedAt: { gte: ninetyDaysAgo } },
orderBy: { moodLoggedAt: "asc" },
select: { date: true, score: true, moodLoggedAt: true },
});
const moodByDay = new Map<string, { sum: number; count: number }>();
for (const entry of moodEntries) {
const current = moodByDay.get(entry.date) ?? { sum: 0, count: 0 };
current.sum += entry.score;
current.count += 1;
moodByDay.set(entry.date, current);
}
dailyMoodEntries = Array.from(moodByDay.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([day, stats]) => ({
day,
value: Math.round((stats.sum / stats.count) * 100) / 100,
}));
// Feed per-day means into `summarize()` so the live-fallback path
// shares the rollup-tier semantic (one DataPoint per day, the
// daily mean) — mirrors the v1.4.39 mood/analytics QA UX-H1 fix.
moodDataPoints = dailyMoodEntries.map((e) => ({
date: new Date(`${e.day}T12:00:00.000Z`),
value: e.value,
}));
moodEntryCount = moodEntries.length;
}
const moodSummary =
moodDataPoints.length > 0 ? summarize(moodDataPoints) : null;
const summaries: Record<string, DataSummary> = aggregate.summaries;
// ── BMI ──────────────────────────────────────────────────
let bmi: number | null = null;
let bmiClassification = null;
if (dbUser?.heightCm && summaries.WEIGHT?.latest) {
const heightM = dbUser.heightCm / 100;
bmi = Math.round((summaries.WEIGHT.latest / (heightM * heightM)) * 10) / 10;
bmiClassification = classifyBMI(bmi);
}
// ── BP classification (30-day average) ────────────────────
let bpClassification = null;
const bpTargets = getBpTargets(dbUser?.dateOfBirth ?? null);
if (
summaries.BLOOD_PRESSURE_SYS?.avg30 &&
summaries.BLOOD_PRESSURE_DIA?.avg30
) {
bpClassification = classifyBP(
summaries.BLOOD_PRESSURE_SYS.avg30,
summaries.BLOOD_PRESSURE_DIA.avg30,
);
}
// ── BP target adherence (trailing 90 days) ───────────────
// Route through the canonical `computeBpInTargetPct` so this figure
// shares ONE definition with the dashboard tile, the insight cards, the
// AI snapshot and the targets endpoint (per the bp-in-target.ts
// contract — the inline copy that used to live here drifted: it paired
// sys+dia on the 5-minute window ONLY, dropping readings whose sys/dia
// timestamps drift past 5 min on Withings / Apple-Health imports, which
// skewed the share iOS renders on the Home Health Score). The helper
// adds the same-local-day pairing fallback (v1.30.3 QA F7 — keyed on
// the user's OWN tz, not a hardcoded Berlin day) so those readings
// pair, and keeps the ESH-2023 band + 90/50 hypotension floor (both sys
// AND dia at or below the age-band ceiling). Window is the aggregator's
// 90-day raw pull — a "recent control" figure, not the all-time headline.
let bpPctInTarget: number | null = null;
if (bpTargets) {
bpPctInTarget =
computeBpInTargetPct(
aggregate.bpRawRows.sys,
aggregate.bpRawRows.dia,
bpTargets,
userTz,
)?.pct ?? null;
}
// ── Correlations: weight × Sys BP ─────────────────────────
// Daily-key pairing (the legacy `pairByTimestamp` ran with a
// 24-hour default tolerance — daily-mean buckets are the
// SQL-side equivalent and the directive explicitly accepts the
// small semantic shift to bound the raw-row footprint).
//
// v1.32.17 — this pair stays on the UTC `dailyByType` series: BOTH
// sides come from `dailyByType` (weight × sys), so they share one UTC
// day-key space and are internally consistent. Re-keying it to local
// is R12's read-swap (it migrates with the rollup tier), NOT this
// hotfix — doing it here would mean per-request raw re-aggregation
// without the local-bucketed rollup behind it.
const weightDaily = aggregate.dailyByType.WEIGHT ?? [];
const sysDaily = aggregate.dailyByType.BLOOD_PRESSURE_SYS ?? [];
const weightBpPairs: PairedPoint[] = joinDailyByDay(weightDaily, sysDaily);
const weightBpCorrelation = significantPearsonCorrelation(weightBpPairs);
const scatterData = weightBpPairs.map((p) => ({
weight: p.a,
sysBP: p.b,
}));
// ── Correlations: mood × {sys BP, weight, pulse} ─────────
// v1.32.17 — the mood series is LOCAL-day-keyed (since v1.32.12, see the
// rollup note above). `pairMoodWithDaily` joins on an EXACT YYYY-MM-DD
// key, so the metric partners MUST also be local-day-keyed or every
// evening logger west of UTC gets a lag-1 pairing sold as same-day.
// Derive the sys / weight partners as exact local-day means from the raw
// rows already on the request path (canonical-source deduped in the
// aggregator), NOT from the UTC `dailyByType` — a UTC-day MEAN cannot be
// relabeled into a local-day mean. `round2` mirrors the aggregator's
// `ROUND(AVG, 2)` so correlation thresholds don't drift.
const sysDailyLocal = deriveDailyLocalMeans(aggregate.bpRawRows.sys, userTz);
const weightDailyLocal = deriveDailyLocalMeans(
aggregate.weightRawRows,
userTz,
);
const moodBpPairs = pairMoodWithDaily(dailyMoodEntries, sysDailyLocal);
const moodBpCorrelation = significantPearsonCorrelation(moodBpPairs);
const moodBpScatterData = moodBpPairs.map((p) => ({ mood: p.a, sysBP: p.b }));
const moodWeightPairs = pairMoodWithDaily(dailyMoodEntries, weightDailyLocal);
const moodWeightCorrelation = significantPearsonCorrelation(moodWeightPairs);
const moodWeightScatterData = moodWeightPairs.map((p) => ({
mood: p.a,
weight: p.b,
}));
// v1.2.5 — this correlation is labelled "Resting pulse (bpm)";
// score it against the RESTING series, not the raw PULSE day-mean. The
// aggregator's `dailyByType.PULSE` is a per-day AVG that folds in workout
// HR for Apple-Health users and inflates the "resting" figure. Prefer the
// clean RESTING_HEART_RATE rows; only pull RAW PULSE (for the
// low-percentile daily proxy) when the user has no resting rows — the
// proxy needs the per-day distribution, which the day-mean discards.
//
// v1.32.17 — key this resting series on `userDayKey(d, userTz)`. Its ONLY
// pairing partner is `dailyMoodEntries`, which is LOCAL-day-keyed since
// v1.32.12 (the mood rollup writer now mints `bucketStart` from the
// canonical local `MoodEntry.date` label, not a UTC truncation). Keeping
// UTC here would offset every evening logger west of UTC by one day and
// sell a lag-1 correlation as same-day. This is exactly the flip the
// v1.30.3 F8 note said becomes correct once the mood side goes local —
// it now has. (Unlike the mood side, this series builds from raw rows in
// this route, so it re-keys directly with no rollup-tier change.)
const restingPulseRows = await prisma.measurement.findMany({
where: {
userId,
type: "RESTING_HEART_RATE",
deletedAt: null,
measuredAt: { gte: ninetyDaysAgo },
},
select: { measuredAt: true, value: true },
});
//
// #1023 — the proxy folds in Postgres. It needs one percentile per day, and
// reading the raw samples to get it walked 130 000 rows per page view on a
// watch that records once a minute, blocking the event loop for over a
// second. `readRestingPulseProxy` returns the same series one row per day.
const localDayKey = (d: Date) => userDayKey(d, userTz);
const restingPulseSeries =
restingPulseRows.length > 0
? resolveRestingPulseSeries({
restingSamples: restingPulseRows,
pulseSamples: [],
dayKeyOf: localDayKey,
}).series
: await readRestingPulseProxy({
userId,
since: ninetyDaysAgo,
timeZone: userTz,
});
const restingByDay = new Map<string, { sum: number; count: number }>();
for (const s of restingPulseSeries) {
const key = localDayKey(s.measuredAt);
const cur = restingByDay.get(key) ?? { sum: 0, count: 0 };
cur.sum += s.value;
cur.count += 1;
restingByDay.set(key, cur);
}
const restingPulseDaily = Array.from(restingByDay.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([day, stats]) => ({ day, value: stats.sum / stats.count }));
const moodPulsePairs = pairMoodWithDaily(dailyMoodEntries, restingPulseDaily);
const moodPulseCorrelation = significantPearsonCorrelation(moodPulsePairs);
const moodPulseScatterData = moodPulsePairs.map((p) => ({
mood: p.a,
pulse: p.b,
}));
// ── Medication compliance ────────────────────────────────
// Bounded reads — left untouched per the directive.
const medicationRows = await prisma.medication.findMany({
// v1.16.11 — as-needed (PRN) medications never surface a compliance
// rate (no expected doses).
where: { userId, active: true, asNeeded: false, ...TRACKED_INTAKE_WHERE },
// v1.15.20 — schedules through the shared compliance select so the
// configured per-dose windows reach this surface like every other.
include: {
schedules: { select: SCHEDULE_COMPLIANCE_SELECT },
// v1.16.3 — archived schedule eras for era-aware compliance.
scheduleRevisions: { orderBy: { validFrom: "asc" } },
// v1.25 — pause eras so paused days drop out of the denominator.
pauseEras: { select: { pausedAt: true, resumedAt: true } },
// v1.40 (#1024) — the courses, so a gap between two expects nothing.
courses: COURSES_COMPLIANCE_SELECT,
},
});
// A medication with no schedule expects no dose, and `calculateCompliance`
// answers 100 % for that case. The `asNeeded: false` filter above catches
// only the PRN arm; the empty schedule is reachable on a scheduled
// medication and would otherwise report perfect adherence.
const medications = medicationRows.filter(expectsDoses);
const resolvedCategories = await resolveMedicationCategories(
medications.map((m) => m.id),
);
const categoryMap = Object.fromEntries(
Object.entries(resolvedCategories).map(([id, r]) => [id, r.category]),
);
const medCompliance = [];
const bpMedicationEvents: Array<{
scheduledFor: Date;
takenAt: Date | null;
skipped: boolean;
}> = [];
const bpMedications = medications.filter(
(med) => (categoryMap[med.id] ?? "OTHER") === "BLOOD_PRESSURE",
);
// Single round-trip for all medications instead of N+1: one query keyed
// on `medicationId IN (...)`, then group in memory.
const allEvents = medications.length
? await prisma.medicationIntakeEvent.findMany({
where: {
medicationId: { in: medications.map((m) => m.id) },
userId,
// v1.7.0 sync — exclude tombstoned rows.
deletedAt: null,
scheduledFor: { gte: ninetyDaysAgo },
},
orderBy: { scheduledFor: "desc" },
})
: [];
const eventsByMed = new Map<string, typeof allEvents>();
for (const ev of allEvents) {
const list = eventsByMed.get(ev.medicationId);
if (list) list.push(ev);
else eventsByMed.set(ev.medicationId, [ev]);
}
for (const med of medications) {
const events = eventsByMed.get(med.id) ?? [];
const mapped = events.map((e) => ({
takenAt: e.takenAt,
skipped: e.skipped,
scheduledFor: e.scheduledFor,
}));
// v1.7.0 — engine-routed denominator.
const medicationContext = buildComplianceMedicationContext(
med,
lastNonSkippedTakenAt(mapped),
user.timezone || DEFAULT_TIMEZONE,
);
const c7 = calculateCompliance(mapped, med.schedules, 7, med.createdAt, {
medicationContext,
});
const c30 = calculateCompliance(mapped, med.schedules, 30, med.createdAt, {
medicationContext,
});
medCompliance.push({
id: med.id,
name: med.name,
dose: med.dose,
category: categoryMap[med.id] ?? "OTHER",
// v1.40 (#1041) — the label of a custom category; null for a built-in.
categoryLabel: resolvedCategories[med.id]?.categoryLabel ?? null,
compliance7: c7.rate,
compliance30: c30.rate,
streak: c7.streak,
taken7: c7.taken,
skipped7: c7.skipped,
missed7: c7.missed,
});
if ((categoryMap[med.id] ?? "OTHER") === "BLOOD_PRESSURE") {
bpMedicationEvents.push(
...events.map((e) => ({
scheduledFor: e.scheduledFor,
takenAt: e.takenAt,
skipped: e.skipped,
})),
);
}
}
// ── Correlation: BP medication continuity × systolic BP ──
// Uses the same daily BP_SYS series the aggregator pre-computed —
// no second round-trip. The intake-side aggregation matches the
// legacy code: continuity = min(1, taken / expectedPerDay), pairing
// every day that had a sys reading.
let bpMedicationCorrelation: {
r: number;
strength: string;
n: number;
medicationCount: number;
} | null = null;
const bpMedicationScatterData: Array<{
continuityPct: number;
sysBP: number;
}> = [];
const expectedBpIntakesPerDay = bpMedications.reduce(
(sum, med) => sum + med.schedules.length,
0,
);
if (expectedBpIntakesPerDay > 0) {
// Daily systolic means (already keyed YYYY-MM-DD) from the SQL pass.
// v1.32.17 — `sysDaily` (UTC `date_trunc` from the aggregator's
// `dailyByType`) and `takenByDay` below (intake `scheduledFor`
// truncated to UTC) both live in the SAME UTC day-key space and are
// internally consistent with each other — this join is NOT broken and
// stays as is. Only the mood pairings above moved to a separately
// derived local-day series (their partner, the mood series, is
// local-day-keyed). This correlation migrates to local when the
// measurement-rollup tier buckets per-user tz (R12), not in this hotfix
// — re-keying it here without the local-bucketed rollup would mean
// per-request raw re-aggregation.
const sysByDay = new Map<string, number>();
for (const row of sysDaily) {
sysByDay.set(row.day, row.value);
}
const takenByDay = new Map<string, number>();
// Days that carry an intake record of ANY kind. A logged skip is a real
// zero and belongs in the series; a day the person never touched is not a
// zero, it is nothing, and pairing it against that day's systolic reading
// invented a data point they never produced. Absence is not zero, here as
// everywhere else in this project, so the unlogged day drops out of both
// the scatter and the coefficient rather than dragging continuity down.
const loggedDays = new Set<string>();
for (const event of bpMedicationEvents) {
// eslint-disable-next-line healthlog/no-utc-day-key -- UTC by design: pairs intake days with the DAY rollup buckets in sysByDay, which are UTC days
const dayKey = event.scheduledFor.toISOString().slice(0, 10);
loggedDays.add(dayKey);
if (event.skipped || !event.takenAt) continue;
takenByDay.set(dayKey, (takenByDay.get(dayKey) ?? 0) + 1);
}
const pairs: Array<{ a: number; b: number; date: Date }> = [];
for (const [dayKey, avgSys] of sysByDay.entries()) {
if (!loggedDays.has(dayKey)) continue;
const taken = takenByDay.get(dayKey) ?? 0;
const continuity = Math.min(1, taken / expectedBpIntakesPerDay);
pairs.push({
a: continuity,
b: avgSys,
date: dayKeyAsUtcMidnight(dayKey),
});
bpMedicationScatterData.push({
continuityPct: Math.round(continuity * 100),
sysBP: Math.round(avgSys * 10) / 10,
});
}
const corr = significantPearsonCorrelation(pairs);
if (corr) {
bpMedicationCorrelation = {
...corr,
medicationCount: bpMedications.length,
};
}
}
// Generate alerts
const alerts = generateAlerts({
bmi,
bpAvgSys: summaries.BLOOD_PRESSURE_SYS?.avg30 ?? null,
bpAvgDia: summaries.BLOOD_PRESSURE_DIA?.avg30 ?? null,
bpPctInTarget,
weightSlope30: summaries.WEIGHT?.slope30?.slope ?? null,
pulseAvg30: summaries.PULSE?.avg30 ?? null,
pulseAnomalyCount: summaries.PULSE?.anomalyCount ?? 0,
medications: medCompliance.map((m) => ({
name: m.name,
compliance7: m.compliance7,
compliance30: m.compliance30,
})),
});
// Data span — derived from the earliest 90-day measurement.
const dataSpanDays = aggregate.firstMeasurementAt
? Math.ceil(
(Date.now() - aggregate.firstMeasurementAt.getTime()) /
(24 * 60 * 60 * 1000),
)
: 0;
annotate({
action: { name: "insights.comprehensive" },
meta: {
totalMeasurements: aggregate.totalMeasurements,
moodEntries: moodEntryCount,
medications: medications.length,
},
});
return {
summaries,
bmi,
bmiClassification,
bpClassification,
bpPctInTarget,
bpTargets,
weightBpCorrelation,
scatterData,
bpMedicationCorrelation,
bpMedicationScatterData,
moodSummary,
moodBpCorrelation,
moodBpScatterData,
moodWeightCorrelation,
moodWeightScatterData,
moodPulseCorrelation,
moodPulseScatterData,
medications: medCompliance,
alerts,
// Presence only: no client is built and no token is refreshed on a read.
hasProvider: await probeProviderPresence(userId),
dataSpanDays,
totalMeasurements: aggregate.totalMeasurements,
};
}
// ─────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────
/**
* v1.32.17 — derive exact local-day mean series from raw measurement rows,
* keyed by the user's timezone via `userDayKey`. The mood series is
* local-day-keyed (since v1.32.12), and `pairMoodWithDaily` joins on an
* exact YYYY-MM-DD key, so its metric partners must be local-day-keyed too.
* A UTC-day mean (from `dailyByType`) cannot be relabeled into a local-day
* mean, so the mean is recomputed here from raw rows already on the request
* path. `userDayKey` formats via a memoised `Intl.DateTimeFormat` — no
* offset arithmetic — so DST-transition days (23 h / 25 h) bucket correctly
* and the result is host-TZ-independent (CI runs UTC). `round2` mirrors the
* aggregator's `ROUND(AVG, 2)` so correlation thresholds don't drift.
* Returns `{ day, value }[]` sorted ASC by day, the shape
* `pairMoodWithDaily` expects.
*/
function deriveDailyLocalMeans(
rows: Array<{ measuredAt: Date; value: number }>,
tz: string,
): Array<{ day: string; value: number }> {
const byDay = new Map<string, { sum: number; count: number }>();
for (const r of rows) {
const key = userDayKey(r.measuredAt, tz);
const cur = byDay.get(key) ?? { sum: 0, count: 0 };
cur.sum += r.value;
cur.count += 1;
byDay.set(key, cur);
}
return Array.from(byDay.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([day, stats]) => ({
day,
value: Math.round((stats.sum / stats.count) * 100) / 100,
}));
}
/**
* Daily-key inner join between two daily-aggregated series. Returns
* `PairedPoint` shapes compatible with `significantPearsonCorrelation`.
*
* Inputs are sorted ASC by `day` (the aggregator's ORDER BY
* guarantees that). The walk is O(n + m) merge-join style instead of
* Map.lookup-per-element so it scales with the worst-case 90 buckets
* x 90 buckets case without surprises.
*/
function joinDailyByDay(
a: Array<{ day: string; value: number }>,
b: Array<{ day: string; value: number }>,
): PairedPoint[] {
const pairs: PairedPoint[] = [];
let i = 0;
let j = 0;
while (i < a.length && j < b.length) {
const ai = a[i];
const bj = b[j];
if (ai.day === bj.day) {
pairs.push({
a: ai.value,
b: bj.value,
date: new Date(`${ai.day}T12:00:00.000Z`),
});
i += 1;
j += 1;
} else if (ai.day < bj.day) {
i += 1;
} else {
j += 1;
}
}
return pairs;
}
/**
* Pair the daily mood series (left side) with a daily-aggregated
* metric (right side). Matches the legacy `buildMoodMetricPairs`
* helper byte-for-byte: same daily YYYY-MM-DD key, same
* `T12:00:00.000Z` anchor for the `date` field.
*/
function pairMoodWithDaily(
dailyMood: Array<{ day: string; value: number }>,
dailyMetric: Array<{ day: string; value: number }>,
): PairedPoint[] {
if (dailyMood.length === 0 || dailyMetric.length === 0) return [];
const metricByDay = new Map<string, number>();
for (const m of dailyMetric) {
metricByDay.set(m.day, m.value);
}
const pairs: PairedPoint[] = [];
for (const mood of dailyMood) {
const metric = metricByDay.get(mood.day);
if (metric !== undefined) {
pairs.push({
a: mood.value,
b: metric,
date: new Date(`${mood.day}T12:00:00.000Z`),
});
}
}
return pairs;
}
File diff suppressed because it is too large. Load diff
@@ -31,7 +31,7 @@ const CALL_SITES: Array<{ file: string; surface: string }> = [
surface: "insight targets",
},
{
file: "src/app/api/insights/comprehensive/route.ts",
file: "src/lib/insights/comprehensive-overview/medication-compliance.ts",
surface: "comprehensive insight",
},
{
+133
View File
@@ -0,0 +1,133 @@
/**
* The generating POST's cache short-circuit, and the cached payload it
* degrades to when the hour's generation quota is spent.
*
* A fresh cache WITHOUT a briefing must not satisfy a briefing-expecting
* caller: after a failed nightly warm the cached payload can sit inside the
* 24 h window all day, and an unconditional short-circuit served it to every
* POST (three regenerate attempts, three `cached: true` responses, zero
* generations). The short-circuit holds only when the cached payload carries
* today's briefing OR the account has no usable provider (regenerating would
* be futile — serve what exists). When the briefingless fall-through is
* rate-limited, the cached payload is served after all, so a POST-as-read
* client degrades to the old behaviour instead of a 429.
*/
import { isLegacyInsightPayload } from "@/lib/ai/legacy-payload";
import {
generatedOnLocalToday,
insightsPayloadForToday,
} from "@/lib/daily/briefing-today-read";
import { readBriefingGeneratedAt } from "@/lib/insights/briefing-generated-at";
import { cachedPayloadCarriesBriefing } from "@/lib/insights/briefing-payload";
import { hasUsableStatusProvider } from "@/lib/insights/status-provider";
import { annotate } from "@/lib/logging/context";
import type { GenerationContext } from "./context";
/** A fresh cached payload kept aside for the quota fallback. */
export interface CachedFallback {
cached: unknown;
legacyPayload: boolean;
cachedAt: Date;
}
export type CacheDecision =
| { kind: "serve"; data: Record<string, unknown> }
| { kind: "generate"; fallback: CachedFallback | null };
export async function briefingFromCache(
ctx: GenerationContext,
forceRefresh: boolean,
): Promise<CacheDecision> {
const { dbUser, cachedText, userId, locale } = ctx;
// A cache tagged with another language is not this reader's cache and
// never short-circuits; the generation writes the reader's own.
const cachedLocaleMatches =
dbUser?.insightsCachedLocale == null ||
dbUser.insightsCachedLocale === locale;
if (
!forceRefresh &&
dbUser?.insightsCachedAt &&
cachedText &&
cachedLocaleMatches &&
Date.now() - dbUser.insightsCachedAt.getTime() < 24 * 60 * 60 * 1000
) {
try {
const cached = JSON.parse(cachedText);
// Legacy payloads (pre-rationale) are flagged so the UI can show a
// "regenerate for new explainability features" CTA. No automatic
// regeneration: that would burn rate-limit tokens on a cache hit
// silently. User-initiated only.
const legacyPayload = isLegacyInsightPayload(cached);
// A briefing generated on an earlier day is not today's read: like a
// briefingless cache, it does not short-circuit a provider-backed
// account, and degrades to the cached payload on a 429.
const carriesBriefing =
cachedPayloadCarriesBriefing(cachedText) &&
generatedOnLocalToday(
readBriefingGeneratedAt(cachedText),
ctx.timezone,
new Date(),
);
if (carriesBriefing || !(await hasUsableStatusProvider(userId))) {
annotate({
action: { name: "insights.generate" },
meta: {
cached: true,
legacyPayload,
briefingless: !carriesBriefing,
},
});
return {
kind: "serve",
data: {
insights: await insightsPayloadForToday(cached, {
userId,
timezone: ctx.timezone,
language: locale,
}),
cached: true,
cachedAt: dbUser.insightsCachedAt,
legacyPayload,
},
};
}
annotate({
action: { name: "insights.generate.briefingless_cache_bypassed" },
meta: { locale },
});
return {
kind: "generate",
fallback: {
cached,
legacyPayload,
cachedAt: dbUser.insightsCachedAt,
},
};
} catch {
// Invalid cache, regenerate
}
}
return { kind: "generate", fallback: null };
}
/** The cached payload, served when the hour's quota refused a generation. */
export async function serveCachedFallback(
ctx: GenerationContext,
fallback: CachedFallback,
): Promise<Record<string, unknown>> {
annotate({
action: { name: "insights.generate" },
meta: { cached: true, rate_limited_fallback: true },
});
return {
insights: await insightsPayloadForToday(fallback.cached, {
userId: ctx.userId,
timezone: ctx.timezone,
language: ctx.locale,
}),
cached: true,
cachedAt: fallback.cachedAt,
legacyPayload: fallback.legacyPayload,
};
}
+202
View File
@@ -0,0 +1,202 @@
/**
* The on-demand briefing's commit: the outbound safety screen, the two scope
* guards, the cache write, and everything that follows a written briefing.
*
* Contract with the client (`AdvisorSettleState` in
* `src/components/insights/use-insights-advisor.ts`, refs #786): on success
* this writes the fresh briefing to the user cache (`insightsCachedAt`
* advances) — even when the client has long since aborted its 45 s request —
* and a failed inline generation leaves a failure marker the read reports as
* `generationFailed`. The client settles a timed-out regenerate on exactly
* those two signals, so renaming or short-circuiting either write breaks
* that convergence.
*/
import { prisma } from "@/lib/db";
import { auditLog } from "@/lib/auth/audit";
import { getSelfContextTextForUser } from "@/lib/ai/coach/about-me";
import { screenInsightPayloadProse } from "@/lib/ai/safety/insight-payload-screen";
import { invalidateUserInsights } from "@/lib/cache/invalidate";
import { insightsPayloadForToday } from "@/lib/daily/briefing-today-read";
import { recordBriefingFailure } from "@/lib/insights/briefing-failure-marker";
import { withBriefingGeneratedAt } from "@/lib/insights/briefing-generated-at";
import { applyBriefingShortForm } from "@/lib/insights/briefing-short-form";
import { insightsCacheWrite } from "@/lib/insights/cached-text-cipher";
import { enqueueStatusRefillForUser } from "@/lib/insights/comprehensive-generate";
import { hashInsightSnapshot } from "@/lib/insights/snapshot-hash";
import { normalizeLocale } from "@/lib/insights/status-shared";
import { annotate } from "@/lib/logging/context";
import type { BriefingCall } from "./completion";
import type { BriefingRefusal } from "./context";
import type { PreparedFeatures } from "./features";
import type { GroundedReply } from "./grounding";
export type CommitOutcome =
| { ok: true; data: Record<string, unknown> }
| { ok: false; refusal: BriefingRefusal };
/** Write the briefing, guarded on the scope the prompt was built from. */
async function writeBriefing(
call: BriefingCall,
prepared: PreparedFeatures,
stored: object,
): Promise<boolean> {
const { ctx, prompt } = call;
const { scope, locale } = ctx;
const committed = await prisma.user.updateMany({
where: {
id: ctx.userId,
insightsPrivacyMode: scope.mode,
insightsExcludeMetrics: { equals: scope.excludeMetrics },
locale: scope.locale,
dateOfBirth: scope.dateOfBirth,
gender: scope.gender,
heightCm: scope.heightCm,
displayName: scope.displayName,
},
data: {
insightsCachedAt: new Date(),
...insightsCacheWrite(JSON.stringify(stored)),
insightsCachedLocale: locale,
insightsSnapshotHash: hashInsightSnapshot({
features: prepared.compactFeatures,
aboutMe: prompt.aboutMe ?? null,
comparisonBaseline: prompt.comparisonSnapshot?.baseline ?? "none",
generationLocale: locale,
...(prompt.briefingForm === "short"
? { briefingForm: prompt.briefingForm }
: {}),
}),
},
});
return committed.count > 0;
}
/** What follows a written briefing: the marker, the cards, the audit row. */
async function afterWrite(
call: BriefingCall,
reply: GroundedReply,
ipAddress: string | null,
fallbackHopCount: number,
): Promise<void> {
const { ctx } = call;
const { userId, locale } = ctx;
const { result, workingProviderType } = reply;
// The grounding omission persists (marker AFTER the cache write so it is
// newer than `insightsCachedAt`). The transient `briefingOmittedReason` on
// the response only reached the caller that forced the regenerate; every
// later read showed a generic "no briefing yet". The dated marker lets the
// read surface the same honest "briefing withheld" state until the next
// successful generation.
if (reply.briefingOmittedReason === "ungrounded") {
void recordBriefingFailure({
userId,
reason: "briefing-ungrounded",
locale,
});
}
// No blanket per-status eviction (nuking ~45 cache rows per manual
// regenerate deleted every hash baseline and caused a card-regeneration
// storm). The manual regenerate takes the cards along through the
// hash-gated queue: every scope the user has data for is enqueued, the
// worker forces each generator past its same-day cache read, and the
// content-hash gate regenerates exactly the cards whose data changed while
// refreshing the unchanged ones for free. This is the user's escape hatch
// for a card they can SEE is stale — without it the cards had no refresh
// path until the next nightly warm.
const refillScopes = await enqueueStatusRefillForUser(
userId,
normalizeLocale(locale),
);
annotate({
action: { name: "insights.generate.cards_refill" },
meta: { scopes: refillScopes },
});
// The dashboard snapshot embeds the pre-generated daily briefing
// read-only; drop it so the next snapshot carries the fresh briefing
// instead of the stale one.
invalidateUserInsights(userId);
await auditLog("insights.generate", {
userId,
ipAddress,
details: {
privacyMode: ctx.dbUser?.insightsPrivacyMode,
tokensUsed: result.tokensUsed,
providerType: result.providerType,
chainProviderType: workingProviderType,
fallbackHopCount,
model: result.model,
},
});
annotate({
action: { name: "insights.generate" },
meta: {
cached: false,
providerType: result.providerType,
chainProviderType: workingProviderType,
fallbackHopCount,
model: result.model,
tokensUsed: result.tokensUsed,
},
});
}
export async function commitBriefing(
call: BriefingCall,
prepared: PreparedFeatures,
reply: GroundedReply,
options: { ipAddress: string | null; fallbackHopCount: number },
): Promise<CommitOutcome> {
const { ctx, prompt } = call;
const { userId, locale } = ctx;
let { insights } = reply;
// Outbound safety screen over the whole prose surface, before the
// persist. This path is user-initiated but writes the SAME cached payload
// the background generator writes, so a violation must not be persisted
// here either. The user is waiting, so unlike the background path they get
// an explicit error rather than silence — and because nothing was written,
// the previous cached insight is still what the app renders.
const screened = screenInsightPayloadProse(insights, locale);
if (screened) {
annotate({
action: { name: "insights.generate.outbound_blocked" },
meta: { locale, reason: screened },
});
return { ok: false, refusal: "outbound-screened" };
}
const currentSelfContext = await getSelfContextTextForUser(userId, locale);
if (currentSelfContext !== prompt.aboutMe) {
return { ok: false, refusal: "profile-scope-changed" };
}
if (prompt.briefingForm === "short") {
insights = applyBriefingShortForm(insights);
}
const stored = withBriefingGeneratedAt(insights, new Date());
if (!(await writeBriefing(call, prepared, stored))) {
return { ok: false, refusal: "scope-changed" };
}
await afterWrite(call, reply, options.ipAddress, options.fallbackHopCount);
return {
ok: true,
data: {
insights: await insightsPayloadForToday(stored, {
userId,
timezone: ctx.timezone,
language: locale,
}),
cached: false,
legacyPayload: false,
// Additive: non-null when the grounding gate stripped the briefing
// from THIS generation, so the card can explain the hole (#470).
briefingOmittedReason: reply.briefingOmittedReason,
},
};
}
@@ -0,0 +1,192 @@
/**
* The on-demand briefing's provider calls, and what a failed call means for
* the person waiting on it.
*/
import { resolveInsightsMaxTokens } from "@/lib/ai/ai-budgets";
import { providerFailureKind } from "@/lib/ai/failure-kind";
import { buildSystemPromptWithReferences } from "@/lib/ai/prompts/insight-generator";
import {
AllProvidersFailedError,
type ProviderChainResolved,
type RunRawWithFallbackResult,
} from "@/lib/ai/provider-runner";
import {
BriefingBudgetExceededError,
runBriefingCompletion,
} from "@/lib/insights/briefing-provider";
import { recordBriefingFailure } from "@/lib/insights/briefing-failure-marker";
import { annotate } from "@/lib/logging/context";
import type { BriefingRefusal, GenerationContext } from "./context";
import type { BriefingPrompt } from "./prompt";
export interface BriefingCall {
ctx: GenerationContext;
chain: ProviderChainResolved[];
prompt: BriefingPrompt;
}
/** One briefing completion with the route's fixed parameters. */
export function runBriefingCall(
call: BriefingCall,
options: {
userPrompt: string;
stage: "generate" | "grounding-retry";
reasoning: boolean;
},
): Promise<RunRawWithFallbackResult> {
const { ctx, chain, prompt } = call;
return runBriefingCompletion({
userId: ctx.userId,
chain,
// The strict prompt carries GROUND RULE 8 — emit a top-level
// `dailyBriefing` block when the snapshot has analysable signal — plus
// the trendAnnotations and storyboardAnnotations rules. The legacy
// `getInsightsSystemPrompt` returned the old `{changed, stable, drivers,
// …}` shape and never asked the model for dailyBriefing, so the hero
// strip's "Re-run analysis" button kept producing a payload with no
// briefing block. The reply tolerates the strict shape via
// `insightResultSchema.safeParse`'s soft fallback to the parsed object.
systemPrompt: buildSystemPromptWithReferences(
ctx.locale,
prompt.referenceMetrics,
),
userPrompt: options.userPrompt,
temperature: 0.3,
// Env-tunable output ceiling (INSIGHTS_MAX_TOKENS, default 2500, #470).
// A fixed 1500 truncated verbose models' briefing JSON mid-string →
// generic invalid-JSON 422.
maxTokens: resolveInsightsMaxTokens(),
// A wide upstream budget, honouring the per-user response-timeout
// setting, so the reasoning-heavy briefing generation is not aborted at
// the client's 60 s default on large accounts.
timeoutMs: ctx.effectiveTimeoutMs,
stage: options.stage,
// A briefing asked for by hand reasons exactly like the scheduled one;
// the retries repair a reply and do not.
...(options.reasoning ? { reasoningJob: "daily_briefing" as const } : {}),
});
}
export type FirstCompletion =
| {
ok: true;
result: RunRawWithFallbackResult["result"];
workingProviderType: string;
fallbackHopCount: number;
}
| { ok: false; refusal: BriefingRefusal };
/** The refusal for a chain on which every provider failed. */
function chainRefusal(e: AllProvidersFailedError): BriefingRefusal {
annotate({
meta: {
insights_chain_outcome: "all-failed",
insights_chain_attempts: e.attempts.length,
insights_chain_first_provider: e.attempts[0]?.providerType ?? null,
insights_chain_first_status: e.attempts[0]?.httpStatus ?? null,
},
});
// The most specific user-facing message available. Auth-class failures
// across the entire chain mean every provider's credential is bad — the
// "check your API key" wording tells the user where to act. 429 across the
// chain is rate-limit; 5xx is upstream brown-out; transport-only
// (ECONNRESET, no httpStatus) maps to the generic connection hint so users
// on flaky networks don't see "rate-limited".
if (e.attempts.every((a) => a.httpStatus === 401 || a.httpStatus === 403)) {
return "provider-rejected";
}
if (e.attempts.every((a) => a.httpStatus === 429)) {
return "provider-rate-limited";
}
if (e.attempts.some((a) => a.httpStatus !== null && a.httpStatus >= 500)) {
return "provider-unavailable";
}
if (e.attempts.every((a) => a.httpStatus === null || a.httpStatus <= 0)) {
return "provider-connection";
}
return "provider-unavailable";
}
/**
* The refusal for a single provider error that did not cascade (e.g. a
* custom 4xx from a self-hosted local model). Cloudflare rewrites 5xx to its
* own HTML error page, which breaks `await res.json()` on the dashboard, so
* the statuses map onto the ai/test categorisation.
*/
function providerRefusal(e: unknown): BriefingRefusal {
const err = e as Error & { httpStatus?: number; bodyExcerpt?: string };
annotate({
meta: {
insights_provider_error: err.message.slice(0, 500),
insights_provider_status: err.httpStatus ?? null,
insights_provider_body_excerpt: err.bodyExcerpt?.slice(0, 500) ?? null,
},
});
const status = err.httpStatus ?? 0;
if (status === 401 || status === 403) return "provider-rejected";
if (status === 429) return "provider-rate-limited";
if (status >= 500) return "provider-unavailable";
return "provider-connection";
}
export async function runFirstCompletion(
call: BriefingCall,
): Promise<FirstCompletion> {
try {
const fallback = await runBriefingCall(call, {
userPrompt: call.prompt.userPrompt,
stage: "generate",
reasoning: true,
});
return {
ok: true,
result: fallback.result,
workingProviderType: fallback.workingProvider.providerType,
fallbackHopCount: fallback.fallbackHops.length,
};
} catch (e) {
// The day's token ceiling refused the reservation — no provider was
// contacted, so this is NOT a provider failure and must not record a
// briefing-failure marker (which would make the read path claim the
// refresh broke). The hourly rate limit bounds request frequency; this
// bounds cost.
if (e instanceof BriefingBudgetExceededError) {
return { ok: false, refusal: "budget-exceeded" };
}
// A dated failure marker (no cache row is written on this path, so the
// last good briefing stays intact). The read path pairs the preserved
// text with a discreet "couldn't refresh" hint, or shows a retry empty
// state when there is no last good text. The upstream status rides
// along so the hint can tell an auth / rate-limit failure apart from a
// plain timeout: for an all-providers-failed error the first hop's
// status is the most representative; a single provider error exposes its
// own `httpStatus`.
const failureHttpStatus =
e instanceof AllProvidersFailedError
? (e.attempts[0]?.httpStatus ?? null)
: typeof (e as { httpStatus?: number }).httpStatus === "number"
? (e as { httpStatus?: number }).httpStatus
: null;
void recordBriefingFailure({
userId: call.ctx.userId,
reason:
e instanceof AllProvidersFailedError
? "all-providers-failed"
: "provider-error",
locale: call.ctx.locale,
httpStatus: failureHttpStatus,
failureKind:
e instanceof AllProvidersFailedError
? (e.attempts[0]?.failureKind ?? null)
: providerFailureKind(e).failure,
});
return {
ok: false,
refusal:
e instanceof AllProvidersFailedError
? chainRefusal(e)
: providerRefusal(e),
};
}
}
@@ -0,0 +1,137 @@
/**
* What an on-demand briefing generation reads before it decides anything:
* the account row, the cached payload, the scope columns the commit guard
* matches on, the response timeout, the reader's language and zone.
*/
import type { NextRequest } from "next/server";
import type { Prisma } from "@/generated/prisma/client";
import { prisma } from "@/lib/db";
import { AI_BUDGETS } from "@/lib/ai/ai-budgets";
import { resolveEffectiveTimeoutMs } from "@/lib/ai/effective-timeout";
import type { Locale } from "@/lib/i18n/config";
import { resolveServerLocale } from "@/lib/i18n/server-locale";
import {
INSIGHTS_CACHE_SELECT,
openInsightsCachedText,
} from "@/lib/insights/cached-text-cipher";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
export const GENERATION_USER_SELECT = {
insightsPrivacyMode: true,
insightsCachedAt: true,
...INSIGHTS_CACHE_SELECT,
insightsCachedLocale: true,
timezone: true,
// The per-user opt-out list mirroring the Coach settings. Filtered off
// `features` before serialisation so the model never sees the excluded
// blocks.
insightsExcludeMetrics: true,
locale: true,
// The profile for the derived-signal briefing detector.
dateOfBirth: true,
gender: true,
heightCm: true,
// The first name for the sparse, hash-gated briefing opener.
displayName: true,
// The per-user response timeout (seconds), threaded onto the generation
// calls so a slow self-hosted backend honours the setting.
aiResponseTimeoutSeconds: true,
// The reader's units; the feature set is converted into them before it is
// serialised, hashed and checked for grounding.
unitPreference: true,
glucoseUnit: true,
} as const;
export type GenerationUserRow = Prisma.UserGetPayload<{
select: typeof GENERATION_USER_SELECT;
}>;
/**
* The columns the commit guard matches on, captured at read time.
*
* Every User write bumps `updatedAt`, including `resolveProviderChain()`'s
* OAuth token-refresh update later in the same request, so keying the
* optimistic-concurrency check on it produced a false 409 whenever a
* provider credential happened to refresh mid-generation. These are exactly
* the selected columns that feed the prompt; requiring an exact match at
* commit still catches a genuine privacy-mode / exclude-list / locale /
* profile change without colliding on unrelated column writes.
*/
export interface GenerationScope {
mode: GenerationUserRow["insightsPrivacyMode"];
excludeMetrics: GenerationUserRow["insightsExcludeMetrics"];
locale: GenerationUserRow["locale"];
dateOfBirth: GenerationUserRow["dateOfBirth"];
gender: GenerationUserRow["gender"];
heightCm: GenerationUserRow["heightCm"];
displayName: GenerationUserRow["displayName"];
}
export interface GenerationContext {
userId: string;
dbUser: GenerationUserRow | null;
cachedText: string | null;
scope: GenerationScope;
effectiveTimeoutMs: number;
locale: Locale;
timezone: string;
}
/**
* Why a generation ended without a briefing. The route owns the status and
* the wording for each.
*/
export type BriefingRefusal =
| "payload-too-large"
| "budget-exceeded"
| "provider-rejected"
| "provider-rate-limited"
| "provider-unavailable"
| "provider-connection"
| "response-truncated"
| "response-invalid-json"
| "outbound-screened"
| "profile-scope-changed"
| "scope-changed";
export async function loadGenerationContext(args: {
request: NextRequest;
userId: string;
userLocale: string | null;
}): Promise<GenerationContext> {
const { request, userId } = args;
const dbUser = await prisma.user.findUnique({
where: { id: userId },
select: GENERATION_USER_SELECT,
});
const cachedText = openInsightsCachedText(dbUser);
const scope: GenerationScope = {
mode: dbUser?.insightsPrivacyMode ?? "aggregated",
excludeMetrics: dbUser?.insightsExcludeMetrics ?? [],
locale: dbUser?.locale ?? null,
dateOfBirth: dbUser?.dateOfBirth ?? null,
gender: dbUser?.gender ?? null,
heightCm: dbUser?.heightCm ?? null,
displayName: dbUser?.displayName ?? null,
};
const effectiveTimeoutMs = resolveEffectiveTimeoutMs(
dbUser?.aiResponseTimeoutSeconds,
AI_BUDGETS.comprehensive.timeoutMs,
);
const locale = await resolveServerLocale({
request,
userLocale: dbUser?.locale ?? args.userLocale ?? null,
});
return {
userId,
dbUser,
cachedText,
scope,
effectiveTimeoutMs,
locale,
timezone: dbUser?.timezone ?? DEFAULT_TIMEZONE,
};
}
@@ -0,0 +1,150 @@
/**
* The feature set an on-demand briefing is written from: the bounded read,
* its downgrade ladder for an oversized payload, the user's exclusions, the
* reader's units, and the compact wire form.
*/
import { compactSections } from "@/lib/ai/prompts/compact-sections";
import { applyInsightsExcludeFilter } from "@/lib/insights/exclude-filter";
import { getCachedFeatures } from "@/lib/insights/feature-cache";
import {
BRIEFING_FEATURE_WINDOW_DAYS,
extractFeatures,
FeaturesPayloadTooLargeError,
} from "@/lib/insights/features";
import { featuresInReaderUnits } from "@/lib/insights/features-units";
import { annotate } from "@/lib/logging/context";
import { resolveUnitPreferences } from "@/lib/measurements/display-transform";
import type { GenerationContext } from "./context";
export type BriefingFeatures = Awaited<ReturnType<typeof extractFeatures>>;
/**
* The blocks dropped when even the aggregated shape crosses the ceiling
* (very large medication history + multi-year context), before one last try.
*/
const MAX_DOWNGRADE_TOKENS: ReadonlyArray<string> = [
"anthropometrics",
"medications",
"compliance",
"sleep",
"steps",
"hrv",
"resting_hr",
];
interface Downgrade {
payloadDowngraded: boolean;
payloadHardDowngraded: boolean;
payloadOversizeBytes: number | null;
}
/**
* The bounded feature read, computed once per request through the scope the
* route opened. `extractFeatures` enforces a 5 MB ceiling on the serialised
* payload and throws `FeaturesPayloadTooLargeError` past it; the raw
* (bucketed) shape downgrades to the aggregated one rather than 500-ing, so
* the user still gets an insight, and the annotation surfaces the
* regression to ops. Null when every rung is still too large.
*/
async function readWithDowngrade(
userId: string,
includeRaw: boolean,
downgrade: Downgrade,
): Promise<BriefingFeatures | null> {
// The bulk read is bounded to a recent window (all-time extremes are
// sourced separately, see features.ts); the ladder is the safety net for
// the rare oversize payload.
const featureWindow = { sinceDays: BRIEFING_FEATURE_WINDOW_DAYS };
const read = (raw: boolean) =>
getCachedFeatures({
userId,
includeRaw: raw,
sinceDays: featureWindow.sinceDays,
compute: () => extractFeatures(userId, raw, featureWindow),
});
try {
return await read(includeRaw);
} catch (err) {
if (!(err instanceof FeaturesPayloadTooLargeError)) throw err;
downgrade.payloadDowngraded = true;
downgrade.payloadOversizeBytes = err.sizeBytes;
}
try {
return await read(false);
} catch (retryErr) {
if (!(retryErr instanceof FeaturesPayloadTooLargeError)) throw retryErr;
// The aggregated shape ALSO crossed the ceiling. Drop the optional
// context blocks via the exclude filter and try once more.
downgrade.payloadHardDowngraded = true;
downgrade.payloadOversizeBytes = retryErr.sizeBytes;
}
try {
return applyInsightsExcludeFilter(await read(false), MAX_DOWNGRADE_TOKENS);
} catch (finalErr) {
// Even the last read blew up. Annotate and refuse rather than let the
// uncaught throw 500 out.
annotate({
meta: {
insights_payload_too_large: true,
insights_payload_oversize_bytes:
finalErr instanceof FeaturesPayloadTooLargeError
? finalErr.sizeBytes
: downgrade.payloadOversizeBytes,
},
});
return null;
}
}
export interface PreparedFeatures {
features: BriefingFeatures;
compactFeatures: Record<string, unknown>;
featuresJson: string;
units: ReturnType<typeof resolveUnitPreferences>;
}
export async function prepareBriefingFeatures(
ctx: GenerationContext,
): Promise<PreparedFeatures | null> {
const { dbUser, userId } = ctx;
const includeRaw = dbUser?.insightsPrivacyMode === "raw";
const downgrade: Downgrade = {
payloadDowngraded: false,
payloadHardDowngraded: false,
payloadOversizeBytes: null,
};
let features = await readWithDowngrade(userId, includeRaw, downgrade);
if (features === null) return null;
// The user's exclude-metrics opt-out list, applied BEFORE serialisation so
// the model never sees the dropped blocks.
const excludeList = dbUser?.insightsExcludeMetrics ?? [];
features = applyInsightsExcludeFilter(features, excludeList);
// The reader's units, after all canonical arithmetic: the prompt, the
// stored hash and the grounding check read the same converted figures.
const units = resolveUnitPreferences({
unitPreference: dbUser?.unitPreference,
glucoseUnit: dbUser?.glucoseUnit,
});
features = featuresInReaderUnits(features, units);
// Zero-row blocks are dropped so the prompt never carries labelled-empty
// sections (`"sleep": []`, `"medications": []`, etc.), which made the
// model narrate "there are no medications in your data" when the user
// explicitly excluded the block.
const compactFeatures = compactSections(
features as unknown as Record<string, unknown>,
);
// Compact on the wire: indentation is about 40% of a pretty-printed
// payload, and the model reads the structure without it (#1217).
const featuresJson = JSON.stringify(compactFeatures);
if (downgrade.payloadDowngraded) {
annotate({
meta: {
insights_features_downgraded: true,
insights_features_oversize_bytes: downgrade.payloadOversizeBytes,
insights_features_hard_downgraded: downgrade.payloadHardDowngraded,
},
});
}
return { features, compactFeatures, featuresJson, units };
}
@@ -0,0 +1,96 @@
/**
* An on-demand briefing generation, from the resolved provider chain to the
* committed payload. The route owns authentication, the capability gate,
* the body, the hourly quota and the status of every refusal.
*/
import type { ProviderChainResolved } from "@/lib/ai/provider-runner";
import { assertConsentForChain } from "@/lib/ai/consent-guard";
import { resolveProvider, resolveProviderChain } from "@/lib/ai/provider";
import { recordBriefingFailure } from "@/lib/insights/briefing-failure-marker";
import { annotate } from "@/lib/logging/context";
import { commitBriefing } from "./commit";
import { runFirstCompletion, type BriefingCall } from "./completion";
import type { BriefingRefusal, GenerationContext } from "./context";
import { prepareBriefingFeatures } from "./features";
import { groundBriefing, parseBriefingReply } from "./grounding";
import { buildBriefingPrompt } from "./prompt";
/**
* The user's full provider chain, so a hard failure on the primary (401,
* 5xx, network) cascades to the next configured fallback rather than
* surfacing to the dashboard. An empty chain falls back to the
* single-provider resolution when the legacy `aiProvider` field still points
* at something usable, so accounts that never customised the chain keep
* working. Null when there is no provider at all.
*
* The consent gate runs on the resolved chain before anything leaves for a
* model. Required for the operator's global key (`admin-openai`); BYOK /
* local / ChatGPT-OAuth chains stay ungated. It throws ConsentRequiredError,
* which `apiHandler` answers with 403 + `consent.ai.required`.
*/
export async function resolveConsentedBriefingChain(
userId: string,
): Promise<ProviderChainResolved[] | null> {
const chain = await resolveProviderChain(userId);
if (chain.length === 0) {
const legacy = await resolveProvider(userId);
if (legacy.type === "none") return null;
chain.push({ providerType: "admin-openai", instance: legacy });
}
await assertConsentForChain({ userId, chain, surface: "insights" });
return chain;
}
export type GenerateOutcome =
| { ok: true; data: Record<string, unknown> }
| { ok: false; refusal: BriefingRefusal };
export async function generateBriefing(args: {
ctx: GenerationContext;
chain: ProviderChainResolved[];
ipAddress: string | null;
}): Promise<GenerateOutcome> {
const { ctx, chain } = args;
const prepared = await prepareBriefingFeatures(ctx);
if (prepared === null) return { ok: false, refusal: "payload-too-large" };
const prompt = await buildBriefingPrompt(ctx, prepared);
const call: BriefingCall = { ctx, chain, prompt };
const first = await runFirstCompletion(call);
if (!first.ok) return first;
let insights;
try {
insights = parseBriefingReply(first.result.content);
} catch {
// A 502 here triggers Cloudflare's HTML error rewrite, which breaks
// `await res.json()` on the client; the refusal stays a JSON 422 so the
// React Query mutation can read the body and surface a readable message.
void recordBriefingFailure({
userId: ctx.userId,
reason: "invalid-json",
locale: ctx.locale,
});
// Truncation-aware (#470): `finishReason === "length"` means the model
// hit the output-token ceiling and the JSON was cut off mid-string, which
// is a budget problem, not a model-quality one.
if (first.result.finishReason === "length") {
annotate({ meta: { insights_response_truncated: true } });
return { ok: false, refusal: "response-truncated" };
}
return { ok: false, refusal: "response-invalid-json" };
}
const reply = await groundBriefing(call, prepared, {
insights,
result: first.result,
workingProviderType: first.workingProviderType,
});
return commitBriefing(call, prepared, reply, {
ipAddress: args.ipAddress,
fallbackHopCount: first.fallbackHopCount,
});
}
@@ -0,0 +1,127 @@
/**
* The reply's parse and the daily-briefing number-grounding gate.
*
* The schema only checks the briefing's SHAPE; the gate asserts every
* number the briefing prose restates traces to a `features.signalsOfDay`
* figure, mirroring the recommendations citation cross-check. On a miss: one
* corrective retry, then the briefing block is dropped rather than shipping
* a fabricated number.
*/
import {
buildBriefingGroundingCorrection,
findUngroundedBriefingNumbers,
readBriefingBlock,
} from "@/lib/ai/briefing-grounding";
import type { RunRawWithFallbackResult } from "@/lib/ai/provider-runner";
import { insightResultSchema, type InsightResult } from "@/lib/ai/types";
import { annotate } from "@/lib/logging/context";
import { runBriefingCall, type BriefingCall } from "./completion";
import type { PreparedFeatures } from "./features";
export type BriefingInsights = InsightResult | Record<string, unknown>;
/** The parsed reply: the strict shape when it validates, the raw object if not. */
export function parseBriefingReply(content: string): BriefingInsights {
const parsed = JSON.parse(content);
// Try the strict schema first, fall back to the raw parsed object.
const validated = insightResultSchema.safeParse(parsed);
return validated.success ? validated.data : parsed;
}
export interface GroundedReply {
insights: BriefingInsights;
result: RunRawWithFallbackResult["result"];
workingProviderType: string;
/**
* Non-null when the gate stripped the briefing from THIS generation and no
* previous one could stand in, so the card can explain the hole instead
* of reading "no briefing yet" (#470).
*/
briefingOmittedReason: "ungrounded" | null;
}
/** The previous cached briefing, when the payload parses and carries one. */
function previousBriefing(cachedText: string | null): unknown {
if (!cachedText) return null;
try {
const prev = JSON.parse(cachedText) as Record<string, unknown>;
return prev.dailyBriefing ?? null;
} catch {
// Unparseable previous payload — nothing safe to stand in.
return null;
}
}
export async function groundBriefing(
call: BriefingCall,
prepared: PreparedFeatures,
reply: Omit<GroundedReply, "briefingOmittedReason">,
): Promise<GroundedReply> {
const { features } = prepared;
const { comparisonSnapshot } = call.prompt;
let { insights, result, workingProviderType } = reply;
let briefingOmittedReason: "ungrounded" | null = null;
const signals = features.signalsOfDay ?? null;
let ungrounded = findUngroundedBriefingNumbers(
readBriefingBlock(insights),
signals,
features,
comparisonSnapshot,
);
if (ungrounded.length > 0) {
annotate({
action: { name: "insights.generate.briefing_grounding_retry" },
meta: { ungroundedCount: ungrounded.length },
});
try {
const retry = await runBriefingCall(call, {
userPrompt: `${call.prompt.userPrompt}\n\n${buildBriefingGroundingCorrection(ungrounded)}`,
stage: "grounding-retry",
reasoning: false,
});
const retryInsights = parseBriefingReply(retry.result.content);
ungrounded = findUngroundedBriefingNumbers(
readBriefingBlock(retryInsights),
signals,
features,
comparisonSnapshot,
);
if (ungrounded.length === 0) {
insights = retryInsights;
result = retry.result;
workingProviderType = retry.workingProvider.providerType;
}
} catch {
// Retry failed to produce parseable JSON, or the day's ceiling refused
// the correction pass — either way fall through to the strip. An
// ungrounded figure is never persisted.
}
}
// Still ungrounded after the retry: the freshly generated briefing is
// discarded rather than shipped — the anti-fabrication guarantee is
// absolute here too. But rather than leave a hole, the PREVIOUS cached
// briefing stands in its place: it passed this identical gate when it was
// written, so it carries no fabricated figure, and a day-old grounded
// briefing beats a vanished one. This mirrors the background generator,
// which is the path that writes the same cached payload; the two must not
// disagree on disposal.
//
// `briefingOmittedReason` is set ONLY when no previous briefing exists to
// stand in, so the card's honest "a figure couldn't be verified" state
// still fires exactly when the user really has no briefing.
if (ungrounded.length > 0 && insights && typeof insights === "object") {
const fallbackBriefing = previousBriefing(call.ctx.cachedText);
(insights as Record<string, unknown>).dailyBriefing = fallbackBriefing;
briefingOmittedReason = fallbackBriefing == null ? "ungrounded" : null;
annotate({
action: { name: "insights.generate.briefing_grounding_stripped" },
meta: {
ungroundedCount: ungrounded.length,
briefing_fallback:
fallbackBriefing != null ? "previous-cached" : "stripped",
},
});
}
return { insights, result, workingProviderType, briefingOmittedReason };
}
+129
View File
@@ -0,0 +1,129 @@
/**
* The on-demand briefing's prompt: the feature snapshot, and every
* SYSTEM CONTEXT block that rides on it.
*/
import { metricsFromPresentSections } from "@/lib/ai/medical-references";
import {
buildAboutMeInsightBlock,
getSelfContextTextForUser,
} from "@/lib/ai/coach/about-me";
import { buildBriefingPersonalisationBlock } from "@/lib/ai/prompts/insight-generator";
import { buildUserPrompt } from "@/lib/ai/prompts/insight-system-prompt";
import { getAgeFromDateOfBirth } from "@/lib/analytics/pulse-targets";
import {
briefingFormForRecord,
buildBriefingShortFormBlock,
} from "@/lib/insights/briefing-short-form";
import { buildComparisonSnapshotForUser } from "@/lib/insights/comprehensive-generate";
import {
buildDerivedBriefingPrompt,
detectDerivedBriefingSignals,
} from "@/lib/insights/derived-briefing";
import {
buildGlp1PlateauPrompt,
detectGlp1Plateau,
} from "@/lib/insights/glp1-plateau";
import {
buildBriefingIllnessCycleContext,
buildBriefingIllnessCyclePrompt,
} from "@/lib/insights/illness-cycle-briefing";
import { toProfileSex } from "@/lib/profile/sex";
import { resolveUserTimezone } from "@/lib/tz/resolver";
import type { GenerationContext } from "./context";
import type { PreparedFeatures } from "./features";
export type BriefingPrompt = Awaited<ReturnType<typeof buildBriefingPrompt>>;
export async function buildBriefingPrompt(
ctx: GenerationContext,
prepared: PreparedFeatures,
) {
const { userId, dbUser, locale } = ctx;
const { features, featuresJson, units } = prepared;
// The persisted comparison toggle: when active, a compact prior-period
// snapshot built from the `features` summaries that already include
// `avg30LastMonth` / `avg30LastYear`. The narrative is default-on whenever
// the toggle is on — the pulldown is the single affordance.
const comparisonSnapshot = await buildComparisonSnapshotForUser(userId);
// GLP-1 plateau detector: a SYSTEM CONTEXT block when the active GLP-1
// medication has been on a stable dose for ≥21 days with the weight delta
// within ±0.5 kg, asking for an observational glp1_plateau keyFinding (no
// dose recommendation per GROUND RULE 13). Null — the common case — costs
// no tokens.
const plateauContext = await detectGlp1Plateau(userId);
let userPrompt = buildUserPrompt(
featuresJson,
dbUser?.insightsPrivacyMode ?? "aggregated",
locale,
comparisonSnapshot ?? undefined,
);
if (plateauContext) {
userPrompt += buildGlp1PlateauPrompt(plateauContext, locale, units.system);
}
// The user-authored "about me" self-description (Settings → AI), as a
// delimited, user-provided SYSTEM CONTEXT block. Null (no text /
// undecryptable) costs nothing.
const aboutMe = await getSelfContextTextForUser(userId, locale);
if (aboutMe) {
userPrompt += buildAboutMeInsightBlock(aboutMe, locale);
}
// A notable derived wellness signal (readiness / recovery shift,
// confidence-gated), read from the one derived contract; null (the common
// case) costs nothing.
const derivedSex = toProfileSex(dbUser?.gender);
const derivedBriefing = await detectDerivedBriefingSignals(userId, {
ageYears: getAgeFromDateOfBirth(dbUser?.dateOfBirth ?? null),
sex: derivedSex,
heightCm: dbUser?.heightCm ?? null,
});
if (derivedBriefing) {
userPrompt += buildDerivedBriefingPrompt(derivedBriefing, locale);
}
// Illness-episode + cycle state, so a mid-illness or mid-cycle user gets
// context-aware prose. The same server-authoritative builders the Coach
// assembles; both are module-gated and short-circuit to null (zero token
// cost) for users without them.
const illnessCycleCtx = await buildBriefingIllnessCycleContext(
userId,
dbUser?.gender ?? null,
await resolveUserTimezone(userId),
);
if (illnessCycleCtx) {
userPrompt += buildBriefingIllnessCyclePrompt(illnessCycleCtx, locale);
}
// Opener-archetype rotation + sparse first-name personalization
// (deterministic per user+day; omitted for unnamed accounts).
userPrompt += buildBriefingPersonalisationBlock(
userId,
dbUser?.displayName ?? null,
locale,
await resolveUserTimezone(userId),
);
// The short form on the `frugal` usage level; the full contract on every
// other level (#1217).
const briefingForm = await briefingFormForRecord(userId);
userPrompt += buildBriefingShortFormBlock(locale, briefingForm);
// The curated SOURCES block for the metric sections this briefing
// actually carries, so a normative claim can cite a real `referenceId` the
// schema + UI footnote already support. The plain prompt stays unchanged
// when no applicable metric section is present.
const referenceMetrics = metricsFromPresentSections({
bloodPressure: features.bloodPressure != null,
weight: features.weight != null,
pulse: features.pulse != null,
mood: features.mood != null,
medication: (features.medications?.length ?? 0) > 0,
});
return {
userPrompt,
comparisonSnapshot,
aboutMe,
briefingForm,
referenceMetrics,
};
}
+224
View File
@@ -0,0 +1,224 @@
/**
* The read-only advisor briefing: the cached payload, served immediately and
* never generated here.
*
* The advisor consumers (hero strip, daily-briefing card, trends row) used
* to mount against the generating POST, which blocked the page-load path on
* the full provider chain. This read serves `User.insightsCachedTextEncrypted`
* as it is and, when the cache is stale or missing AND a provider is
* configured, enqueues an out-of-band warm (the same `insight-pregenerate`
* queue the nightly cron and the "prepare assessments" button use). The next
* read reflects the fresh briefing.
*/
import type { NextRequest } from "next/server";
import { prisma } from "@/lib/db";
import {
generatedOnLocalToday,
insightsPayloadForToday,
} from "@/lib/daily/briefing-today-read";
import { isLegacyInsightPayload } from "@/lib/ai/legacy-payload";
import { resolveServerLocale } from "@/lib/i18n/server-locale";
import { readBriefingFailure } from "@/lib/insights/briefing-failure-marker";
import { readBriefingGeneratedAt } from "@/lib/insights/briefing-generated-at";
import {
INSIGHTS_CACHE_SELECT,
openInsightsCachedText,
} from "@/lib/insights/cached-text-cipher";
import { normalizeLocale } from "@/lib/insights/status-shared";
import { hasUsableStatusProvider } from "@/lib/insights/status-provider";
import { enqueueForceWarm } from "@/lib/jobs/insight-pregenerate-shared";
import { annotate } from "@/lib/logging/context";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
/**
* Briefing freshness window for the read. A cached briefing read this
* recently is fresh and triggers no warm; older / missing enqueues an
* out-of-band regeneration. Mirrors the 24 h cache-hit window the POST path
* honours so the two stay consistent.
*/
const BRIEFING_FRESH_MS = 24 * 60 * 60 * 1000;
/**
* The honesty signals beside the payload: whether a provider exists, and
* what the last generation attempt left behind.
*/
async function readGenerationSignals(userId: string, cachedAt: Date | null) {
// Provider availability, resolved once. The read never blocks on or
// generates through the provider, but it must REPORT whether one exists:
// a provider-less account's cached briefing freezes at the last successful
// generation (the pregenerate cron can't refill it), and the UI presented
// that stale text as current. `hasProvider: false` lets the insights
// surfaces pair the honest "Stand: vor X Tagen" age with a discreet
// connect-an-AI-provider hint instead of implying the read is live.
const hasProvider = await hasUsableStatusProvider(userId);
// Whether the most recent generation attempt failed (a marker newer than
// the last successful generation). The briefing keeps its last good text
// on failure, so this is the only honest signal that a shown briefing is
// held rather than freshly refreshed — and, when there is no last good
// text, that the empty state should read "couldn't generate" with a retry
// rather than the generic "no briefing yet".
const briefingFailure = await readBriefingFailure({
userId,
since: cachedAt,
});
// A `briefing-ungrounded` marker is NOT a failure: the generation
// succeeded but the grounding gate withheld the briefing (marker written
// after the cache write, so it survives the `since` filter). It is the
// persistent omission signal — the card's "briefing withheld" state used
// to exist only on the transient POST response, so a nightly strip read as
// a silent generic empty card.
const briefingOmittedReason: "ungrounded" | null =
briefingFailure?.reason === "briefing-ungrounded" ? "ungrounded" : null;
const generationFailed =
briefingFailure !== null && briefingOmittedReason === null;
// The failure class lets the empty state point its hint at the right
// lever (raise the response timeout vs re-check the provider).
const generationFailureClass = generationFailed
? (briefingFailure?.failureClass ?? null)
: null;
return {
hasProvider,
generationFailed,
generationFailureClass,
briefingOmittedReason,
};
}
export async function readAdvisorBriefing(args: {
request: NextRequest;
userId: string;
userLocale: string | null;
}) {
const { request, userId } = args;
const dbUser = await prisma.user.findUnique({
where: { id: userId },
select: {
insightsCachedAt: true,
...INSIGHTS_CACHE_SELECT,
insightsCachedLocale: true,
locale: true,
timezone: true,
},
});
const cachedText = openInsightsCachedText(dbUser);
// The locale the caller is actually reading (cookie / Accept-Language
// fall-back when `User.locale` is unset), validated against the shipped
// list — the same convention the nightly's `normalizeLocale` and the status
// routes follow. Collapsing a NULL `User.locale` to German filled the wrong
// cache family for a client reading the app in English.
const readerLocale = normalizeLocale(
await resolveServerLocale({
request,
userLocale: dbUser?.locale ?? args.userLocale ?? null,
}),
);
// The cache is one slot per user. Text written in another language is
// never the reader's briefing, however fresh: treat the slot as empty for
// prose, and re-warm in the reader's language below. An untagged row
// predates the tag and is served as before.
const languageMismatch =
dbUser?.insightsCachedLocale != null &&
dbUser.insightsCachedLocale !== readerLocale;
const cachedAt = dbUser?.insightsCachedAt ?? null;
const timezone = dbUser?.timezone ?? DEFAULT_TIMEZONE;
// Fresh means written today on the reader's calendar, by the moment the
// text was generated: `insightsCachedAt` also moves when an unchanged
// warm only re-stamps yesterday's text.
const generatedToday = generatedOnLocalToday(
readBriefingGeneratedAt(cachedText),
timezone,
new Date(),
);
const isFresh =
!languageMismatch &&
cachedAt !== null &&
generatedToday &&
Date.now() - cachedAt.getTime() < BRIEFING_FRESH_MS;
const signals = await readGenerationSignals(userId, cachedAt);
const { hasProvider } = signals;
// Never block on the provider. Warm out of band only when the cached
// briefing is stale / missing AND a provider is configured (a
// provider-less account costs one cheap chain-resolve and shows the
// empty / connect-AI state instead of a wasted enqueue).
let revalidating = false;
if (!isFresh && hasProvider) {
void enqueueForceWarm({ userId, locale: readerLocale });
revalidating = true;
}
if (cachedText && !languageMismatch) {
try {
const cached = JSON.parse(cachedText);
const legacyPayload = isLegacyInsightPayload(cached);
annotate({
action: { name: "insights.generate.read" },
meta: { cached: true, legacyPayload, revalidating, hasProvider },
});
return {
// The briefing as every surface serves it: today's text only, its
// signals measured today, its deltas at display precision.
insights: await insightsPayloadForToday(cached, {
userId,
timezone,
language: readerLocale,
}),
cached: true,
cachedAt,
legacyPayload,
// Honest stale-serve marker: true while the out-of-band warm is in
// flight, so the client can poll (bounded) until the fresh briefing
// lands instead of sitting on the stale payload for the rest of the
// session.
revalidating,
// False when no AI provider is configured anywhere: the served
// briefing can never refresh, so the UI pairs its age with a
// connect-provider affordance.
hasProvider,
// True when the last generation attempt failed (held text).
generationFailed: signals.generationFailed,
// Coarse failure class for the empty-state hint (null when the last
// attempt succeeded).
generationFailureClass: signals.generationFailureClass,
// Persistent grounding-omission signal (see above), so a
// nightly-stripped briefing renders "withheld", not "no briefing
// yet".
briefingOmittedReason: signals.briefingOmittedReason,
};
} catch {
// Invalid cache row — fall through to the empty payload below. The
// warm enqueue above (when a provider exists) repairs it for the next
// read.
}
}
annotate({
action: { name: "insights.generate.read" },
meta: {
cached: false,
revalidating,
hasProvider,
languageMismatch,
readerLocale,
},
});
return {
insights: null,
cached: false,
legacyPayload: false,
revalidating,
hasProvider,
// No last-good text AND the last attempt failed: the UI shows a
// "couldn't generate" empty state with a retry instead of the generic
// one.
generationFailed: signals.generationFailed,
generationFailureClass: signals.generationFailureClass,
briefingOmittedReason: signals.briefingOmittedReason,
};
}
@@ -0,0 +1,198 @@
/**
* The Insights overview's response body, computed from the record.
*
* SQL-side aggregates served by `buildComprehensiveAggregate` replace the
* 100k-row-in-JS aggregation the route once ran; the medication-compliance
* block keeps its bounded Prisma reads (medications + intake events). No
* provider is called anywhere on the path: `hasProvider` is the presence
* probe only.
*
* Every day-bucketing pass (resting-pulse proxy, medication continuity, BP
* pairing) shares the one per-user tz resolved here.
*/
import { prisma } from "@/lib/db";
import { probeProviderPresence } from "@/lib/ai/provider";
import type { DataSummary } from "@/lib/analytics/trends";
import { summarize } from "@/lib/analytics/trends";
import { getBpTargets } from "@/lib/analytics/bp-targets";
import { computeBpInTargetPct } from "@/lib/analytics/bp-in-target";
import {
classifyBMI,
classifyBP,
generateAlerts,
} from "@/lib/analytics/classifications";
import { annotate } from "@/lib/logging/context";
import { buildComprehensiveAggregate } from "@/lib/insights/comprehensive-aggregator";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
import { overviewCorrelations } from "./correlations";
import {
bpMedicationContinuity,
readMedicationCompliance,
} from "./medication-compliance";
import { readOverviewMoodSeries } from "./mood-series";
import { readRestingPulseDaily } from "./resting-pulse-daily";
/** Who the overview is computed for: the record, with its zone. */
export interface OverviewRecord {
id: string;
timezone?: string | null;
}
export type ComprehensiveOverviewBody = Awaited<
ReturnType<typeof buildComprehensiveResponse>
>;
/** BMI and the 30-day BP classification against the record's profile. */
function classifyProfile(
summaries: Record<string, DataSummary>,
dbUser: { heightCm: number | null; dateOfBirth: Date | null } | null,
) {
let bmi: number | null = null;
let bmiClassification = null;
if (dbUser?.heightCm && summaries.WEIGHT?.latest) {
const heightM = dbUser.heightCm / 100;
bmi = Math.round((summaries.WEIGHT.latest / (heightM * heightM)) * 10) / 10;
bmiClassification = classifyBMI(bmi);
}
let bpClassification = null;
const bpTargets = getBpTargets(dbUser?.dateOfBirth ?? null);
if (
summaries.BLOOD_PRESSURE_SYS?.avg30 &&
summaries.BLOOD_PRESSURE_DIA?.avg30
) {
bpClassification = classifyBP(
summaries.BLOOD_PRESSURE_SYS.avg30,
summaries.BLOOD_PRESSURE_DIA.avg30,
);
}
return { bmi, bmiClassification, bpClassification, bpTargets };
}
export async function buildComprehensiveResponse(user: OverviewRecord) {
const userId = user.id;
const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
const userTz = user.timezone || DEFAULT_TIMEZONE;
// Height + DOB drive BMI + BP targets.
const dbUser = await prisma.user.findUnique({
where: { id: userId },
select: {
heightCm: true,
dateOfBirth: true,
},
});
// SQL-side rollup: per-type DataSummary identical to the JS `summarize()`
// semantics over the 90-day window, plus the bounded raw rows the BP target
// adherence pairing needs, plus daily-mean buckets for the correlations.
const aggregate = await buildComprehensiveAggregate(userId);
const { dailyMoodEntries, moodDataPoints, moodEntryCount } =
await readOverviewMoodSeries(userId, ninetyDaysAgo);
const moodSummary =
moodDataPoints.length > 0 ? summarize(moodDataPoints) : null;
const summaries: Record<string, DataSummary> = aggregate.summaries;
const { bmi, bmiClassification, bpClassification, bpTargets } =
classifyProfile(summaries, dbUser);
// BP target adherence over the trailing 90 days, through the canonical
// `computeBpInTargetPct` so this figure shares ONE definition with the
// dashboard tile, the insight cards, the AI snapshot and the targets
// endpoint. The helper pairs sys+dia on the 5-minute window with the
// same-local-day fallback (keyed on the user's OWN tz) and keeps the
// ESH-2023 band + 90/50 hypotension floor. Window is the aggregator's
// 90-day raw pull — a "recent control" figure, not the all-time headline.
let bpPctInTarget: number | null = null;
if (bpTargets) {
bpPctInTarget =
computeBpInTargetPct(
aggregate.bpRawRows.sys,
aggregate.bpRawRows.dia,
bpTargets,
userTz,
)?.pct ?? null;
}
const restingPulseDaily = await readRestingPulseDaily(
userId,
ninetyDaysAgo,
userTz,
);
const sysDaily = aggregate.dailyByType.BLOOD_PRESSURE_SYS ?? [];
const correlations = overviewCorrelations({
aggregate,
dailyMoodEntries,
restingPulseDaily,
userTz,
});
const { medications, medCompliance, bpMedications, bpMedicationEvents } =
await readMedicationCompliance({
userId,
since: ninetyDaysAgo,
timezone: user.timezone || DEFAULT_TIMEZONE,
});
const { bpMedicationCorrelation, bpMedicationScatterData } =
bpMedicationContinuity({ bpMedications, bpMedicationEvents, sysDaily });
const alerts = generateAlerts({
bmi,
bpAvgSys: summaries.BLOOD_PRESSURE_SYS?.avg30 ?? null,
bpAvgDia: summaries.BLOOD_PRESSURE_DIA?.avg30 ?? null,
bpPctInTarget,
weightSlope30: summaries.WEIGHT?.slope30?.slope ?? null,
pulseAvg30: summaries.PULSE?.avg30 ?? null,
pulseAnomalyCount: summaries.PULSE?.anomalyCount ?? 0,
medications: medCompliance.map((m) => ({
name: m.name,
compliance7: m.compliance7,
compliance30: m.compliance30,
})),
});
// Data span — derived from the earliest 90-day measurement.
const dataSpanDays = aggregate.firstMeasurementAt
? Math.ceil(
(Date.now() - aggregate.firstMeasurementAt.getTime()) /
(24 * 60 * 60 * 1000),
)
: 0;
annotate({
action: { name: "insights.comprehensive" },
meta: {
totalMeasurements: aggregate.totalMeasurements,
moodEntries: moodEntryCount,
medications: medications.length,
},
});
return {
summaries,
bmi,
bmiClassification,
bpClassification,
bpPctInTarget,
bpTargets,
weightBpCorrelation: correlations.weightBpCorrelation,
scatterData: correlations.scatterData,
bpMedicationCorrelation,
bpMedicationScatterData,
moodSummary,
moodBpCorrelation: correlations.moodBpCorrelation,
moodBpScatterData: correlations.moodBpScatterData,
moodWeightCorrelation: correlations.moodWeightCorrelation,
moodWeightScatterData: correlations.moodWeightScatterData,
moodPulseCorrelation: correlations.moodPulseCorrelation,
moodPulseScatterData: correlations.moodPulseScatterData,
medications: medCompliance,
alerts,
// Presence only: no client is built and no token is refreshed on a read.
hasProvider: await probeProviderPresence(userId),
dataSpanDays,
totalMeasurements: aggregate.totalMeasurements,
};
}
@@ -0,0 +1,78 @@
/**
* The overview's correlations: weight × systolic, and mood against
* systolic, weight and resting pulse. Pure: every series is already read.
*/
import {
significantPearsonCorrelation,
type PairedPoint,
} from "@/lib/analytics/correlations";
import type { ComprehensiveAggregate } from "@/lib/insights/comprehensive-aggregator";
import {
deriveDailyLocalMeans,
joinDailyByDay,
pairMoodWithDaily,
type DailyValue,
} from "./daily-series";
export function overviewCorrelations(args: {
aggregate: ComprehensiveAggregate;
dailyMoodEntries: DailyValue[];
restingPulseDaily: DailyValue[];
userTz: string;
}) {
const { aggregate, dailyMoodEntries, restingPulseDaily, userTz } = args;
// Weight × Sys BP, daily-key pairing. Both sides come from the UTC
// `dailyByType` series, so they share one UTC day-key space and are
// internally consistent; re-keying it to local migrates with the rollup
// tier.
const weightDaily = aggregate.dailyByType.WEIGHT ?? [];
const sysDaily = aggregate.dailyByType.BLOOD_PRESSURE_SYS ?? [];
const weightBpPairs: PairedPoint[] = joinDailyByDay(weightDaily, sysDaily);
const weightBpCorrelation = significantPearsonCorrelation(weightBpPairs);
const scatterData = weightBpPairs.map((p) => ({
weight: p.a,
sysBP: p.b,
}));
// Mood × {sys BP, weight, pulse}. The mood series is LOCAL-day-keyed and
// `pairMoodWithDaily` joins on an EXACT YYYY-MM-DD key, so the partners
// are exact local-day means from the raw rows already on the request path
// (canonical-source deduped in the aggregator), NOT the UTC `dailyByType` —
// a UTC-day MEAN cannot be relabeled into a local-day mean.
const sysDailyLocal = deriveDailyLocalMeans(aggregate.bpRawRows.sys, userTz);
const weightDailyLocal = deriveDailyLocalMeans(
aggregate.weightRawRows,
userTz,
);
const moodBpPairs = pairMoodWithDaily(dailyMoodEntries, sysDailyLocal);
const moodBpCorrelation = significantPearsonCorrelation(moodBpPairs);
const moodBpScatterData = moodBpPairs.map((p) => ({ mood: p.a, sysBP: p.b }));
const moodWeightPairs = pairMoodWithDaily(dailyMoodEntries, weightDailyLocal);
const moodWeightCorrelation = significantPearsonCorrelation(moodWeightPairs);
const moodWeightScatterData = moodWeightPairs.map((p) => ({
mood: p.a,
weight: p.b,
}));
const moodPulsePairs = pairMoodWithDaily(dailyMoodEntries, restingPulseDaily);
const moodPulseCorrelation = significantPearsonCorrelation(moodPulsePairs);
const moodPulseScatterData = moodPulsePairs.map((p) => ({
mood: p.a,
pulse: p.b,
}));
return {
weightBpCorrelation,
scatterData,
moodBpCorrelation,
moodBpScatterData,
moodWeightCorrelation,
moodWeightScatterData,
moodPulseCorrelation,
moodPulseScatterData,
};
}
@@ -0,0 +1,110 @@
/**
* Day-keyed series helpers for the Insights overview's correlations: local
* day means from raw rows, and the two joins that pair one daily series with
* another.
*/
import type { PairedPoint } from "@/lib/analytics/correlations";
import { userDayKey } from "@/lib/tz/format";
export interface DailyValue {
day: string;
value: number;
}
/**
* Derive exact local-day mean series from raw measurement rows, keyed by the
* user's timezone via `userDayKey`. The mood series is local-day-keyed, and
* `pairMoodWithDaily` joins on an exact YYYY-MM-DD key, so its metric
* partners must be local-day-keyed too. A UTC-day mean (from `dailyByType`)
* cannot be relabeled into a local-day mean, so the mean is recomputed here
* from raw rows already on the request path. `userDayKey` formats via a
* memoised `Intl.DateTimeFormat` — no offset arithmetic — so DST-transition
* days (23 h / 25 h) bucket correctly and the result is host-TZ-independent
* (CI runs UTC). `round2` mirrors the aggregator's `ROUND(AVG, 2)` so
* correlation thresholds don't drift. Returns `{ day, value }[]` sorted ASC
* by day, the shape `pairMoodWithDaily` expects.
*/
export function deriveDailyLocalMeans(
rows: Array<{ measuredAt: Date; value: number }>,
tz: string,
): DailyValue[] {
const byDay = new Map<string, { sum: number; count: number }>();
for (const r of rows) {
const key = userDayKey(r.measuredAt, tz);
const cur = byDay.get(key) ?? { sum: 0, count: 0 };
cur.sum += r.value;
cur.count += 1;
byDay.set(key, cur);
}
return Array.from(byDay.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([day, stats]) => ({
day,
value: Math.round((stats.sum / stats.count) * 100) / 100,
}));
}
/**
* Daily-key inner join between two daily-aggregated series. Returns
* `PairedPoint` shapes compatible with `significantPearsonCorrelation`.
*
* Inputs are sorted ASC by `day` (the aggregator's ORDER BY
* guarantees that). The walk is O(n + m) merge-join style instead of
* Map.lookup-per-element so it scales with the worst-case 90 buckets
* x 90 buckets case without surprises.
*/
export function joinDailyByDay(
a: DailyValue[],
b: DailyValue[],
): PairedPoint[] {
const pairs: PairedPoint[] = [];
let i = 0;
let j = 0;
while (i < a.length && j < b.length) {
const ai = a[i];
const bj = b[j];
if (ai.day === bj.day) {
pairs.push({
a: ai.value,
b: bj.value,
date: new Date(`${ai.day}T12:00:00.000Z`),
});
i += 1;
j += 1;
} else if (ai.day < bj.day) {
i += 1;
} else {
j += 1;
}
}
return pairs;
}
/**
* Pair the daily mood series (left side) with a daily-aggregated
* metric (right side). Matches the legacy `buildMoodMetricPairs`
* helper byte-for-byte: same daily YYYY-MM-DD key, same
* `T12:00:00.000Z` anchor for the `date` field.
*/
export function pairMoodWithDaily(
dailyMood: DailyValue[],
dailyMetric: DailyValue[],
): PairedPoint[] {
if (dailyMood.length === 0 || dailyMetric.length === 0) return [];
const metricByDay = new Map<string, number>();
for (const m of dailyMetric) {
metricByDay.set(m.day, m.value);
}
const pairs: PairedPoint[] = [];
for (const mood of dailyMood) {
const metric = metricByDay.get(mood.day);
if (metric !== undefined) {
pairs.push({
a: mood.value,
b: metric,
date: new Date(`${mood.day}T12:00:00.000Z`),
});
}
}
return pairs;
}
@@ -0,0 +1,235 @@
/**
* The overview's medication block: per-medication compliance over 7 and 30
* days, and the blood-pressure medication continuity × systolic correlation
* that pairs the BP medications' intake days with the daily systolic means.
*/
import { prisma } from "@/lib/db";
import {
buildComplianceMedicationContext,
COURSES_COMPLIANCE_SELECT,
calculateCompliance,
expectsDoses,
lastNonSkippedTakenAt,
SCHEDULE_COMPLIANCE_SELECT,
} from "@/lib/analytics/compliance";
import { significantPearsonCorrelation } from "@/lib/analytics/correlations";
import { resolveMedicationCategories } from "@/lib/medication-category";
import { TRACKED_INTAKE_WHERE } from "@/lib/medications/intake-tracking";
import { dayKeyAsUtcMidnight } from "@/lib/tz/date-only";
import type { DailyValue } from "./daily-series";
interface BpIntakeEvent {
scheduledFor: Date;
takenAt: Date | null;
skipped: boolean;
}
/** Read the tracked scheduled medications with what compliance needs. */
async function readScheduledMedications(userId: string) {
const medicationRows = await prisma.medication.findMany({
// As-needed (PRN) medications never surface a compliance rate (no
// expected doses).
where: { userId, active: true, asNeeded: false, ...TRACKED_INTAKE_WHERE },
include: {
// Schedules through the shared compliance select so the configured
// per-dose windows reach this surface like every other.
schedules: { select: SCHEDULE_COMPLIANCE_SELECT },
// Archived schedule eras for era-aware compliance.
scheduleRevisions: { orderBy: { validFrom: "asc" } },
// Pause eras so paused days drop out of the denominator.
pauseEras: { select: { pausedAt: true, resumedAt: true } },
// The courses, so a gap between two expects nothing (#1024).
courses: COURSES_COMPLIANCE_SELECT,
},
});
// A medication with no schedule expects no dose, and `calculateCompliance`
// answers 100 % for that case. The `asNeeded: false` filter above catches
// only the PRN arm; the empty schedule is reachable on a scheduled
// medication and would otherwise report perfect adherence.
return medicationRows.filter(expectsDoses);
}
/**
* Per-medication compliance, plus the intake events of the blood-pressure
* medications for the continuity correlation.
*/
export async function readMedicationCompliance(args: {
userId: string;
since: Date;
timezone: string;
}) {
const { userId, since, timezone } = args;
const medications = await readScheduledMedications(userId);
const resolvedCategories = await resolveMedicationCategories(
medications.map((m) => m.id),
);
const categoryMap = Object.fromEntries(
Object.entries(resolvedCategories).map(([id, r]) => [id, r.category]),
);
const medCompliance = [];
const bpMedicationEvents: BpIntakeEvent[] = [];
const bpMedications = medications.filter(
(med) => (categoryMap[med.id] ?? "OTHER") === "BLOOD_PRESSURE",
);
// Single round-trip for all medications instead of N+1: one query keyed
// on `medicationId IN (...)`, then group in memory.
const allEvents = medications.length
? await prisma.medicationIntakeEvent.findMany({
where: {
medicationId: { in: medications.map((m) => m.id) },
userId,
// Tombstoned rows are excluded.
deletedAt: null,
scheduledFor: { gte: since },
},
orderBy: { scheduledFor: "desc" },
})
: [];
const eventsByMed = new Map<string, typeof allEvents>();
for (const ev of allEvents) {
const list = eventsByMed.get(ev.medicationId);
if (list) list.push(ev);
else eventsByMed.set(ev.medicationId, [ev]);
}
for (const med of medications) {
const events = eventsByMed.get(med.id) ?? [];
const mapped = events.map((e) => ({
takenAt: e.takenAt,
skipped: e.skipped,
scheduledFor: e.scheduledFor,
}));
// Engine-routed denominator.
const medicationContext = buildComplianceMedicationContext(
med,
lastNonSkippedTakenAt(mapped),
timezone,
);
const c7 = calculateCompliance(mapped, med.schedules, 7, med.createdAt, {
medicationContext,
});
const c30 = calculateCompliance(mapped, med.schedules, 30, med.createdAt, {
medicationContext,
});
medCompliance.push({
id: med.id,
name: med.name,
dose: med.dose,
category: categoryMap[med.id] ?? "OTHER",
// The label of a custom category; null for a built-in (#1041).
categoryLabel: resolvedCategories[med.id]?.categoryLabel ?? null,
compliance7: c7.rate,
compliance30: c30.rate,
streak: c7.streak,
taken7: c7.taken,
skipped7: c7.skipped,
missed7: c7.missed,
});
if ((categoryMap[med.id] ?? "OTHER") === "BLOOD_PRESSURE") {
bpMedicationEvents.push(
...events.map((e) => ({
scheduledFor: e.scheduledFor,
takenAt: e.takenAt,
skipped: e.skipped,
})),
);
}
}
return { medications, medCompliance, bpMedications, bpMedicationEvents };
}
/**
* Correlation: BP medication continuity × systolic BP.
*
* Uses the same daily BP_SYS series the aggregator pre-computed — no second
* round-trip. The intake-side aggregation matches the legacy code:
* continuity = min(1, taken / expectedPerDay), pairing every day that had a
* sys reading.
*/
export function bpMedicationContinuity(args: {
bpMedications: Array<{ schedules: unknown[] }>;
bpMedicationEvents: BpIntakeEvent[];
sysDaily: DailyValue[];
}) {
const { bpMedications, bpMedicationEvents, sysDaily } = args;
let bpMedicationCorrelation: {
r: number;
strength: string;
n: number;
medicationCount: number;
} | null = null;
const bpMedicationScatterData: Array<{
continuityPct: number;
sysBP: number;
}> = [];
const expectedBpIntakesPerDay = bpMedications.reduce(
(sum, med) => sum + med.schedules.length,
0,
);
if (expectedBpIntakesPerDay > 0) {
// Daily systolic means (already keyed YYYY-MM-DD) from the SQL pass.
// `sysDaily` (UTC `date_trunc` from the aggregator's `dailyByType`) and
// `takenByDay` below (intake `scheduledFor` truncated to UTC) both live
// in the SAME UTC day-key space and are internally consistent with each
// other, so this join stays UTC. Only the mood pairings moved to a
// separately derived local-day series (their partner, the mood series,
// is local-day-keyed). This correlation migrates to local when the
// measurement-rollup tier buckets per-user tz; re-keying it here without
// the local-bucketed rollup would mean per-request raw re-aggregation.
const sysByDay = new Map<string, number>();
for (const row of sysDaily) {
sysByDay.set(row.day, row.value);
}
const takenByDay = new Map<string, number>();
// Days that carry an intake record of ANY kind. A logged skip is a real
// zero and belongs in the series; a day the person never touched is not a
// zero, it is nothing, and pairing it against that day's systolic reading
// invented a data point they never produced. Absence is not zero, here as
// everywhere else in this project, so the unlogged day drops out of both
// the scatter and the coefficient rather than dragging continuity down.
const loggedDays = new Set<string>();
for (const event of bpMedicationEvents) {
// eslint-disable-next-line healthlog/no-utc-day-key -- UTC by design: pairs intake days with the DAY rollup buckets in sysByDay, which are UTC days
const dayKey = event.scheduledFor.toISOString().slice(0, 10);
loggedDays.add(dayKey);
if (event.skipped || !event.takenAt) continue;
takenByDay.set(dayKey, (takenByDay.get(dayKey) ?? 0) + 1);
}
const pairs: Array<{ a: number; b: number; date: Date }> = [];
for (const [dayKey, avgSys] of sysByDay.entries()) {
if (!loggedDays.has(dayKey)) continue;
const taken = takenByDay.get(dayKey) ?? 0;
const continuity = Math.min(1, taken / expectedBpIntakesPerDay);
pairs.push({
a: continuity,
b: avgSys,
date: dayKeyAsUtcMidnight(dayKey),
});
bpMedicationScatterData.push({
continuityPct: Math.round(continuity * 100),
sysBP: Math.round(avgSys * 10) / 10,
});
}
const corr = significantPearsonCorrelation(pairs);
if (corr) {
bpMedicationCorrelation = {
...corr,
medicationCount: bpMedications.length,
};
}
}
return { bpMedicationCorrelation, bpMedicationScatterData };
}
@@ -0,0 +1,92 @@
/**
* The overview's 90-day mood series, read from the mood-rollup DAY tier.
*
* The two consumers downstream — `dailyMoodEntries` for the mood × metric
* correlations and the mood summary for the dashboard mood block — read at a
* per-day resolution, which is precisely the shape the rollup writer emits,
* so the tier replaces an unbounded `prisma.moodEntry.findMany`.
*
* Coverage-fallback: when the user has mood entries but zero rollup rows yet
* (legacy account before the boot-time backfill has caught up), fall back to
* a bounded 90-day raw walk once and fire the warm-up so the next request
* lands on the rollup tier. Same posture as `/api/mood/analytics`.
*
* The mood rollup writer mints `bucketStart` from the canonical local
* `MoodEntry.date` label at UTC midnight, so `.toISOString().slice(0, 10)`
* recovers the user's LOCAL calendar day — byte-identical to the live
* fallback's `entry.date`. Both mood paths therefore emit local-day keys, and
* the mood × metric pairing partners (sys BP, weight, resting pulse) are
* derived in the user's tz so both join sides share ONE local-day key space.
* The measurement-rollup tier itself still buckets UTC (`dailyByType`); the
* two internally-consistent UTC joins (weight × BP, BP-med continuity) are
* left on it deliberately (see their call sites in `build.ts`).
*/
import { prisma } from "@/lib/db";
import type { DataPoint } from "@/lib/analytics/trends";
import {
ensureUserMoodRollupsFresh,
readMoodDayRollups,
} from "@/lib/rollups/mood-rollups";
import { dateOnlyKey } from "@/lib/tz/date-only";
import type { DailyValue } from "./daily-series";
export interface OverviewMoodSeries {
dailyMoodEntries: DailyValue[];
moodDataPoints: DataPoint[];
moodEntryCount: number;
}
export async function readOverviewMoodSeries(
userId: string,
since: Date,
): Promise<OverviewMoodSeries> {
void ensureUserMoodRollupsFresh(userId);
const moodRollupDayRows = await readMoodDayRollups(userId, since);
if (moodRollupDayRows.length > 0) {
return {
dailyMoodEntries: moodRollupDayRows.map((r) => ({
day: dateOnlyKey(r.bucketStart),
value: Math.round(r.mean * 100) / 100,
})),
moodDataPoints: moodRollupDayRows.map((r) => ({
date: r.bucketStart,
value: r.mean,
})),
moodEntryCount: moodRollupDayRows.reduce((s, r) => s + r.count, 0),
};
}
// Coverage-fallback: legacy account with mood entries but no
// rollup coverage yet. Bounded by the 90-day window so the walk is
// capped even when the rollup miss happens.
const moodEntries = await prisma.moodEntry.findMany({
// Tombstoned rows are excluded.
where: { userId, deletedAt: null, moodLoggedAt: { gte: since } },
orderBy: { moodLoggedAt: "asc" },
select: { date: true, score: true, moodLoggedAt: true },
});
const moodByDay = new Map<string, { sum: number; count: number }>();
for (const entry of moodEntries) {
const current = moodByDay.get(entry.date) ?? { sum: 0, count: 0 };
current.sum += entry.score;
current.count += 1;
moodByDay.set(entry.date, current);
}
const dailyMoodEntries = Array.from(moodByDay.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([day, stats]) => ({
day,
value: Math.round((stats.sum / stats.count) * 100) / 100,
}));
return {
dailyMoodEntries,
// Per-day means feed `summarize()` so the live-fallback path shares the
// rollup-tier semantic (one DataPoint per day, the daily mean), the same
// reading mood/analytics uses.
moodDataPoints: dailyMoodEntries.map((e) => ({
date: new Date(`${e.day}T12:00:00.000Z`),
value: e.value,
})),
moodEntryCount: moodEntries.length,
};
}
@@ -0,0 +1,49 @@
/**
* The Insights overview read: the computed body through the analytics
* cache. The route resolves the per-request AI state beside it, which is
* never cached with the body.
*/
import { annotate } from "@/lib/logging/context";
import {
cachedSwrWithMeta,
caches,
type ServerCache,
} from "@/lib/cache/server-cache";
import {
buildComprehensiveResponse,
type ComprehensiveOverviewBody,
type OverviewRecord,
} from "./build";
export async function readComprehensiveOverview(
user: OverviewRecord,
): Promise<{ body: ComprehensiveOverviewBody; revalidating: boolean }> {
// Read-through the analytics cache keyed on (userId, "comprehensive"). The
// /insights page mount routinely fans out to this endpoint alongside the
// Coach drawer + the recommendations grid; the 60s TTL converts every
// duplicate mount within the window to a Map lookup. Invalidation is
// handled by `invalidateUserMeasurements`, which marks every key under the
// `${userId}|` prefix stale (see `lib/cache/invalidate.ts`).
//
// Stale-while-revalidate: this is the single heaviest SQL-side aggregation
// on the /insights mount; serving the prior body instantly while ONE
// background recompute refreshes it keeps the page interactive across the
// bucket's 10-minute stale window (a measurement sync used to bust the
// entry into a blocking cold rebuild on the very next mount, all day). The
// truly-cold first read of the day still computes inline — there is no
// prior body to serve — but it no longer recurs per visit.
//
// The `revalidating` flag marks a stale-served body honestly so the client
// can poll (bounded) until the background rebuild lands — mirroring the
// `/api/insights/generate` GET contract. It rides on the response, never
// inside the cached body, so a warmed entry doesn't freeze the flag.
const { value: body, outcome } = await cachedSwrWithMeta(
caches.analytics as ServerCache<ComprehensiveOverviewBody>,
`${user.id}|comprehensive`,
() => buildComprehensiveResponse(user),
annotate,
);
return { body, revalidating: outcome === "stale" };
}
@@ -0,0 +1,65 @@
/**
* The overview's daily resting-pulse series, the mood × pulse correlation's
* partner.
*
* The correlation is labelled "Resting pulse (bpm)", so it is scored against
* the RESTING series, not the raw PULSE day-mean: the aggregator's
* `dailyByType.PULSE` is a per-day AVG that folds in workout HR for
* Apple-Health users and inflates the "resting" figure. The clean
* RESTING_HEART_RATE rows win; only without them does the low-percentile
* daily proxy over raw pulse stand in. The proxy folds in Postgres (#1023):
* it needs one percentile per day, and reading the raw samples to get it
* walked 130 000 rows per page view on a watch that records once a minute.
*
* Keyed on `userDayKey(d, userTz)`: its only pairing partner is the mood
* series, which is local-day-keyed, and a UTC key here would offset every
* evening logger west of UTC by one day and sell a lag-1 correlation as
* same-day. Unlike the mood side, this series builds from raw rows, so it
* re-keys directly with no rollup-tier change.
*/
import { prisma } from "@/lib/db";
import { resolveRestingPulseSeries } from "@/lib/analytics/resting-pulse";
import { readRestingPulseProxy } from "@/lib/analytics/resting-pulse-read";
import { userDayKey } from "@/lib/tz/format";
import type { DailyValue } from "./daily-series";
export async function readRestingPulseDaily(
userId: string,
since: Date,
userTz: string,
): Promise<DailyValue[]> {
const restingPulseRows = await prisma.measurement.findMany({
where: {
userId,
type: "RESTING_HEART_RATE",
deletedAt: null,
measuredAt: { gte: since },
},
select: { measuredAt: true, value: true },
});
const localDayKey = (d: Date) => userDayKey(d, userTz);
const restingPulseSeries =
restingPulseRows.length > 0
? resolveRestingPulseSeries({
restingSamples: restingPulseRows,
pulseSamples: [],
dayKeyOf: localDayKey,
}).series
: await readRestingPulseProxy({
userId,
since,
timeZone: userTz,
});
const restingByDay = new Map<string, { sum: number; count: number }>();
for (const s of restingPulseSeries) {
const key = localDayKey(s.measuredAt);
const cur = restingByDay.get(key) ?? { sum: 0, count: 0 };
cur.sum += s.value;
cur.count += 1;
restingByDay.set(key, cur);
}
return Array.from(restingByDay.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([day, stats]) => ({ day, value: stats.sum / stats.count }));
}
@@ -0,0 +1,24 @@
{
"status": 401,
"body": {
"data": null,
"error": "Not authenticated",
"meta": {
"errorCode": "auth.missing",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 401,
"action": null,
"meta": null,
"warnings": null,
"error": {
"type": "HttpError",
"message": "Not authenticated"
}
}
],
"effects": []
}
@@ -0,0 +1,26 @@
{
"status": 403,
"body": {
"data": null,
"error": "Account access denied",
"meta": {
"errorCode": "sharing.access.denied",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 403,
"action": null,
"meta": {
"sharing_refusal": "insufficient_access"
},
"warnings": null,
"error": {
"type": "SharingAccessDeniedError",
"message": "Account access denied"
}
}
],
"effects": []
}
@@ -0,0 +1,21 @@
{
"status": 429,
"body": {
"data": null,
"error": "Too many analytics requests. Please retry later.",
"meta": {
"errorCode": "http.rate_limited",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 429,
"action": null,
"meta": null,
"warnings": null,
"error": null
}
],
"effects": []
}
@@ -0,0 +1,884 @@
{
"status": 200,
"body": {
"data": {
"summaries": {
"WEIGHT": {
"count": 47,
"latest": 70.3,
"min": 70,
"max": 71.2,
"mean": 70.63,
"median": null,
"avg7": 70.55,
"avg30": 70.62,
"slope7": {
"slope": 0.043,
"direction": "up",
"confidence": 0.03
},
"slope30": {
"slope": -0.004,
"direction": "stable",
"confidence": 0.01
},
"anomalyCount": 0,
"avg30LastMonth": 70.63,
"avg30LastYear": null
},
"BLOOD_PRESSURE_SYS": {
"count": 47,
"latest": 134,
"min": 131,
"max": 163,
"mean": 139.66,
"median": null,
"avg7": 139.67,
"avg30": 139.93,
"slope7": {
"slope": -2.057,
"direction": "down",
"confidence": 0.18
},
"slope30": {
"slope": -0.03,
"direction": "down",
"confidence": 0
},
"anomalyCount": 4,
"avg30LastMonth": 139.26,
"avg30LastYear": null
},
"BLOOD_PRESSURE_DIA": {
"count": 47,
"latest": 86,
"min": 84,
"max": 90,
"mean": 87.04,
"median": null,
"avg7": 87.5,
"avg30": 87.07,
"slope7": {
"slope": -0.2,
"direction": "down",
"confidence": 0.04
},
"slope30": {
"slope": 0.034,
"direction": "up",
"confidence": 0.02
},
"anomalyCount": 0,
"avg30LastMonth": 87,
"avg30LastYear": null
},
"PULSE": {
"count": 141,
"latest": 93,
"min": 66,
"max": 96,
"mean": 81.67,
"median": null,
"avg7": 81.61,
"avg30": 81.61,
"slope7": {
"slope": 0.186,
"direction": "up",
"confidence": 0
},
"slope30": {
"slope": 0.027,
"direction": "up",
"confidence": 0
},
"anomalyCount": 0,
"avg30LastMonth": 81.75,
"avg30LastYear": null
}
},
"bmi": null,
"bmiClassification": null,
"bpClassification": {
"category": "High-normal",
"color": "var(--dracula-yellow)",
"severity": "elevated"
},
"bpPctInTarget": 0,
"bpTargets": {
"sysLow": 130,
"sysHigh": 139,
"diaLow": 70,
"diaHigh": 79
},
"weightBpCorrelation": {
"r": -0.631,
"strength": "moderat",
"n": 47
},
"scatterData": [
{
"weight": 71.2,
"sysBP": 135
},
{
"weight": 70.9,
"sysBP": 132
},
{
"weight": 70.6,
"sysBP": 140
},
{
"weight": 70.3,
"sysBP": 137
},
{
"weight": 70,
"sysBP": 156
},
{
"weight": 71.2,
"sysBP": 131
},
{
"weight": 70.9,
"sysBP": 139
},
{
"weight": 70.6,
"sysBP": 136
},
{
"weight": 70.3,
"sysBP": 133
},
{
"weight": 70,
"sysBP": 163
},
{
"weight": 71.2,
"sysBP": 138
},
{
"weight": 70.9,
"sysBP": 135
},
{
"weight": 70.6,
"sysBP": 132
},
{
"weight": 70.3,
"sysBP": 140
},
{
"weight": 70,
"sysBP": 159
},
{
"weight": 71.2,
"sysBP": 134
},
{
"weight": 70.9,
"sysBP": 131
},
{
"weight": 70.6,
"sysBP": 139
},
{
"weight": 70.3,
"sysBP": 136
},
{
"weight": 71.2,
"sysBP": 141
},
{
"weight": 70.9,
"sysBP": 138
},
{
"weight": 70.6,
"sysBP": 135
},
{
"weight": 70.3,
"sysBP": 132
},
{
"weight": 70,
"sysBP": 162
},
{
"weight": 71.2,
"sysBP": 137
},
{
"weight": 70.9,
"sysBP": 134
},
{
"weight": 70.6,
"sysBP": 131
},
{
"weight": 70.3,
"sysBP": 139
},
{
"weight": 70,
"sysBP": 158
},
{
"weight": 71.2,
"sysBP": 133
},
{
"weight": 70.9,
"sysBP": 141
},
{
"weight": 70.6,
"sysBP": 138
},
{
"weight": 70.3,
"sysBP": 135
},
{
"weight": 70,
"sysBP": 154
},
{
"weight": 71.2,
"sysBP": 140
},
{
"weight": 70.9,
"sysBP": 137
},
{
"weight": 70.6,
"sysBP": 134
},
{
"weight": 70.3,
"sysBP": 131
},
{
"weight": 70,
"sysBP": 161
},
{
"weight": 71.2,
"sysBP": 136
},
{
"weight": 70.9,
"sysBP": 133
},
{
"weight": 70.3,
"sysBP": 138
},
{
"weight": 70,
"sysBP": 157
},
{
"weight": 71.2,
"sysBP": 132
},
{
"weight": 70.9,
"sysBP": 140
},
{
"weight": 70.6,
"sysBP": 137
},
{
"weight": 70.3,
"sysBP": 134
}
],
"bpMedicationCorrelation": {
"r": -0.941,
"strength": "stark",
"n": 28,
"medicationCount": 1
},
"bpMedicationScatterData": [
{
"continuityPct": 100,
"sysBP": 138
},
{
"continuityPct": 100,
"sysBP": 135
},
{
"continuityPct": 100,
"sysBP": 132
},
{
"continuityPct": 0,
"sysBP": 159
},
{
"continuityPct": 100,
"sysBP": 134
},
{
"continuityPct": 100,
"sysBP": 131
},
{
"continuityPct": 100,
"sysBP": 136
},
{
"continuityPct": 100,
"sysBP": 141
},
{
"continuityPct": 100,
"sysBP": 135
},
{
"continuityPct": 100,
"sysBP": 132
},
{
"continuityPct": 0,
"sysBP": 162
},
{
"continuityPct": 100,
"sysBP": 134
},
{
"continuityPct": 100,
"sysBP": 131
},
{
"continuityPct": 100,
"sysBP": 139
},
{
"continuityPct": 100,
"sysBP": 133
},
{
"continuityPct": 100,
"sysBP": 141
},
{
"continuityPct": 100,
"sysBP": 138
},
{
"continuityPct": 0,
"sysBP": 154
},
{
"continuityPct": 100,
"sysBP": 140
},
{
"continuityPct": 100,
"sysBP": 137
},
{
"continuityPct": 100,
"sysBP": 131
},
{
"continuityPct": 0,
"sysBP": 161
},
{
"continuityPct": 100,
"sysBP": 136
},
{
"continuityPct": 100,
"sysBP": 138
},
{
"continuityPct": 0,
"sysBP": 157
},
{
"continuityPct": 100,
"sysBP": 140
},
{
"continuityPct": 100,
"sysBP": 137
},
{
"continuityPct": 100,
"sysBP": 134
}
],
"moodSummary": {
"count": 29,
"latest": 3,
"min": 1,
"max": 5,
"mean": 3.07,
"median": 3,
"avg7": 3,
"avg30": 3.04,
"slope7": {
"slope": 0.286,
"direction": "up",
"confidence": 0.14
},
"slope30": {
"slope": 0,
"direction": "stable",
"confidence": 0
},
"anomalyCount": 0,
"avg30LastMonth": 4,
"avg30LastYear": null
},
"moodBpCorrelation": {
"r": -0.683,
"strength": "moderat",
"n": 28
},
"moodBpScatterData": [
{
"mood": 4,
"sysBP": 141
},
{
"mood": 2,
"sysBP": 138
},
{
"mood": 5,
"sysBP": 135
},
{
"mood": 3,
"sysBP": 132
},
{
"mood": 1,
"sysBP": 162
},
{
"mood": 4,
"sysBP": 137
},
{
"mood": 2,
"sysBP": 134
},
{
"mood": 5,
"sysBP": 131
},
{
"mood": 3,
"sysBP": 139
},
{
"mood": 1,
"sysBP": 158
},
{
"mood": 4,
"sysBP": 133
},
{
"mood": 2,
"sysBP": 141
},
{
"mood": 5,
"sysBP": 138
},
{
"mood": 3,
"sysBP": 135
},
{
"mood": 1,
"sysBP": 154
},
{
"mood": 4,
"sysBP": 140
},
{
"mood": 2,
"sysBP": 137
},
{
"mood": 5,
"sysBP": 134
},
{
"mood": 3,
"sysBP": 131
},
{
"mood": 1,
"sysBP": 161
},
{
"mood": 4,
"sysBP": 136
},
{
"mood": 2,
"sysBP": 133
},
{
"mood": 3,
"sysBP": 138
},
{
"mood": 1,
"sysBP": 157
},
{
"mood": 4,
"sysBP": 132
},
{
"mood": 2,
"sysBP": 140
},
{
"mood": 5,
"sysBP": 137
},
{
"mood": 3,
"sysBP": 134
}
],
"moodWeightCorrelation": {
"r": 0.482,
"strength": "moderat",
"n": 28
},
"moodWeightScatterData": [
{
"mood": 4,
"weight": 71.2
},
{
"mood": 2,
"weight": 70.9
},
{
"mood": 5,
"weight": 70.6
},
{
"mood": 3,
"weight": 70.3
},
{
"mood": 1,
"weight": 70
},
{
"mood": 4,
"weight": 71.2
},
{
"mood": 2,
"weight": 70.9
},
{
"mood": 5,
"weight": 70.6
},
{
"mood": 3,
"weight": 70.3
},
{
"mood": 1,
"weight": 70
},
{
"mood": 4,
"weight": 71.2
},
{
"mood": 2,
"weight": 70.9
},
{
"mood": 5,
"weight": 70.6
},
{
"mood": 3,
"weight": 70.3
},
{
"mood": 1,
"weight": 70
},
{
"mood": 4,
"weight": 71.2
},
{
"mood": 2,
"weight": 70.9
},
{
"mood": 5,
"weight": 70.6
},
{
"mood": 3,
"weight": 70.3
},
{
"mood": 1,
"weight": 70
},
{
"mood": 4,
"weight": 71.2
},
{
"mood": 2,
"weight": 70.9
},
{
"mood": 3,
"weight": 70.3
},
{
"mood": 1,
"weight": 70
},
{
"mood": 4,
"weight": 71.2
},
{
"mood": 2,
"weight": 70.9
},
{
"mood": 5,
"weight": 70.6
},
{
"mood": 3,
"weight": 70.3
}
],
"moodPulseCorrelation": null,
"moodPulseScatterData": [
{
"mood": 2,
"pulse": 72
},
{
"mood": 5,
"pulse": 71
},
{
"mood": 3,
"pulse": 78
},
{
"mood": 1,
"pulse": 77
},
{
"mood": 4,
"pulse": 76
},
{
"mood": 2,
"pulse": 75
},
{
"mood": 5,
"pulse": 74
},
{
"mood": 3,
"pulse": 73
},
{
"mood": 1,
"pulse": 72
},
{
"mood": 4,
"pulse": 74
},
{
"mood": 2,
"pulse": 73
},
{
"mood": 5,
"pulse": 77
},
{
"mood": 3,
"pulse": 76
},
{
"mood": 1,
"pulse": 75
},
{
"mood": 4,
"pulse": 74
},
{
"mood": 2,
"pulse": 73
},
{
"mood": 5,
"pulse": 75
},
{
"mood": 3,
"pulse": 74
},
{
"mood": 1,
"pulse": 73
},
{
"mood": 4,
"pulse": 72
},
{
"mood": 2,
"pulse": 76
},
{
"mood": 1,
"pulse": 76
},
{
"mood": 4,
"pulse": 75
},
{
"mood": 2,
"pulse": 74
},
{
"mood": 5,
"pulse": 73
},
{
"mood": 3,
"pulse": 72
}
],
"medications": [
{
"id": "<id:1>",
"name": "Amlodipine",
"dose": "5mg",
"category": "BLOOD_PRESSURE",
"categoryLabel": null,
"compliance7": 67,
"compliance30": 65,
"streak": 4,
"taken7": 4,
"skipped7": 1,
"missed7": 2
}
],
"alerts": [
{
"level": "warning",
"title": "Blood pressure often outside target range",
"message": "Only 0% of your readings are within the target range."
},
{
"level": "warning",
"title": "Low compliance: Amlodipine",
"message": "Your 7-day compliance for Amlodipine is only 67%. Regular intake is important."
}
],
"hasProvider": false,
"dataSpanDays": 50,
"totalMeasurements": 282,
"revalidating": false,
"ai": {
"briefing": {
"available": false,
"reason": "no_provider",
"onDeviceAllowed": true
},
"statusText": {
"available": false,
"reason": "no_provider",
"onDeviceAllowed": true
}
}
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.comprehensive"
},
"meta": {
"cache.analytics.key_hash": 2016842294,
"cache.analytics.outcome": "miss",
"insights": {
"slope_source": "rollup",
"anomaly_source": "live",
"regression_source": "rollup"
},
"insights.sub_bpRaw_ms": "<insights.sub_bpRaw_ms>",
"insights.sub_buckets_ms": "<insights.sub_buckets_ms>",
"insights.sub_coverage_ms": "<insights.sub_coverage_ms>",
"insights.sub_firstAt_ms": "<insights.sub_firstAt_ms>",
"insights.sub_latests_ms": "<insights.sub_latests_ms>",
"insights.sub_narrow_ms": "<insights.sub_narrow_ms>",
"medications": 1,
"moodEntries": 29,
"totalMeasurements": 282
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserRollupsFresh",
"args": [
"<id:2>"
]
},
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:2>"
]
}
]
}
@@ -0,0 +1,79 @@
{
"status": 200,
"body": {
"data": {
"summaries": {},
"bmi": null,
"bmiClassification": null,
"bpClassification": null,
"bpPctInTarget": null,
"bpTargets": null,
"weightBpCorrelation": null,
"scatterData": [],
"bpMedicationCorrelation": null,
"bpMedicationScatterData": [],
"moodSummary": null,
"moodBpCorrelation": null,
"moodBpScatterData": [],
"moodWeightCorrelation": null,
"moodWeightScatterData": [],
"moodPulseCorrelation": null,
"moodPulseScatterData": [],
"medications": [],
"alerts": [],
"hasProvider": false,
"dataSpanDays": 0,
"totalMeasurements": 0,
"revalidating": false,
"ai": {
"briefing": {
"available": false,
"reason": "no_provider",
"onDeviceAllowed": true
},
"statusText": {
"available": false,
"reason": "no_provider",
"onDeviceAllowed": true
}
}
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.comprehensive"
},
"meta": {
"cache.analytics.key_hash": 461242359,
"cache.analytics.outcome": "miss",
"insights.sub_bpRaw_ms": "<insights.sub_bpRaw_ms>",
"insights.sub_buckets_ms": "<insights.sub_buckets_ms>",
"insights.sub_coverage_ms": "<insights.sub_coverage_ms>",
"insights.sub_heavy_ms": "<insights.sub_heavy_ms>",
"insights.sub_latests_ms": "<insights.sub_latests_ms>",
"medications": 0,
"moodEntries": 0,
"totalMeasurements": 0
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
}
]
}
@@ -0,0 +1,32 @@
{
"status": 401,
"body": {
"data": null,
"error": "Not authenticated",
"meta": {
"errorCode": "auth.missing",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 401,
"action": null,
"meta": null,
"warnings": null,
"error": {
"type": "HttpError",
"message": "Not authenticated"
}
}
],
"effects": [],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [],
"usage": []
}
}
@@ -0,0 +1,35 @@
{
"status": 403,
"body": {
"data": null,
"error": "This AI feature is turned off in your settings",
"meta": {
"capability": "briefing",
"reason": "user_disabled",
"errorCode": "module.disabled",
"module": "insights",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 403,
"action": null,
"meta": null,
"warnings": null,
"error": {
"type": "AiUnavailableError",
"message": "This AI feature is turned off in your settings"
}
}
],
"effects": [],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [],
"usage": []
}
}
@@ -0,0 +1,52 @@
{
"status": 200,
"body": {
"data": {
"insights": null,
"cached": false,
"legacyPayload": false,
"revalidating": true,
"hasProvider": true,
"generationFailed": false,
"generationFailureClass": null,
"briefingOmittedReason": null
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate.read"
},
"meta": {
"cached": false,
"hasProvider": true,
"languageMismatch": false,
"readerLocale": "en",
"revalidating": true
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "enqueueForceWarm",
"args": [
{
"userId": "<id:1>",
"locale": "en"
}
]
}
],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [],
"usage": []
}
}
@@ -0,0 +1,92 @@
{
"status": 200,
"body": {
"data": {
"insights": {
"summary": "Cached summary.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+11:00:00"
},
"cached": true,
"cachedAt": "anchor+0d+11:00:00",
"legacyPayload": false,
"revalidating": false,
"hasProvider": true,
"generationFailed": false,
"generationFailureClass": null,
"briefingOmittedReason": null
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate.read"
},
"meta": {
"cached": true,
"hasProvider": true,
"legacyPayload": false,
"revalidating": false
},
"warnings": null,
"error": null
}
],
"effects": [],
"stored": {
"insightsCachedAt": "anchor+0d+11:00:00",
"insightsCachedLocale": "en",
"cachedPayload": {
"summary": "Cached summary.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+11:00:00"
},
"snapshotHash": null,
"audit": [],
"usage": []
}
}
@@ -0,0 +1,90 @@
{
"status": 200,
"body": {
"data": {
"insights": {
"summary": "Cached summary.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": null,
"briefingGeneratedAt": "anchor-1d+16:00:00"
},
"cached": true,
"cachedAt": "anchor+0d+10:00:00",
"legacyPayload": false,
"revalidating": true,
"hasProvider": true,
"generationFailed": false,
"generationFailureClass": null,
"briefingOmittedReason": null
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate.read"
},
"meta": {
"cached": true,
"hasProvider": true,
"legacyPayload": false,
"revalidating": true
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "enqueueForceWarm",
"args": [
{
"userId": "<id:1>",
"locale": "en"
}
]
}
],
"stored": {
"insightsCachedAt": "anchor+0d+10:00:00",
"insightsCachedLocale": "en",
"cachedPayload": {
"summary": "Cached summary.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor-1d+16:00:00"
},
"snapshotHash": null,
"audit": [],
"usage": []
}
}
@@ -0,0 +1,77 @@
{
"status": 200,
"body": {
"data": {
"insights": null,
"cached": false,
"legacyPayload": false,
"revalidating": true,
"hasProvider": true,
"generationFailed": false,
"generationFailureClass": null,
"briefingOmittedReason": null
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate.read"
},
"meta": {
"cached": false,
"hasProvider": true,
"languageMismatch": true,
"readerLocale": "en",
"revalidating": true
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "enqueueForceWarm",
"args": [
{
"userId": "<id:1>",
"locale": "en"
}
]
}
],
"stored": {
"insightsCachedAt": "anchor+0d+11:00:00",
"insightsCachedLocale": "de",
"cachedPayload": {
"summary": "Cached summary.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+11:00:00"
},
"snapshotHash": null,
"audit": [],
"usage": []
}
}
@@ -0,0 +1,52 @@
{
"status": 200,
"body": {
"data": {
"insights": null,
"cached": false,
"legacyPayload": false,
"revalidating": true,
"hasProvider": true,
"generationFailed": false,
"generationFailureClass": null,
"briefingOmittedReason": null
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate.read"
},
"meta": {
"cached": false,
"hasProvider": true,
"languageMismatch": false,
"readerLocale": "en",
"revalidating": true
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "enqueueForceWarm",
"args": [
{
"userId": "<id:1>",
"locale": "en"
}
]
}
],
"stored": {
"insightsCachedAt": "anchor+0d+11:00:00",
"insightsCachedLocale": null,
"cachedPayload": "{not json",
"snapshotHash": null,
"audit": [],
"usage": []
}
}
@@ -0,0 +1,62 @@
{
"status": 200,
"body": {
"data": {
"insights": {
"summary": "Legacy",
"recommendations": [
"Drink water."
]
},
"cached": true,
"cachedAt": "anchor+0d+11:00:00",
"legacyPayload": true,
"revalidating": true,
"hasProvider": true,
"generationFailed": false,
"generationFailureClass": null,
"briefingOmittedReason": null
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate.read"
},
"meta": {
"cached": true,
"hasProvider": true,
"legacyPayload": true,
"revalidating": true
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "enqueueForceWarm",
"args": [
{
"userId": "<id:1>",
"locale": "en"
}
]
}
],
"stored": {
"insightsCachedAt": "anchor+0d+11:00:00",
"insightsCachedLocale": null,
"cachedPayload": {
"summary": "Legacy",
"recommendations": [
"Drink water."
]
},
"snapshotHash": null,
"audit": [],
"usage": []
}
}
@@ -0,0 +1,102 @@
{
"status": 200,
"body": {
"data": {
"insights": {
"summary": "Cached summary.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+09:00:00"
},
"cached": true,
"cachedAt": "anchor+0d+09:00:00",
"legacyPayload": false,
"revalidating": false,
"hasProvider": true,
"generationFailed": true,
"generationFailureClass": "rate-limit",
"briefingOmittedReason": null
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate.read"
},
"meta": {
"cached": true,
"hasProvider": true,
"legacyPayload": false,
"revalidating": false
},
"warnings": null,
"error": null
}
],
"effects": [],
"stored": {
"insightsCachedAt": "anchor+0d+09:00:00",
"insightsCachedLocale": "en",
"cachedPayload": {
"summary": "Cached summary.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+09:00:00"
},
"snapshotHash": null,
"audit": [
{
"action": "insights.briefing-failure",
"details": {
"reason": "all-providers-failed",
"httpStatus": 429,
"failureKind": null
},
"ipAddress": null
}
],
"usage": []
}
}
@@ -0,0 +1,71 @@
{
"status": 200,
"body": {
"data": {
"insights": null,
"cached": false,
"legacyPayload": false,
"revalidating": true,
"hasProvider": true,
"generationFailed": true,
"generationFailureClass": "format",
"briefingOmittedReason": null
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate.read"
},
"meta": {
"cached": false,
"hasProvider": true,
"languageMismatch": false,
"readerLocale": "en",
"revalidating": true
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "enqueueForceWarm",
"args": [
{
"userId": "<id:1>",
"locale": "en"
}
]
}
],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [
{
"action": "insights.briefing-failure",
"details": {
"reason": "provider-error",
"httpStatus": null,
"failureKind": null
},
"ipAddress": null
},
{
"action": "insights.briefing-failure",
"details": {
"reason": "invalid-json",
"httpStatus": null,
"failureKind": null
},
"ipAddress": null
}
],
"usage": []
}
}
@@ -0,0 +1,78 @@
{
"status": 200,
"body": {
"data": {
"insights": {
"summary": "Cached summary.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": null,
"briefingGeneratedAt": "anchor+0d+10:00:00"
},
"cached": true,
"cachedAt": "anchor+0d+10:00:00",
"legacyPayload": false,
"revalidating": false,
"hasProvider": true,
"generationFailed": false,
"generationFailureClass": null,
"briefingOmittedReason": "ungrounded"
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate.read"
},
"meta": {
"cached": true,
"hasProvider": true,
"legacyPayload": false,
"revalidating": false
},
"warnings": null,
"error": null
}
],
"effects": [],
"stored": {
"insightsCachedAt": "anchor+0d+10:00:00",
"insightsCachedLocale": "en",
"cachedPayload": {
"summary": "Cached summary.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": null,
"briefingGeneratedAt": "anchor+0d+10:00:00"
},
"snapshotHash": null,
"audit": [
{
"action": "insights.briefing-failure",
"details": {
"reason": "briefing-ungrounded",
"httpStatus": null,
"failureKind": null
},
"ipAddress": null
}
],
"usage": []
}
}
@@ -0,0 +1,55 @@
{
"status": 401,
"body": {
"data": null,
"error": "Not authenticated",
"meta": {
"errorCode": "auth.missing",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 401,
"action": null,
"meta": null,
"warnings": null,
"error": {
"type": "HttpError",
"message": "Not authenticated"
}
}
],
"effects": [],
"stored": {
"insightsCachedAt": "anchor+0d+10:00:00",
"insightsCachedLocale": "en",
"cachedPayload": {
"summary": "Cached summary.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": null,
"briefingGeneratedAt": "anchor+0d+10:00:00"
},
"snapshotHash": null,
"audit": [
{
"action": "insights.briefing-failure",
"details": {
"reason": "briefing-ungrounded",
"httpStatus": null,
"failureKind": null
},
"ipAddress": null
}
],
"usage": []
}
}
@@ -0,0 +1,35 @@
{
"status": 403,
"body": {
"data": null,
"error": "This AI feature is turned off in your settings",
"meta": {
"capability": "briefing",
"reason": "user_disabled",
"errorCode": "module.disabled",
"module": "insights",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 403,
"action": null,
"meta": null,
"warnings": null,
"error": {
"type": "AiUnavailableError",
"message": "This AI feature is turned off in your settings"
}
}
],
"effects": [],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [],
"usage": []
}
}
@@ -0,0 +1,29 @@
{
"status": 413,
"body": {
"data": null,
"error": "Request body exceeds 16384 bytes",
"meta": {
"errorCode": "http.payload_too_large",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 413,
"action": null,
"meta": null,
"warnings": null,
"error": null
}
],
"effects": [],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [],
"usage": []
}
}
@@ -0,0 +1,86 @@
{
"status": 200,
"body": {
"data": {
"insights": {
"summary": "Cached summary.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+11:00:00"
},
"cached": true,
"cachedAt": "anchor+0d+11:00:00",
"legacyPayload": false
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate"
},
"meta": {
"briefingless": false,
"cached": true,
"legacyPayload": false
},
"warnings": null,
"error": null
}
],
"effects": [],
"stored": {
"insightsCachedAt": "anchor+0d+11:00:00",
"insightsCachedLocale": "en",
"cachedPayload": {
"summary": "Cached summary.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+11:00:00"
},
"snapshotHash": null,
"audit": [],
"usage": []
}
}
@@ -0,0 +1,276 @@
{
"status": 200,
"body": {
"data": {
"insights": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"cached": false,
"legacyPayload": false,
"briefingOmittedReason": null
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"cached": false,
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"include_raw": false,
"locale": "en",
"model": "claude-parity",
"providerType": "anthropic",
"scopes": 13,
"since_days": 400,
"tokensUsed": 1200
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "blood-pressure",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "pulse",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "weight",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "bmi",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "mood",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "medication-compliance",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "general",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:SLEEP_DURATION",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:STEPS",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:BLOOD_GLUCOSE",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:OXYGEN_SATURATION",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:HEART_RATE_VARIABILITY",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:RESTING_HEART_RATE",
"locale": "en"
}
]
}
],
"stored": {
"insightsCachedAt": "anchor+0d+12:00:00",
"insightsCachedLocale": "en",
"cachedPayload": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a",
"audit": [
{
"action": "insights.generate",
"details": {
"privacyMode": "aggregated",
"tokensUsed": 1200,
"providerType": "anthropic",
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"model": "claude-parity"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 1200,
"operatorTokens": 0,
"messageCount": 1
}
]
}
}
@@ -0,0 +1,275 @@
{
"status": 200,
"body": {
"data": {
"insights": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"cached": false,
"legacyPayload": false,
"briefingOmittedReason": null
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"cached": false,
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"include_raw": false,
"model": "claude-parity",
"providerType": "anthropic",
"scopes": 13,
"since_days": 400,
"tokensUsed": 1200
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "blood-pressure",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "pulse",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "weight",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "bmi",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "mood",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "medication-compliance",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "general",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:SLEEP_DURATION",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:STEPS",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:BLOOD_GLUCOSE",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:OXYGEN_SATURATION",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:HEART_RATE_VARIABILITY",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:RESTING_HEART_RATE",
"locale": "en"
}
]
}
],
"stored": {
"insightsCachedAt": "anchor+0d+12:00:00",
"insightsCachedLocale": "en",
"cachedPayload": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a",
"audit": [
{
"action": "insights.generate",
"details": {
"privacyMode": "aggregated",
"tokensUsed": 1200,
"providerType": "anthropic",
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"model": "claude-parity"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 1200,
"operatorTokens": 0,
"messageCount": 1
}
]
}
}
@@ -0,0 +1,62 @@
{
"status": 200,
"body": {
"data": {
"insights": {
"summary": "Cached summary.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": null,
"briefingGeneratedAt": "anchor+0d+11:00:00"
},
"cached": true,
"cachedAt": "anchor+0d+11:00:00",
"legacyPayload": false
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate"
},
"meta": {
"cached": true,
"locale": "en",
"rate_limited_fallback": true
},
"warnings": null,
"error": null
}
],
"effects": [],
"stored": {
"insightsCachedAt": "anchor+0d+11:00:00",
"insightsCachedLocale": "en",
"cachedPayload": {
"summary": "Cached summary.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": null,
"briefingGeneratedAt": "anchor+0d+11:00:00"
},
"snapshotHash": null,
"audit": [],
"usage": []
}
}
@@ -0,0 +1,29 @@
{
"status": 429,
"body": {
"data": null,
"error": "Maximum 10 insight generations per hour.",
"meta": {
"errorCode": "http.rate_limited",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 429,
"action": null,
"meta": null,
"warnings": null,
"error": null
}
],
"effects": [],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [],
"usage": []
}
}
@@ -0,0 +1,32 @@
{
"status": 500,
"body": {
"data": null,
"error": "Interner Serverfehler",
"meta": {
"errorCode": "http.internal",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 500,
"action": null,
"meta": null,
"warnings": null,
"error": {
"type": "TypeError",
"message": "Invalid authentication tag length: 0"
}
}
],
"effects": [],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [],
"usage": []
}
}
@@ -0,0 +1,275 @@
{
"status": 200,
"body": {
"data": {
"insights": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"cached": false,
"legacyPayload": false,
"briefingOmittedReason": null
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"cached": false,
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"include_raw": false,
"model": "claude-parity",
"providerType": "anthropic",
"scopes": 13,
"since_days": 400,
"tokensUsed": 2100
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "blood-pressure",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "pulse",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "weight",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "bmi",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "mood",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "medication-compliance",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "general",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:SLEEP_DURATION",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:STEPS",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:BLOOD_GLUCOSE",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:OXYGEN_SATURATION",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:HEART_RATE_VARIABILITY",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:RESTING_HEART_RATE",
"locale": "en"
}
]
}
],
"stored": {
"insightsCachedAt": "anchor+0d+12:00:00",
"insightsCachedLocale": "en",
"cachedPayload": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a",
"audit": [
{
"action": "insights.generate",
"details": {
"privacyMode": "aggregated",
"tokensUsed": 2100,
"providerType": "anthropic",
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"model": "claude-parity"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 2100,
"operatorTokens": 0,
"messageCount": 1
}
]
}
}
@@ -0,0 +1,251 @@
{
"status": 200,
"body": {
"data": {
"insights": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"disclaimer": "Consult your doctor.",
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"cached": false,
"legacyPayload": false,
"briefingOmittedReason": null
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"cached": false,
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"include_raw": false,
"model": "claude-parity",
"providerType": "anthropic",
"scopes": 13,
"since_days": 400,
"tokensUsed": 1200
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "blood-pressure",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "pulse",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "weight",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "bmi",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "mood",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "medication-compliance",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "general",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:SLEEP_DURATION",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:STEPS",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:BLOOD_GLUCOSE",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:OXYGEN_SATURATION",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:HEART_RATE_VARIABILITY",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:RESTING_HEART_RATE",
"locale": "en"
}
]
}
],
"stored": {
"insightsCachedAt": "anchor+0d+12:00:00",
"insightsCachedLocale": "en",
"cachedPayload": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"disclaimer": "Consult your doctor.",
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a",
"audit": [
{
"action": "insights.generate",
"details": {
"privacyMode": "aggregated",
"tokensUsed": 1200,
"providerType": "anthropic",
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"model": "claude-parity"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 1200,
"operatorTokens": 0,
"messageCount": 1
}
]
}
}
@@ -0,0 +1,271 @@
{
"status": 200,
"body": {
"data": {
"insights": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"cached": false,
"legacyPayload": false,
"briefingOmittedReason": null
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate"
},
"meta": {
"ai_reasoning_effort": "off",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_usage_level": "frugal",
"cached": false,
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"include_raw": false,
"model": "claude-parity",
"providerType": "anthropic",
"scopes": 13,
"since_days": 400,
"tokensUsed": 1200
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000
},
"promptChars": 39012,
"promptSha256": "9b74ece939faf8f6643d894cc718bd3e0925133e2c6c12d98dfb3f9d68c82d61"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "blood-pressure",
"locale": "de"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "pulse",
"locale": "de"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "weight",
"locale": "de"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "bmi",
"locale": "de"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "mood",
"locale": "de"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "medication-compliance",
"locale": "de"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "general",
"locale": "de"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:SLEEP_DURATION",
"locale": "de"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:STEPS",
"locale": "de"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:BLOOD_GLUCOSE",
"locale": "de"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:OXYGEN_SATURATION",
"locale": "de"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:HEART_RATE_VARIABILITY",
"locale": "de"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:RESTING_HEART_RATE",
"locale": "de"
}
]
}
],
"stored": {
"insightsCachedAt": "anchor+0d+12:00:00",
"insightsCachedLocale": "de",
"cachedPayload": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"snapshotHash": "c98e2d8e9f112318293af870eaa7c15d342f7f37a12047b0fd4e2a7f66d84676",
"audit": [
{
"action": "insights.generate",
"details": {
"privacyMode": "aggregated",
"tokensUsed": 1200,
"providerType": "anthropic",
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"model": "claude-parity"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 1200,
"operatorTokens": 0,
"messageCount": 1
}
]
}
}
@@ -0,0 +1,278 @@
{
"status": 200,
"body": {
"data": {
"insights": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"cached": false,
"legacyPayload": false,
"briefingOmittedReason": null
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"cached": false,
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"include_raw": false,
"insights_features_downgraded": true,
"insights_features_hard_downgraded": false,
"insights_features_oversize_bytes": 6100000,
"model": "claude-parity",
"providerType": "anthropic",
"scopes": 13,
"since_days": 400,
"tokensUsed": 1200
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34783,
"promptSha256": "2aeca4de2fc6397815465ac4de27a2267fdfc69cd4a607b2e19d7b06502bc040"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "blood-pressure",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "pulse",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "weight",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "bmi",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "mood",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "medication-compliance",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "general",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:SLEEP_DURATION",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:STEPS",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:BLOOD_GLUCOSE",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:OXYGEN_SATURATION",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:HEART_RATE_VARIABILITY",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:RESTING_HEART_RATE",
"locale": "en"
}
]
}
],
"stored": {
"insightsCachedAt": "anchor+0d+12:00:00",
"insightsCachedLocale": "en",
"cachedPayload": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a",
"audit": [
{
"action": "insights.generate",
"details": {
"privacyMode": "raw",
"tokensUsed": 1200,
"providerType": "anthropic",
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"model": "claude-parity"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 1200,
"operatorTokens": 0,
"messageCount": 1
}
]
}
}
@@ -0,0 +1,278 @@
{
"status": 200,
"body": {
"data": {
"insights": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"cached": false,
"legacyPayload": false,
"briefingOmittedReason": null
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"cached": false,
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"include_raw": false,
"insights_features_downgraded": true,
"insights_features_hard_downgraded": true,
"insights_features_oversize_bytes": 5400000,
"model": "claude-parity",
"providerType": "anthropic",
"scopes": 13,
"since_days": 400,
"tokensUsed": 1200
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34193,
"promptSha256": "f11bae5de41f1344113977970c832df38427bcdf4b24a6f7fb8b05cef899c5af"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "blood-pressure",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "pulse",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "weight",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "bmi",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "mood",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "medication-compliance",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "general",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:SLEEP_DURATION",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:STEPS",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:BLOOD_GLUCOSE",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:OXYGEN_SATURATION",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:HEART_RATE_VARIABILITY",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:RESTING_HEART_RATE",
"locale": "en"
}
]
}
],
"stored": {
"insightsCachedAt": "anchor+0d+12:00:00",
"insightsCachedLocale": "en",
"cachedPayload": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"snapshotHash": "11a4fb0ae0188e5a95cbe69124afaccdb326b76ea8e8ad73affbc819255a01cc",
"audit": [
{
"action": "insights.generate",
"details": {
"privacyMode": "raw",
"tokensUsed": 1200,
"providerType": "anthropic",
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"model": "claude-parity"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 1200,
"operatorTokens": 0,
"messageCount": 1
}
]
}
}
@@ -0,0 +1,32 @@
{
"status": 422,
"body": {
"data": null,
"error": "Your data exceeds the AI payload size limit. Reduce the data window or exclude optional blocks in Settings > AI.",
"meta": {
"errorCode": "http.invalid",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 422,
"action": null,
"meta": {
"insights_payload_oversize_bytes": 5200000,
"insights_payload_too_large": true
},
"warnings": null,
"error": null
}
],
"effects": [],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [],
"usage": []
}
}
@@ -0,0 +1,58 @@
{
"status": 429,
"body": {
"data": null,
"error": "Daily AI token budget reached. Insights will be available again tomorrow.",
"meta": {
"errorCode": "insights.generate.budgetExceeded",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 429,
"action": {
"name": "insights.briefing.budget_exceeded"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"cap": 1600000,
"include_raw": false,
"limit": "owner-cap",
"operatorAfter": 0,
"owner": "user",
"since_days": 400,
"stage": "generate",
"surface": "job",
"totalAfter": 5000000
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
}
],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 5000000,
"operatorTokens": 0,
"messageCount": 0
}
]
}
}
@@ -0,0 +1,103 @@
{
"status": 422,
"body": {
"data": null,
"error": "AI provider rejected the request. Check your API key in Settings > AI",
"meta": {
"errorCode": "http.invalid",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 422,
"action": {
"name": "insights.feature_cache.miss"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"include_raw": false,
"insights_chain_attempts": 2,
"insights_chain_first_provider": "anthropic",
"insights_chain_first_status": 401,
"insights_chain_outcome": "all-failed",
"since_days": 400
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
},
{
"effect": "recordBriefingFailure",
"args": [
{
"userId": "<id:1>",
"reason": "all-providers-failed",
"locale": "en",
"httpStatus": 401,
"failureKind": null
}
]
}
],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [
{
"action": "insights.briefing-failure",
"details": {
"dateKey": "day+0",
"reason": "all-providers-failed",
"httpStatus": 401,
"failureKind": null,
"locale": "en",
"triedAt": "anchor+0d+12:00:00"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 0,
"operatorTokens": 0,
"messageCount": 1
}
]
}
}
@@ -0,0 +1,103 @@
{
"status": 429,
"body": {
"data": null,
"error": "AI provider rate-limited the request",
"meta": {
"errorCode": "http.rate_limited",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 429,
"action": {
"name": "insights.feature_cache.miss"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"include_raw": false,
"insights_chain_attempts": 1,
"insights_chain_first_provider": "anthropic",
"insights_chain_first_status": 429,
"insights_chain_outcome": "all-failed",
"since_days": 400
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
},
{
"effect": "recordBriefingFailure",
"args": [
{
"userId": "<id:1>",
"reason": "all-providers-failed",
"locale": "en",
"httpStatus": 429,
"failureKind": null
}
]
}
],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [
{
"action": "insights.briefing-failure",
"details": {
"dateKey": "day+0",
"reason": "all-providers-failed",
"httpStatus": 429,
"failureKind": null,
"locale": "en",
"triedAt": "anchor+0d+12:00:00"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 0,
"operatorTokens": 0,
"messageCount": 1
}
]
}
}
@@ -0,0 +1,103 @@
{
"status": 503,
"body": {
"data": null,
"error": "AI provider temporarily unavailable, try again in a moment",
"meta": {
"errorCode": "http.unavailable",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 503,
"action": {
"name": "insights.feature_cache.miss"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"include_raw": false,
"insights_chain_attempts": 2,
"insights_chain_first_provider": "anthropic",
"insights_chain_first_status": 401,
"insights_chain_outcome": "all-failed",
"since_days": 400
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
},
{
"effect": "recordBriefingFailure",
"args": [
{
"userId": "<id:1>",
"reason": "all-providers-failed",
"locale": "en",
"httpStatus": 401,
"failureKind": null
}
]
}
],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [
{
"action": "insights.briefing-failure",
"details": {
"dateKey": "day+0",
"reason": "all-providers-failed",
"httpStatus": 401,
"failureKind": null,
"locale": "en",
"triedAt": "anchor+0d+12:00:00"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 0,
"operatorTokens": 0,
"messageCount": 1
}
]
}
}
@@ -0,0 +1,103 @@
{
"status": 422,
"body": {
"data": null,
"error": "AI provider connection failed. Check your AI settings",
"meta": {
"errorCode": "http.invalid",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 422,
"action": {
"name": "insights.feature_cache.miss"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"include_raw": false,
"insights_chain_attempts": 2,
"insights_chain_first_provider": "anthropic",
"insights_chain_first_status": null,
"insights_chain_outcome": "all-failed",
"since_days": 400
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
},
{
"effect": "recordBriefingFailure",
"args": [
{
"userId": "<id:1>",
"reason": "all-providers-failed",
"locale": "en",
"httpStatus": null,
"failureKind": null
}
]
}
],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [
{
"action": "insights.briefing-failure",
"details": {
"dateKey": "day+0",
"reason": "all-providers-failed",
"httpStatus": null,
"failureKind": null,
"locale": "en",
"triedAt": "anchor+0d+12:00:00"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 0,
"operatorTokens": 0,
"messageCount": 1
}
]
}
}
@@ -0,0 +1,103 @@
{
"status": 503,
"body": {
"data": null,
"error": "AI provider temporarily unavailable, try again in a moment",
"meta": {
"errorCode": "http.unavailable",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 503,
"action": {
"name": "insights.feature_cache.miss"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"include_raw": false,
"insights_chain_attempts": 2,
"insights_chain_first_provider": "anthropic",
"insights_chain_first_status": 404,
"insights_chain_outcome": "all-failed",
"since_days": 400
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
},
{
"effect": "recordBriefingFailure",
"args": [
{
"userId": "<id:1>",
"reason": "all-providers-failed",
"locale": "en",
"httpStatus": 404,
"failureKind": null
}
]
}
],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [
{
"action": "insights.briefing-failure",
"details": {
"dateKey": "day+0",
"reason": "all-providers-failed",
"httpStatus": 404,
"failureKind": null,
"locale": "en",
"triedAt": "anchor+0d+12:00:00"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 0,
"operatorTokens": 0,
"messageCount": 1
}
]
}
}
@@ -0,0 +1,102 @@
{
"status": 422,
"body": {
"data": null,
"error": "AI provider connection failed. Check your AI settings",
"meta": {
"errorCode": "http.invalid",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 422,
"action": {
"name": "insights.feature_cache.miss"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"include_raw": false,
"insights_provider_body_excerpt": "{\"error\":\"status 400\"}",
"insights_provider_error": "upstream answered 400",
"insights_provider_status": 400,
"since_days": 400
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
},
{
"effect": "recordBriefingFailure",
"args": [
{
"userId": "<id:1>",
"reason": "provider-error",
"locale": "en",
"httpStatus": 400,
"failureKind": "http"
}
]
}
],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [
{
"action": "insights.briefing-failure",
"details": {
"dateKey": "day+0",
"reason": "provider-error",
"httpStatus": 400,
"failureKind": "http",
"locale": "en",
"triedAt": "anchor+0d+12:00:00"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 0,
"operatorTokens": 0,
"messageCount": 1
}
]
}
}
@@ -0,0 +1,102 @@
{
"status": 422,
"body": {
"data": null,
"error": "AI provider rejected the request. Check your API key in Settings > AI",
"meta": {
"errorCode": "http.invalid",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 422,
"action": {
"name": "insights.feature_cache.miss"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"include_raw": false,
"insights_provider_body_excerpt": "{\"error\":\"status 401\"}",
"insights_provider_error": "upstream answered 401",
"insights_provider_status": 401,
"since_days": 400
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
},
{
"effect": "recordBriefingFailure",
"args": [
{
"userId": "<id:1>",
"reason": "provider-error",
"locale": "en",
"httpStatus": 401,
"failureKind": "http"
}
]
}
],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [
{
"action": "insights.briefing-failure",
"details": {
"dateKey": "day+0",
"reason": "provider-error",
"httpStatus": 401,
"failureKind": "http",
"locale": "en",
"triedAt": "anchor+0d+12:00:00"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 0,
"operatorTokens": 0,
"messageCount": 1
}
]
}
}
@@ -0,0 +1,102 @@
{
"status": 429,
"body": {
"data": null,
"error": "AI provider rate-limited the request",
"meta": {
"errorCode": "http.rate_limited",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 429,
"action": {
"name": "insights.feature_cache.miss"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"include_raw": false,
"insights_provider_body_excerpt": "{\"error\":\"status 429\"}",
"insights_provider_error": "upstream answered 429",
"insights_provider_status": 429,
"since_days": 400
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
},
{
"effect": "recordBriefingFailure",
"args": [
{
"userId": "<id:1>",
"reason": "provider-error",
"locale": "en",
"httpStatus": 429,
"failureKind": "http"
}
]
}
],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [
{
"action": "insights.briefing-failure",
"details": {
"dateKey": "day+0",
"reason": "provider-error",
"httpStatus": 429,
"failureKind": "http",
"locale": "en",
"triedAt": "anchor+0d+12:00:00"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 0,
"operatorTokens": 0,
"messageCount": 1
}
]
}
}
@@ -0,0 +1,102 @@
{
"status": 503,
"body": {
"data": null,
"error": "AI provider temporarily unavailable, try again in a moment",
"meta": {
"errorCode": "http.unavailable",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 503,
"action": {
"name": "insights.feature_cache.miss"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"include_raw": false,
"insights_provider_body_excerpt": "{\"error\":\"status 500\"}",
"insights_provider_error": "upstream answered 500",
"insights_provider_status": 500,
"since_days": 400
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
},
{
"effect": "recordBriefingFailure",
"args": [
{
"userId": "<id:1>",
"reason": "provider-error",
"locale": "en",
"httpStatus": 500,
"failureKind": "http"
}
]
}
],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [
{
"action": "insights.briefing-failure",
"details": {
"dateKey": "day+0",
"reason": "provider-error",
"httpStatus": 500,
"failureKind": "http",
"locale": "en",
"triedAt": "anchor+0d+12:00:00"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 0,
"operatorTokens": 0,
"messageCount": 1
}
]
}
}
@@ -0,0 +1,97 @@
{
"status": 422,
"body": {
"data": null,
"error": "AI response was not valid JSON",
"meta": {
"errorCode": "http.invalid",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 422,
"action": {
"name": "insights.feature_cache.miss"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"include_raw": false,
"since_days": 400
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
},
{
"effect": "recordBriefingFailure",
"args": [
{
"userId": "<id:1>",
"reason": "invalid-json",
"locale": "en"
}
]
}
],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [
{
"action": "insights.briefing-failure",
"details": {
"dateKey": "day+0",
"reason": "invalid-json",
"httpStatus": null,
"failureKind": null,
"locale": "en",
"triedAt": "anchor+0d+12:00:00"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 1200,
"operatorTokens": 0,
"messageCount": 1
}
]
}
}
@@ -0,0 +1,98 @@
{
"status": 422,
"body": {
"data": null,
"error": "AI response was cut off before the JSON completed. Raise the token limit (INSIGHTS_MAX_TOKENS)",
"meta": {
"errorCode": "ai_response_truncated",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 422,
"action": {
"name": "insights.feature_cache.miss"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"include_raw": false,
"insights_response_truncated": true,
"since_days": 400
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
},
{
"effect": "recordBriefingFailure",
"args": [
{
"userId": "<id:1>",
"reason": "invalid-json",
"locale": "en"
}
]
}
],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [
{
"action": "insights.briefing-failure",
"details": {
"dateKey": "day+0",
"reason": "invalid-json",
"httpStatus": null,
"failureKind": null,
"locale": "en",
"triedAt": "anchor+0d+12:00:00"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 1200,
"operatorTokens": 0,
"messageCount": 1
}
]
}
}
@@ -0,0 +1,295 @@
{
"status": 200,
"body": {
"data": {
"insights": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"cached": false,
"legacyPayload": false,
"briefingOmittedReason": null
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"cached": false,
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"include_raw": false,
"model": "claude-parity",
"providerType": "anthropic",
"scopes": 13,
"since_days": 400,
"tokensUsed": 900,
"ungroundedCount": 1
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000
},
"promptChars": 35302,
"promptSha256": "2b3978fcf41077f56835864a67264bea403c0dc31a6bd211ecc469afa16d8a7d"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "blood-pressure",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "pulse",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "weight",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "bmi",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "mood",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "medication-compliance",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "general",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:SLEEP_DURATION",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:STEPS",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:BLOOD_GLUCOSE",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:OXYGEN_SATURATION",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:HEART_RATE_VARIABILITY",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:RESTING_HEART_RATE",
"locale": "en"
}
]
}
],
"stored": {
"insightsCachedAt": "anchor+0d+12:00:00",
"insightsCachedLocale": "en",
"cachedPayload": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Your readings held steady this week, with no surprises.",
"signalsOfDay": [
{
"sourceMetric": "weight",
"tone": "info",
"headline": "Weight is steady",
"nudge": "Keep weighing in at the same time each morning.",
"delta": null
}
],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a",
"audit": [
{
"action": "insights.generate",
"details": {
"privacyMode": "aggregated",
"tokensUsed": 900,
"providerType": "anthropic",
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"model": "claude-parity"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 2100,
"operatorTokens": 0,
"messageCount": 2
}
]
}
}
@@ -0,0 +1,280 @@
{
"status": 200,
"body": {
"data": {
"insights": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Yesterday's briefing, grounded when it was written.",
"signalsOfDay": [],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"cached": false,
"legacyPayload": false,
"briefingOmittedReason": null
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"briefing_fallback": "previous-cached",
"cached": false,
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"include_raw": false,
"model": "claude-parity",
"providerType": "anthropic",
"scopes": 13,
"since_days": 400,
"tokensUsed": 1200,
"ungroundedCount": 1
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000
},
"promptChars": 35302,
"promptSha256": "2b3978fcf41077f56835864a67264bea403c0dc31a6bd211ecc469afa16d8a7d"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "blood-pressure",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "pulse",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "weight",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "bmi",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "mood",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "medication-compliance",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "general",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:SLEEP_DURATION",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:STEPS",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:BLOOD_GLUCOSE",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:OXYGEN_SATURATION",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:HEART_RATE_VARIABILITY",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:RESTING_HEART_RATE",
"locale": "en"
}
]
}
],
"stored": {
"insightsCachedAt": "anchor+0d+12:00:00",
"insightsCachedLocale": "en",
"cachedPayload": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": {
"paragraph": "Yesterday's briefing, grounded when it was written.",
"signalsOfDay": [],
"keyFindings": []
},
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a",
"audit": [
{
"action": "insights.generate",
"details": {
"privacyMode": "aggregated",
"tokensUsed": 1200,
"providerType": "anthropic",
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"model": "claude-parity"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 2400,
"operatorTokens": 0,
"messageCount": 2
}
]
}
}
@@ -0,0 +1,294 @@
{
"status": 200,
"body": {
"data": {
"insights": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": null,
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"cached": false,
"legacyPayload": false,
"briefingOmittedReason": "ungrounded"
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"briefing_fallback": "stripped",
"cached": false,
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"include_raw": false,
"model": "claude-parity",
"providerType": "anthropic",
"scopes": 13,
"since_days": 400,
"tokensUsed": 1200,
"ungroundedCount": 1
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000
},
"promptChars": 35302,
"promptSha256": "2b3978fcf41077f56835864a67264bea403c0dc31a6bd211ecc469afa16d8a7d"
}
]
},
{
"effect": "recordBriefingFailure",
"args": [
{
"userId": "<id:1>",
"reason": "briefing-ungrounded",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "blood-pressure",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "pulse",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "weight",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "bmi",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "mood",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "medication-compliance",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "general",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:SLEEP_DURATION",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:STEPS",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:BLOOD_GLUCOSE",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:OXYGEN_SATURATION",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:HEART_RATE_VARIABILITY",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:RESTING_HEART_RATE",
"locale": "en"
}
]
}
],
"stored": {
"insightsCachedAt": "anchor+0d+12:00:00",
"insightsCachedLocale": "en",
"cachedPayload": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": null,
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a",
"audit": [
{
"action": "insights.briefing-failure",
"details": {
"dateKey": "day+0",
"reason": "briefing-ungrounded",
"httpStatus": null,
"failureKind": null,
"locale": "en",
"triedAt": "anchor+0d+12:00:00"
},
"ipAddress": null
},
{
"action": "insights.generate",
"details": {
"privacyMode": "aggregated",
"tokensUsed": 1200,
"providerType": "anthropic",
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"model": "claude-parity"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 2400,
"operatorTokens": 0,
"messageCount": 2
}
]
}
}
@@ -0,0 +1,294 @@
{
"status": 200,
"body": {
"data": {
"insights": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": null,
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"cached": false,
"legacyPayload": false,
"briefingOmittedReason": "ungrounded"
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "insights.generate"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"briefing_fallback": "stripped",
"cached": false,
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"include_raw": false,
"model": "claude-parity",
"providerType": "anthropic",
"scopes": 13,
"since_days": 400,
"tokensUsed": 1200,
"ungroundedCount": 1
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000
},
"promptChars": 35302,
"promptSha256": "2b3978fcf41077f56835864a67264bea403c0dc31a6bd211ecc469afa16d8a7d"
}
]
},
{
"effect": "recordBriefingFailure",
"args": [
{
"userId": "<id:1>",
"reason": "briefing-ungrounded",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "blood-pressure",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "pulse",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "weight",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "bmi",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "mood",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "medication-compliance",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "general",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:SLEEP_DURATION",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:STEPS",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:BLOOD_GLUCOSE",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:OXYGEN_SATURATION",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:HEART_RATE_VARIABILITY",
"locale": "en"
}
]
},
{
"effect": "enqueueStatusGeneration",
"args": [
{
"userId": "<id:1>",
"metric": "metric:RESTING_HEART_RATE",
"locale": "en"
}
]
}
],
"stored": {
"insightsCachedAt": "anchor+0d+12:00:00",
"insightsCachedLocale": "en",
"cachedPayload": {
"summary": "Your tracked metrics held steady over the last 30 days.",
"classification": "gut",
"findings": [],
"correlations": [],
"recommendations": [],
"dataQuality": {
"coverage": "good",
"gaps": [],
"confidence": "hoch"
},
"dailyBriefing": null,
"briefingGeneratedAt": "anchor+0d+12:00:00"
},
"snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a",
"audit": [
{
"action": "insights.briefing-failure",
"details": {
"dateKey": "day+0",
"reason": "briefing-ungrounded",
"httpStatus": null,
"failureKind": null,
"locale": "en",
"triedAt": "anchor+0d+12:00:00"
},
"ipAddress": null
},
{
"action": "insights.generate",
"details": {
"privacyMode": "aggregated",
"tokensUsed": 1200,
"providerType": "anthropic",
"chainProviderType": "anthropic",
"fallbackHopCount": 0,
"model": "claude-parity"
},
"ipAddress": null
}
],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 2400,
"operatorTokens": 0,
"messageCount": 2
}
]
}
}
@@ -0,0 +1,76 @@
{
"status": 502,
"body": {
"data": null,
"error": "The generated insight did not pass the safety check and was discarded. Try regenerating.",
"meta": {
"errorCode": "insights.generate.outboundScreened",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 502,
"action": {
"name": "insights.generate.outbound_blocked"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"include_raw": false,
"locale": "en",
"reason": "risk_score",
"since_days": 400
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
}
],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 1200,
"operatorTokens": 0,
"messageCount": 1
}
]
}
}
@@ -0,0 +1,74 @@
{
"status": 409,
"body": {
"data": null,
"error": "Profile scope changed during insight generation",
"meta": {
"errorCode": "insights.generate.profileScopeChanged",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 409,
"action": {
"name": "insights.feature_cache.miss"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"include_raw": false,
"since_days": 400
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
}
],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 1200,
"operatorTokens": 0,
"messageCount": 1
}
]
}
}
@@ -0,0 +1,74 @@
{
"status": 409,
"body": {
"data": null,
"error": "Insight scope changed during generation",
"meta": {
"errorCode": "insights.generate.scopeChanged",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 409,
"action": {
"name": "insights.feature_cache.miss"
},
"meta": {
"ai_reasoning_effort": "medium",
"ai_reasoning_job": "daily_briefing",
"ai_reasoning_source": "job",
"include_raw": false,
"since_days": 400
},
"warnings": null,
"error": null
}
],
"effects": [
{
"effect": "ensureUserMoodRollupsFresh",
"args": [
"<id:1>"
]
},
{
"effect": "provider",
"args": [
{
"surface": "job",
"chain": [
"anthropic"
],
"params": {
"temperature": 0.3,
"maxTokens": 2500,
"responseFormat": "json",
"timeoutMs": 180000,
"reasoning": {
"effort": "medium",
"summaries": false
}
},
"promptChars": 34773,
"promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72"
}
]
}
],
"stored": {
"insightsCachedAt": null,
"insightsCachedLocale": null,
"cachedPayload": null,
"snapshotHash": null,
"audit": [],
"usage": [
{
"dateKey": "day+0",
"totalTokens": 1200,
"operatorTokens": 0,
"messageCount": 1
}
]
}
}
@@ -0,0 +1,471 @@
/**
* Parity for `GET /api/insights/comprehensive` while the overview builder
* moves out of the route into `src/lib/insights/`.
*
* One account with a full record (see `record-fixture.ts`) walks through the
* cases the route tells apart, in order, each request seeing the cache the
* earlier ones filled: an anonymous call, the overview cold, warm, and stale
* after a measurement write (served while one rebuild runs behind it), the
* `ai` block after the AI analysis opt-out and after the operator's master
* switch (both off the cached body), a delegate with MANAGE access reading
* the record cold, one with READ access refused, and the shared analytics
* read ceiling. A second account in a zone west of UTC carries the sparse
* shapes: mood entries with no rollup tier behind them, pulse without a
* resting series, a medication with no schedule, an as-needed one, and no
* height. An account with no data closes the walk.
*
* The clock is pinned (`Date` only) to noon of the day the test runs. The
* aggregator cuts its 7-, 30- and 60-day windows in SQL against the
* database clock, which the pin cannot reach, so the record's measurements
* on exactly those days back are removed: a reading there would fall in or
* out of a window with the hour the test runs. Every instant and day key in
* the snapshot is written relative to the start of the day.
*
* The rollup refreshes the route starts and does not await are recorded
* instead of run; the record's rollups are built once before the walk.
*/
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { GET } from "@/app/api/insights/comprehensive/route";
import { __resetAllCachesForTests } from "@/lib/cache/server-cache";
import { invalidateUserMeasurements } from "@/lib/cache/invalidate";
import { settleBackgroundTasks } from "@/lib/logging/background-tasks";
import { anonymous, callRoute, type RouteAuth } from "../../../kit/call-route";
import {
makeGrant,
makeSession,
makeUser,
type SessionFixture,
} from "../../../kit/factories";
import {
effectRecorder,
eventFacts,
expectParity,
relativeToAnchor,
stable,
startOfUtcToday,
wideEventRecorder,
} from "../../../kit/parity";
import {
getPrismaClient,
switchSessionTo,
truncateAllTables,
} from "../../setup";
import { parityId, seedParityRecord } from "./record-fixture";
vi.mock("@/lib/logging/transports", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@/lib/logging/transports")>();
const { wideEventRecorder: recorder } = await import("../../../kit/parity");
return {
...actual,
emitIfSampled: (event: (typeof recorder.events)[number]) => {
recorder.events.push(event);
},
};
});
vi.mock("@/lib/rollups/measurement-rollups", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@/lib/rollups/measurement-rollups")>();
const { effectRecorder } = await import("../../../kit/parity");
return {
...actual,
ensureUserRollupsFresh: async (...args: unknown[]) => {
effectRecorder.record("ensureUserRollupsFresh", args);
},
};
});
vi.mock("@/lib/rollups/mood-rollups", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@/lib/rollups/mood-rollups")>();
const { effectRecorder } = await import("../../../kit/parity");
return {
...actual,
ensureUserMoodRollupsFresh: async (...args: unknown[]) => {
effectRecorder.record("ensureUserMoodRollupsFresh", args);
},
};
});
const prisma = getPrismaClient();
const DAY = 86_400_000;
const ANCHOR = startOfUtcToday();
const NOW = new Date(ANCHOR.getTime() + 12 * 60 * 60 * 1000);
/** Days back on which a SQL window edge falls (see the header). */
const EDGE_DAYS = [7, 30, 60];
const OWNER_ID = parityId("insightscomp", 1);
const SPARSE_ID = parityId("insightscomp", 2);
const EMPTY_ID = parityId("insightscomp", 3);
const MANAGER_ID = parityId("insightscomp", 4);
const READER_ID = parityId("insightscomp", 5);
let owner: SessionFixture;
let sparse: RouteAuth;
let empty: RouteAuth;
let manager: SessionFixture;
let reader: SessionFixture;
/**
* Keys of the aggregator's per-phase timings, which the clock and the load
* decide. Their presence is kept, their value is not.
*/
function timingKeys(value: unknown, out = new Set<string>()): Set<string> {
if (Array.isArray(value)) value.forEach((v) => timingKeys(v, out));
else if (value && typeof value === "object") {
for (const [k, v] of Object.entries(value)) {
if (/_ms$/.test(k)) out.add(k);
timingKeys(v, out);
}
}
return out;
}
/**
* A stale serve starts one rebuild and returns without waiting for it. The
* rebuild annotates the request's event too, and whether it gets there before
* the event is emitted is a race between two sets of queries. On that case
* only the cache outcome is kept, which the route decides alone.
*/
function cacheFactsOnly(facts: unknown): unknown {
const f = facts as { meta?: Record<string, unknown> | null };
const meta = Object.fromEntries(
Object.entries(f.meta ?? {}).filter(([k]) => k.startsWith("cache.")),
);
return { ...f, action: "<raced>", meta };
}
/**
* The aggregator's sub-queries run in parallel and annotate as each one
* finishes, so the order of their keys in the event follows the database, not
* the code. The keys are sorted; what they hold is unchanged.
*/
function sortedMeta(facts: unknown): unknown {
const f = facts as { meta?: Record<string, unknown> | null };
if (!f.meta) return facts;
const meta = Object.fromEntries(
Object.entries(f.meta).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
);
return { ...f, meta };
}
async function step(
name: string,
options: { auth?: RouteAuth; raced?: boolean } = {},
): Promise<void> {
wideEventRecorder.take();
effectRecorder.take();
const res = await callRoute(GET, {
method: "GET",
route: "/api/insights/comprehensive",
auth: options.auth ?? owner,
});
// A stale serve leaves one rebuild running; it finishes before the
// snapshot so the next case starts from the same cache.
await settleBackgroundTasks();
const events = wideEventRecorder.take();
expect(events.length).toBeGreaterThan(0);
const facts = events
.map(eventFacts)
.map(sortedMeta)
.map((f) => (options.raced ? cacheFactsOnly(f) : f));
await expectParity(
`insights-comprehensive.${name}`,
stable(
relativeToAnchor(
{
status: res.status,
body: res.body,
events: facts,
effects: effectRecorder.take(),
},
ANCHOR,
),
{ since: NOW, volatileKeys: [...timingKeys(facts)] },
),
);
}
/** Remove the record's measurements on the SQL window edge days. */
async function dropEdgeDays(userId: string): Promise<void> {
for (const d of EDGE_DAYS) {
await prisma.measurement.deleteMany({
where: {
userId,
measuredAt: {
gte: new Date(ANCHOR.getTime() - d * DAY),
lt: new Date(ANCHOR.getTime() - (d - 1) * DAY),
},
},
});
}
}
/** Mood without rollups, pulse without a resting series, odd medications. */
async function seedSparseRecord(userId: string): Promise<void> {
const at = (d: number, hh: number, mm = 0) =>
new Date(ANCHOR.getTime() - d * DAY + (hh * 60 + mm) * 60_000);
const rows = [];
for (let d = 1; d < 50; d++) {
if (EDGE_DAYS.includes(d)) continue;
rows.push(
{
userId,
type: "WEIGHT" as const,
value: 70 + (d % 5) * 0.3,
unit: "kg",
measuredAt: at(d, 3, 30),
},
{
userId,
type: "BLOOD_PRESSURE_SYS" as const,
// Higher on the days the morning dose was skipped, so the
// continuity correlation has something to find.
value: 131 + ((d * 3) % 11) + (d % 5 === 0 ? 22 : 0),
unit: "mmHg",
measuredAt: at(d, 4, 10),
},
{
userId,
type: "BLOOD_PRESSURE_DIA" as const,
value: 84 + ((d * 2) % 7),
unit: "mmHg",
measuredAt: at(d, 4, 10),
},
{
userId,
type: "PULSE" as const,
value: 66 + (d % 9),
unit: "bpm",
measuredAt: at(d, 4, 0),
},
{
userId,
type: "PULSE" as const,
value: 92 + (d % 5),
unit: "bpm",
measuredAt: at(d, 18, 0),
},
{
userId,
type: "PULSE" as const,
value: 78 + (d % 7),
unit: "bpm",
measuredAt: at(d, 15, 0),
},
);
}
await prisma.measurement.createMany({ data: rows });
const moods = ["LAUSIG", "SCHLECHT", "OKAY", "GUT", "SUPER_GUT"];
const moodRows = [];
for (let d = 1; d < 30; d++) {
const loggedAt = at(d, 2, 15);
const score = 1 + ((d * 2) % 5);
moodRows.push({
userId,
// 02:15 UTC is the evening before in Bogotá.
date: new Date(loggedAt.getTime() - 5 * 60 * 60 * 1000)
.toISOString()
.slice(0, 10),
mood: moods[score - 1],
score,
moodLoggedAt: loggedAt,
tz: "America/Bogota",
createdAt: loggedAt,
});
}
await prisma.moodEntry.createMany({ data: moodRows });
const created = new Date(ANCHOR.getTime() - 60 * DAY);
await prisma.medication.create({
data: {
id: parityId("insightscompmed", 1),
userId,
name: "Amlodipine",
dose: "5mg",
createdAt: created,
schedules: {
create: [
{
id: parityId("insightscompsch", 1),
windowStart: "07:00",
windowEnd: "09:00",
},
],
},
categoryAssignment: { create: { category: "BLOOD_PRESSURE" } },
},
});
await prisma.medication.create({
data: {
id: parityId("insightscompmed", 2),
userId,
name: "No schedule",
dose: "1 tab",
createdAt: created,
},
});
await prisma.medication.create({
data: {
id: parityId("insightscompmed", 3),
userId,
name: "Ibuprofen",
dose: "400mg",
asNeeded: true,
createdAt: created,
},
});
const intakes = [];
for (let d = 1; d <= 40; d++) {
if (d % 4 === 0 || EDGE_DAYS.includes(d)) continue;
intakes.push({
id: parityId("insightscompie", d),
userId,
medicationId: parityId("insightscompmed", 1),
scheduledFor: at(d, 12, 0),
takenAt: d % 5 === 0 ? null : at(d, 12, 30),
skipped: d % 5 === 0,
createdAt: at(d, 12, 0),
});
}
await prisma.medicationIntakeEvent.createMany({ data: intakes });
}
beforeAll(async () => {
vi.useFakeTimers({ toFake: ["Date"], now: NOW });
await truncateAllTables(prisma);
__resetAllCachesForTests();
const ownerRow = await makeUser({
id: OWNER_ID,
timezone: "UTC",
locale: "en",
heightCm: 178,
dateOfBirth: new Date("1975-04-02T00:00:00.000Z"),
gender: "male",
aiProvider: "ANTHROPIC",
aiModel: "claude-sonnet-4-6",
// Presence only: the overview never decrypts a key.
aiAnthropicKeyEncrypted: "v1:presence-only",
});
await prisma.consentReceipt.create({
data: {
userId: OWNER_ID,
kind: "ai_full",
artefact: "test",
signedAt: NOW,
},
});
await seedParityRecord({ userId: OWNER_ID, now: NOW, tz: "UTC", tag: "ic" });
await dropEdgeDays(OWNER_ID);
const rollups = await vi.importActual<
typeof import("@/lib/rollups/measurement-rollups")
>("@/lib/rollups/measurement-rollups");
await rollups.ensureUserRollupsFresh(OWNER_ID);
const moodRollups = await vi.importActual<
typeof import("@/lib/rollups/mood-rollups")
>("@/lib/rollups/mood-rollups");
await moodRollups.ensureUserMoodRollupsFresh(OWNER_ID);
await settleBackgroundTasks();
owner = await makeSession(ownerRow);
const sparseRow = await makeUser({
id: SPARSE_ID,
timezone: "America/Bogota",
locale: "de",
dateOfBirth: new Date("1950-08-20T00:00:00.000Z"),
});
await seedSparseRecord(SPARSE_ID);
// Measurement rollups only: the mood tier stays empty on purpose.
await rollups.ensureUserRollupsFresh(SPARSE_ID);
await settleBackgroundTasks();
sparse = await makeSession(sparseRow);
const emptyRow = await makeUser({ id: EMPTY_ID, timezone: "UTC" });
empty = await makeSession(emptyRow);
const managerRow = await makeUser({ id: MANAGER_ID, timezone: "UTC" });
await makeGrant(ownerRow, managerRow, { access: "MANAGE" });
manager = await makeSession(managerRow);
await switchSessionTo(manager.sessionId, OWNER_ID);
const readerRow = await makeUser({ id: READER_ID, timezone: "UTC" });
await makeGrant(ownerRow, readerRow, { access: "READ" });
reader = await makeSession(readerRow);
await switchSessionTo(reader.sessionId, OWNER_ID);
});
afterAll(() => {
vi.useRealTimers();
});
describe("GET /api/insights/comprehensive parity", () => {
it("01 an anonymous call", async () => {
await step("01-anonymous", { auth: anonymous });
});
it("02 the overview, cold", async () => {
await step("02-cold");
});
it("03 the overview, warm", async () => {
await step("03-warm");
});
it("04 the overview, stale after a measurement write", async () => {
invalidateUserMeasurements(OWNER_ID);
await step("04-stale", { raced: true });
});
it("05 the ai block after the AI analysis opt-out", async () => {
await prisma.user.update({
where: { id: OWNER_ID },
data: { modulePreferencesJson: { insights: false } },
});
await step("05-ai-analysis-off");
await prisma.user.update({
where: { id: OWNER_ID },
data: { modulePreferencesJson: {} },
});
});
it("06 the ai block with the operator's master switch off", async () => {
await prisma.appSettings.upsert({
where: { id: "singleton" },
create: { id: "singleton", assistantEnabled: false },
update: { assistantEnabled: false },
});
await step("06-operator-off");
await prisma.appSettings.update({
where: { id: "singleton" },
data: { assistantEnabled: true },
});
});
it("07 a delegate with MANAGE access, cold", async () => {
__resetAllCachesForTests();
await step("07-manage-delegate", { auth: manager });
});
it("08 a delegate with READ access", async () => {
await step("08-read-delegate", { auth: reader });
});
it("09 the shared analytics read ceiling", async () => {
await prisma.$executeRaw`
INSERT INTO rate_limits (key, count, reset_at)
VALUES (${`analytics-read:${OWNER_ID}`}, 100000, (NOW() AT TIME ZONE 'UTC') + INTERVAL '1 hour')
ON CONFLICT (key) DO UPDATE SET count = 100000`;
await step("09-rate-limited");
await prisma.$executeRaw`DELETE FROM rate_limits WHERE key = ${`analytics-read:${OWNER_ID}`}`;
});
it("10 a sparse record west of UTC", async () => {
await step("10-sparse", { auth: sparse });
});
it("11 an account with no data", async () => {
await step("11-empty", { auth: empty });
});
});
File diff suppressed because it is too large. Load diff