mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(insights): gate the batch derived route on the module map
The batch derived endpoint (the one the dashboard wellness strip reads) computed Recovery/Strain/Stress unconditionally while the single derived route already gated them, so disabling the recovery module did not hide the rings. Reuse the single route's DERIVED_MODULE map (now exported, with READINESS added since it equals the computed recovery score) and drop disabled-module metrics from the batch payload per request, without mutating the cached map.
This commit is contained in:
1 parent
00d16ea275
commit
ca4952b2c8
3 files changed
+41
-7
No files matched your search
@@ -61,7 +61,16 @@ vi.mock("@/lib/insights/derived", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
// v1.18.0 — the route resolves the per-user module map to drop disabled-
|
||||
// module derived scores; stub it so the batch tests stay focused (default:
|
||||
// every module enabled) and the profile-read count isn't perturbed.
|
||||
vi.mock("@/lib/modules/gate", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@/lib/modules/gate")>();
|
||||
return { ...actual, resolveModuleMap: vi.fn() };
|
||||
});
|
||||
|
||||
import { GET } from "../route";
|
||||
import { resolveModuleMap } from "@/lib/modules/gate";
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { prisma } from "@/lib/db";
|
||||
@@ -96,6 +105,7 @@ beforeEach(() => {
|
||||
__resetAllCachesForTests();
|
||||
vi.mocked(checkRateLimit).mockResolvedValue({ allowed: true } as never);
|
||||
vi.mocked(prisma.appSettings.findUnique).mockResolvedValue(null as never);
|
||||
vi.mocked(resolveModuleMap).mockResolvedValue({} as never);
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||
dateOfBirth: new Date("1986-01-01"),
|
||||
gender: "MALE",
|
||||
|
||||
@@ -39,6 +39,8 @@ import {
|
||||
} from "@/lib/insights/derived";
|
||||
import { resolveDeterministicAssessment } from "@/lib/insights/derived/derived-assessment";
|
||||
import { resolveServerLocale } from "@/lib/i18n/server-locale";
|
||||
import { resolveModuleMap } from "@/lib/modules/gate";
|
||||
import { DERIVED_MODULE } from "../route";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -201,17 +203,35 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
annotate,
|
||||
);
|
||||
|
||||
// v1.18.0 — module gate. A derived score native to a toggleable module
|
||||
// (sleep / recovery, per DERIVED_MODULE — the SAME map the single route
|
||||
// uses, so the two cannot drift) must not reach the client when that
|
||||
// module is disabled for the user. Resolved per request (NOT folded into
|
||||
// the SWR key) and applied to a NEW object so the cached map is never
|
||||
// mutated; a module toggle takes effect on the next read.
|
||||
const moduleMap = await resolveModuleMap(user.id);
|
||||
const gatedMetrics = Object.fromEntries(
|
||||
Object.entries(map).filter(([key]) => {
|
||||
const metric = key.includes(":") ? key.slice(0, key.indexOf(":")) : key;
|
||||
const mod = DERIVED_MODULE[metric as DerivedMetricId];
|
||||
return !(mod && moduleMap[mod] === false);
|
||||
}),
|
||||
);
|
||||
|
||||
annotate({
|
||||
action: { name: "insights.derived.batch" },
|
||||
meta: {
|
||||
requested: items.length,
|
||||
ok: Object.values(map).filter((entry) => entry.status === "ok").length,
|
||||
ok: Object.values(gatedMetrics).filter((entry) => entry.status === "ok")
|
||||
.length,
|
||||
moduleGated: Object.keys(map).length - Object.keys(gatedMetrics).length,
|
||||
},
|
||||
});
|
||||
|
||||
// `metrics` is a map keyed by the per-request token so the client reads
|
||||
// back exactly what it asked for (`VITALS_BASELINE:WEIGHT`, `READINESS`).
|
||||
return apiSuccess({ metrics: map });
|
||||
// back exactly what it asked for (`VITALS_BASELINE:WEIGHT`, `READINESS`),
|
||||
// minus any metric whose owning module the user disabled.
|
||||
return apiSuccess({ metrics: gatedMetrics });
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -41,15 +41,19 @@ export const dynamic = "force-dynamic";
|
||||
/**
|
||||
* Derived scores that are NATIVE to a toggleable module rather than a core
|
||||
* vital. SLEEP_SCORE is the sleep module's; the strain / recovery / stress
|
||||
* trio belongs to the WHOOP/Polar recovery module. The composite baselines
|
||||
* (VITALS_BASELINE, READINESS, HRV_BALANCE, …) read off core vitals and
|
||||
* stay open — a metric absent from this map carries no module gate.
|
||||
* trio belongs to the WHOOP/Polar recovery module. READINESS equals the
|
||||
* computed recovery score (same number, recovery-native), so it is gated on
|
||||
* the recovery module too. The remaining composite baselines
|
||||
* (VITALS_BASELINE, HRV_BALANCE, …) read off core vitals and stay open — a
|
||||
* metric absent from this map carries no module gate. Exported so the batch
|
||||
* route gates on the SAME map and the two routes cannot drift.
|
||||
*/
|
||||
const DERIVED_MODULE: Partial<Record<DerivedMetricId, ModuleKey>> = {
|
||||
export const DERIVED_MODULE: Partial<Record<DerivedMetricId, ModuleKey>> = {
|
||||
SLEEP_SCORE: "sleep",
|
||||
RECOVERY_SCORE: "recovery",
|
||||
STRAIN_SCORE: "recovery",
|
||||
STRESS_SCORE: "recovery",
|
||||
READINESS: "recovery",
|
||||
};
|
||||
|
||||
// Closed enum derived from the registry so the route + registry cannot
|
||||
|
||||
Reference in new issue
Block a user