feat(api): publish the resolved notification preferences on the account payload

The medication detail's notification section decides between the
server-side reminder switch and the client-managed chip on
`notificationPrefs.medication.clientManaged`, read off `GET /api/auth/me`.
That payload never carried the field — the preferences live on their own
endpoint, which is not on the app-boot path — so the flag was `undefined`
for every account: the chip never rendered, and a person whose phone owns
the reminders was shown a switch that no longer decides anything. The
write path and the dispatch gate were correct the whole time; only the
web reflection was missing its end.

The payload now carries the fully-resolved block from the same resolver
the dedicated endpoint uses, so the two cannot disagree, and an account
that never opted in reads `false` rather than absent. No extra query: the
session already loads the row the column sits on.

The pair guard over this payload gained the direction that could not see
it. It asked that every published field has a reader; a reader with no
field was invisible. It now also reads the response types clients declare
for this endpoint and holds them against what the route publishes. A
typed guard was tried first and does not work here — each consumer
declares its own local interface, so the compiler checks every file
against its own private claim and agrees with all of them.
This commit is contained in:
Marc-André Bombeck committed 2026-09-10 06:04:39 +02:00
1 parent f3321b8031
commit fd6fa18913
5 files changed
+138 -21

No files matched your search

+11
View File
@@ -30,6 +30,7 @@ import {
getOperatorModuleAvailability,
} from "@/lib/modules/gate";
import { parseTourProgress } from "@/lib/onboarding/tour-progress";
import { parseNotificationPrefs } from "@/lib/validations/notification-prefs";
import { resolveAccountAccess } from "@/lib/sharing/account-access";
import { recordSessionForPayload } from "@/lib/sharing/record-session-fence";
@@ -145,6 +146,16 @@ export const GET = apiHandler(async () => {
// the account has never saved one, which is what makes the panel show the
// named template on the first run rather than a silent server default.
reportSelection: user.reportSelectionJson ?? null,
// The account's resolved notification preferences — the same resolver
// `GET /api/auth/me/notification-prefs` answers with, so the two surfaces
// cannot disagree. Published here because the medication detail's
// notification section decides between the server-side reminder switch and
// the client-managed chip on `medication.clientManaged`, and it reads that
// decision off THIS payload: the dedicated endpoint is not on the app-boot
// path. An account that has never opted in resolves to the documented
// defaults rather than to `undefined`, so the switch is what renders.
// No extra query — the session already loads the row this column is on.
notificationPrefs: parseNotificationPrefs(user.notificationPrefs ?? null),
// v1.4.47 W3 — per-user Coach opt-out. Default `false` if the
// column is absent (partial-deploy rollback safety, see migration
// 0078 commentary). Every Coach mount point on the client checks