Add a notification for a dose somebody else marked

The owner of a shared record gets an alert when a delegate marks one of
their doses. It is the one exception to the rule that bookkeeping stays
in the activity view: whether the tablets were taken is a fact about the
owner's own body, and they should not have to go looking for it.

Default ON, against the opt-in precedent of PERSONAL_RECORD, because the
event only exists once the owner has invited somebody and given them
write access. The per-channel row in the matrix is the valve for anyone
who finds it noisy, and both positions of that preference are covered.
This commit is contained in:
Marc-André Bombeck committed 2026-08-03 09:51:42 +02:00
1 parent 1efd337f53
commit 427e25d1ae
2 files changed
+70

No files matched your search

@@ -381,6 +381,58 @@ describe("dispatchNotification — per-event default policy (v1.4.25 W16c)", ()
expect(sendViaNtfyMock).toHaveBeenCalledTimes(1);
});
// v1.36.x — the delegated-dose event goes the other way from
// PERSONAL_RECORD: ON without a row, because the owner invited the person
// whose action produces it. Both positions of the preference are asserted,
// so "default ON" cannot be true only because the dispatcher ignores the
// event entirely.
it("SHARED_RECORD_INTAKE defaults ON when no preference row exists", async () => {
const channel = makeChannel({
type: "NTFY",
config: JSON.stringify({
serverUrl: "https://ntfy.example",
topic: "t",
}),
preferences: [],
});
vi.mocked(prisma.notificationChannel.findMany).mockResolvedValueOnce([
channel,
] as never);
sendViaNtfyMock.mockResolvedValueOnce({ ok: true });
await dispatchNotification({
eventType: "SHARED_RECORD_INTAKE",
userId: "u-1",
title: "Wren marked a dose",
message: "Ramipril: marked as taken by Wren.",
});
expect(sendViaNtfyMock).toHaveBeenCalledTimes(1);
});
it("SHARED_RECORD_INTAKE is suppressed by an explicit per-channel opt-out", async () => {
const channel = makeChannel({
type: "NTFY",
config: JSON.stringify({
serverUrl: "https://ntfy.example",
topic: "t",
}),
preferences: [{ eventType: "SHARED_RECORD_INTAKE", enabled: false }],
});
vi.mocked(prisma.notificationChannel.findMany).mockResolvedValueOnce([
channel,
] as never);
await dispatchNotification({
eventType: "SHARED_RECORD_INTAKE",
userId: "u-1",
title: "Wren marked a dose",
message: "Ramipril: marked as taken by Wren.",
});
expect(sendViaNtfyMock).not.toHaveBeenCalled();
});
});
describe("dispatchNotification — MOOD_REMINDER single source of truth (v1.7.0)", () => {
+18
View File
@@ -112,6 +112,16 @@ export const EVENT_TYPES = [
// morning window, so a concerning reading always stays on
// `MEASUREMENT_ANOMALY` and this never bypasses Focus.
"DAILY_BRIEFING",
// v1.36.x — somebody with a write grant on this record marked one of the
// owner's doses. The only notification a delegated write produces: every
// other shared-record write is bookkeeping and stays in the owner's activity
// view, while "somebody said I took my tablets" is a fact about the owner's
// own body that they should not have to go looking for. Addressed to the
// OWNER — the delegate learns what they did from the response in their own
// session, not from a push (there is no channel that could carry it: the
// dispatcher addresses a userId, and under record substitution that userId
// is the owner by design). Default ON, see EVENT_DEFAULT_ENABLED.
"SHARED_RECORD_INTAKE",
] as const;
export type EventType = (typeof EVENT_TYPES)[number];
@@ -179,6 +189,14 @@ export const EVENT_DEFAULT_ENABLED: Record<EventType, boolean> = {
// channel that has no explicit enabled row, so a fresh account never
// receives a morning push it did not ask for.
DAILY_BRIEFING: false,
// v1.36.x — ON by default, deliberately against the PERSONAL_RECORD opt-in
// precedent. The event cannot exist unless the owner invited a second person
// and gave them write access, so there is no backfill class of volume to
// protect against; and an owner who has to discover a setting before they
// learn that somebody else marks their doses was never told at all. The row
// sits in the /notifications matrix from day one, so an owner who finds it
// noisy silences it per channel with one switch.
SHARED_RECORD_INTAKE: true,
};
export const CHANNEL_TYPE_LABELS: Record<ChannelType, string> = {