mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
feat(api): say why a module's surfaces are not there, not only that they are not
`GET /api/auth/me` publishes a boolean per module, and three different situations collapse into the same `false`: the record has the module switched off, the active grant's sections do not open it, or the operator switched it off for the whole instance. A client holding the boolean has to guess between them, so the empty state either says nothing or says something that may be untrue -- offering a delegate a switch that is not theirs to reach, or telling an account holder to turn on a module the operator removed. `moduleAccess` carries the reason beside the booleans, which keep their exact meaning: `enabled`, `disabled`, `not_granted`, `unavailable`, resolved outside-in so the reason published is the one nothing further in can change. `modules[key]` stays exactly `moduleAccess[key] === "enabled"` for every key, and the two halves are built independently -- the boolean still runs through `maskModulesToSections` -- so the test of that pairing is an assertion rather than a restatement of a derivation. It is checked on the pure builder across a matrix of grants, records and operator blobs, and again on the real route for a scoped delegate, an unscoped one, and an operator-disabled module. Both ends in one change, because a payload field with no reader is the failure this repository keeps rediscovering. The reader is `ModuleDisabledNotice`, the shared module-off empty state the empty-states audit asks for: it names the reason in the reader's own language and offers the enable control only when the record's own switch is what is off, since neither of the other two is fixable from that page. The nutrients page adopts it and drops its bespoke block, which had been handing every delegate a button that would refuse them. Registered in the OpenAPI registry as `ModuleAccessMap` and carried on the account payload; the native contract gains a field and loses nothing, since a Bearer request carries no switch and never reads `not_granted`.
This commit is contained in:
1 parent
0c46df1413
commit
47a5079cfa
17 files changed
+859
-21
No files matched your search
@@ -57,7 +57,7 @@ import {
|
||||
import { parseTourProgress } from "@/lib/onboarding/tour-progress";
|
||||
import { resolveAccountAccess } from "@/lib/sharing/account-access";
|
||||
import {
|
||||
maskModulesToSections,
|
||||
buildModuleDisclosure,
|
||||
sectionsOpen,
|
||||
} from "@/lib/sharing/module-disclosure";
|
||||
import { recordSessionForPayload } from "@/lib/sharing/record-session-fence";
|
||||
@@ -144,7 +144,15 @@ export const GET = apiHandler(async () => {
|
||||
// opens. A closed section reads `false`, the same answer a module that is
|
||||
// off already gives, and the navigation the client builds from it drops
|
||||
// exactly the doors `isSharedRecordPathPresentable` drops anyway.
|
||||
const modules = maskModulesToSections(resolvedModules, sections);
|
||||
// Two maps out of one pass: `modules` is the boolean gate map, masked
|
||||
// exactly as it was, and `moduleAccess` is the same answer with the reason
|
||||
// attached — the record's own switch, the grant's edge, or the operator's.
|
||||
// Every client that only wants "paint it or not" keeps reading the boolean.
|
||||
const { modules, moduleAccess } = buildModuleDisclosure(
|
||||
resolvedModules,
|
||||
moduleAvailability,
|
||||
sections,
|
||||
);
|
||||
// The cycle flag is masked on its own rather than read off `modules.cycle`:
|
||||
// that key also carries the operator's server-wide availability, and this
|
||||
// field never has. Same section, same answer, without borrowing a second
|
||||
@@ -249,6 +257,15 @@ export const GET = apiHandler(async () => {
|
||||
// this to show a "disabled server-wide" read-only row; everywhere else
|
||||
// the already-AND-ed `modules` map is the single gate to read.
|
||||
moduleAvailability,
|
||||
// Why each module's surfaces are or are not there, for the record this
|
||||
// session is inside: `enabled`, `disabled` (the record's own switch),
|
||||
// `not_granted` (the active grant does not open the module's section) or
|
||||
// `unavailable` (the operator switched it off for the whole instance).
|
||||
// Precedence outside-in: unavailable > not_granted > disabled > enabled.
|
||||
// `modules[key]` stays exactly `moduleAccess[key] === "enabled"`, so this
|
||||
// adds a reason without moving a gate; empty states read it to say the
|
||||
// honest thing rather than offering a switch the reader cannot reach.
|
||||
moduleAccess,
|
||||
// v1.36.0 — account sharing, resolved. `accounts` is the switcher's menu,
|
||||
// `active` is the record this session is inside (null when it is in its
|
||||
// own), `canSwitch` and per-entry `canWrite` are the booleans the UI binds
|
||||
|
||||
Reference in new issue
Block a user