mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
feat(modules): direct on/off switches for the delegated Coach + Cycle rows
The Modules hub listed Coach and Cycle as read-only "manage in X" deep-links because their enabled-state is owned outside `modulePreferencesJson` (Coach in Coach settings, Cycle in the Account card). Give both a real switch like every other module, driving the SAME canonical state instead of a second copy: - Coach → `PATCH /api/auth/me/disable-coach` (`User.disableCoach`, inverted); the switch reads `!disableCoach`. - Cycle → `PATCH /api/auth/me/cycle-prefs` (`cycleTrackingEnabled`); the switch reads the resolved per-user gate. Precedence stays honest. A module the operator turned off server-wide can never be re-enabled by a per-user toggle, so the switch renders disabled with a "disabled server-wide" hint. For Coach the operator layer is BOTH the module-availability blob AND the assistant master flag (`flags.coach`); for Cycle it is the module-availability blob. Each delegated row keeps a small "manage" deep-link beside its switch, since the canonical surface carries more than on/off (Coach cadence/memory; cycle goal, predictions, lengths). Flipping either switch invalidates `authMe()` so every gated surface re-gates live; the cycle flip also evicts its own `cyclePrefs`/`cycle` reads, matching the canonical cycle card. Reuses the existing endpoints — no new route, no OpenAPI change.
This commit is contained in:
1 parent
421c407811
commit
8bc2f0f2ef
6 files changed
+337
-120
No files matched your search
@@ -296,6 +296,13 @@ describe("Coach disable cascade invariant", () => {
|
||||
// the nudges. Server-side cron, not a render path — the cascade
|
||||
// fixture cannot mount it, so the allowlist entry is the pin.
|
||||
"src/lib/jobs/coach-nudge.ts",
|
||||
// The Modules hub renders a live Coach on/off switch. It reads
|
||||
// `flags.coach` (the operator assistant master flag) so the switch goes
|
||||
// disabled + "disabled server-wide" when the operator killed Coach — a
|
||||
// per-user toggle cannot re-enable it. Distinct from the render-path
|
||||
// gates above: here the flag drives the switch's disabled state, not a
|
||||
// null-return.
|
||||
"src/components/settings/modules-section.tsx",
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -335,6 +335,13 @@ describe("Coach per-user disableCoach invariant", () => {
|
||||
// second copy in `modulePreferencesJson`. Covered by the coach-
|
||||
// delegation cases in `src/lib/modules/__tests__/gate.test.ts`.
|
||||
"src/lib/modules/gate.ts",
|
||||
// The Modules hub Coach row renders a live switch bound to the SAME
|
||||
// `user.disableCoach` source of truth: `checked` is `!disableCoach` and a
|
||||
// flip writes the inverted flag via `PATCH /api/auth/me/disable-coach`.
|
||||
// Reads the flag off `useAuth().user` (no second copy). Covered by the
|
||||
// delegated-coach cases in
|
||||
// `src/components/settings/__tests__/modules-section.test.tsx`.
|
||||
"src/components/settings/modules-section.tsx",
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -79,10 +79,13 @@ export interface ModuleDefinition {
|
||||
*/
|
||||
optIn?: boolean;
|
||||
/**
|
||||
* For delegated modules only: where the real on/off control lives, so the
|
||||
* Modules hub can render a read-only "managed in X" row that deep-links to
|
||||
* the canonical control instead of a dead toggle. `href` is the in-app
|
||||
* link; `labelKey` names the destination ("Account", "Coach settings").
|
||||
* For delegated modules only: where the canonical settings surface lives.
|
||||
* The Modules hub renders a LIVE switch for these keys (driving the real
|
||||
* column — coach → `User.disableCoach`, cycle → `cycleTrackingEnabled`) and
|
||||
* keeps this as a small "manage" deep-link beside the switch, because that
|
||||
* surface carries more than on/off (Coach cadence/memory; cycle
|
||||
* goal/predictions/lengths). `href` is the in-app link; `labelKey` names the
|
||||
* destination ("Account", "Coach settings").
|
||||
*/
|
||||
managedAt?: { href: string; labelKey: string };
|
||||
}
|
||||
@@ -193,7 +196,8 @@ export const MODULE_REGISTRY: Readonly<Record<ModuleKey, ModuleDefinition>> =
|
||||
descriptionKey: "modules.cycle.description",
|
||||
category: "tracking",
|
||||
delegatesTo: "cycle",
|
||||
// The real on/off lives in the Account section's cycle-tracking card.
|
||||
// The hub switch drives `cycleTrackingEnabled`; the fuller cycle-tracking
|
||||
// card (goal, predictions, cycle lengths) lives in the Account section.
|
||||
managedAt: {
|
||||
href: "/settings/account#cycle-tracking",
|
||||
labelKey: "settings.sections.account.title",
|
||||
@@ -253,7 +257,8 @@ export const MODULE_REGISTRY: Readonly<Record<ModuleKey, ModuleDefinition>> =
|
||||
descriptionKey: "modules.coach.description",
|
||||
category: "intelligence",
|
||||
delegatesTo: "coach",
|
||||
// The real on/off lives in the dedicated Coach settings section.
|
||||
// The hub switch drives `User.disableCoach`; the dedicated Coach settings
|
||||
// section carries the rest (cadence, memory, activation copy).
|
||||
managedAt: {
|
||||
href: "/settings/coach",
|
||||
labelKey: "settings.sections.coach.title",
|
||||
|
||||
Reference in new issue
Block a user