Record each local day's score on the path that computes it

Every surface that shows a score now goes through one entry point that
computes it and writes the day down, rather than three surfaces each
remembering to. The write is an insert that yields on conflict, so a second
computation of the same day leaves the first row alone, and a failure is
annotated rather than thrown: a score that renders must not disappear
because a bookkeeping insert did.

The fingerprint covers exactly what the stored composite and band depend on
- the algorithm version, the composition, and each counted pillar's score
with its scoring-mode identity. A pillar that was graded but not counted
stays out of both the fingerprint and the row, because it did not
contribute to the number being stored.
This commit is contained in:
Marc-André Bombeck committed 2026-07-31 15:05:51 +02:00
1 parent cb75986eb5
commit 860a1fa299
5 files changed
+664 -6

No files matched your search

+2 -2
View File
@@ -20,7 +20,7 @@ import {
computeBpInTargetFastPath,
type BpInTargetEnvelope,
} from "@/lib/analytics/bp-in-target-fast-path";
import { computeUserHealthScore } from "@/lib/analytics/score/reader";
import { computeAndRecordUserHealthScore } from "@/lib/analytics/score/record";
import { isModuleEnabled } from "@/lib/modules/gate";
import { resolveRestMode } from "@/lib/illness/rest-mode";
import { deriveBpWindow90 } from "@/lib/analytics/window-confidence";
@@ -554,7 +554,7 @@ async function buildAnalyticsResponse(user: AuthedUser, locale: Locale) {
const glucoseByContextOut = glucoseEnabled ? glucoseByContext : null;
const glucoseClinicalOut = glucoseEnabled ? glucoseClinical : null;
const scoredAt = new Date();
const healthScore = await computeUserHealthScore({
const healthScore = await computeAndRecordUserHealthScore({
userId: user.id,
now: scoredAt,
profile: {