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:
Marc-André Bombeck committed 2026-07-07 16:50:09 +02:00
1 parent 421c407811
commit 8bc2f0f2ef
6 files changed
+337 -120

No files matched your search

+11 -6
View File
@@ -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",