diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4058e701d..c45030898 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,22 @@
## [Unreleased]
+### Added
+
+- **A note on a medication.** Directions such as "30 to 60 minutes before
+ food" now have a place of their own: type them into the last step of the
+ medication wizard, and they show under the medication's name on its
+ page, above the Taken and Skip buttons on its card, under the medication
+ you pick when logging a dose, and in the list of doses when you take all
+ due ones at once. The note is up to 500 characters, encrypted like your
+ other notes, and travels with backups, restores and the data export. The
+ MCP medication schedule and medication resources include it. It is not
+ part of the doctor report or the FHIR export. `POST /api/medications`
+ and `PUT /api/medications/{id}` take an optional `note` (trimmed, at
+ most 500 characters; an empty string or `null` clears it, leaving it
+ out keeps it), and the list and detail reads answer `note`, `null` when
+ there is none. Migration `0498_medication_note`. (#1371)
+
## [1.47.5] — 2026-11-12
### Fixed
diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml
index b5974f168..2c0ca48af 100644
--- a/docs/api/openapi.yaml
+++ b/docs/api/openapi.yaml
@@ -30694,6 +30694,15 @@ components:
maxLength: 20
pattern: ^\d+$
- type: "null"
+ note:
+ description: "#1371 — the person's own note on the medication, for directions such as \"30 to 60 minutes before food\".
+ Free text, trimmed, at most 500 characters (counted after trimming). An empty string or NULL clears it;
+ absent leaves it untouched. Encrypted at rest and returned decrypted as `note` on the medication read
+ paths."
+ anyOf:
+ - type: string
+ maxLength: 500
+ - type: "null"
externalSource:
description: "Marks the medication as MIRRORED from an external medication list. Only `APPLE_HEALTH` (the iOS 26+
HealthKit Medications sync) is accepted. Must be supplied together with `externalId`. A mirrored medication
@@ -31122,6 +31131,15 @@ components:
maxLength: 20
pattern: ^\d+$
- type: "null"
+ note:
+ description: "#1371 — the person's own note on the medication, for directions such as \"30 to 60 minutes before food\".
+ Free text, trimmed, at most 500 characters (counted after trimming). An empty string or NULL clears it;
+ absent leaves it untouched. Encrypted at rest and returned decrypted as `note` on the medication read
+ paths."
+ anyOf:
+ - type: string
+ maxLength: 500
+ - type: "null"
startsOn:
description: Date the medication course begins (ISO `YYYY-MM-DD`). Anchors RRULE BYDAY / BYMONTHDAY patterns and the
rolling-interval countdown's first window. NULL = active from creation (the legacy implicit behaviour).
@@ -43003,6 +43021,16 @@ components:
type:
- string
- "null"
+ note:
+ anyOf:
+ - type: string
+ maxLength: 500
+ - type: "null"
+ noteEncrypted:
+ anyOf:
+ - type: string
+ minLength: 1
+ - type: "null"
lowStockNotifiedAt:
anyOf:
- type: string
@@ -55621,6 +55649,14 @@ components:
type:
- string
- "null"
+ note:
+ description: "#1371 — the person's own note on the medication, for directions such as \"30 to 60 minutes before food\".
+ Free text, at most 500 characters, decrypted on the way out (encrypted at rest). NULL = no note. Show it
+ with the medication and wherever a dose is logged. Set or cleared through `note` on the create and update
+ bodies."
+ type:
+ - string
+ - "null"
pausedAt:
anyOf:
- type: string
@@ -55848,6 +55884,7 @@ components:
- criticalAlarmEnabled
- atcCode
- rxNormCode
+ - note
- pausedAt
- snoozedUntil
- remindersActive
@@ -56078,6 +56115,14 @@ components:
type:
- string
- "null"
+ note:
+ description: "#1371 — the person's own note on the medication, for directions such as \"30 to 60 minutes before food\".
+ Free text, at most 500 characters, decrypted on the way out (encrypted at rest). NULL = no note. Show it
+ with the medication and wherever a dose is logged. Set or cleared through `note` on the create and update
+ bodies."
+ type:
+ - string
+ - "null"
pausedAt:
anyOf:
- type: string
@@ -56290,6 +56335,7 @@ components:
- criticalAlarmEnabled
- atcCode
- rxNormCode
+ - note
- pausedAt
- snoozedUntil
- remindersActive
@@ -96838,6 +96884,14 @@ components:
type:
- string
- "null"
+ note:
+ description: "#1371 — the person's own note on the medication, for directions such as \"30 to 60 minutes before food\".
+ Free text, at most 500 characters, decrypted on the way out (encrypted at rest). NULL = no note. Show it
+ with the medication and wherever a dose is logged. Set or cleared through `note` on the create and update
+ bodies."
+ type:
+ - string
+ - "null"
pausedAt:
anyOf:
- type: string
@@ -96969,6 +97023,7 @@ components:
- criticalAlarmEnabled
- atcCode
- rxNormCode
+ - note
- pausedAt
- snoozedUntil
- startsOn
diff --git a/e2e/medications-wizard-daily.spec.ts b/e2e/medications-wizard-daily.spec.ts
index fb6b1dd7d..4477edd0b 100644
--- a/e2e/medications-wizard-daily.spec.ts
+++ b/e2e/medications-wizard-daily.spec.ts
@@ -42,6 +42,7 @@ test.describe("medication wizard — daily", () => {
rrule: "FREQ=DAILY",
rollingIntervalDays: null,
oneShot: false,
+ note: "Mit dem Frühstück",
});
await openCreateWizard(page);
@@ -91,6 +92,8 @@ test.describe("medication wizard — daily", () => {
await expectStep(page, 7, 7);
const summary = page.locator('[data-slot="wizard-summary"]');
await expect(summary).toContainText(/Jeden Tag/i);
+ // #1371 — the medication's own note, typed on the last step.
+ await page.locator("#wizard-note").fill(" Mit dem Frühstück ");
await clickSave(page);
@@ -98,6 +101,8 @@ test.describe("medication wizard — daily", () => {
expect(capture.postBody).toMatchObject({
name: "Vitamin D",
oneShot: false,
+ // Trimmed on the way out.
+ note: "Mit dem Frühstück",
schedules: [{ rrule: "FREQ=DAILY" }],
});
@@ -105,5 +110,9 @@ test.describe("medication wizard — daily", () => {
name: "Vitamin D",
withNextDueChip: true,
});
+ // The note sits on the card, above the dose actions.
+ await expect(
+ page.locator('main [data-slot="medication-card-note"]').first(),
+ ).toHaveText(/Mit dem Frühstück/);
});
});
diff --git a/e2e/medications-wizard-helpers.ts b/e2e/medications-wizard-helpers.ts
index 921fa101a..e958f1c9e 100644
--- a/e2e/medications-wizard-helpers.ts
+++ b/e2e/medications-wizard-helpers.ts
@@ -27,6 +27,8 @@ interface StubbedMedication {
rrule: string | null;
rollingIntervalDays: number | null;
oneShot: boolean;
+ /** #1371 — the medication's own note, echoed on the list. */
+ note?: string | null;
}
export function mockMedicationsApi(page: Page, stubbed: StubbedMedication) {
@@ -57,6 +59,7 @@ export function mockMedicationsApi(page: Page, stubbed: StubbedMedication) {
startsOn: null,
endsOn: null,
oneShot: stubbed.oneShot,
+ note: stubbed.note ?? null,
schedules: [
{
id: "sch_e2e_1",
diff --git a/messages/de.json b/messages/de.json
index 77b23e200..c44aeeebb 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -1506,6 +1506,11 @@
"disabledToast": "Einnahmen werden nicht erfasst",
"toggleFailed": "Einnahme-Erfassung konnte nicht geändert werden."
},
+ "note": {
+ "label": "Notiz",
+ "placeholder": "z. B. 30 bis 60 Minuten vor dem Essen",
+ "helper": "Wird beim Medikament und beim Erfassen einer Einnahme angezeigt."
+ },
"autoMarkTaken": {
"label": "Einnahmen automatisch erfassen",
"helper": "HealthLog erfasst jede geplante Dosis zu ihrer Uhrzeit als eingenommen. Du kannst eine Dosis weiterhin selbst ändern oder überspringen. Erinnerungen entfallen.",
diff --git a/messages/en.json b/messages/en.json
index 6705917dc..3bf85c752 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -1506,6 +1506,11 @@
"disabledToast": "Intake tracking off",
"toggleFailed": "Could not change intake tracking."
},
+ "note": {
+ "label": "Note",
+ "placeholder": "e.g. 30 to 60 minutes before food",
+ "helper": "Shown on the medication and when you log a dose."
+ },
"autoMarkTaken": {
"label": "Record doses automatically",
"helper": "HealthLog records each scheduled dose as taken at its time. You can still change or skip a dose by hand. No reminders are sent.",
diff --git a/messages/es.json b/messages/es.json
index 791a45a5b..e4725eee4 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -1506,6 +1506,11 @@
"disabledToast": "Registro de tomas desactivado",
"toggleFailed": "No se pudo cambiar el registro de tomas."
},
+ "note": {
+ "label": "Nota",
+ "placeholder": "p. ej. de 30 a 60 minutos antes de comer",
+ "helper": "Se muestra en el medicamento y al registrar una toma."
+ },
"autoMarkTaken": {
"label": "Registrar las tomas automáticamente",
"helper": "HealthLog registra cada dosis programada como tomada a su hora. Puedes seguir cambiando u omitiendo una dosis a mano. No se envían recordatorios.",
diff --git a/messages/fr.json b/messages/fr.json
index 574570c36..57a0eb527 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -1506,6 +1506,11 @@
"disabledToast": "Suivi des prises désactivé",
"toggleFailed": "Impossible de modifier le suivi des prises."
},
+ "note": {
+ "label": "Note",
+ "placeholder": "p. ex. 30 à 60 minutes avant le repas",
+ "helper": "Affichée sur le médicament et lors de l'enregistrement d'une prise."
+ },
"autoMarkTaken": {
"label": "Enregistrer les prises automatiquement",
"helper": "HealthLog enregistre chaque dose prévue comme prise à son heure. Vous pouvez toujours modifier ou sauter une dose à la main. Aucun rappel n'est envoyé.",
diff --git a/messages/it.json b/messages/it.json
index c0158d044..0b5ae69d1 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -1506,6 +1506,11 @@
"disabledToast": "Registrazione assunzioni disattivata",
"toggleFailed": "Impossibile modificare la registrazione delle assunzioni."
},
+ "note": {
+ "label": "Nota",
+ "placeholder": "ad es. da 30 a 60 minuti prima dei pasti",
+ "helper": "Mostrata sul farmaco e quando registri un'assunzione."
+ },
"autoMarkTaken": {
"label": "Registra le assunzioni automaticamente",
"helper": "HealthLog registra ogni dose programmata come assunta al suo orario. Puoi comunque modificare o saltare una dose a mano. Non vengono inviati promemoria.",
diff --git a/messages/ko.json b/messages/ko.json
index 5ff7cafa4..91b641672 100644
--- a/messages/ko.json
+++ b/messages/ko.json
@@ -1506,6 +1506,11 @@
"disabledToast": "복용 기록 꺼짐",
"toggleFailed": "복용 기록 설정을 바꾸지 못했어요."
},
+ "note": {
+ "label": "메모",
+ "placeholder": "예: 식사 30~60분 전",
+ "helper": "약 정보와 복용을 기록할 때 표시됩니다."
+ },
"autoMarkTaken": {
"label": "복용 자동 기록",
"helper": "HealthLog가 예정된 각 복용을 해당 시간에 복용한 것으로 기록합니다. 복용은 언제든 직접 변경하거나 건너뛸 수 있습니다. 알림은 보내지 않습니다.",
diff --git a/messages/nl.json b/messages/nl.json
index 6c21d8933..46faa876f 100644
--- a/messages/nl.json
+++ b/messages/nl.json
@@ -1506,6 +1506,11 @@
"disabledToast": "Inname bijhouden uit",
"toggleFailed": "Inname bijhouden kon niet worden gewijzigd."
},
+ "note": {
+ "label": "Notitie",
+ "placeholder": "bijv. 30 tot 60 minuten voor het eten",
+ "helper": "Zichtbaar bij het medicijn en wanneer je een inname vastlegt."
+ },
"autoMarkTaken": {
"label": "Doses automatisch registreren",
"helper": "HealthLog registreert elke geplande dosis op het geplande tijdstip als ingenomen. Je kunt een dosis nog steeds handmatig wijzigen of overslaan. Er worden geen herinneringen verstuurd.",
diff --git a/messages/pl.json b/messages/pl.json
index d99f6a64d..97ca12000 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -1506,6 +1506,11 @@
"disabledToast": "Rejestrowanie przyjęć wyłączone",
"toggleFailed": "Nie udało się zmienić rejestrowania przyjęć."
},
+ "note": {
+ "label": "Notatka",
+ "placeholder": "np. od 30 do 60 minut przed posiłkiem",
+ "helper": "Widoczna przy leku i podczas zapisywania przyjęcia."
+ },
"autoMarkTaken": {
"label": "Zapisuj przyjęcia automatycznie",
"helper": "HealthLog zapisuje każdą zaplanowaną dawkę jako przyjętą o jej porze. Nadal możesz ręcznie zmienić lub pominąć dawkę. Przypomnienia nie są wysyłane.",
diff --git a/messages/pt-BR.json b/messages/pt-BR.json
index 1ea7da102..68616d43d 100644
--- a/messages/pt-BR.json
+++ b/messages/pt-BR.json
@@ -1506,6 +1506,11 @@
"disabledToast": "Acompanhamento de tomadas desligado",
"toggleFailed": "Não foi possível alterar o acompanhamento de tomadas."
},
+ "note": {
+ "label": "Observação",
+ "placeholder": "ex.: 30 a 60 minutos antes de comer",
+ "helper": "Aparece no medicamento e quando você registra uma tomada."
+ },
"autoMarkTaken": {
"label": "Registrar doses automaticamente",
"helper": "O HealthLog registra cada dose programada como tomada no horário dela. Você ainda pode alterar ou pular uma dose manualmente. Nenhum lembrete é enviado.",
diff --git a/messages/ru.json b/messages/ru.json
index 0d451a0c5..0eeee6f6f 100644
--- a/messages/ru.json
+++ b/messages/ru.json
@@ -1506,6 +1506,11 @@
"disabledToast": "Учёт приёма выключен",
"toggleFailed": "Не удалось изменить учёт приёма."
},
+ "note": {
+ "label": "Заметка",
+ "placeholder": "например, от 30 до 60 минут до еды",
+ "helper": "Показывается у лекарства и при записи приёма."
+ },
"autoMarkTaken": {
"label": "Записывать дозы автоматически",
"helper": "HealthLog записывает каждую запланированную дозу как принятую в её время. Дозу по-прежнему можно изменить или пропустить вручную. Напоминания не отправляются.",
diff --git a/prisma/migrations/0498_medication_note/migration.sql b/prisma/migrations/0498_medication_note/migration.sql
new file mode 100644
index 000000000..f83f66985
--- /dev/null
+++ b/prisma/migrations/0498_medication_note/migration.sql
@@ -0,0 +1,19 @@
+-- A free-text note on a medication (#1371), for directions such as "30 to 60
+-- minutes before food". Shown on the medication and when a dose is logged.
+--
+-- Sealed at rest like every other free-text column (AES-256-GCM, the shared
+-- Bytes codec); there is no plaintext twin and nothing to backfill. Nullable,
+-- no default: every existing medication reads as "no note", and adding the
+-- column rewrites nothing. IF NOT EXISTS keeps a replay on a database that
+-- already ran a build carrying the column harmless. Reversibility: dropping
+-- the column restores the previous schema.
+--
+-- @retry-on-lock-timeout
+
+DO $migration$
+BEGIN
+ PERFORM set_config('lock_timeout', '5s', true);
+ ALTER TABLE "medications"
+ ADD COLUMN IF NOT EXISTS "note_encrypted" BYTEA;
+END
+$migration$;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index d5943dc37..c1077c52d 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -3057,6 +3057,12 @@ model Medication {
/// under `http://www.nlm.nih.gov/research/umls/rxnorm` alongside any
/// ATC code, never instead of the free-text `.text` anchor.
rxNormCode String? @map("rxnorm_code")
+ /// #1371 — the person's own note on the medication, for directions such as
+ /// "30 to 60 minutes before food". Shown on the medication and wherever a
+ /// dose is logged. AES-256-GCM at rest; there is no plaintext column. NULL =
+ /// no note. Written by the create and edit routes and the restore only,
+ /// trimmed, at most 500 characters.
+ noteEncrypted Bytes? @map("note_encrypted")
/// v1.16.11 — low-stock notification stamp. Set when the daily
/// low-stock pass dispatched a notification for the current
/// threshold crossing; cleared (re-armed) when the projected runway
diff --git a/scripts/rotate-encryption-key.ts b/scripts/rotate-encryption-key.ts
index 9a4754f7a..0152f6e82 100644
--- a/scripts/rotate-encryption-key.ts
+++ b/scripts/rotate-encryption-key.ts
@@ -734,6 +734,10 @@ async function main() {
prisma.medicationDoseChange,
),
);
+ // #1371 — the note on the medication itself.
+ results.push(
+ await rotateBytesColumn("Medication", "noteEncrypted", prisma.medication),
+ );
// v1.40 (#1024) — the note on a medication course.
results.push(
await rotateBytesColumn(
diff --git a/src/__tests__/baselines/source-file-lines.json b/src/__tests__/baselines/source-file-lines.json
index a4e3f316c..8511c398b 100644
--- a/src/__tests__/baselines/source-file-lines.json
+++ b/src/__tests__/baselines/source-file-lines.json
@@ -3,7 +3,7 @@
"count": 84,
"total": 93816,
"entries": {
- "app/medications/page-client.tsx": 839,
+ "app/medications/page-client.tsx": 842,
"components/charts/mood-chart.tsx": 1166,
"components/cycle/log-day-sheet.tsx": 1646,
"components/insights/coach-panel/chat-bubble.tsx": 821,
@@ -13,9 +13,9 @@
"components/insights/health-score-card.tsx": 1023,
"components/insights/insights-tab-strip.tsx": 1084,
"components/measurements/measurement-form.tsx": 815,
- "components/medications/detail/medication-detail-tabs.tsx": 1129,
+ "components/medications/detail/medication-detail-tabs.tsx": 1133,
"components/medications/medication-table.tsx": 853,
- "components/medications/wizard/wizard-payload.ts": 1414,
+ "components/medications/wizard/wizard-payload.ts": 1431,
"components/mood/mood-list.tsx": 1408,
"components/records/life-circumstances-manager.tsx": 828,
"components/timeline/timeline-chart.tsx": 1093,
diff --git a/src/__tests__/baselines/source-function-lines.json b/src/__tests__/baselines/source-function-lines.json
index e785bbee3..4fe2dbf58 100644
--- a/src/__tests__/baselines/source-function-lines.json
+++ b/src/__tests__/baselines/source-function-lines.json
@@ -46,7 +46,7 @@
"app/insights/medications/page.tsx :: InsightsMedikamentePage": 265,
"app/insights/page-client.tsx :: InsightsPageClient": 375,
"app/labs/page.tsx :: LabsPage": 217,
- "app/medications/page-client.tsx :: MedicationsPageClient": 590,
+ "app/medications/page-client.tsx :: MedicationsPageClient": 591,
"app/notifications/page.tsx :: NotificationsPage": 327,
"components/activity/activity-calendar-card.tsx :: ActivityCalendarCard": 470,
"components/admin/admin-shell.tsx :: AdminShell": 254,
@@ -145,15 +145,15 @@
"components/measurement-reminders/vorsorge-section.tsx :: VorsorgeSection": 354,
"components/measurements/measurement-form.tsx :: MeasurementForm": 496,
"components/medications/detail/efficacy/efficacy-chart.tsx :: EfficacyChart": 361,
- "components/medications/detail/medication-detail-tabs.tsx :: MedicationDetailTabs": 760,
+ "components/medications/detail/medication-detail-tabs.tsx :: MedicationDetailTabs": 761,
"components/medications/dose-history-add-dialog.tsx :: LedgerAddDialog": 221,
"components/medications/dose-strength-curve.tsx :: DoseStrengthCurve": 266,
- "components/medications/glp1-medication-card.tsx :: Glp1MedicationCard": 396,
+ "components/medications/glp1-medication-card.tsx :: Glp1MedicationCard": 397,
"components/medications/injection-site-picker.tsx :: InjectionSitePicker": 287,
"components/medications/intake-history-list-v2.tsx :: IntakeHistoryListV2": 403,
"components/medications/intake-import-dialog.tsx :: IntakeImportDialog": 201,
- "components/medications/log-intake-dialog.tsx :: LogIntakeDialog": 267,
- "components/medications/medication-card.tsx :: MedicationCard": 413,
+ "components/medications/log-intake-dialog.tsx :: LogIntakeDialog": 268,
+ "components/medications/medication-card.tsx :: MedicationCard": 414,
"components/medications/medication-table.tsx :: MedicationTableRowItem": 377,
"components/medications/scheduling/natural-language-extractor.tsx :: NaturalLanguageExtractor": 211,
"components/medications/scheduling/schedule-history-timeline.tsx :: EraDialog": 219,
@@ -165,7 +165,7 @@
"components/medications/side-effects-section.tsx :: SideEffectsSection": 518,
"components/medications/wizard/medication-wizard-dialog.tsx :: WizardDialogShell": 514,
"components/medications/wizard/steps/step3-dose.tsx :: Step3Dose": 222,
- "components/medications/wizard/steps/step8-summary.tsx :: Step8Summary": 230,
+ "components/medications/wizard/steps/step8-summary.tsx :: Step8Summary": 231,
"components/mood/manage/tag-groups-card.tsx :: TagGroupsCard": 252,
"components/mood/manage/tag-manager-card.tsx :: TagManagerCard": 261,
"components/mood/mood-form.tsx :: MoodForm": 514,
@@ -318,7 +318,7 @@
"lib/measurements/import-apple-health-export.ts :: streamParseExportXml > (anonymous) #2": 323,
"lib/medications/efficacy/build-efficacy.ts :: buildMedicationEfficacy": 268,
"lib/medications/intake-slot-dedup.ts :: dedupeUserIntakeSlots": 319,
- "lib/medications/list-read.ts :: buildMedicationsList": 308,
+ "lib/medications/list-read.ts :: buildMedicationsList": 310,
"lib/medications/scheduling/dose-history.ts :: reconstructDoseHistory": 218,
"lib/multipart/stream-to-disk.ts :: streamMultipartToDisk": 272,
"lib/notifications/dispatcher.ts :: dispatchNotification": 366,
diff --git a/src/app/api/export/medications/route.ts b/src/app/api/export/medications/route.ts
index 05c0dd6ca..c356acbde 100644
--- a/src/app/api/export/medications/route.ts
+++ b/src/app/api/export/medications/route.ts
@@ -32,6 +32,7 @@ import {
formatIntakeEventsForExport,
} from "@/lib/export";
import { resolveUserTimezone } from "@/lib/tz/resolver";
+import { withMedicationNote } from "@/lib/medications/medication-note";
import { NextRequest, NextResponse } from "next/server";
export const GET = apiHandler(async (request: NextRequest) => {
@@ -67,6 +68,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
dose: true,
active: true,
trackIntake: true,
+ noteEncrypted: true,
schedules: {
select: {
windowStart: true,
@@ -90,7 +92,8 @@ export const GET = apiHandler(async (request: NextRequest) => {
const sections: string[] = [
"# Medications",
- toCSV(formatMedicationsForExport(medications)),
+ // The note travels decrypted (#1371); the ciphertext never does.
+ toCSV(formatMedicationsForExport(medications.map(withMedicationNote))),
];
let intakeCount = 0;
diff --git a/src/app/api/export/route.ts b/src/app/api/export/route.ts
index f3c6c89ec..44a917ad0 100644
--- a/src/app/api/export/route.ts
+++ b/src/app/api/export/route.ts
@@ -20,6 +20,7 @@ import { resolveUserTimezone } from "@/lib/tz/resolver";
import { loadUserSourcePriority } from "@/lib/rollups/measurement-read";
import { NextRequest, NextResponse } from "next/server";
import { MEDICATION_SCHEDULE_ORDER } from "@/lib/medications/read-order";
+import { withMedicationNote } from "@/lib/medications/medication-note";
/**
* Export user data.
@@ -126,10 +127,12 @@ export const GET = apiHandler(async (request: NextRequest) => {
include: { schedules: { orderBy: MEDICATION_SCHEDULE_ORDER } },
orderBy: [{ createdAt: "desc" }, { id: "desc" }],
});
+ // The note travels decrypted (#1371); the ciphertext never does.
+ const readable = medications.map(withMedicationNote);
data.medications =
format === "csv"
- ? toCSV(formatMedicationsForExport(medications))
- : formatMedicationsForExport(medications);
+ ? toCSV(formatMedicationsForExport(readable))
+ : formatMedicationsForExport(readable);
}
if (type === "intake" || type === "all") {
diff --git a/src/app/medications/page-client.tsx b/src/app/medications/page-client.tsx
index f35ecf09b..2f4628b3c 100644
--- a/src/app/medications/page-client.tsx
+++ b/src/app/medications/page-client.tsx
@@ -85,6 +85,8 @@ interface Medication {
id: string;
name: string;
dose: string;
+ /** #1371 — the person's own note; null = none. */
+ note?: string | null;
category: string;
/** v1.40 — the label of a custom category; null for a built-in one. */
categoryLabel?: string | null;
@@ -816,6 +818,7 @@ export default function MedicationsPageClient() {
id: m.id,
name: m.name,
dose: m.dose,
+ note: m.note ?? null,
active: m.active,
intakeActionable: m.intakeActionable === true,
lastTakenAt: m.lastTakenAt,
diff --git a/src/components/medications/__tests__/log-intake-dialog.test.tsx b/src/components/medications/__tests__/log-intake-dialog.test.tsx
index 538e47e25..1be40cc66 100644
--- a/src/components/medications/__tests__/log-intake-dialog.test.tsx
+++ b/src/components/medications/__tests__/log-intake-dialog.test.tsx
@@ -166,3 +166,36 @@ describe("