From 0518c3905015c564fa62a77f612e27194e2246e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 17 Jul 2026 23:02:45 +0200 Subject: [PATCH] fix(illness): glucose safety-floor push honours the user's display unit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hypoglycemia/hyperglycemia escalation push hardcoded mg/dL in all six locale strings and interpolated the raw mg/dL reading regardless of the user's glucose display preference. A mmol/L user hitting the confirmed-hypo alarm read "70 mg/dL (65 mg/dL)" — the wrong unit at the single most safety-critical moment. Resolve User.glucoseUnit in the safety-floor write hook and convert both the reading and the floor threshold before handing them to the notification copy. The bundle strings now take {threshold} {unit} / {value} {unit} instead of a baked-in "70 mg/dL" / "54 mg/dL". --- messages/de.json | 12 ++--- messages/en.json | 12 ++--- messages/es.json | 12 ++--- messages/fr.json | 12 ++--- messages/it.json | 12 ++--- messages/pl.json | 12 ++--- .../__tests__/safety-floor-notify.test.ts | 12 ++++- src/lib/illness/safety-floor-check.ts | 13 +++++- src/lib/illness/safety-floor-notify.ts | 44 ++++++++++++++++--- 9 files changed, 97 insertions(+), 44 deletions(-) diff --git a/messages/de.json b/messages/de.json index 32ecf86db..89e2eee88 100644 --- a/messages/de.json +++ b/messages/de.json @@ -7898,14 +7898,14 @@ "bpLowDoctor": "Zwei aktuelle Messungen hatten einen systolischen Wert unter {systolic} mmHg. Wenn dir schwindelig oder schummrig ist, setz oder leg dich hin und kontaktiere deine Ärztin oder deinen Arzt, wenn es anhält.", "bpLowEmergency": "Zwei aktuelle Messungen hatten einen systolischen Wert unter {systolic} mmHg und du hast Beschwerden angegeben. Bei Ohnmachtsgefühl, Verwirrtheit oder kalter, feuchter Haut sofort den Notruf wählen.", "glucoseLowTitle": "Niedriger Blutzucker", - "glucoseLowDoctor": "Zwei aktuelle Messungen lagen unter 70 mg/dL ({value} mg/dL). Nimm schnell wirkende Kohlenhydrate, miss in 15 Minuten erneut und kontaktiere deine Ärztin oder deinen Arzt, wenn die Tiefs anhalten.", - "glucoseLowEmergency": "Zwei aktuelle Messungen lagen unter 70 mg/dL ({value} mg/dL) und du hast Beschwerden angegeben. Behandle jetzt mit schnellem Zucker; wenn das nicht geht oder dir sehr unwohl ist, wähle den Notruf.", + "glucoseLowDoctor": "Zwei aktuelle Messungen lagen unter {threshold} {unit} ({value} {unit}). Nimm schnell wirkende Kohlenhydrate, miss in 15 Minuten erneut und kontaktiere deine Ärztin oder deinen Arzt, wenn die Tiefs anhalten.", + "glucoseLowEmergency": "Zwei aktuelle Messungen lagen unter {threshold} {unit} ({value} {unit}) und du hast Beschwerden angegeben. Behandle jetzt mit schnellem Zucker; wenn das nicht geht oder dir sehr unwohl ist, wähle den Notruf.", "glucoseLowSevereTitle": "Sehr niedriger Blutzucker", - "glucoseLowSevereDoctor": "Zwei aktuelle Messungen lagen unter 54 mg/dL ({value} mg/dL). Behandle sofort mit schnell wirkenden Kohlenhydraten, miss in 15 Minuten erneut und kontaktiere deine Ärztin oder deinen Arzt.", - "glucoseLowSevereEmergency": "Zwei aktuelle Messungen lagen unter 54 mg/dL ({value} mg/dL) und du hast Beschwerden angegeben. Behandle jetzt; wenn du nichts bei dir behalten kannst oder dir sehr unwohl ist, wähle den Notruf.", + "glucoseLowSevereDoctor": "Zwei aktuelle Messungen lagen unter {threshold} {unit} ({value} {unit}). Behandle sofort mit schnell wirkenden Kohlenhydraten, miss in 15 Minuten erneut und kontaktiere deine Ärztin oder deinen Arzt.", + "glucoseLowSevereEmergency": "Zwei aktuelle Messungen lagen unter {threshold} {unit} ({value} {unit}) und du hast Beschwerden angegeben. Behandle jetzt; wenn du nichts bei dir behalten kannst oder dir sehr unwohl ist, wähle den Notruf.", "glucoseHighTitle": "Sehr hoher Blutzucker", - "glucoseHighDoctor": "Zwei aktuelle Messungen lagen bei {value} mg/dL oder höher. Miss erneut, trink ausreichend und kontaktiere möglichst bald deine Ärztin oder deinen Arzt, wenn es so hoch bleibt.", - "glucoseHighEmergency": "Zwei aktuelle Messungen lagen bei {value} mg/dL oder höher und du hast Beschwerden angegeben. Bei Erbrechen, schneller Atmung oder Verwirrtheit sofort den Notruf wählen." + "glucoseHighDoctor": "Zwei aktuelle Messungen lagen bei {value} {unit} oder höher. Miss erneut, trink ausreichend und kontaktiere möglichst bald deine Ärztin oder deinen Arzt, wenn es so hoch bleibt.", + "glucoseHighEmergency": "Zwei aktuelle Messungen lagen bei {value} {unit} oder höher und du hast Beschwerden angegeben. Bei Erbrechen, schneller Atmung oder Verwirrtheit sofort den Notruf wählen." } }, "version": { diff --git a/messages/en.json b/messages/en.json index 33e1b8d63..e56ab4870 100644 --- a/messages/en.json +++ b/messages/en.json @@ -7898,14 +7898,14 @@ "bpLowDoctor": "Two recent readings had a systolic below {systolic} mmHg. If you feel dizzy or faint, sit or lie down, and contact your doctor if it continues.", "bpLowEmergency": "Two recent readings had a systolic below {systolic} mmHg and you noted symptoms. If you feel faint, confused, or have cold clammy skin, call emergency services now.", "glucoseLowTitle": "Low blood sugar", - "glucoseLowDoctor": "Two recent readings were below 70 mg/dL ({value} mg/dL). Take fast-acting carbohydrate, re-check in 15 minutes, and contact your doctor if lows continue.", - "glucoseLowEmergency": "Two recent readings were below 70 mg/dL ({value} mg/dL) and you noted symptoms. Treat now with fast-acting sugar; if you cannot, or you feel very unwell, call emergency services.", + "glucoseLowDoctor": "Two recent readings were below {threshold} {unit} ({value} {unit}). Take fast-acting carbohydrate, re-check in 15 minutes, and contact your doctor if lows continue.", + "glucoseLowEmergency": "Two recent readings were below {threshold} {unit} ({value} {unit}) and you noted symptoms. Treat now with fast-acting sugar; if you cannot, or you feel very unwell, call emergency services.", "glucoseLowSevereTitle": "Very low blood sugar", - "glucoseLowSevereDoctor": "Two recent readings were below 54 mg/dL ({value} mg/dL). Treat immediately with fast-acting carbohydrate, re-check in 15 minutes, and contact your doctor.", - "glucoseLowSevereEmergency": "Two recent readings were below 54 mg/dL ({value} mg/dL) and you noted symptoms. Treat now; if you cannot keep food down or feel very unwell, call emergency services.", + "glucoseLowSevereDoctor": "Two recent readings were below {threshold} {unit} ({value} {unit}). Treat immediately with fast-acting carbohydrate, re-check in 15 minutes, and contact your doctor.", + "glucoseLowSevereEmergency": "Two recent readings were below {threshold} {unit} ({value} {unit}) and you noted symptoms. Treat now; if you cannot keep food down or feel very unwell, call emergency services.", "glucoseHighTitle": "Very high blood sugar", - "glucoseHighDoctor": "Two recent readings were {value} mg/dL or higher. Re-check, hydrate, and contact your doctor as soon as possible if it stays this high.", - "glucoseHighEmergency": "Two recent readings were {value} mg/dL or higher and you noted symptoms. If you have vomiting, fast breathing, or confusion, call emergency services now." + "glucoseHighDoctor": "Two recent readings were {value} {unit} or higher. Re-check, hydrate, and contact your doctor as soon as possible if it stays this high.", + "glucoseHighEmergency": "Two recent readings were {value} {unit} or higher and you noted symptoms. If you have vomiting, fast breathing, or confusion, call emergency services now." } }, "version": { diff --git a/messages/es.json b/messages/es.json index bd5e14013..cfec2263e 100644 --- a/messages/es.json +++ b/messages/es.json @@ -7898,14 +7898,14 @@ "bpLowDoctor": "Dos lecturas recientes tuvieron una sistólica por debajo de {systolic} mmHg. Si te mareas o te sientes débil, siéntate o recuéstate y contacta a tu médico si continúa.", "bpLowEmergency": "Dos lecturas recientes tuvieron una sistólica por debajo de {systolic} mmHg e indicaste síntomas. Si te sientes a punto de desmayar, confuso o con piel fría y húmeda, llama a emergencias ya.", "glucoseLowTitle": "Glucosa baja", - "glucoseLowDoctor": "Dos lecturas recientes estuvieron por debajo de 70 mg/dL ({value} mg/dL). Toma carbohidratos de acción rápida, vuelve a medir en 15 minutos y contacta a tu médico si los bajones continúan.", - "glucoseLowEmergency": "Dos lecturas recientes estuvieron por debajo de 70 mg/dL ({value} mg/dL) e indicaste síntomas. Trátalo ahora con azúcar rápida; si no puedes o te sientes muy mal, llama a emergencias.", + "glucoseLowDoctor": "Dos lecturas recientes estuvieron por debajo de {threshold} {unit} ({value} {unit}). Toma carbohidratos de acción rápida, vuelve a medir en 15 minutos y contacta a tu médico si los bajones continúan.", + "glucoseLowEmergency": "Dos lecturas recientes estuvieron por debajo de {threshold} {unit} ({value} {unit}) e indicaste síntomas. Trátalo ahora con azúcar rápida; si no puedes o te sientes muy mal, llama a emergencias.", "glucoseLowSevereTitle": "Glucosa muy baja", - "glucoseLowSevereDoctor": "Dos lecturas recientes estuvieron por debajo de 54 mg/dL ({value} mg/dL). Trátalo de inmediato con carbohidratos de acción rápida, vuelve a medir en 15 minutos y contacta a tu médico.", - "glucoseLowSevereEmergency": "Dos lecturas recientes estuvieron por debajo de 54 mg/dL ({value} mg/dL) e indicaste síntomas. Trátalo ahora; si no retienes alimentos o te sientes muy mal, llama a emergencias.", + "glucoseLowSevereDoctor": "Dos lecturas recientes estuvieron por debajo de {threshold} {unit} ({value} {unit}). Trátalo de inmediato con carbohidratos de acción rápida, vuelve a medir en 15 minutos y contacta a tu médico.", + "glucoseLowSevereEmergency": "Dos lecturas recientes estuvieron por debajo de {threshold} {unit} ({value} {unit}) e indicaste síntomas. Trátalo ahora; si no retienes alimentos o te sientes muy mal, llama a emergencias.", "glucoseHighTitle": "Glucosa muy alta", - "glucoseHighDoctor": "Dos lecturas recientes fueron de {value} mg/dL o más. Vuelve a medir, hidrátate y contacta a tu médico cuanto antes si sigue así de alta.", - "glucoseHighEmergency": "Dos lecturas recientes fueron de {value} mg/dL o más e indicaste síntomas. Si tienes vómitos, respiración rápida o confusión, llama a emergencias ya." + "glucoseHighDoctor": "Dos lecturas recientes fueron de {value} {unit} o más. Vuelve a medir, hidrátate y contacta a tu médico cuanto antes si sigue así de alta.", + "glucoseHighEmergency": "Dos lecturas recientes fueron de {value} {unit} o más e indicaste síntomas. Si tienes vómitos, respiración rápida o confusión, llama a emergencias ya." } }, "version": { diff --git a/messages/fr.json b/messages/fr.json index 3ccd752e1..aed77f4c2 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -7898,14 +7898,14 @@ "bpLowDoctor": "Deux mesures récentes avaient une systolique inférieure à {systolic} mmHg. Si tu te sens étourdi ou faible, assieds-toi ou allonge-toi et contacte ton médecin si cela persiste.", "bpLowEmergency": "Deux mesures récentes avaient une systolique inférieure à {systolic} mmHg et tu as signalé des symptômes. Si tu te sens près de t'évanouir, confus ou as la peau froide et moite, appelle les secours immédiatement.", "glucoseLowTitle": "Glycémie basse", - "glucoseLowDoctor": "Deux mesures récentes étaient en dessous de 70 mg/dL ({value} mg/dL). Prends des glucides à action rapide, remesure dans 15 minutes et contacte ton médecin si les baisses persistent.", - "glucoseLowEmergency": "Deux mesures récentes étaient en dessous de 70 mg/dL ({value} mg/dL) et tu as signalé des symptômes. Traite maintenant avec du sucre rapide ; si tu ne peux pas ou te sens très mal, appelle les secours.", + "glucoseLowDoctor": "Deux mesures récentes étaient en dessous de {threshold} {unit} ({value} {unit}). Prends des glucides à action rapide, remesure dans 15 minutes et contacte ton médecin si les baisses persistent.", + "glucoseLowEmergency": "Deux mesures récentes étaient en dessous de {threshold} {unit} ({value} {unit}) et tu as signalé des symptômes. Traite maintenant avec du sucre rapide ; si tu ne peux pas ou te sens très mal, appelle les secours.", "glucoseLowSevereTitle": "Glycémie très basse", - "glucoseLowSevereDoctor": "Deux mesures récentes étaient en dessous de 54 mg/dL ({value} mg/dL). Traite immédiatement avec des glucides à action rapide, remesure dans 15 minutes et contacte ton médecin.", - "glucoseLowSevereEmergency": "Deux mesures récentes étaient en dessous de 54 mg/dL ({value} mg/dL) et tu as signalé des symptômes. Traite maintenant ; si tu ne gardes pas la nourriture ou te sens très mal, appelle les secours.", + "glucoseLowSevereDoctor": "Deux mesures récentes étaient en dessous de {threshold} {unit} ({value} {unit}). Traite immédiatement avec des glucides à action rapide, remesure dans 15 minutes et contacte ton médecin.", + "glucoseLowSevereEmergency": "Deux mesures récentes étaient en dessous de {threshold} {unit} ({value} {unit}) et tu as signalé des symptômes. Traite maintenant ; si tu ne gardes pas la nourriture ou te sens très mal, appelle les secours.", "glucoseHighTitle": "Glycémie très élevée", - "glucoseHighDoctor": "Deux mesures récentes étaient à {value} mg/dL ou plus. Remesure, hydrate-toi et contacte ton médecin dès que possible si cela reste aussi élevé.", - "glucoseHighEmergency": "Deux mesures récentes étaient à {value} mg/dL ou plus et tu as signalé des symptômes. En cas de vomissements, de respiration rapide ou de confusion, appelle les secours immédiatement." + "glucoseHighDoctor": "Deux mesures récentes étaient à {value} {unit} ou plus. Remesure, hydrate-toi et contacte ton médecin dès que possible si cela reste aussi élevé.", + "glucoseHighEmergency": "Deux mesures récentes étaient à {value} {unit} ou plus et tu as signalé des symptômes. En cas de vomissements, de respiration rapide ou de confusion, appelle les secours immédiatement." } }, "version": { diff --git a/messages/it.json b/messages/it.json index c8c05e877..b14c333b4 100644 --- a/messages/it.json +++ b/messages/it.json @@ -7898,14 +7898,14 @@ "bpLowDoctor": "Due misurazioni recenti avevano una sistolica sotto {systolic} mmHg. Se ti senti stordito o debole, siediti o sdraiati e contatta il medico se continua.", "bpLowEmergency": "Due misurazioni recenti avevano una sistolica sotto {systolic} mmHg e hai segnalato sintomi. Se senti che stai per svenire, sei confuso o hai la pelle fredda e sudata, chiama subito i soccorsi.", "glucoseLowTitle": "Glicemia bassa", - "glucoseLowDoctor": "Due misurazioni recenti erano sotto 70 mg/dL ({value} mg/dL). Assumi carboidrati ad azione rapida, rimisura tra 15 minuti e contatta il medico se i cali continuano.", - "glucoseLowEmergency": "Due misurazioni recenti erano sotto 70 mg/dL ({value} mg/dL) e hai segnalato sintomi. Tratta subito con zucchero rapido; se non puoi o ti senti molto male, chiama i soccorsi.", + "glucoseLowDoctor": "Due misurazioni recenti erano sotto {threshold} {unit} ({value} {unit}). Assumi carboidrati ad azione rapida, rimisura tra 15 minuti e contatta il medico se i cali continuano.", + "glucoseLowEmergency": "Due misurazioni recenti erano sotto {threshold} {unit} ({value} {unit}) e hai segnalato sintomi. Tratta subito con zucchero rapido; se non puoi o ti senti molto male, chiama i soccorsi.", "glucoseLowSevereTitle": "Glicemia molto bassa", - "glucoseLowSevereDoctor": "Due misurazioni recenti erano sotto 54 mg/dL ({value} mg/dL). Tratta subito con carboidrati ad azione rapida, rimisura tra 15 minuti e contatta il medico.", - "glucoseLowSevereEmergency": "Due misurazioni recenti erano sotto 54 mg/dL ({value} mg/dL) e hai segnalato sintomi. Tratta subito; se non trattieni il cibo o ti senti molto male, chiama i soccorsi.", + "glucoseLowSevereDoctor": "Due misurazioni recenti erano sotto {threshold} {unit} ({value} {unit}). Tratta subito con carboidrati ad azione rapida, rimisura tra 15 minuti e contatta il medico.", + "glucoseLowSevereEmergency": "Due misurazioni recenti erano sotto {threshold} {unit} ({value} {unit}) e hai segnalato sintomi. Tratta subito; se non trattieni il cibo o ti senti molto male, chiama i soccorsi.", "glucoseHighTitle": "Glicemia molto alta", - "glucoseHighDoctor": "Due misurazioni recenti erano di {value} mg/dL o superiori. Rimisura, idratati e contatta il medico al più presto se resta così alta.", - "glucoseHighEmergency": "Due misurazioni recenti erano di {value} mg/dL o superiori e hai segnalato sintomi. In caso di vomito, respiro affannoso o confusione, chiama subito i soccorsi." + "glucoseHighDoctor": "Due misurazioni recenti erano di {value} {unit} o superiori. Rimisura, idratati e contatta il medico al più presto se resta così alta.", + "glucoseHighEmergency": "Due misurazioni recenti erano di {value} {unit} o superiori e hai segnalato sintomi. In caso di vomito, respiro affannoso o confusione, chiama subito i soccorsi." } }, "version": { diff --git a/messages/pl.json b/messages/pl.json index 5951c89df..a01a2123f 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -7898,14 +7898,14 @@ "bpLowDoctor": "Dwa ostatnie pomiary miały ciśnienie skurczowe poniżej {systolic} mmHg. Jeśli kręci ci się w głowie lub czujesz osłabienie, usiądź lub połóż się i skontaktuj się z lekarzem, jeśli to się utrzymuje.", "bpLowEmergency": "Dwa ostatnie pomiary miały ciśnienie skurczowe poniżej {systolic} mmHg i zgłosiłeś objawy. Jeśli czujesz, że możesz zemdleć, jesteś zdezorientowany lub masz zimną, lepką skórę, natychmiast zadzwoń po pomoc.", "glucoseLowTitle": "Niski poziom cukru", - "glucoseLowDoctor": "Dwa ostatnie pomiary były poniżej 70 mg/dL ({value} mg/dL). Przyjmij szybko działające węglowodany, zmierz ponownie za 15 minut i skontaktuj się z lekarzem, jeśli spadki się utrzymują.", - "glucoseLowEmergency": "Dwa ostatnie pomiary były poniżej 70 mg/dL ({value} mg/dL) i zgłosiłeś objawy. Lecz to teraz szybkim cukrem; jeśli nie możesz lub czujesz się bardzo źle, zadzwoń po pomoc.", + "glucoseLowDoctor": "Dwa ostatnie pomiary były poniżej {threshold} {unit} ({value} {unit}). Przyjmij szybko działające węglowodany, zmierz ponownie za 15 minut i skontaktuj się z lekarzem, jeśli spadki się utrzymują.", + "glucoseLowEmergency": "Dwa ostatnie pomiary były poniżej {threshold} {unit} ({value} {unit}) i zgłosiłeś objawy. Lecz to teraz szybkim cukrem; jeśli nie możesz lub czujesz się bardzo źle, zadzwoń po pomoc.", "glucoseLowSevereTitle": "Bardzo niski poziom cukru", - "glucoseLowSevereDoctor": "Dwa ostatnie pomiary były poniżej 54 mg/dL ({value} mg/dL). Lecz natychmiast szybko działającymi węglowodanami, zmierz ponownie za 15 minut i skontaktuj się z lekarzem.", - "glucoseLowSevereEmergency": "Dwa ostatnie pomiary były poniżej 54 mg/dL ({value} mg/dL) i zgłosiłeś objawy. Lecz to teraz; jeśli nie utrzymujesz pokarmu lub czujesz się bardzo źle, zadzwoń po pomoc.", + "glucoseLowSevereDoctor": "Dwa ostatnie pomiary były poniżej {threshold} {unit} ({value} {unit}). Lecz natychmiast szybko działającymi węglowodanami, zmierz ponownie za 15 minut i skontaktuj się z lekarzem.", + "glucoseLowSevereEmergency": "Dwa ostatnie pomiary były poniżej {threshold} {unit} ({value} {unit}) i zgłosiłeś objawy. Lecz to teraz; jeśli nie utrzymujesz pokarmu lub czujesz się bardzo źle, zadzwoń po pomoc.", "glucoseHighTitle": "Bardzo wysoki poziom cukru", - "glucoseHighDoctor": "Dwa ostatnie pomiary wyniosły {value} mg/dL lub więcej. Zmierz ponownie, nawodnij się i jak najszybciej skontaktuj się z lekarzem, jeśli pozostaje tak wysoki.", - "glucoseHighEmergency": "Dwa ostatnie pomiary wyniosły {value} mg/dL lub więcej i zgłosiłeś objawy. W razie wymiotów, przyspieszonego oddechu lub dezorientacji natychmiast zadzwoń po pomoc." + "glucoseHighDoctor": "Dwa ostatnie pomiary wyniosły {value} {unit} lub więcej. Zmierz ponownie, nawodnij się i jak najszybciej skontaktuj się z lekarzem, jeśli pozostaje tak wysoki.", + "glucoseHighEmergency": "Dwa ostatnie pomiary wyniosły {value} {unit} lub więcej i zgłosiłeś objawy. W razie wymiotów, przyspieszonego oddechu lub dezorientacji natychmiast zadzwoń po pomoc." } }, "version": { diff --git a/src/lib/illness/__tests__/safety-floor-notify.test.ts b/src/lib/illness/__tests__/safety-floor-notify.test.ts index dc1766edf..cd2a5a6b1 100644 --- a/src/lib/illness/__tests__/safety-floor-notify.test.ts +++ b/src/lib/illness/__tests__/safety-floor-notify.test.ts @@ -81,7 +81,17 @@ describe("notifySafetyFloor", () => { const opts = dispatchMock.mock.calls[0][0]; expect(opts.messageKey).toBe("safety.floor.glucoseLowSevereEmergency"); expect(opts.titleKey).toBe("safety.floor.glucoseLowSevereTitle"); - expect(opts.params).toEqual({ value: 48 }); + expect(opts.params).toEqual({ value: 48, unit: "mg/dL", threshold: 54 }); + }); + + it("converts the glucose value + threshold to the caller's display unit", async () => { + await notifySafetyFloor({ + userId: "u1", + decision: glucoseLowSymptomatic, + glucoseUnit: "mmol/L", + }); + const opts = dispatchMock.mock.calls[0][0]; + expect(opts.params).toEqual({ value: 2.7, unit: "mmol/L", threshold: 3 }); }); it("de-dupes by reason when a recent ledger row exists", async () => { diff --git a/src/lib/illness/safety-floor-check.ts b/src/lib/illness/safety-floor-check.ts index 908c0cd4f..00ada6197 100644 --- a/src/lib/illness/safety-floor-check.ts +++ b/src/lib/illness/safety-floor-check.ts @@ -19,6 +19,7 @@ import type { MeasurementType } from "@/generated/prisma/client"; import { prisma } from "@/lib/db"; import { getEvent } from "@/lib/logging/context"; import { isModuleEnabled } from "@/lib/modules/gate"; +import { resolveGlucoseUnit } from "@/lib/glucose"; import { FEVER_RED_FLAG_C, SPO2_RED_FLAG_PCT } from "@/lib/clinical-floors"; import { evaluateBloodPressure, @@ -194,7 +195,17 @@ async function checkGlucose( mgdl: r.value, })); const decision = evaluateGlucose({ candidate, recent, symptomCoupled }); - await notifySafetyFloor({ userId, decision }); + if (!decision) return; + + // Resolve the user's display unit only when there's actually a breach to + // report — the escalation push must speak the user's own unit (mmol/L + // users get "3.9 mmol/L", not a raw mg/dL figure mid-hypo). + const profile = await prisma.user.findUnique({ + where: { id: userId }, + select: { glucoseUnit: true }, + }); + const glucoseUnit = resolveGlucoseUnit(profile?.glucoseUnit ?? null); + await notifySafetyFloor({ userId, decision, glucoseUnit }); } /* ── sustained fever / low-SpO2 (multi-day calendar runs) ─────────────── */ diff --git a/src/lib/illness/safety-floor-notify.ts b/src/lib/illness/safety-floor-notify.ts index 5e206088b..23df53951 100644 --- a/src/lib/illness/safety-floor-notify.ts +++ b/src/lib/illness/safety-floor-notify.ts @@ -20,9 +20,12 @@ import { prisma } from "@/lib/db"; import { getEvent } from "@/lib/logging/context"; import { dispatchLocalisedNotification } from "@/lib/notifications/dispatch-localised"; -import type { - SafetyFloorDecision, - SafetyFloorReason, +import { convertGlucose, type GlucoseUnit } from "@/lib/glucose"; +import { + GLUCOSE_HYPO, + GLUCOSE_HYPO_SEVERE, + type SafetyFloorDecision, + type SafetyFloorReason, } from "@/lib/illness/safety-floors"; /** One escalation per (user, reason) per this window. */ @@ -55,9 +58,20 @@ function copyKeysFor(decision: SafetyFloorDecision): { }; } -/** Body params — the re-confirmed reading value(s) echoed into the copy. */ +/** + * Body params — the re-confirmed reading value(s) echoed into the copy. + * + * Glucose is stored canonically in mg/dL (`decision.value`), but the escalation + * push must speak the user's own display unit — a mmol/L user reading "70 + * mg/dL" mid-hypo is the wrong unit at the single most safety-critical moment. + * `glucoseUnit` is resolved by the caller (`safety-floor-check.ts`) from + * `User.glucoseUnit`; every glucose copy key takes `{value} {unit}` and the + * hypo variants also take `{threshold} {unit}` for the "below X" clause — + * the literal "70 mg/dL" / "54 mg/dL" text no longer lives in the bundle. + */ function paramsFor( decision: SafetyFloorDecision, + glucoseUnit: GlucoseUnit, ): Record { if (decision.kind === "BLOOD_PRESSURE") { return { @@ -65,17 +79,35 @@ function paramsFor( diastolic: decision.diastolic ?? 0, }; } - return { value: decision.value }; + const params: Record = { + value: convertGlucose(decision.value, glucoseUnit), + unit: glucoseUnit, + }; + if ( + decision.reason === "glucose_hypo" || + decision.reason === "glucose_hypo_severe" + ) { + const thresholdMgdl = + decision.reason === "glucose_hypo_severe" + ? GLUCOSE_HYPO_SEVERE + : GLUCOSE_HYPO; + params.threshold = convertGlucose(thresholdMgdl, glucoseUnit); + } + return params; } /** * Emit an urgent escalation push for a confirmed safety-floor breach, at most * once per (user, reason) per `DEDUPE_WINDOW_MS`. No-op when `decision` is * null. Never throws. + * + * `glucoseUnit` is only consulted for `GLUCOSE` decisions; defaults to + * canonical mg/dL when the caller omits it (BP-only callers never need it). */ export async function notifySafetyFloor(input: { userId: string; decision: SafetyFloorDecision | null; + glucoseUnit?: GlucoseUnit; }): Promise { const { userId, decision } = input; if (!decision) return; @@ -107,7 +139,7 @@ export async function notifySafetyFloor(input: { userId, titleKey, messageKey, - params: paramsFor(decision), + params: paramsFor(decision, input.glucoseUnit ?? "mg/dL"), eventType: "SYSTEM_ALERT", urgent: true, });