From 9d32a8acb0fee9511f45f1cee58ce0a71d79e34a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 15 Oct 2026 23:37:26 +0200 Subject: [PATCH] feat(medications): let a delegate manage a composition, bypass the proxy for label uploads, and keep the guards current --- messages/de.json | 3 +- messages/en.json | 3 +- messages/es.json | 7 +- messages/fr.json | 3 +- messages/it.json | 7 +- messages/ko.json | 3 +- messages/pl.json | 7 +- .../insights-unit-literal-guard.test.ts | 74 ++++++++++ .../medication-intake-tracking-guard.test.ts | 6 + src/__tests__/proxy-body-limit.test.ts | 6 +- .../medications/[id]/__tests__/route.test.ts | 6 + .../[id]/intake/purge/__tests__/route.test.ts | 2 + .../api/medications/__tests__/route.test.ts | 8 +- src/app/api/nutrients/__tests__/route.test.ts | 132 ++++++++++++++++-- src/lib/http/proxy-bypass-routes.ts | 21 ++- src/lib/mcp/__tests__/nutrients-read.test.ts | 27 +++- .../medications/ingredients/recompute-job.ts | 2 +- src/lib/record-activity/activity-verb.ts | 4 + src/proxy.ts | 2 +- 19 files changed, 293 insertions(+), 30 deletions(-) diff --git a/messages/de.json b/messages/de.json index aed18e997..3c0e66920 100644 --- a/messages/de.json +++ b/messages/de.json @@ -12030,7 +12030,8 @@ "customVaccineDelete": "{name} hat einen eigenen Impfstoff entfernt", "lifeEventCreate": "{name} hat ein Lebensereignis hinzugefügt", "lifeEventUpdate": "{name} hat ein Lebensereignis geändert", - "lifeEventDelete": "{name} hat ein Lebensereignis entfernt" + "lifeEventDelete": "{name} hat ein Lebensereignis entfernt", + "medicationIngredientsUpdate": "{name} hat die Zusammensetzung eines Präparats geändert" }, "section": { "measurements": { diff --git a/messages/en.json b/messages/en.json index 35723dfa1..b038fc09c 100644 --- a/messages/en.json +++ b/messages/en.json @@ -12030,7 +12030,8 @@ "customVaccineDelete": "{name} removed an own vaccine", "lifeEventCreate": "{name} added a life event", "lifeEventUpdate": "{name} changed a life event", - "lifeEventDelete": "{name} removed a life event" + "lifeEventDelete": "{name} removed a life event", + "medicationIngredientsUpdate": "{name} changed what a supplement contains" }, "section": { "measurements": { diff --git a/messages/es.json b/messages/es.json index 64529adbb..1667ea2e5 100644 --- a/messages/es.json +++ b/messages/es.json @@ -11365,8 +11365,8 @@ "iodine": "Yodo", "water": "Agua", "caffeine": "Cafeína", - "dha": "DHA (omega-3)", - "epa": "EPA (omega-3)" + "dha": "DHA (ácido graso omega-3)", + "epa": "EPA (ácido graso omega-3)" }, "page": { "title": "Ingesta de nutrientes", @@ -12030,7 +12030,8 @@ "customVaccineDelete": "{name} quitó una vacuna propia", "lifeEventCreate": "{name} añadió un acontecimiento vital", "lifeEventUpdate": "{name} cambió un acontecimiento vital", - "lifeEventDelete": "{name} eliminó un acontecimiento vital" + "lifeEventDelete": "{name} eliminó un acontecimiento vital", + "medicationIngredientsUpdate": "{name} cambió la composición de un suplemento" }, "section": { "measurements": { diff --git a/messages/fr.json b/messages/fr.json index 4d1c39ec0..b7bde5bef 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -12030,7 +12030,8 @@ "customVaccineDelete": "{name} a retiré un vaccin personnel", "lifeEventCreate": "{name} a ajouté un événement de vie", "lifeEventUpdate": "{name} a modifié un événement de vie", - "lifeEventDelete": "{name} a supprimé un événement de vie" + "lifeEventDelete": "{name} a supprimé un événement de vie", + "medicationIngredientsUpdate": "{name} a modifié la composition d’un complément" }, "section": { "measurements": { diff --git a/messages/it.json b/messages/it.json index 6d895f5d6..5bfa94c1e 100644 --- a/messages/it.json +++ b/messages/it.json @@ -11365,8 +11365,8 @@ "iodine": "Iodio", "water": "Acqua", "caffeine": "Caffeina", - "dha": "DHA (omega-3)", - "epa": "EPA (omega-3)" + "dha": "DHA (acido grasso omega-3)", + "epa": "EPA (acido grasso omega-3)" }, "page": { "title": "Apporto di nutrienti", @@ -12030,7 +12030,8 @@ "customVaccineDelete": "{name} ha rimosso un vaccino personale", "lifeEventCreate": "{name} ha aggiunto un evento della vita", "lifeEventUpdate": "{name} ha modificato un evento della vita", - "lifeEventDelete": "{name} ha rimosso un evento della vita" + "lifeEventDelete": "{name} ha rimosso un evento della vita", + "medicationIngredientsUpdate": "{name} ha modificato la composizione di un integratore" }, "section": { "measurements": { diff --git a/messages/ko.json b/messages/ko.json index 89f97da2a..cd52cc2b1 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -12030,7 +12030,8 @@ "customVaccineDelete": "{name} 님이 직접 추가한 백신을 삭제했어요", "lifeEventCreate": "{name} 님이 삶의 사건을 추가했어요", "lifeEventUpdate": "{name} 님이 삶의 사건을 수정했어요", - "lifeEventDelete": "{name} 님이 삶의 사건을 삭제했어요" + "lifeEventDelete": "{name} 님이 삶의 사건을 삭제했어요", + "medicationIngredientsUpdate": "{name} 님이 보충제 성분을 수정했어요" }, "section": { "measurements": { diff --git a/messages/pl.json b/messages/pl.json index 2330eb0e8..2b9a15871 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -11365,8 +11365,8 @@ "iodine": "Jod", "water": "Woda", "caffeine": "Kofeina", - "dha": "DHA (omega-3)", - "epa": "EPA (omega-3)" + "dha": "DHA (kwas omega-3)", + "epa": "EPA (kwas omega-3)" }, "page": { "title": "Spożycie składników odżywczych", @@ -12030,7 +12030,8 @@ "customVaccineDelete": "{name} usunął własną szczepionkę", "lifeEventCreate": "{name} dodał wydarzenie życiowe", "lifeEventUpdate": "{name} zmienił wydarzenie życiowe", - "lifeEventDelete": "{name} usunął wydarzenie życiowe" + "lifeEventDelete": "{name} usunął wydarzenie życiowe", + "medicationIngredientsUpdate": "{name} zmienił skład suplementu" }, "section": { "measurements": { diff --git a/src/__tests__/insights-unit-literal-guard.test.ts b/src/__tests__/insights-unit-literal-guard.test.ts index 4af147f2a..a3422f501 100644 --- a/src/__tests__/insights-unit-literal-guard.test.ts +++ b/src/__tests__/insights-unit-literal-guard.test.ts @@ -139,3 +139,77 @@ describe("insights unit-literal guard", () => { expect(missing).toEqual([]); }); }); + +/** + * v1.43 — no nutrient unit spelled outside the nutrient catalogue. + * + * A supplement's composition converts an entered amount to the catalogue's + * canonical unit, and the nutrient reads judge a day against an upper limit + * in that unit. A µg/mg slip is a 1000× error, and the way it gets in is a + * second place that spells a unit or a factor: a converter that knows "IU" + * itself, a read that compares against "mg". So the rule is that the code + * under `src/lib/nutrients/` and `src/lib/medications/ingredients/` names no + * unit as a string literal; units and factors come from + * `src/lib/nutrients/catalog.ts` (`NUTRIENT_UNIT`, `inputUnits`, + * `toCanonicalNutrientAmount`, `parseNutrientInputUnit`), the one file allowed + * to spell them. + */ +const NUTRIENT_UNIT_DIRS = [ + join(process.cwd(), "src/lib/nutrients"), + join(process.cwd(), "src/lib/medications/ingredients"), +]; +const NUTRIENT_CATALOG_FILE = join( + process.cwd(), + "src/lib/nutrients/catalog.ts", +); + +/** A string literal that is nothing but a nutrient unit. */ +const UNIT_LITERAL = + /(["'`])(mg|ug|µg|μg|mcg|g|kg|ml|mL|l|L|IU|IE|UI|iu|ie|ui)\1/g; + +function sourceFilesUnder(dir: string): string[] { + const out: string[] = []; + for (const entry of readdirSync(dir)) { + const full = join(dir, entry); + if (statSync(full).isDirectory()) { + if (entry === "__tests__") continue; + out.push(...sourceFilesUnder(full)); + } else if (/\.tsx?$/.test(entry) && !/\.test\.tsx?$/.test(entry)) { + out.push(full); + } + } + return out; +} + +describe("nutrient unit-literal guard", () => { + const files = NUTRIENT_UNIT_DIRS.flatMap(sourceFilesUnder); + + it("scans the nutrient and composition code", () => { + expect(files.length).toBeGreaterThanOrEqual(6); + expect(files).toContain(NUTRIENT_CATALOG_FILE); + }); + + it("the matcher sees the catalogue's own spellings", () => { + // The catalogue is where units live, so a matcher that finds nothing + // there would make the next assertion vacuously true. + const catalog = readFileSync(NUTRIENT_CATALOG_FILE, "utf8"); + const seen = new Set([...catalog.matchAll(UNIT_LITERAL)].map((m) => m[2])); + for (const unit of ["mg", "ug", "IU", "ml"]) { + expect(seen.has(unit), `expected to see "${unit}" in the catalogue`).toBe( + true, + ); + } + }); + + it("names no unit outside the catalogue", () => { + const offenders: string[] = []; + for (const file of files) { + if (file === NUTRIENT_CATALOG_FILE) continue; + const source = readFileSync(file, "utf8"); + for (const match of source.matchAll(UNIT_LITERAL)) { + offenders.push(`${file.slice(file.indexOf("src/"))}: ${match[0]}`); + } + } + expect(offenders).toEqual([]); + }); +}); diff --git a/src/__tests__/medication-intake-tracking-guard.test.ts b/src/__tests__/medication-intake-tracking-guard.test.ts index 7691b4e7e..6469bcfa4 100644 --- a/src/__tests__/medication-intake-tracking-guard.test.ts +++ b/src/__tests__/medication-intake-tracking-guard.test.ts @@ -83,6 +83,8 @@ const INTAKE_MAY_SEE_EVERY_ROW: Record = { "folds duplicate rows of doses already recorded", "lib/doctor-report/collect.ts": "administration ledger lists recorded doses; rates gate via expectsDoses", + "lib/medications/ingredients/recompute.ts": + "sums the nutrients of doses that were taken; a recorded dose counts whatever the switch says", }; /** @@ -94,6 +96,10 @@ const MAY_READ_EVERY_ROW: Record = { // as stored, schedule and switch included. "lib/export/full-backup-payload.ts": "backup carries the stored rows", "lib/export/restore-backup.ts": "restore writes the stored rows back", + // v1.43 — the units a taken dose consumed, resolved per slot exactly as + // the inventory does. It reads only doses already recorded as taken. + "lib/medications/ingredients/recompute.ts": + "units per slot of doses already taken", "app/api/export/route.ts": "data export of the stored record", "app/api/export/medications/route.ts": "medication CSV export of the record", // Intake WRITE helpers: they run only when a dose is being recorded, bind diff --git a/src/__tests__/proxy-body-limit.test.ts b/src/__tests__/proxy-body-limit.test.ts index a14e33dbe..07bb01a09 100644 --- a/src/__tests__/proxy-body-limit.test.ts +++ b/src/__tests__/proxy-body-limit.test.ts @@ -74,7 +74,9 @@ describe("proxy matcher", () => { const routes = apiRoutePaths(); expect(routes.length).toBeGreaterThan(300); - for (const path of PROXY_BYPASS_ROUTES) { + for (const route of PROXY_BYPASS_ROUTES) { + // A dynamic segment is filled the way `apiRoutePaths` fills it. + const path = route.replace(/\[[^\]]+\]/g, "x"); expect(routes, `${path} is not a route`).toContain(path); expect(re.test(path), `${path} still passes the proxy`).toBe(false); expect(re.test(`${path}/`), `${path}/ still passes the proxy`).toBe( @@ -98,6 +100,8 @@ describe("proxy matcher", () => { "/api/user/avatar/abc", "/api/documents/inbound/abc/extract", "/api/import/csv/preview", + // The composition PUT beside the label upload stays behind the proxy. + "/api/medications/abc/ingredients", ]) { expect(re.test(path), path).toBe(true); } diff --git a/src/app/api/medications/[id]/__tests__/route.test.ts b/src/app/api/medications/[id]/__tests__/route.test.ts index 67d9fe8f5..81104bf40 100644 --- a/src/app/api/medications/[id]/__tests__/route.test.ts +++ b/src/app/api/medications/[id]/__tests__/route.test.ts @@ -31,6 +31,12 @@ vi.mock("@/lib/db", () => ({ apiToken: { updateMany: vi.fn().mockResolvedValue({ count: 0 }), }, + // v1.43 — no composition: the GET carries `ingredients: []` and a PUT + // queues no supplement recompute. + medicationIngredient: { + findFirst: vi.fn().mockResolvedValue(null), + findMany: vi.fn().mockResolvedValue([]), + }, $transaction: vi.fn(), }, })); diff --git a/src/app/api/medications/[id]/intake/purge/__tests__/route.test.ts b/src/app/api/medications/[id]/intake/purge/__tests__/route.test.ts index d0aa31c67..6a8e8c12e 100644 --- a/src/app/api/medications/[id]/intake/purge/__tests__/route.test.ts +++ b/src/app/api/medications/[id]/intake/purge/__tests__/route.test.ts @@ -17,6 +17,8 @@ vi.mock("@/lib/db", () => ({ medication: { findUnique: vi.fn() }, medicationIntakeEvent: { updateMany: vi.fn() }, medicationComplianceRollup: { deleteMany: vi.fn() }, + // v1.43 — no composition, so no supplement recompute is queued. + medicationIngredient: { findFirst: vi.fn().mockResolvedValue(null) }, }, })); diff --git a/src/app/api/medications/__tests__/route.test.ts b/src/app/api/medications/__tests__/route.test.ts index a60495d7e..a94070d52 100644 --- a/src/app/api/medications/__tests__/route.test.ts +++ b/src/app/api/medications/__tests__/route.test.ts @@ -890,7 +890,11 @@ describe("GET /api/medications — provenance echo (v1.32.25)", () => { end: new Date("2026-07-24T23:59:59Z"), } as never); vi.mocked(prisma.medication.findMany).mockResolvedValue( - medications as never, + // v1.43 — every row carries the ingredient count the list reads. + medications.map((m) => ({ + _count: { ingredients: 0 }, + ...(m as object), + })) as never, ); vi.mocked(prisma.medicationIntakeEvent.groupBy).mockResolvedValue( [] as never, @@ -1018,6 +1022,8 @@ describe("GET /api/medications — intake actions follow the course (#1040)", () dose: null, }, ], + // v1.43 — the ingredient count the list reads for `hasIngredients`. + _count: { ingredients: 0 }, ...over, }; } diff --git a/src/app/api/nutrients/__tests__/route.test.ts b/src/app/api/nutrients/__tests__/route.test.ts index a13a1c089..8f2d5100c 100644 --- a/src/app/api/nutrients/__tests__/route.test.ts +++ b/src/app/api/nutrients/__tests__/route.test.ts @@ -5,6 +5,7 @@ * per-nutrient summaries (latest day + total, days-with-data), the * module gate, and the `days` query validation. */ +import { NUTRIENT_CATALOG } from "@/lib/nutrients/catalog"; import { describe, it, expect, vi, beforeEach } from "vitest"; import { NextRequest } from "next/server"; @@ -111,11 +112,41 @@ describe("GET /api/nutrients", () => { // Rows arrive nutrient ASC, day DESC (the route's orderBy) — the // fold takes the first row per nutrient as the latest. vi.mocked(prisma.nutrientIntakeDay.findMany).mockResolvedValue([ - { nutrient: "caffeine", unit: "mg", day: "2026-07-13", amount: 310 }, - { nutrient: "caffeine", unit: "mg", day: "2026-07-12", amount: 250 }, - { nutrient: "caffeine", unit: "mg", day: "2026-07-11", amount: 400 }, - { nutrient: "vitamin_d", unit: "ug", day: "2026-07-13", amount: 22.5 }, - { nutrient: "vitamin_d", unit: "ug", day: "2026-07-10", amount: 20 }, + { + nutrient: "caffeine", + unit: "mg", + day: "2026-07-13", + amount: 310, + source: "APPLE_HEALTH", + }, + { + nutrient: "caffeine", + unit: "mg", + day: "2026-07-12", + amount: 250, + source: "APPLE_HEALTH", + }, + { + nutrient: "caffeine", + unit: "mg", + day: "2026-07-11", + amount: 400, + source: "APPLE_HEALTH", + }, + { + nutrient: "vitamin_d", + unit: "ug", + day: "2026-07-13", + amount: 22.5, + source: "APPLE_HEALTH", + }, + { + nutrient: "vitamin_d", + unit: "ug", + day: "2026-07-10", + amount: 20, + source: "APPLE_HEALTH", + }, ] as never); const res = await GET(getReq("?days=7")); @@ -129,6 +160,9 @@ describe("GET /api/nutrients", () => { latestDay: "2026-07-13", latestAmount: 22.5, daysWithData: 2, + latestBySource: { APPLE_HEALTH: 22.5 }, + upperLimit: NUTRIENT_CATALOG.vitamin_d.upperLimit, + latestAboveUpperLimit: false, }, { nutrient: "caffeine", @@ -136,6 +170,9 @@ describe("GET /api/nutrients", () => { latestDay: "2026-07-13", latestAmount: 310, daysWithData: 3, + latestBySource: { APPLE_HEALTH: 310 }, + upperLimit: null, + latestAboveUpperLimit: null, }, ]); }); @@ -145,10 +182,34 @@ describe("GET /api/nutrients", () => { // day logged via both APPLE_HEALTH and MANUAL must still count as ONE // day, not two. vi.mocked(prisma.nutrientIntakeDay.findMany).mockResolvedValue([ - { nutrient: "water", unit: "ml", day: "2026-07-13", amount: 900 }, - { nutrient: "water", unit: "ml", day: "2026-07-12", amount: 600 }, - { nutrient: "water", unit: "ml", day: "2026-07-12", amount: 400 }, - { nutrient: "water", unit: "ml", day: "2026-07-11", amount: 500 }, + { + nutrient: "water", + unit: "ml", + day: "2026-07-13", + amount: 900, + source: "APPLE_HEALTH", + }, + { + nutrient: "water", + unit: "ml", + day: "2026-07-12", + amount: 600, + source: "APPLE_HEALTH", + }, + { + nutrient: "water", + unit: "ml", + day: "2026-07-12", + amount: 400, + source: "MANUAL", + }, + { + nutrient: "water", + unit: "ml", + day: "2026-07-11", + amount: 500, + source: "APPLE_HEALTH", + }, ] as never); const res = await GET(getReq("?days=7")); @@ -163,6 +224,59 @@ describe("GET /api/nutrients", () => { // four, since the 12th's second source row didn't match the pinned // `latestDay` (the 13th) and opened a phantom extra day. daysWithData: 3, + latestBySource: { APPLE_HEALTH: 900 }, + upperLimit: null, + latestAboveUpperLimit: null, + }); + }); + + it("splits the latest day by source and judges it against the upper limit (v1.43)", async () => { + vi.mocked(prisma.nutrientIntakeDay.findMany).mockResolvedValue([ + { + nutrient: "vitamin_d", + unit: "ug", + day: "2026-07-13", + amount: 30, + source: "APPLE_HEALTH", + }, + { + nutrient: "vitamin_d", + unit: "ug", + day: "2026-07-13", + amount: 80, + source: "SUPPLEMENT", + }, + { + nutrient: "magnesium", + unit: "mg", + day: "2026-07-13", + amount: 300, + source: "APPLE_HEALTH", + }, + { + nutrient: "magnesium", + unit: "mg", + day: "2026-07-13", + amount: 200, + source: "SUPPLEMENT", + }, + ] as never); + + const res = await GET(getReq("?days=7")); + const body = (await res.json()) as OverviewResponse; + const byCode = new Map(body.data.nutrients.map((n) => [n.nutrient, n])); + // 110 µg in total against a total-scope limit of 100. + expect(byCode.get("vitamin_d")).toMatchObject({ + latestAmount: 110, + latestBySource: { APPLE_HEALTH: 30, SUPPLEMENT: 80 }, + latestAboveUpperLimit: true, + }); + // 500 mg in total, but the limit covers the supplement share only (200). + expect(byCode.get("magnesium")).toMatchObject({ + latestAmount: 500, + latestBySource: { APPLE_HEALTH: 300, SUPPLEMENT: 200 }, + upperLimit: { value: 250, scope: "supplemental" }, + latestAboveUpperLimit: false, }); }); diff --git a/src/lib/http/proxy-bypass-routes.ts b/src/lib/http/proxy-bypass-routes.ts index e01514d39..39434d3b5 100644 --- a/src/lib/http/proxy-bypass-routes.ts +++ b/src/lib/http/proxy-bypass-routes.ts @@ -45,6 +45,9 @@ export const PROXY_BYPASS_ROUTES: readonly string[] = [ "/api/medications/intake/dose-history-import", // Lab scan upload, 12 MB. "/api/labs/ocr/extract", + // Supplement label photo, 10 MB (v1.43). The one entry with a dynamic + // segment: `[id]` stands for any one path segment. + "/api/medications/[id]/ingredients/extract", // Avatar image, 2 MB. "/api/user/avatar", // Native-client batches: measurements 4 MB, workouts 5 MB, the rest 2 MB. @@ -56,10 +59,26 @@ export const PROXY_BYPASS_ROUTES: readonly string[] = [ "/api/cycle/day-logs/bulk", ]; +/** An entry with a `[segment]` matches any one path segment there. */ +const DYNAMIC_BYPASS_PATTERNS: readonly RegExp[] = PROXY_BYPASS_ROUTES.filter( + (route) => route.includes("["), +).map( + (route) => + new RegExp( + `^${route + .split("/") + .map((part) => (/^\[[^\]/]+\]$/.test(part) ? "[^/]+" : part)) + .join("/")}$`, + ), +); + export function isProxyBypassRoute(pathname: string): boolean { const normalised = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname; - return PROXY_BYPASS_ROUTES.includes(normalised); + return ( + PROXY_BYPASS_ROUTES.includes(normalised) || + DYNAMIC_BYPASS_PATTERNS.some((pattern) => pattern.test(normalised)) + ); } /** diff --git a/src/lib/mcp/__tests__/nutrients-read.test.ts b/src/lib/mcp/__tests__/nutrients-read.test.ts index 6f7822ea0..8253f1623 100644 --- a/src/lib/mcp/__tests__/nutrients-read.test.ts +++ b/src/lib/mcp/__tests__/nutrients-read.test.ts @@ -156,9 +156,27 @@ describe("getNutrients — overview mode (no `nutrient` arg)", () => { it("sums a day across sources and reports latest day / total / days-with-data", async () => { vi.mocked(nutrientIntakeDay.findMany).mockResolvedValue([ // Two sources on the SAME latest day — must sum, not double-count the day. - { nutrient: "water", unit: "ml", day: "2026-07-10", amount: 1200 }, - { nutrient: "water", unit: "ml", day: "2026-07-10", amount: 500 }, - { nutrient: "water", unit: "ml", day: "2026-07-09", amount: 2000 }, + { + nutrient: "water", + unit: "ml", + day: "2026-07-10", + amount: 1200, + source: "APPLE_HEALTH", + }, + { + nutrient: "water", + unit: "ml", + day: "2026-07-10", + amount: 500, + source: "MANUAL", + }, + { + nutrient: "water", + unit: "ml", + day: "2026-07-09", + amount: 2000, + source: "APPLE_HEALTH", + }, ] as never); const result = overview(await getNutrients("user-1", {})); @@ -171,6 +189,9 @@ describe("getNutrients — overview mode (no `nutrient` arg)", () => { latestDay: "2026-07-10", latestAmount: 1700, daysWithData: 2, + latestBySource: { APPLE_HEALTH: 1200, MANUAL: 500 }, + upperLimit: null, + latestAboveUpperLimit: null, }, ]); }); diff --git a/src/lib/medications/ingredients/recompute-job.ts b/src/lib/medications/ingredients/recompute-job.ts index 9e08a2d57..2f0e00759 100644 --- a/src/lib/medications/ingredients/recompute-job.ts +++ b/src/lib/medications/ingredients/recompute-job.ts @@ -83,5 +83,5 @@ export async function medicationHasComposition( where: { userId, medicationId }, select: { id: true }, }); - return row !== null; + return Boolean(row); } diff --git a/src/lib/record-activity/activity-verb.ts b/src/lib/record-activity/activity-verb.ts index f370de57e..a9e8b9cdc 100644 --- a/src/lib/record-activity/activity-verb.ts +++ b/src/lib/record-activity/activity-verb.ts @@ -255,6 +255,10 @@ export function recordActivityVerbLine( }); case "medication.delete": return t("recordSharing.activityVerb.medicationDelete", { name }); + case "medication.ingredients.update": + return t("recordSharing.activityVerb.medicationIngredientsUpdate", { + name, + }); case "medication.glp1.update": return t("recordSharing.activityVerb.medicationGlp1Update", { name }); case "medication.schedule_revision.created": diff --git a/src/proxy.ts b/src/proxy.ts index 85bc55282..c24dac7eb 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -825,6 +825,6 @@ export const config = { // the matcher and `apiHandler` performs the proxy's duties for it. The // alternation must stay equal to that list; `proxy-body-limit.test.ts` // compiles this exactly as Next does and checks. - "/((?!_next/static|_next/image|favicon.ico|sw\\.js|manifest\\.json|robots\\.txt|sitemap\\.xml|api/(?:import(?:/csv|/apple-health-export|/health-connect-export)?|admin/import-apple-health-export|admin/backups/upload|documents/inbound|medications/intake/(?:dose-history-import|bulk)|labs/ocr/extract|user/avatar|measurements/batch|workouts/batch|insights/ecg|mood-entries/bulk|cycle/day-logs/bulk)/?$|.*\\.(?:svg|png|jpg|jpeg|gif|webp|txt|xml)$).*)", + "/((?!_next/static|_next/image|favicon.ico|sw\\.js|manifest\\.json|robots\\.txt|sitemap\\.xml|api/(?:import(?:/csv|/apple-health-export|/health-connect-export)?|admin/import-apple-health-export|admin/backups/upload|documents/inbound|medications/intake/(?:dose-history-import|bulk)|labs/ocr/extract|medications/[^/]+/ingredients/extract|user/avatar|measurements/batch|workouts/batch|insights/ecg|mood-entries/bulk|cycle/day-logs/bulk)/?$|.*\\.(?:svg|png|jpg|jpeg|gif|webp|txt|xml)$).*)", ], };