mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
The daily briefing serialised the aggregated feature set as computed: weight, grip strength and the weight target in kilograms, waist in centimetres, glucose and the signals of the day in mg/dL (the glucose signal carried no unit at all), workout distances in kilometres. A reader on pounds or mmol/L got a briefing in units they do not use. One step now converts the feature set after all canonical arithmetic, on both the nightly and the on-demand path: absolute values through the full transform, differences and slopes through the factor alone, every converted block labelled, workout distances under unit-neutral keys. The prompt, the stored fingerprint and the grounding check all read the converted set, so a unit switch moves the fingerprint and the grounding check compares the reply against the figures the model was shown. The weight target sentence, the comparison block and the GLP-1 plateau context quote weight in the reader's unit; the comparison block also labelled sleep as hours while carrying minutes. A unit switch now also re-warms a cached briefing alongside the status notes and narratives. Refs #1067
84 lines
3.1 KiB
TypeScript
84 lines
3.1 KiB
TypeScript
/**
|
|
* Re-warm the stored texts that quote numbers after a unit preference changes.
|
|
*
|
|
* A status note and a period narrative are written once and served all day
|
|
* (the narrative for up to twenty hours). Both now state their figures in the
|
|
* reader's units, so a note written before a switch from mg/dL to mmol/L — or
|
|
* from metric to imperial — keeps saying the old unit until something
|
|
* regenerates it. Nothing did: neither preference write touched the stored
|
|
* text.
|
|
*
|
|
* The refill goes through the same hash-gated path as the manual regenerate:
|
|
* every card the record has is enqueued, the worker forces a fresh snapshot,
|
|
* and a card whose snapshot did not change (a metric that does not depend on
|
|
* the preference) gets a timestamp refresh and no model call. Narratives are
|
|
* re-warmed only where one already exists. The daily briefing (and with it
|
|
* the Coach-adjacent overview text) is re-warmed when one is cached: its
|
|
* fingerprint covers the converted feature set, so the forced warm writes a
|
|
* new one in the new unit. Best-effort and fire-and-forget:
|
|
* the preference write never waits on it and never fails because of it.
|
|
*/
|
|
import { prisma } from "@/lib/db";
|
|
import { enqueueNarrativeWarm } from "@/lib/jobs/period-narrative-shared";
|
|
import { enqueueForceWarm } from "@/lib/jobs/insight-pregenerate-shared";
|
|
import { enqueueStatusRefillForUser } from "@/lib/insights/status-invalidation";
|
|
import { normalizeLocale } from "@/lib/insights/status-shared";
|
|
import { locales, type Locale } from "@/lib/i18n/config";
|
|
import type { NarrativePeriod } from "@/lib/insights/narrative/period-narrative";
|
|
import { annotate } from "@/lib/logging/context";
|
|
|
|
export async function refreshTextsAfterUnitChange(
|
|
userId: string,
|
|
): Promise<void> {
|
|
try {
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: userId },
|
|
select: {
|
|
locale: true,
|
|
insightsCachedAt: true,
|
|
insightsCachedLocale: true,
|
|
},
|
|
});
|
|
const statusScopes = await enqueueStatusRefillForUser(
|
|
userId,
|
|
normalizeLocale(user?.locale),
|
|
);
|
|
|
|
const narratives = await prisma.insightNarrative.findMany({
|
|
where: { userId },
|
|
select: { period: true, locale: true },
|
|
});
|
|
let narrativeCount = 0;
|
|
for (const row of narratives) {
|
|
if (row.period !== "week" && row.period !== "month") continue;
|
|
if (!(locales as readonly string[]).includes(row.locale)) continue;
|
|
void enqueueNarrativeWarm({
|
|
userId,
|
|
period: row.period as NarrativePeriod,
|
|
locale: row.locale as Locale,
|
|
});
|
|
narrativeCount++;
|
|
}
|
|
|
|
const briefing = user?.insightsCachedAt != null;
|
|
if (briefing) {
|
|
void enqueueForceWarm({
|
|
userId,
|
|
locale: normalizeLocale(user?.insightsCachedLocale ?? user?.locale),
|
|
});
|
|
}
|
|
|
|
annotate({
|
|
action: { name: "insights.unit-change.refresh" },
|
|
meta: {
|
|
status_scopes: statusScopes,
|
|
narratives: narrativeCount,
|
|
briefing,
|
|
},
|
|
});
|
|
} catch {
|
|
// Best-effort: the nightly warm and the next ingest still converge the
|
|
// texts; a failed enqueue must not surface on the preference write.
|
|
}
|
|
}
|