mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
fix(settings): make delegated module rows read-only managed deep-links
The Module hub rendered live switches for the delegated keys cycle and coach, but the gate resolves those from their own sources (cycle from CycleProfile, coach from the operator flag + disableCoach), so a toggle wrote an inert entry to modulePreferencesJson, snapped back, and still fired a success toast. Render delegated modules as read-only rows that deep-link to their real control, strip delegated keys from the modules PATCH schema so no inert value is stored, drop the duplicate cycle control, and surface an operator-disabled state with the previously orphaned hint string.
This commit is contained in:
1 parent
7c8d82f565
commit
91d01d154a
14 files changed
+216
-36
No files matched your search
@@ -11,15 +11,18 @@
|
||||
* returns the freshly-resolved module map.
|
||||
*
|
||||
* The body schema (`modulePrefsPatchSchema`) is `strict()` over the
|
||||
* canonical toggleable key set, so a core-domain key (`weight`,
|
||||
* `bloodPressure`, `pulse`, `medications`) or any unknown key is a 422 —
|
||||
* the core measurement engine + meds can never be disabled here.
|
||||
* directly-owned toggleable key set, so a core-domain key (`weight`,
|
||||
* `bloodPressure`, `pulse`, `medications`), a delegated key (`cycle`,
|
||||
* `coach`), or any unknown key is a 422 — the core measurement engine +
|
||||
* meds can never be disabled here, and a value for a delegated module can
|
||||
* never land inert in `modulePreferencesJson`. The delegated modules are
|
||||
* managed at their real control (cycle in Account, coach in Settings →
|
||||
* Coach); the Modules hub deep-links there rather than offering a dead
|
||||
* toggle.
|
||||
*
|
||||
* `userId` is always narrowed from `requireAuth()`; the body never
|
||||
* carries it. The semantics are a DISABLED allowlist: a key set to
|
||||
* `false` disables that module; `true` (or absence) leaves it enabled.
|
||||
* cycle/coach keys are accepted for forward-compat but the gate ignores
|
||||
* the blob for them (they delegate to their existing source of truth).
|
||||
*/
|
||||
import { apiHandler, requireAuth, HttpError } from "@/lib/api-handler";
|
||||
import {
|
||||
|
||||
@@ -6,7 +6,10 @@ import { buildAvatarUrl } from "@/lib/avatar";
|
||||
import { decrypt } from "@/lib/crypto";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { isCycleEnabled } from "@/lib/cycle/gate";
|
||||
import { resolveModuleMap } from "@/lib/modules/gate";
|
||||
import {
|
||||
resolveModuleMap,
|
||||
getOperatorModuleAvailability,
|
||||
} from "@/lib/modules/gate";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -22,7 +25,7 @@ export const GET = apiHandler(async () => {
|
||||
// cycle-tracking gate; no row is forced, a NULL toggle derives from
|
||||
// gender) is a Postgres round-trip. Running them sequentially added
|
||||
// the cookie hop to every /me — and /me sits on every app boot path.
|
||||
const [, cycleProfile, modules] = await Promise.all([
|
||||
const [, cycleProfile, modules, moduleAvailability] = await Promise.all([
|
||||
setOnboardingPendingCookie(user.onboardingCompletedAt == null),
|
||||
prisma.cycleProfile.findUnique({
|
||||
where: { userId: user.id },
|
||||
@@ -34,6 +37,12 @@ export const GET = apiHandler(async () => {
|
||||
// disabled-allowlist `modulePreferencesJson`. Default-on. Clients
|
||||
// hide a whole module surface end-to-end when its key is `false`.
|
||||
resolveModuleMap(user.id),
|
||||
// v1.18.0 — operator-layer availability map (server-wide kill-switch).
|
||||
// The resolved `modules` map above already AND-s this in, so it cannot
|
||||
// distinguish operator-off from user-off. The Modules hub needs that
|
||||
// distinction to render an operator-disabled module as a read-only
|
||||
// "disabled server-wide" row instead of a live toggle that no-ops.
|
||||
getOperatorModuleAvailability(),
|
||||
]);
|
||||
const cycleTrackingEnabled = isCycleEnabled(user.gender, cycleProfile);
|
||||
|
||||
@@ -99,5 +108,11 @@ export const GET = apiHandler(async () => {
|
||||
// resolved `disableCoach` + operator master flag, so this map is the
|
||||
// single thing a client needs to gate every secondary domain.
|
||||
modules,
|
||||
// v1.18.0 — operator-layer availability per toggleable module. `false`
|
||||
// ⇒ the operator turned the module off server-wide (off for every
|
||||
// account regardless of personal preference). The Modules hub reads
|
||||
// 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,
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user