mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
Until now the set of pillars behind the Health Score was assembled from the module switches alone, in a stack of ternaries inside the reader. A person who turned the sleep module off lost the sleep pillar from their number as a side effect of a setting about recording data, and there was nowhere else to say what should count. The reader now reads the stored recipe and narrows it to the pillars a module is actually recording. The mapping from module to pillar moves out of the ternaries into a named map beside the reader, where it can be read and tested. Glycaemia stays fed by either glucose or labs, which was the one entry a one-module-per-pillar map would have lost. Nobody's number moves on upgrade. An account that never opened the new surface resolves to every pillar, and every pillar narrowed by module availability is exactly the set the ternaries built, so the inheritance holds by construction with no backfill. A pillar the recipe selects whose module is off produces no row at all, rather than a "waiting for data" line. Turning a module off is a deliberate act, and asking someone for data they chose to stop recording would be nagging them in the voice of their own settings.
79 lines
2.3 KiB
TypeScript
79 lines
2.3 KiB
TypeScript
import type { Derived } from "@/lib/insights/derived/types";
|
|
import { prisma } from "@/lib/db";
|
|
import { computeBpInTargetFastPath } from "@/lib/analytics/bp-in-target-fast-path";
|
|
import { getBpTargets } from "@/lib/analytics/bp-targets";
|
|
import { resolveModuleMap } from "@/lib/modules/gate";
|
|
import { loadUserSourcePriority } from "@/lib/rollups/measurement-read";
|
|
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
|
|
|
|
import { computeUserHealthScore } from "./reader";
|
|
import type { CompositeValue } from "./types";
|
|
|
|
const DAY_MS = 86_400_000;
|
|
|
|
/** Load the same full Health Score composite served by the analytics surfaces. */
|
|
export async function computeHealthScoreDerived(
|
|
userId: string,
|
|
): Promise<Derived<CompositeValue>> {
|
|
const user = await prisma.user.findUniqueOrThrow({
|
|
where: { id: userId },
|
|
select: {
|
|
dateOfBirth: true,
|
|
gender: true,
|
|
heightCm: true,
|
|
timezone: true,
|
|
thresholdsJson: true,
|
|
healthScoreConfigJson: true,
|
|
},
|
|
});
|
|
const now = new Date();
|
|
const timezone = user.timezone ?? DEFAULT_TIMEZONE;
|
|
const bpTargets = getBpTargets(user.dateOfBirth);
|
|
const bpAt = (at: Date) =>
|
|
bpTargets
|
|
? computeBpInTargetFastPath({
|
|
userId,
|
|
targets: bpTargets,
|
|
now: at,
|
|
userTz: timezone,
|
|
})
|
|
: Promise.resolve(null);
|
|
const [
|
|
modules,
|
|
sourcePriorityJson,
|
|
bpEnvelope,
|
|
bpEnvelopePriorWeek,
|
|
bpEnvelopePriorTwoWeeks,
|
|
] = await Promise.all([
|
|
resolveModuleMap(userId),
|
|
loadUserSourcePriority(userId),
|
|
bpAt(now),
|
|
bpAt(new Date(now.getTime() - 7 * DAY_MS)),
|
|
bpAt(new Date(now.getTime() - 14 * DAY_MS)),
|
|
]);
|
|
const report = await computeUserHealthScore({
|
|
userId,
|
|
now,
|
|
profile: {
|
|
dateOfBirth: user.dateOfBirth,
|
|
gender: user.gender,
|
|
heightCm: user.heightCm,
|
|
timezone,
|
|
sourcePriorityJson,
|
|
thresholdsJson: user.thresholdsJson,
|
|
},
|
|
modules: {
|
|
glucose: modules.glucose !== false,
|
|
labs: modules.labs !== false,
|
|
sleep: modules.sleep !== false,
|
|
mentalHealth: modules.mentalHealth !== false,
|
|
},
|
|
healthScoreConfigJson: user.healthScoreConfigJson,
|
|
bpTargets,
|
|
bpEnvelope,
|
|
bpEnvelopePriorWeek,
|
|
bpEnvelopePriorTwoWeeks,
|
|
});
|
|
return report.composite;
|
|
}
|